Hi,
I have a geoprocessing tool (thanks to the generous help of @SSWoodward) that uses a domain table to to define subtypes in a feature class. It uses the integer code and description from the domain table to create the subtypes, so the domain needs to be coded value and not range. The tool populates a list of available domains from the nominated workspace, and available subtype columns from the target feature class. Subtypes can only be integers, so I want to only have coded value integer domains and integer fields in my dropdown selections.
I have successfully limited the fields dropdowns to integers from the target feature class using a validation script, but my attempts with the domain list have not been successful.
Main script and validator scripts below.
import arcpy
def add_subtypes_from_domain(workspace: str, domain_name: str, target_table: str, subtype_field: str):
"""
Adds subtypes to a feature class or table based on a coded value domain.
Parameters:
workspace (str): Path to the geodatabase containing the domain.
domain_name (str): Name of the domain to extract codes from.
target_table (str): Path to the feature class or table to update.
subtype_field (str): Name of the integer field to use for subtypes.
"""
arcpy.env.workspace = workspace
temp_table = "memory\\temp_out_tbl"
try:
# Convert domain to table
st_tbl = arcpy.management.DomainToTable(workspace, domain_name, temp_table, 'code', 'desc')
# Set the subtype field
arcpy.management.SetSubtypeField(target_table, subtype_field)
# Add subtypes from the domain table
with arcpy.da.SearchCursor(st_tbl, ['code', 'desc']) as cursor:
for code, desc in cursor:
arcpy.management.AddSubtype(target_table, code, desc)
finally:
# Clean up
if 'cursor' in locals():
del cursor
if arcpy.Exists(temp_table):
arcpy.management.Delete(temp_table)
if __name__ == "__main__":
workspace = arcpy.GetParameterAsText(0)
domain_name = arcpy.GetParameterAsText(1)
target_table = arcpy.GetParameterAsText(2)
subtype_field = arcpy.GetParameterAsText(3)
add_subtypes_from_domain(workspace, domain_name, target_table, subtype_field)
Below is a working validation script that lists all domains as dropdowns, and only integer type fields from the target feature class (does not filter to only display coded value integer domains, but at least has dropdowns for easy selection):
import arcpy
class ToolValidator:
def __init__(self):
self.params = arcpy.GetParameterInfo()
def initializeParameters(self):
return
def updateParameters(self):
# Populate domain names from workspace (no type filtering)
if self.params[0].altered and self.params[0].value:
workspace = self.params[0].valueAsText
arcpy.env.workspace = workspace
try:
domains = arcpy.da.ListDomains(workspace)
domain_names = [d.name for d in domains]
self.params[1].filter.list = domain_names
except Exception:
self.params[1].filter.list = []
# Populate subtype field list with only Integer or SmallInteger fields
if self.params[2].altered and self.params[2].value:
target_table = self.params[2].valueAsText
try:
fields = arcpy.ListFields(target_table)
int_fields = [
f.name for f in fields
if f.type in ["Integer", "SmallInteger"]
]
self.params[3].filter.list = int_fields
except Exception:
self.params[3].filter.list = []
return
def updateMessages(self):
return
Can anyone suggest some changes to the domain validator or main script to get this to limit domain dropdowns to coded value with integer field type?
Thanks for reading through to the end.
Hi @LizAbbey,
Here is my suggestion for simplifying and perhaps help troubleshoot your code at least for the validation.
def updateParameters(self):
# Populate domain names from workspace (no type filtering)
db = self.params[0].valueAsText
if db is not None:
if '.sde' in db or '.gdb' in db:
domains = arcpy.da.ListDomains(db)
self.params[1].filter.list = list( set( [ d.name for d in domains for cv in d.codedValues for key, value in cv.items() if d.domainType is "CodedValue" and key in range(10) ] ) )
# Populate subtype field list with only Integer or SmallInteger fields
fcs = {}
if db is not None:
walk = arcpy.da.Walk(workspace, datatype="FeatureClass")
fcs = { filename : os.path.join( root, filename ) for root, directories, filenames in walk for filename in filenames }
self.params[2].filter.list = list( fcs )
if self.params[2].value:
table = self.params[2].valueAsText
if table in fcs:
table = fcs[table]
numfields = [ f.name for f in arcpy.ListFields(table) if f.type in ["Integer", "SmallInteger"] ]
if len( numfields ) > 0:
self.params[3].filter.list = int_fields
# My suspicion is the parameters need to be returned but I would need to double
# check that since it has been some time since I created a python tool.
# -- return self.params
return
This should at least help with the validation and maybe help get you in the right direction. I will need to dig in deeper for the other half but I think @HaydenWelch, @DanPatterson, or @jcarlson may have a better idea and also see if my suggestion is incorrect in some ways.
Here's how I would handle this situation:
from arcpy import (
env as _env,
EnvManager,
Parameter,
GetParameterInfo,
ListFields,
Exists,
)
from arcpy.management import (
SetSubtypeField,
AddSubtype,
)
from arcpy.da import (
ListDomains,
Domain,
)
def add_subtypes_from_domain(workspace: str, domain_name: str, target_table: str, subtype_field: str):
"""Adds subtypes to a feature class or table based on a coded value domain.
Parameters:
workspace (str): Path to the geodatabase containing the domain.
domain_name (str): Name of the domain to extract codes from.
target_table (str): Path to the feature class or table to update.
subtype_field (str): Name of the integer field to use for subtypes.
Returns:
( list[Result1[str|Table]] ): The result values of the AddSubtype operations
"""
SetSubtypeField(target_table, subtype_field)
with EnvManager(workspace=workspace):
return [
AddSubtype(target_table, code, desc)
for domain in ListDomains(workspace)
for code, desc in domain.codedValues.items()
if domain.name == domain_name
]
def get_workspace_domains(workspace: str) -> list[str] | None:
"""Get the domains in the given workspace
Parameters:
workspace (str): Path to workspace
Returns:
( list[str] | None ): A list of domain names in the workspace
"""
if not Exists(workspace):
return None
return [domain.name for domain in ListDomains(workspace)]
def get_table_fields(table: str, types: list[str], workspace: str | None=None):
"""Gets all fields of the specified types from the input table
Parameters:
table (str): Path to table (relative if workspace is set)
types (str): List of field types to get
workspace (Optional[str]): Optionally set a workspace, default is arcpy.env.workspace
Returns:
( list[str] | None ): A list of the field names in the table matching the types list
"""
if not Exists(workspace):
return None
with EnvManager(workspace=workspace or _env.workspace):
return [
field.name
for field in ListFields(table)
if field.type in types
]
class ToolValidator:
def __init__(self):
# Map name to parameter object
self.params = {p.name: p for p in GetParameterInfo()}
def updateParameters(self):
p0_name = self.params['p0_name']
p1_name = self.params['p1_name']
p2_name = self.params['p2_name']
p3_name = self.params['p3_name']
# Populate domain names from workspace (no type filtering)
if p0_name.altered and p0_name.value and p0_name.filter:
p0_name.filter.list = get_workspace_domains(
workspace=p1_name.valueAsText
) or []
# Populate subtype field list with only Integer or SmallInteger fields
if p2_name.altered and p2_name.value and p3_name.filter:
p3_name.filter.list = get_table_fields(
workspace=p1_name.valueAsText,
table=p2_name.valueAsText,
types=['Integer', 'SmallInteger']
) or []
return
def updateMessages(self):
return
Thanks for replying @RPGIS and @HaydenWelch. I'm learning so much by kicking around this community.
I was not able to get either of your updates to work in the tool (probably operator error). And on reflection, I think I didn't accurately describe the enhancements I wanted in my code because I couldn't see any domain filtering based on type in your examples.
However, I was able to re-write my original validation code for the domain to limit selectable domains to only coded value integers:
# Filter coded value domains with integer keys
workspace_param = self.params[0]
domain_param = self.params[1]
if workspace_param.altered and workspace_param.value:
workspace = workspace_param.valueAsText
arcpy.env.workspace = workspace
try:
domains = arcpy.da.ListDomains(workspace)
domain_param.filter.list = [
d.name for d in domains
if d.domainType == "CodedValue"
and d.codedValues
and all(isinstance(k, int) for k in d.codedValues.keys())
]
except Exception:
domain_param.filter.list = []
Perhaps not the most elegant solution, but it's doing what I wanted it to do now.
Just to give you to option to try my solution again, here's an update for the `get_workspace_domains` function that adds domain type and domain key type filtering:
from typing import Literal
def get_workspace_domains(workspace: str, domain_type: Literal['CodedValue', 'Range']='CodedValue', key_filter: type=int) -> list[str] | None:
"""Get the domains in the given workspace
Parameters:
workspace (str): Path to workspace
domain_type (Literal['CodedValue', 'Range']): The type of domain to get (default='CodedValue')
key_filter (type): Run an instance check on the domain keys for this type (default=int)
Returns:
( list[str] | None ): A list of domain names in the workspace
"""
if not Exists(workspace):
return None
return [
domain.name
for domain in ListDomains(workspace)
if domain.domainType == domain_type
and all(isinstance(key, key_filter) for key in domain.codedValues.keys())
]
Glad you got it working! This fix is basically just moving that domain filtering to another function. Only upside of this would be using an lru_cache on that function to prevent your parameter updates from constantly re-checking all the domains everytime it needs to re-validate:
from functools import lru_cache
from typing import Literal
# lru_cache will maintain a rolling cache of functinon calls.
# If a set of arguments is in the cache it will return from the cache and not
# re-run the function. This is super useful for stuff like this that can cause
# large slowdown on parameter manipulation
# Caveat is that all args must be hashable (string, int, tuple, etc.)
@lru_cache
def get_workspace_domains(workspace: str, domain_type: Literal['CodedValue', 'Range']='CodedValue', key_filter: type=int) -> list[str] | None:
"""Get the domains in the given workspace
...
I did include the filter in mine to only look for those specific domain types but I think the issue was I set it to only filter to look for a number within the range specified. Isinstance() would have been the more appropriate approach. Isinstance is much better method so kudos on that. I wrote the sample script in a couple of minutes but had no means of testing it nor did I ask what your coded values were set to.
list( set( [ d.name for d in domains for cv in d.codedValues for key, value in cv.items() if d.domainType is "CodedValue" and key in range(10) ] ) )
I am glad you were able to figure it out.