3

I'm creating a Python Toolbox. One input is a workspace and the second is a list of feature classes and tables. The list of feature classes and tables is generated once the workspace is input by the user, using a series of ListFeatureClasses and ListTables. Annoyingly, once the workspace is input the tool will re-run this code when the user updates ANY of the parameters. This can freeze up ArcGIS for minutes if the workspace contains lots of feature classes/tables and is located on a network. Is there a way to only run code when a specifc parameter is altered? You'd think parameter.altered == True would do the trick but this seems no different than if parameter.valueAsText returns True in a logic test (has a value).

Making the feature class/tables parameter dependent of the workspace doesn't work because feature classes in datasets are ignored.

Code:

class Tool(object):
    def __init__(self):
        self.label = "Copy Feature Classes And Relationship Classes"
        self.description = ""
        self.canRunInBackground = False

    def getParameterInfo(self):
        inGdb = arcpy.Parameter(
            displayName = "Input Geodatabase",
            name = "ingdb",
            datatype = "Workspace",
            parameterType = "Required",
            direction = "Input")

        outGdb = arcpy.Parameter(
            displayName = "Output Geodatabase",
            name = "outgdb",
            datatype = "Workspace",
            parameterType = "Required",
            direction = "Input")

        inFcs = arcpy.Parameter (
            displayName = "Feature Classes And Tables",
            name = "infcs",
            datatype = "GPString",
            parameterType = "Required",
            direction = "Input",
            multiValue = True)

        return [inGdb, outGdb, inFcs]

    def updateParameters(self, parameters):
        if not parameters [0].valueAsText:
            parameters [2].enabled = False
        ###below code runs whenever any new input is input!!!
        if parameters [0].valueAsText and parameters [0].altered: ##altered doesn't work! :(
            try: wst = arcpy.Describe (parameters [0].valueAsText).workspaceType
            except:
                parameters [2].filter.list = []
                parameters [2].value = None
                return
            if  wst == "LocalDatabase":
                parameters [2].enabled = True
                gdb = parameters [0].valueAsText
                arcpy.env.workspace = gdb
                fcs = []
                for typ in ["Point", "Polygon", "Polyline", "Multipoint"]:
                    fcs += ["{} (Feature Class)".format (fc) for fc in arcpy.ListFeatureClasses (feature_type = typ)]
                fcs += ["{} (Table)".format (tab) for tab in arcpy.ListTables ()]
                datasets = arcpy.ListDatasets ()
                for dataset in datasets:
                    for typ in ["Point", "Polygon", "Polyline", "Multipoint"]:
                        dsFcs = arcpy.ListFeatureClasses (None, typ, dataset)
                        for dsFc in dsFcs:
                            fc = os.path.join (dataset, dsFc)
                            fcs += [fc]
                parameters [2].filter.list = fcs
            else:
                parameters [2].filter.list = []
                parameters [2].value = None
  • Can you introduce a global variable (currentValue) then in the sub if valueAsText != currentValue then return ? – Michael Stimson Oct 25 '19 at 0:53
  • @MichaelStimson Thanks for the response Michael. I tried globals but they didn't seem to work. I'll give it another go. – Emil Brundage Oct 25 '19 at 0:54
  • @MichaelStimson That did it! I just wasn't using globals correctly. Thanks. Please feel free to answer and I'll accept. – Emil Brundage Oct 25 '19 at 1:01
  • did you specify global currentValue in the def? Python is a bit finicky about that sort of thing, if you don't tell it you want to use the existing global variable it will assume that it's a new local variable (which has a value of None). – Michael Stimson Oct 25 '19 at 1:03
  • 1
    According to the documentation: "altered is true if the value of a parameter is changed... Once a parameter has been altered, it remains altered until the user empties (blanks out) the value, in which case it returns to being unaltered.". The "arcpy way (tm)" is to check if parameters[0].altered and not parameters[0].hasBeenValidated:. – user2856 Oct 25 '19 at 4:54
4

The "arcpy way ™" is to check if parameters[0].altered and not parameters[0].hasBeenValidated:

According to the documentation:

altered

altered is true if the value of a parameter is changed... Once a parameter has been altered, it remains altered until the user empties (blanks out) the value, in which case it returns to being unaltered.

hasBeenValidated

hasBeenValidated is false if a parameter's value has been modified by the user since the last time updateParameters and internal validate were called. Once internal validate has been called, geoprocessing automatically sets hasBeenValidated to true for every parameter.

hasBeenValidated is used to determine whether the user has changed a value since the last call to updateParameters.

For example, a simplified version of your code (doesn't disable/enable, just empties/populates parameters[2]):

import arcpy

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 = [Tool]

class Tool(object):
    def __init__(self):
        self.label = "Tool"
        self.description = "A Tool"
        self.canRunInBackground = False

    def getParameterInfo(self):
        inGdb = arcpy.Parameter(
            displayName = "Input Geodatabase",
            name = "ingdb",
            datatype = "DEWorkspace",
            parameterType = "Required",
            direction = "Input")
        # Set the filter to accept only local geodatabases
        inGdb.filter.list = ["Local Database"]

        outGdb = arcpy.Parameter(
            displayName = "Output Geodatabase",
            name = "outgdb",
            datatype = "DEWorkspace",
            parameterType = "Required",
            direction = "Input")
        # Set the filter to accept only local geodatabases
        outGdb.filter.list = ["Local Database"]

        inFcs = arcpy.Parameter(
            displayName = "Feature Classes And Tables",
            name = "infcs",
            datatype = "GPString",
            parameterType = "Required",
            direction = "Input",
            multiValue = True)

        return [inGdb, outGdb, inFcs]

    def updateParameters(self, parameters):

        if not parameters[0].altered:

            parameters[2].filter.list = []
            parameters[2].value = None

        elif parameters[0].altered and not parameters[0].hasBeenValidated:

            gdb = parameters[0].valueAsText
            arcpy.env.workspace = gdb
            fcs = []
            for typ in ["Point", "Polygon", "Polyline", "Multipoint"]:
                fcs += ["{} (Feature Class)".format(fc) for fc in arcpy.ListFeatureClasses(feature_type = typ)]
            fcs += ["{} (Table)".format(tab) for tab in arcpy.ListTables()]
            datasets = arcpy.ListDatasets()
            for dataset in datasets:
                for typ in ["Point", "Polygon", "Polyline", "Multipoint"]:
                    dsFcs = arcpy.ListFeatureClasses(None, typ, dataset)
                    for dsFc in dsFcs:
                        fc = os.path.join(dataset, dsFc)
                        fcs += [fc]
            parameters[2].filter.list = fcs
| improve this answer | |
2

For this one a use of a global variable will help:

currentValue = '' # to avoid problems later give it an empty string

def updateParameters(self, parameters):
  global currentValue
  if parameters[0].valueAsText != currentValue:
    currentValue = parameters[0].valueAsText
    # the rest of your code

Python is a bit finicky about that sort of thing, if you don't tell it you want to use the existing global variable it will assume that it's a new local variable (which has a value of None).

Global variables are handy and have a purpose but I would warn future readers of this post to not over-use global variables.. I have read some articles that suggest that globals in python chew up memory needlessly.

| improve this answer | |
  • 3
    Alternatively, you could create a class instance variable (ie, a property) in the __init__(self) function as self.currentValue. And check/update its value in updateParameters() as above. – Son of a Beach Oct 25 '19 at 2:30

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.