Lessons Learned and Ideas for Python Toolbox Coding

Note: A colleague asked for a ready to run PyScripter template implementing these suggestions. This template can now be found on GitHub at: https://github.com/knu2xs/python-toolbox-template. Instructions for using PyScripter to create ArcGIS Python Toolboxes can be found in a previous blog posting.

Recently I dove into and started working with a new feature introduced with ArcGIS 10.1, Python Toolboxes. I really like how everything for a toolbox is contained in a single python script. After a week of working on a tool off and on, I came up with a few modifications to the default template making life a little easier. These are not being presented as best practices. They are nothing more than ideas you can examine and implement if you like them.

Using the template provided in the help, I ran across a few shortcomings easily overcome with a little code organization. First, defining all the functions requires a lot of repetitive code. Second, it can be cumbersome to try and test the code logic if it is all part of the tool in the execute method. Third, if the parameters are defined in the function as suggested by the template, it kind of defeats the purpose of creating a tool class. It makes it extremely difficult to extend the class and create another tool.

Following along with this discussion may be easier if you first take a look at the help documentation discussing exactly what an ArcGIS Python Toolbox is, and also grab the ArcGIS Python Toolbox template since this is the starting point for the discussion. Also, if you are planning on using PyScripter for working with these toolboxes, it may be beneficial to set up PyScripter according to the instructions provided in a previous blog posting.

Global Function for Parameter Creation

Input parameters for the tool are defined in the getParameterInfo method of the template. Parameters are instantiated as arcpy objects. They must first be created with a few necessary properties. Once created, default values can be set. After all the parameters are set, they are assembled in a Python list and returned from the method back to ArcGIS. This is what the suggested method looks like for a script I recently created.

def getParameterInfo(self):

    param0 = arcpy.Parameter(
        displayName='Database Name',
        name='dbName',
        datatype='GPString',
        parameterType='Required',
        direction='Input")

    param1 = arcpy.Parameter(
        displayName = 'Instance',
        name = 'instance',
        datatype = 'GPString',
        parameterType = 'Required',
        direction = 'Input')
    param1.value = 'myComputer'

    param2 = arcpy.Parameter(
        displayName = 'Database Management System',
        name = 'dbms',
        datatype = 'GPString',
        parameterType = 'Required',
        direction = 'Input')
    param2.value = 'PostgreSQL'

    param3 = arcpy.Parameter(
        displayName = 'Superuser Name', 
        name = 'suName',
        datatype = 'GPString',
        parameterType = 'Required'
        direction = 'Input')
    param3.value = 'owner'

    param4 = arcpy.Parameter(
        displayName = 'Superuser Password',
        name = 'suPswd',
        datatype = 'GPEncryptedString',
        parameterType = 'Required',
        direction = 'Input')
    param4.value = 'Monk3ys'

    param5 = arcpy.Parameter(
        displayName = 'SDE User Password', 
        name = 'sdePswd', 
        datatype = 'GPEncryptedString',
        parameterType = 'Required',
        direction = 'Input')
    param5.value = 'Ch1mps'

    param6 = arcpy.Parameter(
        displayName = 'Data Owner Name',
        name = 'ownerName',
        datatype = 'GPString',
        parameterType = 'Required',
        direction = 'Input')
    param6.value = 'dataOwner'

    param7 = arcpy.Parameter(
        displayName = 'Data Owner Password',
        name = 'ownerPswd',
        datatype = 'GPEncryptedString',
        parameterType = 'Required',
        direction = 'Input')
    param7.value = 'B0nobos'

    param8 = arcpy.Parameter(
        displayName = 'Authorization File',
        name = 'authFile',
        datatype = 'DEFile',
        parameterType = 'Required',
        direction = 'Input')
    param.value = \
        r'C:\Program Files\ESRI\License10.1\sysgen\keycodes'

    # parameter list
    params = [parm0, param1, param2, param3, param4, param5,
        param6, param7, param8]

    # return parameter list to ArcGIS
    return params

If the above looks painful...trust me, it is. Notice how much repetition there is. A global method can be created above the toolbox declaration streamlining the parameter creation process and optional default value assignment.

def parameter(displayName, name, datatype, defaultValue=None,  
    parameterType=None, direction=None):
    '''
    The parameter implementation makes it a little difficult to 
    quickly create parameters with defaults. This method
    prepopulates some of these values to make life easier while
    also allowing setting a default vallue.
    '''
    # create parameter with a few default properties
    param = arcpy.Parameter(
        displayName = displayName,
        name = name,
        datatype = datatype,
        parameterType = 'Required',
        direction = 'Input')

    # set new parameter to a default value
    param.value = defaultValue

    # return complete parameter object
    return param

The parameterType, direction and defaultValue parameters are optional so they can be set if needed or desired. Using this function, the same parameter declaration becomes much more concise.

def getParameterInfo(self):

    # create parameters
    param0 = parameter('Database Name', 'dbName', 'GPString')
    param1 = parameter('Instance', 'instance', 'GPString',
        'myComputer')
    param2 = parameter('Database Management System', 'dbms',
        'GPString', 'PostgreSQL')
    param3 = parameter('Superuser Name', 'suName','GPString')
    param4 = parameter('Superuser Password', 'suPswd', 
        'GPEncryptedString', 'Monk3ys')
    param5 = parameter('SDE User Password', 'sdePswd',
        'GPEncryptedString', 'Ch1mps')
    param6 = parameter('Data Owner Name', 'ownerName', 'GPString',
        'dataOwner')
    param7 = parameter('Data Owner Password', 'ownerPswd', 
        'GPEncryptedString', 'B0nobos')
    param8 = parameter('Authorization File', 'authFile', 'DEFile',
        r'C:\Program Files\ESRI\License10.1\sysgen\keycodes')

    # parameter list
    params = [parm0, param1, param2, param3, param4, param5,
        param6, param7, param8]

    # return parameter list to ArcGIS
    return params

The parameter function takes care of the first step, consolidating parameter creation, but there still are a couple of other things to clean up the script.

Separate the Business Logic

The template provided by the help documentation suggests to put all the business logic, the actual part of your script doing the work, in the execute method of the tool. This creates problems for two reasons. First, it makes tracking down errors during debugging difficult since it is hard to separate whether it is a problem with the business logic or something with the tool properties. Second, thorough trial and error I discovered the execute method does not support subclassing. This means you cannot create another tool very similar to the first and call the execute method from the superclass tool.

To avoid these problems, I created a class at the top of the script with all the business logic and tested all this business logic ahead of time. Then, once ready to build the toolbox, there is no doubt about the business logic. It works. Rather than show a huge code sample, you can see this implemented in the SDE Workspace Creation Toolbox. At the top of the script, all the business logic is contained in the sdeWorkspace class created toward the top.

Parameters as Tool Class Attributes

In ArcGIS Python toolboxes, tools are created as classes. However, if strictly following the template provided in the help documentation, establishing tool parameters is done in a function of the tool class. Since tool parameters are created as a list in a method and not as object attributes in the init method, the tool parameters are not accessible as object attributes. Each individual tool class cannot be extended to create new tools. It kind of defeats the purpose of object oriented programming.

This can be overcome by creating a new list as an attribute of the tool object in the init  method. In this way when subclassing the first tool class, parameters can be substituted by replacing items in the list using Python list methods. Borrowing from the larger example referenced above, the SDE Workspace Creation Toolbox demonstrates this.

def parameter(displayName, name, datatype, defaultValue=None,  
    parameterType=None, direction=None):
    '''
    The parameter implementation makes it a little difficult to
    quickly create parameters with defaults. This method
    prepopulates some of these values to make life easier while
    also allowing setting a default vallue.
    '''
    # create parameter with a few default properties
    param = arcpy.Parameter(
        displayName = displayName,
        name = name,
        datatype = datatype,
        parameterType = 'Required',
        direction = 'Input')

    # set new parameter to a default value
    param.value = defaultValue

    # return complete parameter object
    return param

class Toolbox(object):  
    def __init__(self):
        # ArcGIS required properties
        self.label='SDE Tools'
        self.alias='sdeTools'

        # List of tool classes associated with this toolbox
        self.tools=[CreateSdeTool]

class CreateSdeTool(object):

    def __init__(self):

        # ArcGIS tool properties
        self.label = 'Create SDE Workspace'
        self.canRunInBackground = False

        # list of parameters for tool
        self.parameters=[
            parameter('Database Name', 'dbName', 'GPString'),
            parameter('Instance', 'instance', 'GPString',
                globalInstance),
            parameter('Database Management System', 'dbms',
                'GPString', globalDbms),
            parameter('Superuser Name', 'suName','GPString'),
            parameter('Superuser Password', 'suPswd',
                'GPEncryptedString', globalSuPswd),
            parameter('SDE User Password', 'sdePswd',
                'GPEncryptedString', globalSdePswd),
            parameter('Data Owner Name', 'ownerName', 'GPString',
                globalOwnerName),
            parameter('Data Owner Password', 'ownerPswd',
                'GPEncryptedString', globalOwnerPswd),
            parameter('Authorization File', 'authFile', 'DEFile',
                globalAuthFile)]

    def getParameterInfo(self):
        # send parameters to ArcGIS
        return self.parameters

Now when another tool needs to be created extending the first tool's functionality, items in the parameter list can be replaced by directly referencing the item in the list. The business logic is captured in the class created outside of the tool and only referenced in the execute method. Hence, another tool, one importing an xml workspace file into a new SDE geodatabase, takes very little coding to create since it borrows most of the functionality from the original tool.

class FileToSdeTool(CreateSdeTool):

    def __init__(self):

        # Call superclass constructor
        CreateSdeTool.__init__(self)

        # Replace ArcGIS tool properties
        self.label = 'File to SDE Workspace'
        self.canRunInBackground = False

        # Replace first tool parameter to become file geodatabase
        self.parameters[0] = parameter('File Geodatabase',
            'fileGdb', 'DEWorkspace')

        self.parameters[0].filter.list = ["Local Database"]

    def execute(self, parameters, messages):

        # create instance of sdeWorkspace object
        sde = sdeWorkspace()

        # set attributes
        sde.instance = parameters[1].valueAsText
        sde.dbms = parameters[2].valueAsText
        sde.suName = parameters[3].valueAsText
        sde.suPswd = parameters[4].value
        sde.sdePswd = parameters[5].value
        sde.ownerName = parameters[6].valueAsText
        sde.ownerPswd = parameters[7].value
        sde.authFile = parameters[8].valueAsText

        # call fileToSde method
        sde.fileToSde(parameters[0].valueAsText)

        return

Most of the lines, assigning the parameter values to object attributes, are something I could further condense if I further cleaned up my business logic.

It took a while to discover some of the interesting behaviors of the way Python toobloxes are implemented with ArcGIS. Using a function for parameter creation, separating the business logic into another class and making the parameters part of the tool attributes greatly streamlined the process of working with these tools. Now Python Toolboxes likely are going to become the standard way I create geoprocessing toolboxes from now on.