[GIS] Problems using da.UpdateCursor in python Add_in to Edit Feature Class

arcgis-desktoparcpypythonpython-addin

I am currently attempting to create a Python Add_in tool which will perform a set of geoprocessing operations resulting in new values for a specific !shape.area!acres calculation on a new layer and then update rows in an existing feature class with these new values. I am using da.UpdateCursor as my edit tool to update the rows in the feature class from a dictionary of the new values. Basically, my script runs through all of the geoprocessing operations just fine and then either stops or skips over the da.UpdateCursor w/out throwing any errors. The last MessageBox trap that fires is right before the edit.startEditing, after that the code stops in the mxd w/ no errors.

I am open to any suggestions as to modifying the process as I am still fairly new to python programming and many of you might find my methods a little clunky. Also, this a trial tool for intended for later use on a versioned Feature Class in SDE once I get the process and methods understood.

I am using ArcGIS 10.2.1 for Desktop, with Windows 7.

# Import arcpy module
import arcpy, traceback
import pythonaddins
import os, sys
from arcpy import env
arcpy.env.overwriteOutput = True
arcpy.env.addOutputsToMap = False

workspace = r'C:\TEST\GEODATABASE.mdb'
arcpy.env.workspace = workspace
edit = arcpy.da.Editor(arcpy.env.workspace)

class Net_Stands(object):
    """Implementation for Net_Stands_addin.button (Button)"""
    def __init__(self):
        self.enabled = True
        self.checked = False
    def onClick(self):
        try:
            self.mxd = arcpy.mapping.MapDocument('current')
            df = self.mxd.activeDataFrame

            layers = arcpy.mapping.ListLayers(self.mxd)
            Stands = arcpy.mapping.ListLayers(self.mxd, "StandsMaintenance", df)[0]
            CompanyRoads = arcpy.mapping.ListLayers(self.mxd, "roads", df)[0]
            layers = arcpy.mapping.ListLayers(self.mxd)

            #creates Stands layer as temp Stands_lyr
            arcpy.MakeFeatureLayer_management(Stands,"Stands_lyr")

            #defines layer as temp Stands_lyr
            Stands_temp = arcpy.mapping.Layer("Stands_lyr")

            #creates a NetStands_Temp to cut the roads_buffer out of
            arcpy.MakeFeatureLayer_management(Stands_temp, "NetStands_Temp")

            #defines layer as temp NetStands_Temp
            NetStands_Temp = arcpy.mapping.Layer("NetStands_Temp")

            #From the roads feature class in the mxd, it makes a temp TRACT_BOUNDARY in the mxd.
            arcpy.MakeFeatureLayer_management(CompanyRoads,"CompanyRoads_lyr")

            #defines layer as temp TRACTS_BOUNDARY
            CompanyRoads_temp = arcpy.mapping.Layer("CompanyRoads_lyr")

            #Calculates the buffer distance based on value in Row_Feet into meters
            arcpy.CalculateField_management(CompanyRoads_temp,"ROW_METERS_HALF","!ROW_FEET! * 0.1524","PYTHON_9.3","#")

            #creates the buffer layer based on CompanyRoads_temp
            mybuffer = arcpy.Buffer_analysis(CompanyRoads_temp,"#","ROW_METERS_HALF","FULL","ROUND","ALL","#")

            #Erases the area that from mybuffer that overlap with Stands_temp
            Erase_FC = arcpy.Erase_analysis(Stands_temp, mybuffer.getOutput(0))

            #Recalculates the polygon shape since the roads buffer has been erased
            arcpy.CalculateField_management(Erase_FC.getOutput(0),"NET_ACRES","!shape.area@acres!","PYTHON_9.3","#")

            #Marker, this is the last line of code that works.
            saysomething = pythonaddins.MessageBox("Completed Net_Acres Processing", "Clicked", 0)
            print saysomething

            #Start Editing
            edit.startEditing(True,False)
            edit.startOperation()

            try:
                joinfields = ['STID', 'NET_ACRES']
                joindict = {}
                with arcpy.da.SearchCursor(out_fc, joinfields) as rows:
                    for row in rows:
                        #joindict.append(row[0]),joindict.append(row[1])
                        joinval = row[0]
                        val1 = row[1]

                        joindict[joinval]= [val1]
                        print pythonaddins.MessageBox((row[0]) + "  " + str(row[1]) + "  joindict", "Joindict", 1)
                del row, rows

                with arcpy.da.UpdateCursor(Stands, joinfields) as recs:
                    for rec in recs:
                        keyval = rec[0]

                        if joindict.has_key(keyval):
                            print pythonaddins.MessageBox((rec[1]) + "  rec" + str(rec[1]) + "  rec"+ str(joindict[keyval]) + " joindict", "In the Update", 1)

                            rec[1] = joindict[keyval]

                            #print rec
                        recs.updateRow(rec)
                del rec, recs

            except Exception, e:
                # If an error occurred, print line number and error message
                tb = sys.exc_info()[2]
                print pythonaddins.MessageBox("Line %i" % tb.tb_lineno)
                print pythonaddins.MessageBox(e.message)
                #print e.message
                exceptions = pythonaddins.MessageBox(arcpy.GetMessages())
                print exceptions

            edit.stopOperation()
            edit.stopEditing(True)
            print pythonaddins.MessageBox("complete")

            arcpy.RefreshActiveView()
            arcpy.RefreshTOC()

            arcpy.Delete_management(out_fc)
            arcpy.Delete_management(Erase_FC)
            arcpy.Delete_management(mybuffer_temp)
            code_errors = pythonaddins.MessageBox(arcpy.GetMessages(), "Code Errors", 1)
            print code_errors
        except arcpy.ExecuteError:
            lasterrors = pythonaddins.MessageBox(arcpy.GetMessages(), "WTF_Except Errors",1)
            print lasterrors

Best Answer

After many trials and tribulations, I have got a working code with many bells and whistles that I feel obliged to share since they are a culmination of many 'stolen' snippets obtained from this site. I have included comments in much of the script to describe what is going on, so hopefully that is helpful. Basically, I was able to get both a arcpy.UpdateCursor and a arcpy.da.UpdateCursor to work. One of the trickiest aspects was for the code to be able to edit a workspace that was dynamic, in that if they were direct editing a Versioned SDE or if they had 'Checked Out' their data from SDE and working locally. The code had to account for this, but not all of my wishes were granted. The code will kick back a message if the user is NOT in edit mode already ONLY when in SDE. If they are using a local GDB, then the code still runs, but the edits are essentially not written...The new numbers appear in the attribute table, however, if you 'Reload Cache', they revert back and the edits aren't saved regardless. If already the user has already initiated an edit session, the edits aren't saved until the user "Saves Edits". Furthermore, if the SDE connection is not the 'hard coded' SDE_versioned Connection I'm assuming, a DialogMessage will appear for them to choose the Database Connection which must match the intended lyr. If not, it will loop until they choose the correct one. Also, this is all done "in_memory" and is lightening fast when run on a local GDB. So here goes:

class NoFeatures(Exception):
    pass


import arcpy, traceback
import pythonaddins
import os, sys
import arcpy.toolbox
from arcpy import env
arcpy.env.overwriteOutput = True
#If you want to see the "in_memory/Layers being created, change below to "True"
#...then you must "#" comment out the Delete_Management("in_memory") at the bottom of code for them to persist in the TOC.
arcpy.env.addOutputsToMap = False

class Net_Stands(object):
    """Implementation for Net_Stands_addin.button (Button)"""
    def __init__(self):
        self.enabled = True
        self.checked = True

    def onClick(self):

        try:
            self.mxd = arcpy.mapping.MapDocument('current')
            df = self.mxd.activeDataFrame


            Stands = arcpy.mapping.ListLayers(self.mxd, "ForestStands", df)[0]

            layers = arcpy.mapping.ListLayers(self.mxd)

            #This will determine what the data connection is, either to a versioned SDE or Checkout GBD.
            fullPath = os.path.dirname(arcpy.Describe(Stands).catalogPath)

            try:

                # Create a Describe object for an SDE database
                try:
                    #If newDir is a directory, then it is a GDB, if it is not a GDB, then the code will go to the 'else' where is connects with the SDE. This will not enable editing on .mdb
                    if os.path.isdir(str(fullPath.rsplit('\\',1)[0])):

                        workspace = os.path.dirname(fullPath)
                        arcpy.env.workspace = workspace

                        if arcpy.Exists(workspace):
                            arcpy.Compact_management(workspace)

                        else:
                            pass

                        #pythonaddins.MessageBox(arcpy.GetMessages(2) + "I see you have Checked-Out Data", "Messages",1)
                    elif (str(fullPath.rsplit('\\',1)[1])) == "Hard-Coded Versioned SDE.sde":

                            sdeconnect = "Database Connections\Whats in the ArcCatalog Connection.sde"
                            workspace = sdeconnect
                            arcpy.env.workspace = workspace
                    else:
                        loop = True
                        while loop:
                            paramWorkspace = pythonaddins.OpenDialog('Choose Database Connection',False, r'','Open')
                            paramdesc = arcpy.Describe(paramWorkspace)
                            if not (paramWorkspace == (str(fullPath.rsplit('\\',1)[0]))):
                                pythonaddins.MessageBox("Please Choose Database Connections for ForestStands", "Database Connections", 1)
                                loop = True
                                continue
                            else:
                                pythonaddins.MessageBox("You Have Chosen Wisely", "Database Connections", 1)
                                workspace = paramWorkspace
                                arcpy.env.workspace = workspace
                                loop = False
                                break
                except:
                    pass

            except:
                pythonaddins.MessageBox(arcpy.GetMessages(2) + "  Failed to connect to " + fullPath +  "  Please Create a new Database Connection", "Connection Failure",1)
                sys.exit


            #Now that the connection has been made:
            try:

                fields = ("OBJECTID")
                #This da.UpdateCursor will cause an SDE Editor to fail if not already editing, thus initiating the "You are not in Editor" msg.
                #...you must be in Editor to allow for multiple da.UpdateCursor on a layer.
                newCur = arcpy.da.UpdateCursor(Stands, fields)
                with newCur as cursor:
                    for row in cursor:
                        break
                try:
                    if arcpy.Exists(os.path.join(fullPath, "CompanyRoads")):
                        #Retrieves CompanyRoads from Workspace of ForestStands if in a GDB
                        fc = (os.path.join(fullPath,"CompanyRoads"))
                        memoryRoads = "in_memory" + "\\" + "CompanyRoads"
                        arcpy.CopyFeatures_management(fc, memoryRoads)
                        memoryRoads_lyr = arcpy.mapping.Layer(memoryRoads)
                        arcpy.RefreshActiveView
                        arcpy.RefreshTOC

                    elif arcpy.Exists("SDE_FeatureDataset.GISLOADER.CompanyRoads_Maintenance"):
                        #Retrieves CompanyRoads_Maintenance from Workspace of ForestStands if in a SDE
                        fc = ("SDE_FeatureDataset.GISLOADER.CompanyRoads_Maintenance")
                        memoryRoads = "in_memory" + "\\" + "CompanyRoads"
                        arcpy.CopyFeatures_management(fc, memoryRoads)
                        memoryRoads_lyr = arcpy.mapping.Layer(memoryRoads)
                        arcpy.RefreshActiveView
                        arcpy.RefreshTOC
                        #pythonaddins.MessageBox("memoryRoads", "Memory Roads",1)
                    elif arcpy.Exists(paramdesc):
                        #Retrieves CompanyRoads_Maintenance from Workspace choosen by user
                        fc = (paramdesc + ".CompanyRoads")
                        memoryRoads = "in_memory" + "\\" + "CompanyRoads"
                        arcpy.CopyFeatures_management(fc, memoryRoads)
                        memoryRoads_lyr = arcpy.mapping.Layer(memoryRoads)
                        arcpy.RefreshActiveView
                        arcpy.RefreshTOC

                    else:
                        #No Features Found
                        raise NoFeatures()
                except NoFeatures:
                    pythonaddins.MessageBox(arcpy.GetMessages(2) + "  No CompanyRoads Found", "CompanyRoads lyr Missing",1)

                #This try/except on Editor will initiate an edit session.
                #...startEditing/startOperation must be present regardless if already in an edit session for da.UpdateCursor to work in SDE
                try:
                    edit = arcpy.da.Editor(arcpy.env.workspace)
                    # Start an edit session. Must provide the worksapce.
                    # Edit session is started without an undo/redo stack for versioned data
                    #  (for second argument, use False for unversioned data)
                    edit.startEditing(True, False)

                    # Start an edit operation
                    edit.startOperation()
                except:
                    pythonaddins.MessageBox("Not in Edit Session" + arcpy.GetMessages(2), "Edit Errors", 1)
                    pythonaddins.MessageBox(traceback.format_exc(), "Why Edits Failed", 1)
                else:
                    try:
                        #Rest of script...
                        #
                        #Creation of an identical lyr of ForestStands for geoprocessing
                        memoryStands = "in_memory" + "\\" + "Stands_lyr"
                        arcpy.CopyFeatures_management(Stands, memoryStands)
                        #
                        #This creates a buffer of roads of 50 meters and selects only roads that the buffer intersects with any stand.
                        #...this drastically increases processing time since only a select set of raods are used for the calculation.
                        arcpy.SelectLayerByLocation_management(memoryRoads_lyr, 'intersect', memoryStands, "50", "NEW_SELECTION")

                        memoryNetStands_Temp = "in_memory" + "\\" + "NetStands_Temp"
                        arcpy.CopyFeatures_management(Stands, memoryNetStands_Temp)

                        memoryRoads_temp = "in_memory" + "\\" + "roads_lyr"
                        arcpy.CopyFeatures_management(memoryRoads_lyr, memoryRoads_temp)

                        arcpy.AddField_management(memoryRoads_temp, "ROW_METERS_FLOAT", "FLOAT")
                        CursorFieldNames = ["ROW_FEET", "ROW_METERS_FLOAT"]
                        cursor = arcpy.da.UpdateCursor(memoryRoads_temp,CursorFieldNames)
                        for row in cursor:
                            row[1] = row[0] * 0.1524 #Write area value to field
                            cursor.updateRow(row)
                        del row, cursor #Clean up cursor objects

                        arcpy.Buffer_analysis(memoryRoads_temp,"in_memory/my_buffer","ROW_METERS_FLOAT","FULL","ROUND","ALL","#")

                        arcpy.Erase_analysis(memoryStands, "in_memory/my_buffer", "in_memory/Erase_FC")


                        #The spatial reference # can be found in Arcmap for reference Different Projection
                        srNAD = arcpy.SpatialReference()
                        #The spatial reference # can be found in Arcmap for reference NAD 1983 UTM Zone 17
                        srUTM = arcpy.SpatialReference()

                        #This try/except splits out the NorthEast data from the rest to calculate acres in UTM.
                        try:
                            row,rows,cursor= None,None,None

                            #Notice NOT an da.UpdateCursor, just a UpdateCursor, this is bc all fields are required including SHAPE.
                            cur = arcpy.UpdateCursor("in_memory/Erase_FC", ["FIELD_AREA"],srUTM)
                            for row in cur:
                                #Use the FIELD_AREA field to meet the conditional statement.
                                if row.FIELD_AREA == "NorthEast":
                                    row.NET_ACRES = round((row.SHAPE.area * 0.000247104393),3)
                                    cur.updateRow(row)
                                else:
                                    pass


                            curNAD = arcpy.UpdateCursor("in_memory/Erase_FC", ["FIELD_AREA"], srNAD)
                            for row in curNAD:
                                #Use the FIELD_AREA field to meet the conditional statement.
                                if row.FIELD_AREA <> "NorthEast":
                                    row.NET_ACRES = round((row.SHAPE.area * 0.000247104393),3)
                                    curNAD.updateRow(row)

                        except:
                            pythonaddins.MessageBox(traceback.format_exc(), "Try Errors",1)
                            pythonaddins.MessageBox(arcpy.GetMessages(2), "MSG",1)

                        #If ever in doubt, the line below also works for the try/except above, without splitting for UTM/NAD
                        #arcpy.CalculateField_management("in_memory/Erase_FC","NET_ACRES","!shape.area@acres!","PYTHON_9.3","#")


                        #The CROWN JEWEL of the code...try/except on creating a 'data dictionary from the "in_memory/Erase_FC" NET_ACRES field to populate
                        #...the ForestStands NET_ACRES field.  The da.SearchCursor grabs the newly derived values from the "in_memory/Erase_FC".
                        #...Using the da.SearchCursor is effecient since it only grabs the fields you require, without grabing all the other fields that tax performance
                        try:
                            row= None
                            # Create Dictionary
                            with arcpy.da.SearchCursor("in_memory/Erase_FC",["StID", "NET_ACRES"]) as srows:
                                path_dict = dict([srow[0], srow[1]] for srow in srows)
                               #pythonaddins.MessageBox(str(path_dict), "Path_dictionary",1)

                            area = 'NorthEast'
                            #queryfield = 'AREA'
                            whereclauseUTM = "FIELD_AREA = '%s'" % area
                            #query = "[" + queryfield + "] " +  "= " + "\'" + area + "\'" # else create a new query
                            with arcpy.da.UpdateCursor(Stands,["StID", "NET_ACRES", "GROSS_ACRES","SHAPE@AREA", " FIELD_AREA"], whereclauseUTM,srUTM) as urows:
                                for row in urows:
                                    row[2] = round((row[3] * 0.000247104393),3)
                                    if row[0] in path_dict:
                                        row[1] = path_dict[row[0]]
                                        #urows.updateRow(row)
                                    else:
                                        row[1] = 0
                                    urows.updateRow(row)

                            whereclauseNAD = " SD_AREA <> '%s'" % area
                            with arcpy.da.UpdateCursor(Stands,["StID", "NET_ACRES", "GROSS_ACRES","SHAPE@AREA", " SD_AREA"], whereclauseNAD,srNAD) as urows:
                                for row in urows:
                                    row[2] = round((row[3] * 0.000247104393),3)
                                    if row[0] in path_dict:
                                        row[1] = path_dict[row[0]]

                                    else:
                                        row[1] = 0
                                    urows.updateRow(row)

                        except:
                            edit.undoOperation()
                            pythonaddins.MessageBox(arcpy.GetMessages(2), "Undo Operation", 1)
                            pythonaddins.MessageBox(traceback.format_exc(), "Traceback on Excecute Error",1)

                        else:
                            # Stop the edit session and DONT save the changes in code. Requires USER to save changes.
                            edit.stopOperation()

                            #edit.stopEditing(True)
                            arcpy.RefreshActiveView()
                            arcpy.RefreshTOC()

                    except:
                        #If these are triggered, then edits aren't saved, and the faulty lines of code are identified.  No HARM, No Faul.
                        pythonaddins.MessageBox(arcpy.ExecuteError(), "Execute Error",1)
                        pythonaddins.MessageBox(traceback.format_exc(), "Traceback on Excecute Error",1)
                        pythonaddins.MessageBox(arcpy.GetMessages(2), "Error MSG",1)

            except:
                pythonaddins.MessageBox(arcpy.GetMessages(2) + "   You are not in an Edit Session", "Cancelled Operation", 0)
                pythonaddins.MessageBox(traceback.format_exc(), "Traceback",1)

        except:
            pythonaddins.MessageBox(arcpy.GetMessages(2), "Except Errors",1)
            pythonaddins.MessageBox(traceback.format_exc(), "TraceBack", 1)


        finally:
            arcpy.Delete_management("in_memory")
            if os.path.isdir(str(fullPath.rsplit('\\',1)[0])) and arcpy.Exists(workspace):
                arcpy.Compact_management(workspace)
            del workspace, fullPath
            arcpy.RefreshTOC
            arcpy.RefreshActiveView