Select to view content in your preferred language

Struggling with python toolbox tools and parameter validation. updateParameters and updateMessages don't "work"

356
6
Jump to solution
06-14-2024 09:49 AM
Labels (1)
Glasnoct
New Contributor III

I'm currently acquainting myself with the parameter and parameter validation methods of the tool class and I can't quite figure out what's required to get error/warning messages to properly populate and how updateParameters and updateMessages differ for setErrorMessage/setWarningMessage. Either I get no message, the message doesn't update on value change, or its the wrong one. What is the correct code to make sure that a parameter is properly validated after it changes and then displays the proper message (if any)?

For a simple example, I just want to validate that a given parameter is in a list of strings:

 

project_ids = ['123', '456', '789']

class tool:
    def __init__(self):
        """Define the tool (tool name is the name of the class)."""
        self.label = 'test tool'
        self.description = "test"

    def getParameterInfo(self) -> list:
        """Define the tool parameters."""
        project_id = arcpy.Parameter(name="project ID",
                                     displayName="project ID",
                                     datatype="GPString",
                                     parameterType="Required",  # Required|Optional|Derived
                                     direction="Input",  # Input|Output
                                     )
        return [project_id]


    def isLicensed(self):
        """Set whether the 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."""
        if parameters[0].altered:
            if parameters[0].value is None:
                parameters[0].setErrorMessage("No Project Specified")
            elif parameters[0].valueAsText not in project_ids:
                parameters[0].setErrorMessage('Unrecognized project ID')
        return

    def updateMessages(self, parameters):
        """Modify the messages created by internal validation for each tool
        parameter. This method is called after internal validation."""
        # The only time I got messages to show up was when I put the code block from updateParameters here
        return

    @staticmethod
    def execute(self, parameters, messages=None):
        """The source code of the tool."""

        project_id = parameters[0]
        test_function(project_id)
        return

    def postExecute(self, parameters):
        """This method takes place after outputs are processed and
        added to the display."""
        return

 

0 Kudos
1 Solution

Accepted Solutions
HaydenWelch
Occasional Contributor II

Why are you using updateMessages and updateParameters for this? You can specify a value list in the parameter definition:

 

 

import arcpy

class Tool(object):
    def __init__(self):
        """Define the tool (tool name is the name of the class)."""
        self.label = 'test tool'
        self.description = "test"
        self.project_ids = ['123', '456', '789']

    def getParameterInfo(self) -> list:
        """Define the tool parameters."""
        project_id = \
            arcpy.Parameter(
                name="project ID",
                displayName="project ID",
                datatype="GPString",
                parameterType="Required",  # Required|Optional|Derived
                direction="Input",  # Input|Output
        )
        project_id.filter.type = "ValueList"
        project_id.filter.list = self.project_ids
        return [project_id]
    
    def execute(self, parameters, messages) -> None:
        """Execute the tool."""
        project_id = parameters[0].value
        arcpy.AddMessage(f"project_id: {project_id}")
        return

 

 

If you have more checks you need to do and actually need the updateMessages to return a more specific message to the user, then @BlakeTerhune 's or @AlfredBaldenweck's answers are the way to  go, but avoiding those functions is totally valid if you can enforce the filter in the parameter definition.

Addendum: for some reason, the .altered value of a parameter is not reliable. I've never had luck getting it to work. The best way is to skip checking for altered status and just stick with checking the value. I think this is because Arcpy re-initializes the Parameter objects frequently and sets altered to False by default. You can emulate this by storing the state of the parameters outside the tool object and checking against this 'cache' if you have expensive validation methods.

View solution in original post

6 Replies
BlakeTerhune
MVP Regular Contributor

This is what I do and it seems to work. Maybe you're just missing clearMessage()

 

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."""
    project_id = parameters[0].valueAsText
    valid_project_ids = ['123', '456', '789']
    error_msg = f'Unrecognized project ID; please enter one of the following: {valid_project_ids}'
    if project_id:
        if project_id not in valid_project_ids:
            project_id.setErrorMessage(error_msg)
        else:
            project_id.clearMessage()
    else:
        project_id.setErrorMessage(error_msg)
    return

 

0 Kudos
AlfredBaldenweck
MVP Regular Contributor

TLDR:

  1. Use updateMessages()
  2. Change your parameter in getParameterInfo() from Required to Optional if you want to set an error message by default.
    1. You can either just set the error message OR
    2. You can also set the Optional parameter to be conditionally required if you're worried about it running without that parameter.

 

 

Longer message with how I got to that conclusion:

 

Spoiler

So, I think your biggest problem in this specific case is that you're using updateParameters() for messaging instead of updateMessages().

Spoiler
project_ids = ['123', '456', '789']
    def updateMessages(self, parameters):
        """Modify the messages created by internal validation for each tool
        parameter. This method is called after internal validation."""
        if parameters[0].value:
            if parameters[0].valueAsText not in project_ids:
                parameters[0].setErrorMessage('Unrecognized project ID')
            else:
                parameters[0].setWarningMessage(parameters[0].valueAsText)
        # I can't get a default message to show up
        #else: 
        #    parameters[0].setErrorMessage("No Project Specified")
        return

The other issue, and one I couldn't initially solve to my satisfaction, is that you're trying to set a default message to show before anything happens.

I played around a bit, and it seemed to only be a problem if you use setErrorMessage(); setWarningMessage() works just fine as a default. However, setWarningMessage() will override the required bit of the parameter, allowing you to run the tool without giving it a value.

Spoiler
project_ids = ['123', '456', '789']
    def updateMessages(self, parameters):
        """Modify the messages created by internal validation for each tool
        parameter. This method is called after internal validation."""
        if parameters[0].value:
            if parameters[0].valueAsText not in project_ids:
                parameters[0].setErrorMessage('Unrecognized project ID')
            # else:
                # parameters[0].setWarningMessage(parameters[0].valueAsText)
        else: 
            parameters[0].setWarningMessage("No Project Specified")
        return

So, I got to thinking and experimenting, and I think the true issue is actually not in updateParameters() or updateMessages(), but in getParameterInfo().

If you set your parameter to "Optional", rather than "Required", the issue of setting an error message disappears. 

Spoiler
project_ids = ['123', '456', '789']
    def getParameterInfo(self) -> list:
        """Define the tool parameters."""
        project_id = arcpy.Parameter(name="projectID",
                                     displayName="project ID",
                                     datatype="GPString",
                                     parameterType="Optional",  
                                     direction="Input",  
                                     )
                                     
        params = [project_id]
        return params    
    def updateMessages(self, parameters):
        """Modify the messages created by internal validation for each tool
        parameter. This method is called after internal validation."""
        if parameters[0].value:
            if parameters[0].valueAsText not in project_ids:
                parameters[0].setErrorMessage('Unrecognized project ID')
            # else:
                # parameters[0].setWarningMessage(parameters[0].valueAsText)
        else: 
            # Both of these worked for me. 
            # The former is to conditionally set an optional parameter 
            # to "required", the latter just sets an error message. 
            # One or both is fine.
            ##parameters[0].setIDMessage('ERROR', 735)
            parameters[0].setErrorMessage("No Project Specified")
        return

 

 

0 Kudos
HaydenWelch
Occasional Contributor II

Why are you using updateMessages and updateParameters for this? You can specify a value list in the parameter definition:

 

 

import arcpy

class Tool(object):
    def __init__(self):
        """Define the tool (tool name is the name of the class)."""
        self.label = 'test tool'
        self.description = "test"
        self.project_ids = ['123', '456', '789']

    def getParameterInfo(self) -> list:
        """Define the tool parameters."""
        project_id = \
            arcpy.Parameter(
                name="project ID",
                displayName="project ID",
                datatype="GPString",
                parameterType="Required",  # Required|Optional|Derived
                direction="Input",  # Input|Output
        )
        project_id.filter.type = "ValueList"
        project_id.filter.list = self.project_ids
        return [project_id]
    
    def execute(self, parameters, messages) -> None:
        """Execute the tool."""
        project_id = parameters[0].value
        arcpy.AddMessage(f"project_id: {project_id}")
        return

 

 

If you have more checks you need to do and actually need the updateMessages to return a more specific message to the user, then @BlakeTerhune 's or @AlfredBaldenweck's answers are the way to  go, but avoiding those functions is totally valid if you can enforce the filter in the parameter definition.

Addendum: for some reason, the .altered value of a parameter is not reliable. I've never had luck getting it to work. The best way is to skip checking for altered status and just stick with checking the value. I think this is because Arcpy re-initializes the Parameter objects frequently and sets altered to False by default. You can emulate this by storing the state of the parameters outside the tool object and checking against this 'cache' if you have expensive validation methods.

Glasnoct
New Contributor III

I'm just trying to adapt what I've found in the few examples of tools/toolboxes I've found that I can wrap my head around. Understanding it now would mean in the future when I need to use those functions, I'll be able to employ them. Thank you for the solutions; messages are showing up as I'd like them to. @BlakeTerhune  @AlfredBaldenweck

However now I'm receiving a TypeError concerning the parameter:


project_id = parameters[0].value
                 ~~~~~~~~~~^^^
TypeError: 'geoprocessing messages object' object is not subscriptable

GetParameterInfo is returning [project_id] so I'm not sure why I can't subscript it unless single parameters are being...delisted, somehow? 'Parameters' without an index doesn't work either. 

0 Kudos
Glasnoct
New Contributor III

for future reference the error was caused by the existence of a "@staticmethod" decorator on the execute function.

HaydenWelch
Occasional Contributor II

I didn't even notice that...

If you want an explanation as to why, the static method decorator doesn't expect an implicit 'self' argument, so the function call passed the parameters to the 'self' argument, the messages to the 'parameters' argument and None to the 'messages' argument

def test_function(**args): ...

class Tool:
    def __init__(self): ...

    @staticmethod
    def static_execute(self, parameters, messages=None):
        """ Broken execute """
        project_id = parameters[0].value # This raises a TypeError
        project_id = self[0].value # This works
        test_function(project_id)
        return

    def execute(self, parameters, messages):
        """ Working execute """
        project_id = parameters[0].value # This works
0 Kudos