Writing messages in script tools
In your scripts, you need to import ArcPy, as follows:
When the script is run as a script tool, ArcPy is fully aware of the application it is called from, such as ArcMap or ArcCatalog. It is, in fact, using the same objects the application is using rather than creating new ones. All environment settings made in the application, such as overwriteOutput and scratchWorkspace, are available. One major effect of this is that you can write messages with ArcPy and your messages automatically appear on the progress dialog box, the tool's result, and the Python window. It also means that any model or script tool that calls your script tool has access to the messages you write.
Contrast this to a stand-alone script. In a stand-alone script, your script isn't being called from an ArcGIS application, so there is no progress dialog box, result, or Python window where your messages can be viewed. If you call another (stand-alone) script from within your script, the called script (if it uses geoprocessing tools) is completely separate, distinct from the one you created, and there is no sharing of messages between them. One of the main advantages of script tools is the sharing of messages and environments.
The four ArcPy functions for writing messages are as follows:
- AddMessage("message")—For general informative messages (severity = 0).
- AddWarning("message")—For warning (severity = 1).
- AddError("message")—For errors (severity = 2).
- AddIDMessage(MessageType, MessageID, AddArgument1, AddArgument2)—Used for both errors and warnings (the MessageType argument determines severity). A call to AddIDMessage() will display a short message and the message ID, which is a link to an explanation about the cause and solutions to the problem. When you add an error message (using either AddError() or AddIDMessage(), the following occur:
- Your script continues execution. It is up to you to add appropriate error-handling logic and stop execution of your script. For example, you may need to delete intermediate files or cursors.
- Upon return from your script, the calling script or model receives a system error, and execution stops.
Example of adding messages
The next example copies a list of feature classes from one workspace to another. An automatic conversion takes place if the workspaces are of a different type, such as a geodatabase to a folder. Error handling is used to catch any problems and return messages; otherwise, informative messages of success are returned during execution. This example makes use of the from <module> import directive. In this case, the module is ScriptUtils, and its code is also found below. This code also uses try/except blocks.
# ConvertFeatures.py # Converts feature classes by copying them from one workspace to another # # Import the ScriptUtils utility module which, in turn, imports the # standard library modules and imports arcpy # from ScriptUtils import * # Get the list of feature classes to be copied to the output workspace # in_feature_classes = arcpy.GetParameterAsText(0) # Establish an array of input feature classes using the ScriptUtils routine. # in_feature_classes = SplitMulti(in_feature_classes) # Get the output workspace # out_folder = arcpy.GetParameterAsText(1) # Loop through the array copying each feature class to the output workspace # for in_feature_class in in_feature_classes: try: # Create the output name # feature_class_name = arcpy.ValidateTableName(in_feature_class, out_folder) # Add an output message # arcpy.AddMessage("Converting: " + in_feature_class + " To " +\ feature_class_name + ".shp") # Copy the feature class to the output workspace using the ScriptUtils routine # CopyFeatures(in_feature_class, out_folder + os.sep + \ feature_class_name) # If successful, add another message # arcpy.AddMessage("Successfully converted: " + in_feature_class + \ " To " + out_folder) except StandardError, ErrDesc: arcpy.AddWarning("Failed to convert: " + in_feature_class) arcpy.AddWarning(ErrDesc) except: arcpy.AddWarning("Failed to convert: " + in_feature_class) if not arcpy.GetMessages(2) == "": arcpy.AddError(arcpy.GetMessages(2))
Here is the ScriptUtils module used above.
# ScriptUtils.py # Import required modules # import arcpy import sys import string import os def SplitMulti(multi_input): try: # Split the multivalue on the semi-colon delimiter # multi_as_list = string.split(multi_input, ";") return multi_as_list except: ErrDesc = "Error: Failed in parsing the inputs." raise StandardError, ErrDesc def CopyFeatures(in_table, out_table): try: ErrDesc = "CopyFeatures failed" # Copy each feature class to the output workspace # arcpy.CopyFeatures_management(in_table, out_table) except: if arcpy.GetMessages(2) != "": ErrDesc = arcpy.GetMessages(2) raise StandardError, ErrDesc
Notes about the above scripts
Functions, modules, and objects can be imported from other Python scripts to simplify your script and centralize code. The import statement is used to pull everything from another script, or just the entities you want.
Exceptions are events that may modify the flow of control through a program. They may be triggered or intercepted within a script using the try and raise statements. StandardError is a built-in exception on which most exception types are based in Python. It is called if a tool error occurs with the ErrDesc variable set by the tool's error message.
Strings, defined in the string module, are a built-in type used to store and represent text. A number of operations are supported for manipulating strings, such as concatenation, slicing, and indexing.
The operating system (os) module provides a generic interface to the operating system's basic set of tools.
Returning all messages from a tool
There are times when you may want to return all messages from a tool you've called, regardless of message severity. Using an index parameter, the AddReturnMessage function returns a message from ArcPy's message array. The example below shows how to return all a tool's messages:
arcpy.Clip_analysis("roads","urban_area","urban_roads") # Return the resulting messages as script tool output messages # x = 0 while x < arcpy.MessageCount: arcpy.AddReturnMessage(x) x = x + 1
Messaging in a dual-purpose script
You can design your script for dual purposes: It can be used both as a stand-alone script (from the operating system) or as a script tool. There are three considerations:
- When the script is run as stand-alone, there is no way to view messages (there's no application, such as ArcMap, where the messages can be viewed). Instead, you can use a print statement so that messages can be viewed in the command window (also known as standard output).
- When the script is run as a tool, messages appear on the progress dialog box, the Results window, and the Python window (if the tool is run in the Python window). You don't need to use a print statement. If you rely solely on print statements, the Show command window when executing script check box found on the Source tab of the tool's properties must be checked. Showing the command window during execution of a script tool is not good practice—it looks ugly and frequently disappears when you don't want it to.
- When running as a stand-alone script, you want to raise an exception so that the calling program can catch it. When running as a script tool, you use the AddError() function to raise the exception.
Standard practice is to write an error-reporting routine that writes messages to both standard output (using print) and using ArcPy (using AddMessage, AddWarning, and AddError functions). Here is such a routine, taken from the Multiple Ring Buffer tool, a script tool in the Analysis toolbox. You can view the Multiple Ring Buffer script by locating the tool in Analysis Toolbox, right-clicking, then clicking Edit.
def AddMsgAndPrint(msg, severity=0): # Adds a Message (in case this is run as a tool) # and also prints the message to the screen (standard output) # print msg # Split the message on \n first, so that if it's multiple lines, # a GPMessage will be added for each line try: for string in msg.split('\n'): # Add appropriate geoprocessing message # if severity == 0: arcpy.AddMessage(string) elif severity == 1: arcpy.AddWarning(string) elif severity == 2: arcpy.AddError(string) except: pass
Controlling the progress dialog box
Since script tools share the application, you have control of the progress dialog box. You can control the appearance of the progress dialog box by choosing either the default progressor or the step progressor, as illustrated below.
There are four functions you use to control the progress dialog box and its progressor.
Sets the type of progressor (default or step); its label; and the minimum, maximum, and interval for step progressors
Resets the progressor
Moves the step progressor by an increment
Changes the label of the progressor
The code below demonstrates full use of the default and step progressor. Copy this code into your Python editor, save it, then create a script tool for it. The script tool has two input Long parameters as described in the code comments. Then run the script tool, providing different values for the parameters (start with n = 10 and p = 1, then try n = 101 and p = 3).
# Demonstration script showing examples of using the progressor # Parameters: # n - number to count to (a good first choice is 10) # p - interval to count by (a good first choice is 1) # The various time.sleep() calls are just to slow the dialog down # so you can view messages and progressor labels. # import arcpy import time n = int(arcpy.GetParameterAsText(0)) p = int(arcpy.GetParameterAsText(1)) readTime = 2.5 # Pause to read what's written on dialog loopTime = 0.3 # Loop iteration delay arcpy.AddMessage("Running demo with: " + str(n) + " by " + str(p)) # Start by showing the default progress dialog, where the # progress bar goes back and forth. Note how the progress label # mimics working through some "phases", or chunks of work that # a script may perform. # arcpy.SetProgressor("default", "This is the default progressor") time.sleep(readTime) for i in range(3): arcpy.SetProgressorLabel("Working on \"phase\" " + str(i + 1)) arcpy.AddMessage("Messages for phase " + str(i+1)) time.sleep(readTime) arcpy.AddMessage("-------------------------") # Setup the progressor with its initial label, min, max, and interval # arcpy.SetProgressor("step", "Step progressor: Counting from 0 to " + str(n), 0, n, p) time.sleep(readTime) # Loop issuing a new label when the increment is divisible by the # value of countBy (p). The "%" is python's modulus operator - we # only update the position every p'th iteration # for i in range(n): if (i % p) == 0: arcpy.SetProgressorLabel("Iteration: " + str(i)) arcpy.SetProgressorPosition(i) time.sleep(loopTime) # Update the remainder that may be left over due to modulus operation # arcpy.SetProgressorLabel("Iteration: " + str(i+1)) arcpy.SetProgressorPosition(i+1) arcpy.AddMessage("Done counting up") arcpy.AddMessage("-------------------------") time.sleep(readTime) # Just for fun, make the progressor go backwards. # arcpy.SetProgressor("default", "Default progressor: Now we'll do a countdown") time.sleep(readTime) arcpy.AddMessage("Here comes the countdown...") arcpy.SetProgressor("step", "Step progressor: Counting backwards from " + str(n), 0, n, p) time.sleep(readTime) arcpy.AddMessage("Counting down now...") for i in range(n, 0, -1): if (i % p) == 0: arcpy.SetProgressorLabel("Iteration: " + str(i)) arcpy.SetProgressorPosition(i) time.sleep(loopTime) # Update for remainder # arcpy.SetProgressorLabel("Iteration: " + str(i-1)) arcpy.SetProgressorPosition(i-1) time.sleep(readTime) arcpy.AddMessage("-------------------------") arcpy.AddMessage("All done") arcpy.ResetProgressor()
Choosing a good increment when maximum is potentially large
It's not at all uncommon to write scripts that will iterate an unknown number of times. For example, your script may use a SearchCursor to iterate over all rows in a table, and you don't know the number of rows beforehand—your script may be used with tables of any size, from a few thousand rows to millions of rows. Incrementing a step progressor for every row in a large table is a performance bottleneck, and you may want to guard against such performance bottlenecks.
To demonstrate and assess performance issues with step progressors, copy the code below into your Python editor, save it, then create a script tool for it. The tool has two inputs: a table parameter and a field parameter. Run the script tool with a variety of table sizes, but be sure to try a table or feature class containing 10,000 or more rows to see performance differences. (You can also try running the tool in- and out-of-process to see the performance boost running in-process.)
The script executes three separate loops, and each loop fetches all the rows in the table. The loops differ in how they update the step progressor. The first and second loops update the step progressor in large increments, and the last loop increments the step progressor once for each row. When you run the tool, you see that this last loop takes longer to run.
You may want to employ the techniques found in this code for your script tools.
# Demonstrates a step progressor by looping through records # on a table. Use a table with 10,000 or so rows - smaller tables # just whiz by. # 1 = table name # 2 = field on the table import arcpy try: inTable = arcpy.GetParameterAsText(0) inField = arcpy.GetParameterAsText(1) # Determine n, number of records on the table # arcpy.AddMessage("Getting row count") n = arcpy.GetCount_management(inTable) if n == 0: raise "no records" arcpy.AddMessage("Number of rows = " + str(n)) arcpy.AddMessage("") arcpy.AddMessage("---------------------------------") # Method 1: Calculate and use a suitable base 10 increment # =================================== import math p = int(math.log10(n)) if not p: p = 1 increment = int(math.pow(10, p-1)) arcpy.SetProgressor("step", "Incrementing by " + str(increment) + " on " + \ inTable, 0, n, increment) rows = arcpy.SearchCursor(inTable) i = 0 beginTime = time.clock() for row in rows: if (i % increment) == 0: arcpy.SetProgressorPosition(i) fieldValue = row.getValue(inField) i = i + 1 arcpy.SetProgressorPosition(i) arcpy.AddMessage("Method 1") arcpy.AddMessage("Increment = " + str(increment)) arcpy.AddMessage("Elapsed time: " + str(time.clock() - beginTime)) arcpy.AddMessage("---------------------------------") del rows del row # Method 2: let's just move in 10 percent increments # =================================== increment = int(n/10.0) arcpy.SetProgressor("step", "Incrementing by " + str(increment) + " on " + inTable, \ 0, n, increment) rows = arcpy.SearchCursor(inTable) i = 0 beginTime = time.clock() for row in rows: if (i % increment) == 0: arcpy.SetProgressorPosition(i) fieldValue = row.getValue(inField) i = i + 1 arcpy.SetProgressorPosition(i) arcpy.AddMessage("Method 2") arcpy.AddMessage("Increment = " + str(increment)) arcpy.AddMessage("Elapsed time: " + str(time.clock() - beginTime)) arcpy.AddMessage("---------------------------------") del rows del row # Method 3: use increment of 1 # =================================== increment = 1 arcpy.SetProgressor("step", "Incrementing by 1 on " + inTable, 0, n, increment) rows = arcpy.SearchCursor(inTable) beginTime = time.clock() while row: arcpy.SetProgressorPosition() fieldValue = row.getValue(inField) arcpy.SetProgressorPosition(n) arcpy.ResetProgressor() arcpy.AddMessage("Method 3") arcpy.AddMessage("Increment = " + str(increment)) arcpy.AddMessage("Elasped time: " + str(time.clock() - beginTime)) arcpy.AddMessage("---------------------------------") arcpy.AddMessage("") arcpy.AddMessage("Pausing for a moment to allow viewing...") time.sleep(2.0) # Allow viewing of the finished progressor del rows del row except "no records": arcpy.AddWarning(inTable + " has no records to count") except: if rows: del rows if row: del row arcpy.AddError("Exception occurred")