Fundamentally confused. I made a Python Script Toolbox with multiple tools (hopefully proper terminology):
Each tool works fine, and the basic Python code/structure is as shown below. NOTE! I am only showing one of the Tools (metadata_mcm) and not update_attr or xml_element_csv_creator - but they are the same in basic structure. Skip to my questions below before diving into code.
class Toolbox(object):
def __init__(self):
"""Define the toolbox (the name of the toolbox is the name of the
.pyt file)."""
self.label = "Toolbox"
self.alias = "toolbox"
# List of tool classes associated with this toolbox
self.tools = [metadata_mcm, create_xml_element_csv, update_attr]
class ToolValidator(object):
"""Class for validating a tool's parameter values and controlling
the behavior of the tool's dialog."""
def __init__(self):
"""Setup arcpy and the list of tool parameters."""
self.params = arcpy.GetParameterInfo()
def initializeParameters(self):
"""Refine the properties of a tool's parameters. This method is
called when the tool is opened."""
return
def updateParameters(self):
"""Modify the values and properties of parameters before internal
validation is performed. This method is called whenever a parameter
has been changed."""
return
def updateMessages(self):
"""Modify the messages created by internal validation for each tool
parameter. This method is called after internal validation."""
self.params[1].clearMessage()
if self.params[1].value is None:
self.params[1].clearMessage()
else:
if os.path.exists(self.params[1]):
self.params[1].setErrorMessage('output path already exists')
else:
self.params[1].clearMessage()
return
class create_xml_element_csv(object):
def __init__(self):
self.label = "xml element csv creator"
self.desciption = "create inventory of xml elements relevant to fields"
self.canRunInBackground = False
def getParameterInfo(self):
"""Define parameter definitions"""
param0 = arcpy.Parameter(
displayName="Input Feature",
name="fc_in",
datatype="Layer",
parameterType="Required",
direction="Input")
param1 = arcpy.Parameter(
displayName="Path/to/file.csv",
name="fp_csv",
datatype="DEFile",
parameterType="Required",
direction="Input")
params = []
params.append(param0)
params.append(param1)
return params
def isLicensed(self):
"""Set whether tool is licensed to execute."""
return True
def updateParameters(self, parameters):
"""Modify the values and properties of parameters before internal
validation is performed. This method is called whenever a parameter
has been changed."""
return
def updateMessages(self, parameters):
"""Modify the messages created by internal validation for each tool
parameter. This method is called after internal validation."""
def execute(self, parameters, messages):
"""The source code of the tool."""
fc = parameters[0]
desc = arcpy.Describe(fc)
fp_fc = desc.featureClass.catalogPath
fp_csv = parameters[1].valueAsText
st0, st1 = os.path.splitext(fp_csv)
# csv
if st1 == '.csv':
pass
else:
fp_csv = '{}.csv'.format(fp_csv)
flds = [f.name for f in arcpy.ListFields(fp_fc)]
vals = np.column_stack([flds, [None] * len(flds), [None] * len(flds), [None] * len(flds)])
# attrdef = definition
# attrdefs = definition source
cn = ['attrlabl', 'attralias', 'attrdef', 'attrdefs']
df_fields = pd.DataFrame(vals, columns=cn)
df_fields.to_csv(fp_csv)
return
I don't understand a couple things:
1) Can the ToolValidator Class work with multiple tools? It's odd that I have essentially hard-coded the paramaters positionally in updateMessages for ALL subsequent tools. I want to ensure that the output file does not exist prior to running tool. This is argument 1 for create_xml_element_csv but doesn't exist for the two other tools. So can I create a ToolValidator Class and only apply the validation to one tool in the toolbox?
2) Is my syntax within ToolValidator/updateMessages method correct? Found this syntax from a StackExchange response.
The tools run BUT the validation of
if os.path.exists(self.params[1]):
self.params[1].setErrorMessage('output path already exists')
is not being performed. I can pass an argument of a path that exists, and no warming is issued, tool overwrites the existing file. No bueno.
Thanks,
Zach
The ToolValidator class is used by script tools in custom toolboxes (legacy .tbx or new .atbx), not by Python Toolboxes (.pyt).
In python toolboxes (.pyt) all the validation is done is the updateMessages and updateParameters methods of the tool class itself
Customize tool behavior in a Python toolbox
Hi @Luke_Pinner - I appreciate your response. I suspected that may be the case. ESRI's documentation can be really frustrating. I scoured that link you included and all links therein prior to posting. Reread them just now and it's still really confusing. FYI - I am experienced with Object Oriented Programming and Classes. None of the links on that page show me a fleshed out implementation of parameter validation:
1) understanding-validation-in-script-tools
2) programming-a-toolvalidator-class which has instructions on constructing the ToolValidator Class, which I thought were not applicable with the Python Toolbox
So! In the link you sent, they provide this example (below). But I'm confused. What is the parameters argument? I imagine that is params? And more confusingly, what is parameters[6]?
if not parameters[6].altered
The example preceding this has only 3 parameters, so 0,1,2 indices.
def updateParameters(self, parameters):
# Set the default distance threshold to 1/100 of the larger of the width
# or height of the extent of the input features. Do not set if there is no
# input dataset yet, or the user has set a specific distance (Altered is true).
#
if parameters[0].valueAsText:
if not parameters[6].altered:
extent = arcpy.Describe(parameters[0]).extent
if extent.width > extent.height:
parameters[6].value = extent.width / 100
else:
parameters[6].value = extent.height / 100
return
I'm REALLY appreciative of your help! Just confused and critical of the ESRI documentation that always seems to weave in and out of Pro vs Desktop vs whatever. Check reply below - I did solve this. It could be ugly. Suggestions welcome...
You don't need a separate Tool Validator class because its stuff is already inside the tool already. Its functionality is used on a per-tool basis in their updateParameters() and updateMessages(). (See lines 47 and 64 below)
Use the Validator reference page as you were already but populate inside the tool instead and you should be golden.
I find it helpful to assign each parameter in the list as a variable during validation so I can keep track of what I'm doing to each one, as well as making it easier to change their numbering later if I need.
Below is a pared down example of one of the tools in one of my PYTs.
Each tool is listed in self.tools of the Toolbox class, and under each tool is the validation.
class Toolbox(object):
def __init__(self):
"""Define the toolbox (the name of the toolbox is the name of the
.pyt file)."""
self.label = "Toolbox"
self.alias = ""
# List of tool classes associated with this toolbox
self.tools = [ExportPhotos]
class ExportPhotos(object): # The tool
def __init__(self):
"""Define the tool (tool name is the name of the class)."""
self.label = "Export Photos"
self.description = ""
self.canRunInBackground = False
def getParameterInfo(self):
"""Define parameter definitions"""
param0 = arcpy.Parameter(
displayName="GDBs or Feature Classes?",
name="GDBs_or_Feature_Classes?",
datatype="GPString",
parameterType="Required",
direction="Input")
param0.filter.type = "ValueList"
param0.filter.list = ["GDBs", "Feature Classes"]
param0.value = "GDBs"
param1 = arcpy.Parameter(
displayName="Input GDBs",
name="Input_GDBs",
datatype="DEWorkspace",
parameterType="Optional",
direction="Input",
multiValue=True)
param2 = arcpy.Parameter(
displayName="Input Feature Classes",
name="Input_FCs",
datatype="GPFeatureLayer",
parameterType="Optional",
direction="Input",
multiValue=True)
params = [param0, param1, param2)
return params
def updateParameters(self, parameters):
"""Modify the values and properties of parameters before internal
validation is performed. This method is called whenever a parameter
has been changed."""
gdbOrFC = parameters[0] #param0
inGDBS = parameters[1] #param1
inFCs = parameters[2] #param2
#Determines if inFCs or inGDBs is visible
if gdbOrFC.value== "GDBs":
inGDBS.enabled = True #inGDBS
inFCs.enabled = False #inFCs
else:
inGDBS.enabled = False
inFCs.enabled = True
return
def updateMessages(self, parameters):
"""Modify the messages created by internal validation for each tool
parameter. This method is called after internal validation."""
gdbOrFC = parameters[0] #param0
inGDBS = parameters[1] #param1
inFCs = parameters[2] #param2
#Set inGDBs to pretend it's required
if (gdbOrFC.value == "GDBs") and (inGDBS.value is None):
inGDBS.setIDMessage('ERROR', 735)
return
Another thing is that each tool has an updateMessages function. Normally validation is done on a per-tool basis using that tool's updateParameters and updateMessages.
Tentatively found a solution. Any alternatives to the conditional logic: if parameters[1].value would be greatly appreciated! But it works. Code is commented to explain why I did this.
def updateMessages(self, parameters):
"""Modify the messages created by internal validation for each tool
parameter. This method is called after internal validation."""
# This if statement required or else error message will for parameter[0]
# "TypeError: stat: path should be string, bytes, os.PathLike or integer, not NoneType".
# Apparently this method will run through ALL parameters right off the bat. So add
# conditional statements to ensure validation only performed on target parameters i.e. parameter[1] in this case
if parameters[1].value:
fp_csv = parameters[1].valueAsText
if os.path.exists(fp_csv):
parameters[1].setErrorMessage('output path already exists')
else:
parameters[1].clearMessage()
return
Note that when an existing path/to/file.csv is passed for argument 2 (parameters[1]), the message will display:
I don't like hard coding the array indexes since they are a pain to adjust if you move around the order of the parameters. So I wrote a function to get them by name and call that at the start of the update and execute functions.
# Find the parameter by name (which is set in getParametmerInfo)
def get_parm (parms, name, enforce_not_null = False):
for parm in parms:
if parm.name == name:
if enforce_not_null and (parm.value is None or parm.valueAsText == ''):
raise Exception ("'%s' has an invalid value" % (parm.displayName))
return parm
# At the start of update and execute functions, retrieve the relevant parameters by name instead of index
def execute (self, parameters):
gdbOrFC = get_parm (parameters, "GDBs_or_Feature_Classes?")
inGDBS = get_parm (parameters, "Input_GDBs")
inFCs = get_parm (parameters, "Input_FCs")
Perhaps a new post is in order. Do either of you have experience defining an argument as a DETextFile or DEFile? For another Python Toolbox built on the same template you all helped me with (thanks!), I want to pass a path/to/csv as an argument. Spent the hour fleshing out, copying and pasting the .pyt. Looks good. Tool opens, but when I select (or try to select) the input csv in both the third and fourth arguments, it doesn't load - argument input box(es) stay blank. I've tried manually copying the path/to/file.csv into the argument, csv GIS attribute table in this case. Same thing.
When I try running the tool I, an error is thrown about NoneType as would be expected. My first instinct is to think an ESRI bug, but I'll withhold judgement until digging. Just trying to pass a file as an argument.
Super late response, but I had the same problem yesterday and it was because my two parameters had the same name.