Python – Forcing Python Toolbox to Break Loop and Cleanup on Cancel in ArcPy

arccatalogarcpyeventspython-toolbox

I am writing a Python tool that perform heavy tasks in a loop. When the user clicks "Close" I want to break the loop, perform some cleanup, and then finish.

Consider this example tool that counts down from ten:

import arcpy
import time

class Tool(object):

   def __init__(self):
      self.label = "Cancel Test"
      self.description = "Tests how the cancel function works."
      self.canRunInBackground = False

   def execute(self, parameters, messages):
      for i in range(10, -1, -1):
         arcpy.AddMessage(i)
         time.sleep(1)
      arcpy.AddMessage("We have lift-off!")
      return

When I click "Close" in the beginning of the execution it does not brake the loop or give me an opportunity to perform any clean up. Instead it loops to the very end and adds the message "We have lift-off!" before it outputs this:

Completed script CancelTest...
Cancelled function
(CancelTest) aborted by User.
Failed at [TIME] (Elapsed Time: 10,01 seconds)

I have found documentation for ArcGIS Pro (that uses Python 3.4) that describes how you can do this there using isCancelled:

import arcpy
import time

#Make sure it does not auto cancel.
arcpy.env.autoCancelling = False

class Tool(object):

   def __init__(self):
      self.label = "Cancel Test"
      self.description = "Tests how the cancel function works."
      self.canRunInBackground = False

   def execute(self, parameters, messages):
      for i in range(10, -1, -1):
         arcpy.AddMessage(i)
         time.sleep(1)
         #Check if the user clicked "Close"
         if arcpy.env.isCancelled:
            arcpy.AddMessage("Launch aborted!")
            return
      arcpy.AddMessage("We have lift-off!")
      return

However, when I run this from ArcCatalog 10.3 (i.e. not using ArcGIS Pro) it gives me the following error:

Traceback (most recent call last):
  File "H:\Mina Dokument\DGD\Python\CancelTest.py", line 19, in execute
    if arcpy.env.isCancelled:
AttributeError: 'GPEnvironment' object has no attribute 'isCancelled'

Is there anyway to mimic the behavior that is available in ArcGIS Pro in an ordinary Python toolbox using Python 2.7?

Best Answer

Farid Cher's answer is partially correct.

You can use the Progressor to 'catch' a cancel event from within a Python script, and you can also do whatever cleanup necessary as well, if you enclose the call within a try ... except clause.

For example, a python toolbox:

import arcpy
import time

class Toolbox(object):
    def __init__(self):
        self.label = "Toolbox"
        self.alias = ""
        self.tools = [Tool]


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

    def execute(self, parameters, messages):
        some_stuff = [] # a variable to cleanup later
        max = 1000
        arcpy.SetProgressor('step','Testing ArcPy script cancel',0,max,1)

        # You don't have to be in a for loop, but this is the
        # easiest way to show an example
        for i in range(max):
            try: 
                # only put SetProgressorPosition in this clause
                # otherwise you can catch other errors that aren't
                # the user cancelling
                arcpy.SetProgressorPosition()
            except:
                arcpy.AddWarning('User canceled at i = {0}'.format(i))
                del some_stuff # cleanup
                return
            time.sleep(0.1)
            some_stuff.append(i)
        arcpy.AddMessage('User did not cancel')
        del some_stuff

This allows the user to run the tool: before cancel

And then cancel, which is handled gracefully (and still counted as a failed run): after cancel

edit: This doesn't appear to work with 10.3 or ArcGIS Pro.

To catch the cancels (tested in ArcGIS Pro), we bring back our good friend arcgisscripting. This is similar to catching the cancel from the progressor, but the entire execute function, or at least the majority of it, needs to be inside the try:

import arcpy
import arcgisscripting
import time

class Toolbox(object):
    def __init__(self):
        self.label = "Toolbox"
        self.alias = ""
        self.tools = [Tool]


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

    def execute(self, parameters, messages):
        try:
            some_stuff = []
            max = 1000
            arcpy.SetProgressor('step','Testing ArcPy script cancel',0,max,1)

            for i in range(max):
                arcpy.SetProgressorPosition()
                time.sleep(0.1)
                some_stuff.append(i)
        except arcgisscripting.ExecuteAbort:
            arcpy.AddWarning('User cancelled at i={0}'.format(i))
            del some_stuff
            return
        arcpy.AddMessage('User did not cancel')
        del some_stuff

before cancel in pro

after cancel in pro

Related Question