The arcpy.mp module is a course-grained Python API that is designed to provide access to many common map automation tasks. It includes a diverse set of exposed classes, class properties, and helper functions, but it does not provide access to all properties, settings, and capabilities available in ArcGIS Pro. One reason is to keep the API streamlined, simple, and manageable. Another reason is that ArcGIS Pro is being developed at such a rapid pace that the APIs can't keep up. It may take a release or more before you have access through the managed APIs. Starting with ArcGIS Pro 2.4, Python developers will have fine-grained access to the Cartographic Information Model (CIM) and can access many more settings, properties, and capabilities that are persisted in a project or document. The .NET SDK development community has had CIM access since ArcGIS Pro 1.1 and now it is available to the Python development community.
Caution:
The following sections describe what you can do using the CIM, but it is equally important to understand what you shouldn't do. It is recommended that you read the Use caution when modifying the CIM section before attempting to modify an object's CIM document.
What is the CIM
The CIM is the Esri Cartographic Information Model. It is a map content specification used to document how information that describes various project components is persisted when saved, read, referenced, or opened. The specification is represented as JSON and is used for maps, scenes, layouts, layers, symbols, and styles in ArcGIS applications and APIs.
Regardless of its acronym, don't think of the CIM as limited to only cartographic settings. The capabilities exposed to these classes through the CIM extend well beyond that, and the examples below demonstrate some of those extended capabilities.
It is important to understand the JSON structure of an object to successfully modify an object's CIM definition. The structure of JSON parallels the CIM object model exposed to the arcpy.mp module. The access points are through the arcpy.mp classes Layer, Table, Map, and Layout. Each of these classes can be saved to a JSON file format: .lyrx, .mapx, and .pagx. You can open these files in an editor and review how the information is organized and persisted.
Caution:
Do not edit these files directly. Use the API.Modify a CIM definition
The basic workflow is to return an object's CIM definition using the getDefinition() method on the specific object to be modified, make the appropriate CIM API changes, and push the changes back to the same object using the setDefinition() method. When you want to return an object's CIM definition, you must specify a cim_version. Esri follows the semantic versioning specification. This means that until the next major release—for example, 3.0—when breaking API changes are allowed, the value to be used with cim_version is 'V2'. Once 3.0 is released, a new 'V3' enumeration value will become available. This allows Python script authors control over which version of the CIM is used during script execution if there is a possibility breaking changes may be introduced in the new version.
Once a CIM definition is returned, you can try to navigate its structure by viewing its code completion and its intellisense, or you can use a variety of Python statements to learn more about the available attributes. A useful technique for learning how attributes are organized and how to navigate the CIM object model for each class is to create a corresponding export file. For example a map can be saved to a .mapx file, a layer to a .lyrx file, and a layout to a .pagx file. All of these export files are in JSON format and can be viewed in an editor. Sometimes your editor may display better formatting if you change the *.***x file extension to *.json.
Example 1: Basic Boolean, root-level properties
The following is a JSON representation of a modified, abbreviated list of root level properties available to CIMLayerDocument (*.lyrx).{
"type" : "CIMLayerDocument",
"version" : "2.4.0",
"layers" : [
"CIMPATH=map/greatlakes.xml"
],
"layerDefinitions" : [
{
"type" : "CIMFeatureLayer",
"name" : "GreatLakes",
"uRI" : "CIMPATH=map/greatlakes.xml",
"useSourceMetadata" : true,
"description" : "GreatLakes",
"layerType" : "Operational",
"showLegends" : true,
"visibility" : true,
"displayCacheType" : "Permanent",
"maxDisplayCacheAge" : 5,
"showPopups" : true,
"serviceLayerID" : -1,
"autoGenerateFeatureTemplates" : true,
"featureElevationExpression" : "0",
"featureTable" : {
"htmlPopupEnabled" : true,
"selectable" : true,
"featureCacheType" : "Session",
"scaleSymbols" : true,
"snappable" : true
Basic metadata is displayed at the top of the file that describes the document and layer. There are also many root level layerDefinitions properties. The list will vary depending on the settings persisted with the object. The attributes exposed with your IDE code completion will show many more options than what you see in the JSON file above. This is because the JSON file only stores current settings, not all possible settings. One example of how the JSON file is purposely abbreviated is Boolean values. Only the properties that are currently set to true appear in the JSON file. For example, your code completion should show additional attributes such as expanded or showMapTips, but because they are currently set to false, they don't appear in the above JSON file.
The following arcpy.mp script will modify a few root-level attributes for a CIM layer object (l_cim). The selectable attribute appears in the JSON file above because its current value is true, but it will be set to False in the Python script. The showMapTips and expanded attributes don't appear in the JSON file currently because their values are false, but they will be set to True. If you save the layer to another layer file (*.lyrx), the appropriate changes appear in the JSON file.# Reference a project, map, and layer using arcpy.mp
p = arcpy.mp.ArcGISProject('current')
m = p.listMaps('Map')[0]
l = m.listLayers('GreatLakes')[0]
# Return the layer's CIM definition
l_cim = l.getDefinition('V2')
# Modify a few boolean properties
l_cim.showMapTips = True #Turn on map tips for bubble tips to appear
l_cim.selectable = False #Set the layer to not be selectable
l_cim.expanded = True #Expand the Layer in the Contents pane
# Push the changes back to the layer object
l.setDefinition(l_cim)
# Save changes
p.save()
Example 2: Modify field properties
The JSON and Python examples above are relatively straightforward because only root-level attributes were modified. The CIM object model can be nested based on the complexity of the definition of each object and its nested set of capabilities. One object can have zero to many members that are set to another object, and those objects can have dependencies to other objects as well. Each dependent object is indented within the JSON structure. Everything that's indented the same is available to that CIM object. Viewing the JSON structure in a JSON editor allows you to expand and collapse that object hierarchy. Another advantage of working with the JSON file is that you can use search for attributes and determine how to access the nested structure using the CIM API.
This example drills into the object model one level lower than root. If the featureTable member in the JSON example above is expanded, you will see its additional members. The CIMFeatureTable class has a member called fieldDescriptions, which takes a list of CIMFieldDescription classes. Depending on the field type, that class may have a numberFormat member that takes a CIMNumericFormat class as a value. Notice how nicely each level of the dependent classes are organized using indentation. "featureTable" : {
"type" : "CIMFeatureTable",
"displayField" : "NAME",
"editable" : true,
"fieldDescriptions" : [
{
"type" : "CIMFieldDescription",
"alias" : "OBJECTID",
"fieldName" : "OBJECTID",
"numberFormat" : {
"type" : "CIMNumericFormat",
"alignmentOption" : "esriAlignRight",
"alignmentWidth" : 0,
"roundingOption" : "esriRoundNumberOfDecimals",
"roundingValue" : 0
},
"readOnly" : true,
"visible" : true,
"searchMode" : "Exact"
},
A common request is for the ability to modify a table's field alias or visibility, especially when creating a table on the fly using MakeFeatureLayer, for example. This cannot be done using the managed API. This script uses the CIM to access a feature layer's featureTable object and its fieldDescriptions object from which the alias and visible attributes can be set. The Python syntax follows the CIM object model where each dot exposes the next object's set of properties based on the indentation of the JSON structure.# Reference a project, map, and layer using arcpy.mp
p = arcpy.mp.ArcGISProject('current')
m = p.listMaps('Map')[0]
lyr = m.listLayers('GreatLakes')[0]
# Get the layer's CIM definition
cim_lyr = lyr.getDefinition('V2')
# Make changes to field properties
for fd in cim_lyr.featureTable.fieldDescriptions:
if fd.fieldName == "OBJECTID":
fd.visible = False #Do not display this field
if fd.fieldName == "Shape_Area":
fd.alias = "Area (hectares)" #Change field alias
# Push the changes back to the layer object
lyr.setDefinition(cim_lyr)
Example 3: Modify layer symbology
This example shows how nested the CIM object model can become and also shows the advantage of CIM-level access. The managed arcpy.mp API has limited access to renderers and depth of properties. It can only modify simple properties and only for the default layer of a symbol. However, the CIM can access a symbol with multiple layers. The following screen shot shows settings in the application that are not available to the arcpy.mp API:
This is an edited and simplified JSON file that only displays the renderer information for a layer's symbology. The renderer has a symbol of type CIMSimpleRenderer and that has a symbol of type CIMPolygonSymbol. The polygon symbol has two symbolLayers: CIMSolidStroke and CIMSolidFill, each of which displays the properties that are available in the Symbology pane. "layerDefinitions" : [
{
"renderer" : {
"type" : "CIMSimpleRenderer",
"patch" : "Default",
"symbol" : {
"type" : "CIMSymbolReference",
"symbol" : {
"type" : "CIMPolygonSymbol",
"symbolLayers" : [
{
"type" : "CIMSolidStroke",
"effects" : [
{
"type" : "CIMGeometricEffectDashes",
"dashTemplate" : [ 5, 5],
"lineDashEnding" : "NoConstraint",
"controlPointEnding" : "NoConstraint"
}
],
"enable" : true,
"capStyle" : "Round",
"joinStyle" : "Round",
"lineStyle3D" : "Strip",
"miterLimit" : 10,
"width" : 3,
"color" : {
"type" : "CIMRGBColor",
"values" : [0, 0, 0, 100]
}
},
{
"type" : "CIMSolidFill",
"enable" : true,
"color" : {
"type" : "CIMRGBColor",
"values" : [ 255, 127, 0, 100 ]
}
}
this Python code uses CIM access to modify the layer's symbology. Both symbol layers are modified.# Reference a project, map, and layer using arcpy.mp
p = arcpy.mp.ArcGISProject('current')
m = p.listMaps('Trail Routes')[0]
lyr = m.listLayers('Loops')[0]
# Return the layer's CIM definition
cim_lyr = lyr.getDefinition('V2')
# Modify the color, width and dash template for the SolidStroke layer
symLvl1 = cim_lyr.renderer.symbol.symbol.symbolLayers[0]
symLvl1.color.values = [250, 250, 40, 50]
symLvl1.width = 8
ef1 = symLvl1.effects[0] #Note, deeper indentation
ef1.dashTemplate = [20, 30] #Only works if there is an existing dash template
# Modify the color/transparency for the SolidFill layer
symLvl2 = cim_lyr.renderer.symbol.symbol.symbolLayers[1]
symLvl2.color.values = [140, 70, 20, 20]
# Push the changes back to the layer object
lyr.setDefinition(cim_lyr)
Use caution when modifying the CIM
The CIM exposes many useful capabilities, but things can go wrong if you are not careful. The application and managed APIs are designed to block you from making changes that can place the application in a compromised state. The CIM exposes everything, so it's possible to make conflicting changes that may not be possible in the application. It is important that you test scripts that modify the CIM. Ensure that the application doesn't respond oddly after making such changes. Limit yourself to changing settings that don't have dependencies on other settings.
Example 1: You can't change the spatial reference
You may think that changing a maps spatial reference is straight forward since the JSON description includes a tag called spatialReference.
"mapDefinition" : {
"type" : "CIMMap",
"name" : "Map",
"uRI" : "CIMPATH=map/map.xml",
"metadataURI" : "CIMPATH=Metadata/a7afc904584d1037910b2cfe65fe94f8.xml",
"useSourceMetadata" : true,
"illumination" : {
"layers" : [
"standaloneTables" : [
"defaultViewingMode" : "Map",
"mapType" : "Map",
"datumTransforms" : [
"defaultExtent" : {
"elevationSurfaces" : [
"spatialReference" : {
"wkid" : 4326,
"latestWkid" : 4326
},
If you attempt to modify only the map's wkid and latestWkid properties, you won't get the expected results. This is because there are many other parts of the application that are associated with the spatial reference, and many changes would need to be made to the map's CIM to get the change to work properly. For example, a change to the spatial reference also affects datum transformations, a number of extents, clip geometries, and so on. This type of operation should be made in the application or with a managed API, where all the appropriate changes will be made.
Example 2: Don't get the application into an odd state
In this example, the expression engine associated with a layer's labeling properties is modified in the CIM. In the screen shot below, the expression engine is changed from Arcade, the default, to Python. Below are graphics of what it looks like in the UI and how the JSON portion appears.
"labelClasses" : [
{
"type" : "CIMLabelClass",
"expression" : "$feature.NAME",
"expressionEngine" : "Arcade",
"featuresToLabel" : "AllVisibleFeatures",
"maplexLabelPlacementProperties" : {
"name" : "Class 1",
"priority" : -1,
"standardLabelPlacementProperties" : {
"textSymbol" : {
"useCodedValue" : true,
"visibility" : true,
"iD" : -1
}
The following is a Python script that changes only the expresionEngine and not the expression. This causes the application to behave erratically. For example, after running the code below, when you review the label properties for the layer, the expresionEngine is properly set to Python but the expression still appears in Arcade format. In the user interface, when you change the expression engine back to Arcade, the expression displays in Python format, the opposite of what it should be. To avoid this, it is important that you update both the expresionEngine and expression properties.
# Update the label expression engine from Arcade, the default, to Python.
# You must also update the expression otherwise the UI won't behave correctly after.
p = arcpy.mp.ArcGISProject('current')
m = p.listMaps('Map')[0]
l = m.listLayers()[0]
l_cim = l.getDefinition()
lc = l_cim.labelClasses[0]
lc.expressionEngine = 'Python' #From 'Arcade'
lc.expression = '[STATE_NAME]' #From '$feature.STATE_NAME'
l.setDefinition(l_cim)
Tips
Sometimes the attribute tag in the JSON file isn't easy to find because the tag name isn't intuitive, or it can be nested in the object model. One way to approach this is to set a value to something truly unique, and search on that value in the JSON file. For example, you set a width of a layout element to 0.7777 or an RGB color to 111, 111, 111.
You may see in the JSON file an enumeration value you want to change, but you don't know what the new, proper value should be. A solution is to set the correct value in the application, and save the object to a JSON export file and evaluate the updated value.
The ArcGIS.Core.CIM Namespace .NET SDK API Reference help topic provides a list of CIM objects and documentation for each class member.
Creating CIM objects
In versions earlier than 2.5, the arpy.mp module could only create, or more specifically clone, Graphic and Text elements using the managed API. The Python CIM access API only allowed you to change existing properties. Starting at version 2.5, there is a new technique for creating CIM classes that can be used to expand the capabilities exposed in an object's CIM definition. The name of the function is called CreateCIMObjectFromClassName and is located in the arcpy.cim module. The cim_class_name parameter is the name of the CIM class the way it appears in the JSON file, and the cim_version parameter is used the same way as with the GetDefiniton function defined above. CreateCIMObjectFromClassName will return an object that contains the appropriate members but it will not automatically create additional object dependencies. For example, if the class created has a member that requires another class as its value, you will need to execute the function again to also generate that class. That process must be carried out for all dependent classes and subclasses and can require a fair amount of effort. This will be addressed in later examples but let's start with a straightforward scenario in which only one object needs to be created to accomplish a solution.
Example 1: Single Class - RGB Color
If you insert a new, default map and return the Map's CIM backgroundColor, the value will be a NoneType because the value is not defined in the CIM. By default, a new map's background color is set to "No color". You won't even see that member defined in the JSON file. A straightforward way to determine what needs to be done is to author the map with the background color and save the before and after changes as map files (.mapx) and compare the differences. The graphic below represents a screen shot of the before JSON changes on the left and the after changes on the right. Notice, on the right, that backgroundColor member has been inserted between the mapType and bookmarks members. The CIM object is represented in the JSON in double quotes; it starts with the letters CIM and follows the keyword "type". To set a map's backgroundColor, you need to create a color object; in this example, a CIMRGBColor object. The color could have been defined using other color models and the CIM object type would be different.
The following code will create a CIMRGBColor class that is used to set the background color for a map. Note, you may need to turn off your basemap or other layers to see the changes.p = arcpy.mp.ArcGISProject('current')
m = p.listMaps()[0]
m_cim = m.getDefinition('V2') #Get the map's CIM definition
#Check to see if a background color exists, if not create a color
if m_cim.backgroundColor is None:
RGBColor = arcpy.cim.CreateCIMObjectFromClassName('CIMRGBColor', 'V2')
RGBColor.values = [115, 178, 255, 100]
m_cim.backgroundColor = RGBColor #Set the property to the new object
m.setDefinition(m_cim) #Set the map's CIM definition
Example 2: Single Class - Spatial Map Series
Creating a spatial map series using the CIM is almost as straightforward as creating an RGB color. A spatial map series is also a single object but with more properties. The graphic below represents a screen shot of how a spatial map series is persisted in a JSON file. The indexLayerURI is a concept that is unique to the CIM that is not exposed as a setting in the user interface. The URI is a unique identifier for an object persisted in the CIM to assure reference uniqueness. In the case of the layer, the URI won't change even if you change the layer name. The only way to get the indexLayerURI value is through the layer's CIM definition.
The following code will create a CIMSpatialMapSeries class that is used to author a new spatial map series for a layout. The example includes the extra lines of code to get the layer's URI value. Note, the get/setDefinition functions need to be called a second time to ensure the Map Series Pages tab refreshes in the Contents pane.p = arcpy.mp.ArcGISProject('current')
m = p.listMaps('GreatLakes')[0]
l = m.listLayers('GreatLakes')[0]
l_cim = l.getDefinition('V2') #Get layer's CIM / Layer URI
lURI = l_cim.uRI #Needed to specific the index layer
lyt = p.listLayouts('GreatLakes')[0]
lyt_cim = lyt.getDefinition('V2') #Get Layout's CIM definition
#Create CIM Spatial Map Series Object and populate its properties
ms = arcpy.cim.CreateCIMObjectFromClassName('CIMSpatialMapSeries', 'V2')
ms.enabled = True
ms.mapFrameName = "Great Lakes MF"
ms.startingPageNumber = 1
ms.currentPageID = 2
ms.indexLayerURI = lURI #Index layer URI from Layer's CIM
ms.nameField = "NAME"
ms.sortField = "NAME"
ms.sortAscending = True
ms.scaleRounding = 1000
ms.extentOptions = "BestFit"
ms.marginType = "Percent"
ms.margin = 10
lyt_cim.mapSeries = ms #Set new map series to layout
lyt.setDefinition(lyt_cim) #Set the Layout's CIM definition
#Force a refresh of the layout and its associated panes
lyt_cim = lyt.getDefinition('V2')
lyt.setDefinition(lyt_cim)
Example 3: One Dependent Class - Bookmark Map Series
The previous two examples showed CIM classes with simple properties. As stated earlier in this topic, there will be times when a newly created CIM object has property values that are dependent on other CIM objects. The CreateCIMObjectFromClassName function does not automatically create all dependent subclasses, and it is up to you to use the same method to create those dependent objects as well. The CIMBookmarkMapSeries class is a great example. It has a property called pages, which is a collection of individual CIMBookmarkMapSeriesPage objects. The following is a screen shot of how a Bookmark Map Series is persisted in a JSON file. You should notice that the CIMBookmarkMapSeriesPage class is nested inside of the CIMBookmarkMapSeries class. Each page is defined by its bookmarkName and each bookmark has a mapURI that is a unique identifier for the map with which the bookmark is associated.
The following code will create a CIMBookmarkMapSeries class and multiple CIMBookmarkMapSeriesPage classes that are used to author a new Bookmark Map Series for a layout. A bookmark map series requires a reference to a map's URI and similar to a layer's URI, you can only get that value from the map's CIM definition. p = arcpy.mp.ArcGISProject('current')
m = p.listMaps('Map')[0]
m_cim = m.getDefinition('V2') #Get map URI from map CIM
mURI = m_cim.uRI
lyt = p.listLayouts('No Map Series')[0] #Get Layout CIM
lyt_cim = lyt.getDefinition('V2')
#Create the CIMBookmarkMapSeriesPage(s) necessary for creating the CIMBookmarkMapSeries
#Iterate through each bookmark in the order you want them added to the map series
bkmkNames = ["Northeast", "Southeast", "Northwest",]
pageList = []
for bkmk in bkmkNames:
bkmkPage = arcpy.cim.CreateCIMObjectFromClassName('CIMBookmarkMapSeriesPage', 'V2')
bkmkPage.bookmarkName = bkmk
bkmkPage.mapURI = mURI #Obtained from map's CIM above
pageList.append(bkmkPage) #Append each page to the list
#Create a Bookmark Object and populate
bmMS = arcpy.cim.CreateCIMObjectFromClassName('CIMBookmarkMapSeries', 'V2')
bmMS.enabled = True
bmMS.mapFrameName = "Map Frame"
bmMS.startingPageNumber = 1
bmMS.currentPageID = 0
bmMS.extentOptions = "BestFit"
bmMS.marginType = "Percent"
bmMS.pages = pageList #Set the child CIMBookmarkMapSeriesPage(s)
#Set map series to layout and set the layout CIM definition
lyt_cim.mapSeries = bmMS
lyt.setDefinition(lyt_cim)
#Force a refresh of the layout and its associated panes
lyt_cim = lyt.getDefinition('V2')
lyt.setDefinition(lyt_cim)
Example 4: Many Dependent Classes - Create a Polygon Element on a Layout
Depending on the object you are trying to create, the number of nested, dependent objects can get very complex. When possible, it is always recommended that you use the managed API before trying to create objects using the CIM. The next code example is a perfect and typical scenario. You will see that creating something as simple as a polygon element on a layout has many object dependencies. Again, the most straightforward approach is to create a simple layout with a single polygon element and export the layout to a .pagx file and review the JSON structure. Another useful approach to tackling these more complex scenarios is to build the objects in reverse order. Start with the most indented, dependent subclasses and work your way to the final resulting object. The sample code below uses that pattern.
The following code will ultimately create a polygon CIMGraphicElement on a layout. It uses the CreateCIMObjectFromClassName multiple times for all the dependent CIM classes needed to construct a polygon graphic element.# A simplified JSON structure outlining the objects needed to be generated for
# creating a polygon graphic element on a layout.
#
# CIMGraphicElement
# CIMPolygonGraphic
# CIMSymbolReference
# CIMPolygonSymbol
# CIMSolidStroke
# CIMRGBColor
# CIMSolidFill
# CIMRGBColor
#Reference layout and its CIM definition
p = arcpy.mp.ArcGISProject('current')
lyt = p.listLayouts()[0]
lyt_cim = lyt.getDefinition('V2')
#CIMSolidStoke/CIMRGBColor
strokeRGBColor = arcpy.cim.CreateCIMObjectFromClassName('CIMRGBColor', 'V2')
strokeRGBColor.values = [0, 0, 0, 100]
symLyr1 = arcpy.cim.CreateCIMObjectFromClassName('CIMSolidStroke', 'V2')
symLyr1.capStyle = "Round"
symLyr1.joinStyle = "Round"
symLyr1.width = 1
symLyr1.color = strokeRGBColor
#CIMSolidFill/CIMRGBColor
fillRGBColor = arcpy.cim.CreateCIMObjectFromClassName('CIMRGBColor', 'V2')
fillRGBColor.values = [130, 130, 130, 100]
symLyr2 = arcpy.cim.CreateCIMObjectFromClassName('CIMSolidFill', 'V2')
symLyr2.color = fillRGBColor
#CIMPolygonSymbol
polySym = arcpy.cim.CreateCIMObjectFromClassName('CIMPolygonSymbol', 'V2')
polySym.symbolLayers = [symLyr1, symLyr2]
#CIMSymbolReference
symRef = arcpy.cim.CreateCIMObjectFromClassName('CIMSymbolReference', 'V2')
symRef.symbol = polySym
#CIMPolygonGraphic
polyGraphic = arcpy.cim.CreateCIMObjectFromClassName('CIMPolygonGraphic', 'V2')
polyGraphic.symbol = symRef
polyGraphic.blendingMode = "Alpha"
polyGraphic.placement = "Unspecified"
polyGraphic.polygon = {"hasZ": True, "rings": [[[5, 6, None], [1, 6, None], [1, 9, None], [5, 9, None], [5, 6, None]]]}
#CIMGraphicElement
graphicElm = arcpy.cim.CreateCIMObjectFromClassName('CIMGraphicElement', 'V2')
graphicElm.anchor = "BottomLeftCorner"
graphicElm.name = "New Rectangle Graphic"
graphicElm.visible = True
graphicElm.rotationCenter = {"x" : 1, "y": 6}
graphicElm.graphic = polyGraphic
#Add element to EMPTY layout element list and set CIM definition
lyt_cim.elements = [graphicElm]
lyt.setDefinition(lyt_cim)
Additional resources and sample scripts
For a detailed CIM specification and GIT repository, see Cartographic Information Model (cim-spec).
For a collection of 30 packaged sample scripts that use Python CIM access in a variety of ways, see ArcGIS Pro Python CIM Samples.
Python CIM access can also be useful in updating data source workflows. For more CIM examples, see the Updating and fixing data sources help topic.