I have a simple question - I'd like to change the value of this dropdown box programmatically (in Python) for a large number of layers in a large number of layer files.
The default setting for loading .lyr files into pro sets this to 'Layer has its own metadata'. This is also the case when loading programmatically via LayerFile("uri-to-layer.lyr") I want to run a Python script to reset this to 'Show metadata from data source (read-only)', as is possible in the Pro UI.
All of the methods and mechanisms in the Metadata class seem to be based around the assumption I want to do something with metadata for this layer. I do not! I want to make it read-only and show users the metadata stored in our spatial database. But, Metadata.isReadOnly is itself read only!
I then intend to re-save them as .lyrx files.
Any suggestions welcome.
Solved! Go to Solution.
I believe you can kinda get around this by directly modifying the CIM definition of the layer once you import it:
from pathlib import Path
from arcpy.mp import LayerFile, ArcGISProject
from arcpy._mp import Map, Layer # Hidden mp types
from arcpy.cim import CIMDefinition # All CIM types support the useSourceMetadata property
def get_map(project: ArcGISProject, map_name: str) -> Map:
return project.listMaps(map_name)[0]
def set_metadata_readonly(project: ArcGISProject, layerfiles: list[str], output_map: str | None=None):
"""Adds layerfiles to a map then sets the useSourceMetadata property to True.
Parameters:
project (ArcGISProject): The project to add the layers to.
layerfiles (list[str]): The list of layerfiles to add to the map.
output_map (Optional[str]): The name of the map to add the layers to. (default is first map in project)
"""
for layer_path in layerfiles:
# Set Up LayerFile object
lyrpath = Path(layer_path)
lyr = LayerFile(str(lyrpath))
# Add Layer to top of map
target_map = get_map(project, output_map)
target_map.addLayer(lyr, add_position="TOP")
# Get the new layer
new_layer: Layer = target_map.listLayers()[0]
# Get CIM definition for new layer
new_layer_cim: CIMDefinition = new_layer.getDefinition("V3")
# Set CIM useSourceMetadata to True
new_layer_cim.useSourceMetadata = True
# Update CIM definition
new_layer.setDefinition(new_layer_cim)
def main():
## Implement your script here
return
if __name__ == "__main__":
main()
This won't set that property in the layerfile itself, but it will make sure that once the layer is added to a target map/project that the useSourceMetadata CIM property is set to True.
NOTE:
You will need to .save() the ArcGISProject in your implementation, or the edits will be dropped.
This is also untested, so if you still can't get it to work by directly modifying the CIM definition of the layer, creating an Ideas post as per @MErikReedAugusta 's suggestion is the best bet.
Thanks lads. CIM was the solution.
I just need one and done on this, but digging through group layers in several hundred files wasn't especially appealing!
Included a functional example below (no, it's not pretty, but it works).
# Script to resave lyr files as lyrx
# Plan:
# Send a folder location
# Walk through each folder and open lyr file (into new map? Or reuse a single map?)
# Modify to use metadata from source
# Save lyrx with same name in same folder structrure
# https://pro.arcgis.com/en/pro-app/latest/arcpy/data-access/walk.htm
# https://pro.arcgis.com/en/pro-app/latest/arcpy/mapping/python-cim-access.htm
# https://pro.arcgis.com/en/pro-app/latest/arcpy/mapping/layer-class.htm
# https://pro.arcgis.com/en/pro-app/latest/arcpy/metadata/metadata-class.htm
import arcpy
import logging
import os
from arcpy._mp import Layer # Hidden mp types
from arcpy.cim import CIMDefinition # All CIM types support the useSourceMetadata property
workspace = "H:/Data/ESRI"
layerfiles = []
projectLocation = r'C:\Users\user\Documents\ArcGIS\Projects\MyProject\MyProject.aprx'
mapName = "Map"
outputLocation = r'C:\Users\user\Desktop\Layerfiles'
# Configure the logging system
logging.basicConfig(
filename = os.path.join(outputLocation, "updateLyrsToLyrx.log"),
level = logging.DEBUG,
format = "%(asctime)s - %(levelname)s - %(message)s"
)
# Recursive function to step through group layers
def updateMetadataForLayer(layer: Layer):
# dump to log
logging.info("updateMetadataForLayer: " + layer.name)
# Recurse through groups
if layer.isGroupLayer:
for subLayer in layer.listLayers():
updateMetadataForLayer(subLayer)
# Get CIM definition for new layer
new_layer_cim: CIMDefinition = layer.getDefinition("V3")
# Set CIM useSourceMetadata to True
new_layer_cim.useSourceMetadata = True
# Update CIM definition
layer.setDefinition(new_layer_cim)
# Main entry point, uses global variables
def updateLyrsToLyrx():
## Walk the directory to find all layer files
walk = arcpy.da.Walk(workspace, topdown=True)
# Load a project
p = arcpy.mp.ArcGISProject(projectLocation)
# Use the default "Map"
map = p.listMaps(mapName)[0]
for dirpath, dirnames, filenames in walk:
for filename in filenames:
if (filename.find(".lyr") != -1) and (filename.find(".lyrx") == -1): # Only operate on layer files, excluding lyrx
# full filename
layerFilePath = os.path.join(dirpath, filename)
# dump to log
logging.info("Found: " + layerFilePath)
# Add this layer to the map
lyrFile = arcpy.mp.LayerFile(layerFilePath)
# Retain the list
list_of_added_maplayers = map.addLayer(lyrFile)
# Loop through every added layers, catch errors and log failures
for added_layer in list_of_added_maplayers:
try:
updateMetadataForLayer(added_layer)
except:
# Log & fall through - save layer as lyrx anyway - check errored files later
logging.error('Unable to update metadata for: ' + added_layer.name)
# Grab the first, or root, layer
firstLayer = list_of_added_maplayers[0]
# subfolder path - trim the workspace
folderPath = os.path.dirname(layerFilePath[len(workspace):])
# find the filename
filename = os.path.splitext(os.path.basename(layerFilePath))[0]
# new output location
outputFolderPath = os.path.join(outputLocation + folderPath)
# create output folders if necessary
os.makedirs(outputFolderPath, exist_ok=True)
# Now we've updated the layer, flaky save a copy as lyrx (lol)
firstLayer.saveACopy(os.path.join(outputFolderPath, filename + ".lyrx"))
def main():
## Implement your script here
updateLyrsToLyrx()
return
if __name__ == "__main__":
main()
Again, thanks for taking the time to reply. Much appreciated 🙂
From my reading, this doesn't look to be currently possible. You could import Metadata for a fossilized copy of the Metadata at the time you created the LYRX files, but that's the closest I think you can get. Normally, this level of advanced editing is hiding in the CIM, but that seems just to point back to the regular Metadata class object, as you've found.
This probably warrants a post on the Python Ideas Board.
There's also the larger question, though, of the use-case here. Is this something you'll have to regularly do and update, hence the desire for automation? Or is it a one-and-done operation for most/all of the sources? If it's the latter, you're probably better off setting aside time for the obnoxious labor of doing it manually.
I believe you can kinda get around this by directly modifying the CIM definition of the layer once you import it:
from pathlib import Path
from arcpy.mp import LayerFile, ArcGISProject
from arcpy._mp import Map, Layer # Hidden mp types
from arcpy.cim import CIMDefinition # All CIM types support the useSourceMetadata property
def get_map(project: ArcGISProject, map_name: str) -> Map:
return project.listMaps(map_name)[0]
def set_metadata_readonly(project: ArcGISProject, layerfiles: list[str], output_map: str | None=None):
"""Adds layerfiles to a map then sets the useSourceMetadata property to True.
Parameters:
project (ArcGISProject): The project to add the layers to.
layerfiles (list[str]): The list of layerfiles to add to the map.
output_map (Optional[str]): The name of the map to add the layers to. (default is first map in project)
"""
for layer_path in layerfiles:
# Set Up LayerFile object
lyrpath = Path(layer_path)
lyr = LayerFile(str(lyrpath))
# Add Layer to top of map
target_map = get_map(project, output_map)
target_map.addLayer(lyr, add_position="TOP")
# Get the new layer
new_layer: Layer = target_map.listLayers()[0]
# Get CIM definition for new layer
new_layer_cim: CIMDefinition = new_layer.getDefinition("V3")
# Set CIM useSourceMetadata to True
new_layer_cim.useSourceMetadata = True
# Update CIM definition
new_layer.setDefinition(new_layer_cim)
def main():
## Implement your script here
return
if __name__ == "__main__":
main()
This won't set that property in the layerfile itself, but it will make sure that once the layer is added to a target map/project that the useSourceMetadata CIM property is set to True.
NOTE:
You will need to .save() the ArcGISProject in your implementation, or the edits will be dropped.
This is also untested, so if you still can't get it to work by directly modifying the CIM definition of the layer, creating an Ideas post as per @MErikReedAugusta 's suggestion is the best bet.
Actually, speaking of untested options and building on this:
I missed that useSourceMetadata property. If you can set that on an active layer, then theoretically you should then be able to export that layer from the map as a LYRX file, right?
If the untested assumptions above are correct (and if the useSourceMetadata property persists in an LYRX file), then theoretically you could run the code to import the data into a dummy map file, then re-export it back out as a LYRX file.
Try running Hayden's code, followed by calling "Save Layer to File (Data Management)" and passing it the resulting map layer.
If we're both right, and a with a little luck, the end result would hopefully be a LYRX file that points to the source metadata. I likely won't have time to test this for a few days, at least, but if either you is able before then, definitely report back with what you found.
Thanks lads. CIM was the solution.
I just need one and done on this, but digging through group layers in several hundred files wasn't especially appealing!
Included a functional example below (no, it's not pretty, but it works).
# Script to resave lyr files as lyrx
# Plan:
# Send a folder location
# Walk through each folder and open lyr file (into new map? Or reuse a single map?)
# Modify to use metadata from source
# Save lyrx with same name in same folder structrure
# https://pro.arcgis.com/en/pro-app/latest/arcpy/data-access/walk.htm
# https://pro.arcgis.com/en/pro-app/latest/arcpy/mapping/python-cim-access.htm
# https://pro.arcgis.com/en/pro-app/latest/arcpy/mapping/layer-class.htm
# https://pro.arcgis.com/en/pro-app/latest/arcpy/metadata/metadata-class.htm
import arcpy
import logging
import os
from arcpy._mp import Layer # Hidden mp types
from arcpy.cim import CIMDefinition # All CIM types support the useSourceMetadata property
workspace = "H:/Data/ESRI"
layerfiles = []
projectLocation = r'C:\Users\user\Documents\ArcGIS\Projects\MyProject\MyProject.aprx'
mapName = "Map"
outputLocation = r'C:\Users\user\Desktop\Layerfiles'
# Configure the logging system
logging.basicConfig(
filename = os.path.join(outputLocation, "updateLyrsToLyrx.log"),
level = logging.DEBUG,
format = "%(asctime)s - %(levelname)s - %(message)s"
)
# Recursive function to step through group layers
def updateMetadataForLayer(layer: Layer):
# dump to log
logging.info("updateMetadataForLayer: " + layer.name)
# Recurse through groups
if layer.isGroupLayer:
for subLayer in layer.listLayers():
updateMetadataForLayer(subLayer)
# Get CIM definition for new layer
new_layer_cim: CIMDefinition = layer.getDefinition("V3")
# Set CIM useSourceMetadata to True
new_layer_cim.useSourceMetadata = True
# Update CIM definition
layer.setDefinition(new_layer_cim)
# Main entry point, uses global variables
def updateLyrsToLyrx():
## Walk the directory to find all layer files
walk = arcpy.da.Walk(workspace, topdown=True)
# Load a project
p = arcpy.mp.ArcGISProject(projectLocation)
# Use the default "Map"
map = p.listMaps(mapName)[0]
for dirpath, dirnames, filenames in walk:
for filename in filenames:
if (filename.find(".lyr") != -1) and (filename.find(".lyrx") == -1): # Only operate on layer files, excluding lyrx
# full filename
layerFilePath = os.path.join(dirpath, filename)
# dump to log
logging.info("Found: " + layerFilePath)
# Add this layer to the map
lyrFile = arcpy.mp.LayerFile(layerFilePath)
# Retain the list
list_of_added_maplayers = map.addLayer(lyrFile)
# Loop through every added layers, catch errors and log failures
for added_layer in list_of_added_maplayers:
try:
updateMetadataForLayer(added_layer)
except:
# Log & fall through - save layer as lyrx anyway - check errored files later
logging.error('Unable to update metadata for: ' + added_layer.name)
# Grab the first, or root, layer
firstLayer = list_of_added_maplayers[0]
# subfolder path - trim the workspace
folderPath = os.path.dirname(layerFilePath[len(workspace):])
# find the filename
filename = os.path.splitext(os.path.basename(layerFilePath))[0]
# new output location
outputFolderPath = os.path.join(outputLocation + folderPath)
# create output folders if necessary
os.makedirs(outputFolderPath, exist_ok=True)
# Now we've updated the layer, flaky save a copy as lyrx (lol)
firstLayer.saveACopy(os.path.join(outputFolderPath, filename + ".lyrx"))
def main():
## Implement your script here
updateLyrsToLyrx()
return
if __name__ == "__main__":
main()
Again, thanks for taking the time to reply. Much appreciated 🙂