Pro 2.9.5
Touching up a tool I wrote last year, including migrating it from a scripting tool (ATBX) to a python toolbox (PYT). Part of the workflow involves accessing CIM, using a layer from the map as an input.
The tool runs great in the ATBX.
cim_lyr= FC.getDefinition('V2')
However, something REALLY WEIRD happens in the PYT.
I get the following error message:
AttributeError: 'MappingLayerObject' object has no attribute 'getDefinition'
Hmm, let's investigate this and make sure that I'm putting in what I think I am.
In the map:
aprx = arcpy.mp.ArcGISProject("CURRENT")
mp = aprx.activeMap
lay = mp.listLayers("colton*")
lay= lay[0]
print(type(lay))
#<class 'arcpy._mp.Layer'>
print(dir(lay))
# ['URI', '__class__', '__delattr__', '__dict__', '__dir__', '__doc__',
# '__eq__', '__format__', '__from_scripting_arc_object__', '__ge__',
# '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__',
# '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__',
# '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__',
# '__subclasshook__', '__weakref__', '_arc_object', 'brightness',
# 'connectionProperties', 'contrast', 'dataSource', 'definitionQuery',
# 'disableTime', 'enableTime', 'extrusion', 'getDefinition',
# 'getSelectionSet', 'is3DLayer', 'isBasemapLayer', 'isBroken',
# 'isFeatureLayer', 'isGroupLayer', 'isNetworkAnalystLayer',
# 'isNetworkDatasetLayer', 'isRasterLayer', 'isSceneLayer', 'isTimeEnabled',
# 'isWebLayer', 'listDefinitionQueries', 'listLabelClasses', 'listLayers',
# 'listTables', 'longName', 'maxThreshold', 'metadata', 'minThreshold',
# 'name', 'saveACopy', 'setDefinition', 'setSelectionSet', 'showLabels',
# 'supports', 'symbology', 'time', 'transparency',
# 'updateConnectionProperties', 'updateDefinitionQueries',
# 'updateLayerFromJSON', 'visible']
Okay, so clearly a layer and definitely supports getDefinition.
Cool. Let's check in the ATBX script.
arcpy.AddMessage(type(FC))
#<class 'arcpy._mp.Layer'>
arcpy.AddMessage(dir(FC))
# ['URI', '__class__', '__delattr__', '__dict__', '__dir__', '__doc__',
# '__eq__', '__format__', '__from_scripting_arc_object__', '__ge__',
# '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__',
# '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__',
# '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__',
# '__subclasshook__', '__weakref__', '_arc_object', 'brightness',
# 'connectionProperties', 'contrast', 'dataSource', 'definitionQuery',
# 'disableTime', 'enableTime', 'extrusion', 'getDefinition',
# 'getSelectionSet', 'is3DLayer', 'isBasemapLayer', 'isBroken',
# 'isFeatureLayer', 'isGroupLayer', 'isNetworkAnalystLayer',
# 'isNetworkDatasetLayer', 'isRasterLayer', 'isSceneLayer', 'isTimeEnabled',
# 'isWebLayer', 'listDefinitionQueries', 'listLabelClasses', 'listLayers',
# 'listTables', 'longName', 'maxThreshold', 'metadata', 'minThreshold',
# 'name', 'saveACopy', 'setDefinition', 'setSelectionSet', 'showLabels',
# 'supports', 'symbology', 'time', 'transparency',
# 'updateConnectionProperties', 'updateDefinitionQueries',
# 'updateLayerFromJSON', 'visible']
Still supports getDefinition.
Let's check the PYT.
arcpy.AddMessage(type(FC))
<class 'MappingLayerObject'>
arcpy.AddMessage(dir(FC))
# ['GetCimJSONString', 'SetCimJSONString', 'URI', 'UpdateLayerFromJSON',
# '_XML', '__class__', '__delattr__', '__dir__', '__doc__', '__eq__',
# '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__',
# '__init__', '__init_subclass__', '__le__', '__lt__', '__ne__', '__new__',
# '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__',
# '__str__', '__subclasshook__', '_id', '_pUnk', 'brightness',
# 'connectionProperties', 'contrast', 'dataSource', 'definitionQuery',
# 'disableTime', 'enableTime', 'extrusion', 'getSelectionSet', 'is3DLayer',
# 'isBasemapLayer', 'isBroken', 'isFeatureLayer', 'isGroupLayer',
# 'isLayerSame', 'isNetworkAnalystLayer', 'isNetworkDatasetLayer',
# 'isRasterLayer', 'isSceneLayer', 'isTimeEnabled', 'isWebLayer',
# 'listDefinitionQueries', 'listLabelClasses', 'listLayers', 'listTables',
# 'longName', 'maxThreshold', 'metadata', 'minThreshold', 'name',
# 'saveACopy', 'setSelectionSet', 'showLabels', 'supports', 'symbology',
# 'time', 'transparency', 'updateConnectionProperties',
# 'updateDefinitionQueries', 'visible']
Definitely does not support getDefinition. But it has GetCimJSONString, which only appears to be documented in @SamSzotkowski 's post here. (That exact query in Google will yield the same post, as well as site that is actually AliExpress)
He writes, as I have also found, that instead of getting a layer object, we get a mappinglayer object. I can't find any documentation for this.
'''ATBX'''
inFCs = GetParameter(2)
arcpy.AddMessage(type(inFCs))
# <class 'list'>
arcpy.AddMessage(type(inFCs[0]))
# <class 'arcpy._mp.Layer'>
return
'''PYT'''
arcpy.AddMessage(type(parameters[2].value))
#<class 'geoprocessing value table object'>
arcpy.AddMessage(type(parameters[2].value.GetTrueValue(0,0)))
#<class 'MappingLayerObject'>
# I also checked with .GetTrueRow(0), etc.
So uh. What is a mappinglayer and where is anything written down about it?
What about a python toolbox causes a multivalue FeatureLayer parameter to be considered a value table, and the features within to be "MappingLayer"s instead of normal layers? And seriously, why is this not written down anywhere? Why is this not mentioned on the CIM Access Documentation?
Like, cool, I guess I can rewrite the code to use JSON instead of the CIM object. But it would have been great to have been warned about this beforehand.
Interesting...
Just a quick test:
# -*- coding: utf-8 -*-
import arcpy
import json
class Toolbox(object):
def __init__(self):
"""Define the toolbox (the name of the toolbox is the name of the
.pyt file)."""
self.label = "Toolbox"
self.alias = "toolbox"
# List of tool classes associated with this toolbox
self.tools = [Tool]
def msg(txt=""):
arcpy.AddMessage(str(txt))
class Tool(object):
def __init__(self):
self.label = "Tool"
def getParameterInfo(self):
return [
arcpy.Parameter(name="p0", displayName="Layer", datatype="GPFeatureLayer"),
arcpy.Parameter(name="p1", displayName="Layer Multi", datatype="GPFeatureLayer", multiValue=True),
]
def execute(self, parameters, messages):
# print parameter types
for p in parameters:
msg(f"parameter name: {p.displayName}")
msg(f"\tparameter value: {p.value}")
msg(f"\tvalue type: {type(p.value)}")
if p.multiValue:
msg(f"\ttype of first value: {type(p.value.GetTrueValue(0,0))}")
# get the actual arcpy.mp.Layer object
mapping_layer = parameters[0].value
layer = arcpy.mp.ArcGISProject("current").activeMap.listLayers(mapping_layer.name)[0]
# examine similarities and differences between arcpy.mp.Layer and MappingLayerObject
l_dir = set(dir(layer))
ml_dir = set(dir(mapping_layer))
msg("Available in both:")
for a in sorted(l_dir.intersection(ml_dir)):
msg(f"\t{a}")
msg("Not available in MappingLayerObject:")
for a in sorted(l_dir - ml_dir):
msg(f"\t{a}")
msg("Not available in arcpy.mp.Layer:")
for a in sorted(ml_dir - l_dir):
msg(f"\t{a}")
# get the MappingLayerObject's CIM
d = json.loads(mapping_layer.GetCimJSONString())
msg("GetCimJSONString keys:")
for k in sorted(d.keys()):
msg(f"\t{k}")
# try playing with it (doesn't work)
d["visibility"] = False
mapping_layer.SetCimJSONString(json.dumps(d))
# try manipulating the cim (this works)
cim = layer.getDefinition("V3")
cim.visibility = False
layer.setDefinition(cim)
parameter name: Layer
parameter value: <MappingLayerObject object at 0x0000020F36A80F60>
value type: <class 'MappingLayerObject'>
parameter name: Layer Multi
parameter value: TestPoints
value type: <class 'geoprocessing value table object'>
type of first value: <class 'MappingLayerObject'>
Available in both:
URI
__class__
__delattr__
__dir__
__doc__
__eq__
__format__
__ge__
__getattribute__
__gt__
__hash__
__init__
__init_subclass__
__le__
__lt__
__ne__
__new__
__reduce__
__reduce_ex__
__repr__
__setattr__
__sizeof__
__str__
__subclasshook__
brightness
connectionProperties
contrast
dataSource
definitionQuery
disableTime
enableTime
extrusion
getSelectionSet
is3DLayer
isBasemapLayer
isBroken
isFeatureLayer
isGroupLayer
isNetworkAnalystLayer
isNetworkDatasetLayer
isRasterLayer
isSceneLayer
isTimeEnabled
isWebLayer
listDefinitionQueries
listLabelClasses
listLayers
listTables
longName
maxThreshold
metadata
minThreshold
name
saveACopy
setSelectionSet
showLabels
supports
symbology
time
transparency
updateConnectionProperties
updateDefinitionQueries
visible
Not available in MappingLayerObject:
__dict__
__from_scripting_arc_object__
__module__
__weakref__
_arc_object
getDefinition
setDefinition
updateLayerFromJSON
Not available in arcpy.mp.Layer:
GetCimJSONString
SetCimJSONString
UpdateLayerFromJSON
_XML
_id
_pUnk
isLayerSame
GetCimJSONString keys:
allowDrapingOnIntegratedMesh
autoGenerateFeatureTemplates
blendingMode
description
displayCacheType
displayFiltersType
enableDisplayFilters
expanded
featureBlendingMode
featureCacheType
featureElevationExpression
featureTable
featureTemplates
htmlPopupEnabled
labelClasses
layerElevation
layerType
maxDisplayCacheAge
metadataURI
name
popupInfo
refreshRate
refreshRateUnit
renderer
scaleSymbols
selectable
serviceLayerID
showLegends
showPopups
snappable
sourceModifiedTime
symbolLayerDrawing
type
uRI
useSourceMetadata
visibility
Yeah, the multi-value complaint is extraneous; I just hate that it returns a value table instead a list like multivalue parameters do in ATBXs.
Just very confused as to why PYTs don't bring in Layer objects from parameters. (and why it's not documented!?)
In this case, I just needed to read the CIM, but I shudder at the idea of updating it this way.
Seeing as it's not documented, I'd wager it's not something that users are meant to interact with. Might be an oversight/bug.
Just get the arcpy._mp.Layer object from the map using the MappingLayerObject.name property and do your CIM manipulations like normal.
Indeed I think one of the "intended" ways for you to alter layer properties in a PYT is via the postExecute method, matching the Layer in the active map to the tool's input by name, though even in their example snippet they make the shaky assumption that all layers have unique names (best practice though it may be).
I'd hoped when I saw they added this method in 3.0 that it meant we no longer had to do black magic to alter layer properties, but c'est la vie. In truth I don't understand what the point of postExecute is, as far as I know everything you can do in there you can do in regular execute.