Refer to a specific Contents layer — even when there are layers with duplicate names

544
3
01-23-2024 10:57 AM
Status: Open
Bud
by
Notable Contributor

ArcGIS Pro 3.2.1

A colleague has built a tool that converts a selection to a definition query.

Bud_0-1706034992019.png

Resulting definition query:

t_unique_id IN (122, 123, 124, 125, 126)

 

The tool works well, but it has a limitation: If there are multiple layers in the Contents that have the same name, then the tool has no way to differentiate between the two layers.

Note that since you can only get a layer reference in ArcPy by its name, I had to limit the tool to grabbing the first layer in the Map TOC that has the name [that corresponds to the name] in the tool.  So if there are multiple layers or tables with the same name, it will operate on the first one found when looping through them. 

In the following example, there are two layers that have the same name, and the second layer is the one that has the selection to be converted to a definition query. When I run the tool, it throws an error because it refers to the first layer, which doesn't have a selection.

Bud_1-1706035557387.png

As a non-Python person, that seems like an unfortunate limitation of ArcPy, especially since the Layer or Table parameter seems to be aware of the duplicates and refers to them by unique names: "species_records" and "species_records:1". If the parameter can refer to the duplicate layers by unique names, then is there a reason why the Python script can’t differentiate between the two layers as well?


Idea:

Could ArcPy be enhanced so that it can refer to a specific Contents layer, even if there are multiple layers with duplicate names?

Feel free to let me know if I've misunderstood something.

 

Related: 

3 Comments
Bud
by

Tool Requirements:

ArcGIS Pro 3.2.1. When I make a manual selection in the map or attribute table, I want to convert the selection to a definition query.
For the purpose of manually emailing the layer as a .LYRX file. But creating a .LYRX file wouldn't be part of the tool.
 
  1. The Toolbox tool would have a pick list that lets me choose a layer from all the feature layers and standalone tables in the Contents Pane of the map.
  2. The selection from that layer would be used, a definition query would be added to that layer, and the existing selection would persist.
  3. Create a definition query from the selected records like this: ASSET_ID IN (1,2,3).
  4. Has an additional pick list that lets me choose the field I want to use as the definition query SQL expression. For example, ObjectID, ASSET_ID, or a different field with unique values. Only fields with a unique attribute index in the database would be included in the pick list.
  5. Accounts for scenarios where 1) there isn't a selection or 2) there is a selection of zero records. I.e., don't create a definition query in either of those cases.
  6. If there's an existing definition query on the chosen layer, the tool will deactivate the definition query and create a new one.


Selection2Definition.py (old):

Spoiler
import arcpy, sys, traceback

SelectionLayer = sys.argv[1]
Field = sys.argv[2]

try:

    ####   Get the current active map
    aprx = arcpy.mp.ArcGISProject("CURRENT")
    map = aprx.listMaps(aprx.activeMap.name)[0]  

    ####  Deal with group layers if needed
    if("\\" in SelectionLayer):
        arySelectionLayer = SelectionLayer.split('\\')
        SelectionLayer = arySelectionLayer[1]

    ### Get the first layer in the TOC with the name selected
    LayersTablesList = map.listLayers(SelectionLayer) + map.listTables(SelectionLayer)
    target_layer = LayersTablesList[0]

    if not target_layer.getSelectionSet():
        raise Exception("No features currently selected.")

    ####   Collect the selected field's features
    lst = []
    with arcpy.da.SearchCursor(target_layer, [Field]) as cursor:
        for row in cursor:
            lst.append(row[0])
            
    ####   Build a where clause
    where = ""
    fieldtype = ""
    fields = arcpy.ListFields(target_layer, Field)
    for field in fields:
         if field.name == Field:
              fieldtype = field.type
   
    ####   single quote if a text field
    if(fieldtype == 'String'):
        where = " IN ('" + "','".join(lst) + "')"
    else:
        where = " IN (" + ",".join([str(i) for i in lst]) + ")"
         
    arcpy.AddMessage(Field + where)

    target_layer.definitionQuery = Field + where

except:
  
    tb = sys.exc_info()[2]
    tbinfo = traceback.format_tb(tb)[0]
    pymsg = "PYTHON ERRORS:\n  Traceback Info:\n" + tbinfo + "\nError Info:\n    " + \
            str(sys.exc_type)+ ": " + str(sys.exc_value) + "\n"
    msgs = "arcpy ERRORS:\n" + arcpy.GetMessages(2) + "\n"
    arcpy.AddMessage(pymsg)

Toolvalidator.py (old):

Spoiler
class ToolValidator:
  # Class to add custom behavior and properties to the tool and tool parameters.

    def __init__(self):
        # set self.params for use in other function
        self.params = arcpy.GetParameterInfo()
        
    def initializeParameters(self):        
        param0 = arcpy.Parameter(
                displayName="Input Features",
                name="in_features",
                datatype=["GPFeatureLayer", "GPTableView"],
                parameterType="Required",
                direction="Input",
                multiValue=False)
                
        param1 = arcpy.Parameter(
                displayName="Select Field",
                name="inputField",
                datatype="Field",
                parameterType="Required",
                direction="Input")
        param1.parameterDependencies = [param0.name]
        param1.filter.list = []    
                        
        params = [param0,param1]   
        return params             

    def initializeParameters(self):
        # Customize parameter properties. 
        # This gets called when the tool is opened.
        return

    def updateParameters(self):
        # Modify parameter values and properties.
        # This gets called each time a parameter is modified, before 
        # standard validation.
        if self.params[0].altered:
            layer = self.params[0].value
            if layer:
                # Get all indexes for the layer
                indexes = arcpy.ListIndexes(layer)

                # Create a set of all field names that are part of an index
                indexed_fields = set()
                for index in indexes:
                    if index.isUnique:
                        for field in index.fields:
                            indexed_fields.add(field.name)

                # Filter fields to include only those that have an index
                fields_with_index = [f.name for f in arcpy.ListFields(layer) if f.name in indexed_fields]

                self.params[1].filter.list = fields_with_index
            else:
                self.params[1].filter.list = []
        return

    def updateMessages(self):
        

        # def isLicensed(self):
        #     # set tool isLicensed.
        # return True

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

 

SSWoodward

@Bud This tool is explicitly parsing the non-unique string name of the layer out of the input and then using it to search for the layer again using some list functionality.  

What is the roadblock that prevents the tool author from directly using the layer passed to the gp tool?

 

Bud
by

@SSWoodward provided a solution here: Contents — Unique layer names in Properties window (auto-generated)

...try using the arcpy.GetParameter methods instead of sys.argv. These are super handy, and should be the goto syntax when writing script tools. 

When the parameter is fetched in this way it is a layer reference and not a string.

Bud_0-1706813132105.png
Bud_1-1706813143023.png


Updated Python Scripts:
(these are the same scripts that are included in the .zip in the original post)


Selection2Definition.py

Spoiler
import arcpy, sys, traceback


SelectionLayer = arcpy.GetParameter(0)
Field = sys.argv[2]



try:

    ####   Get the current active map
    aprx = arcpy.mp.ArcGISProject("CURRENT")
    map = aprx.listMaps(aprx.activeMap.name)[0]  

    if not SelectionLayer.getSelectionSet():
        raise Exception("No features currently selected.")

    ####   Collect the selected field's features
    lst = []
    with arcpy.da.SearchCursor(SelectionLayer, [Field]) as cursor:
        for row in cursor:
            lst.append(row[0])
            
    ####   Build a where clause
    where = ""
    fieldtype = ""
    fields = arcpy.ListFields(SelectionLayer, Field)
    for field in fields:
         if field.name == Field:
              fieldtype = field.type
              
    
    ####   single quote if a text field
    if(fieldtype == 'String'):
        where = " IN ('" + "','".join(lst) + "')"
    else:
        where = " IN (" + ",".join([str(i) for i in lst]) + ")"
         
    definition_query = Field + where
    arcpy.AddMessage("DEF: " + definition_query)
    SelectionLayer.definitionQuery = definition_query
    arcpy.SetParameter(2, SelectionLayer)


except:
  
    tb = sys.exc_info()[2]
    tbinfo = traceback.format_tb(tb)[0]
    pymsg = "PYTHON ERRORS:\n  Traceback Info:\n" + tbinfo + "\nError Info:\n    " + \
            str(sys.exc_type)+ ": " + str(sys.exc_value) + "\n"
    msgs = "arcpy ERRORS:\n" + arcpy.GetMessages(2) + "\n"
    arcpy.AddMessage(pymsg)
 

Toolvalidator.py

Spoiler
class ToolValidator:
  # Class to add custom behavior and properties to the tool and tool parameters.

    def __init__(self):
        # set self.params for use in other function
        self.params = arcpy.GetParameterInfo()
        
    def initializeParameters(self):        
        param0 = arcpy.Parameter(
                displayName="Input Features",
                name="in_features",
                datatype=["GPFeatureLayer", "GPTableView"],
                parameterType="Required",
                direction="Input",
                multiValue=False)
                
        param1 = arcpy.Parameter(
                displayName="Select Field",
                name="inputField",
                datatype="Field",
                parameterType="Required",
                direction="Input")
        param1.parameterDependencies = [param0.name]
        param1.filter.list = []    
                        
        params = [param0,param1]   
        return params             

    def initializeParameters(self):
        # Customize parameter properties. 
        # This gets called when the tool is opened.
        return

    def updateParameters(self):
        # Modify parameter values and properties.
        # This gets called each time a parameter is modified, before 
        # standard validation.
        if self.params[0].altered:
            layer = self.params[0].value
            if layer:
                # Get all indexes for the layer
                indexes = arcpy.ListIndexes(layer)

                # Create a set of all field names that are part of an index
                indexed_fields = set()
                for index in indexes:
                    if index.isUnique:
                        for field in index.fields:
                            indexed_fields.add(field.name)

                # Filter fields to include only those that have an index
                fields_with_index = [f.name for f in arcpy.ListFields(layer) if f.name in indexed_fields]

                self.params[1].filter.list = fields_with_index
            else:
                self.params[1].filter.list = []
        return

    def updateMessages(self):
        

        # def isLicensed(self):
        #     # set tool isLicensed.
        # return True

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