Select to view content in your preferred language

Validation script to limit domain choices based on field type (e.g., integer) and domain type (e.g., coded value)

134
5
a week ago
LizAbbey
Regular Contributor

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.

 

0 Kudos
5 Replies
RPGIS
by MVP Regular Contributor
MVP Regular Contributor

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.

 

HaydenWelch
MVP Regular Contributor

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
0 Kudos
LizAbbey
Regular Contributor

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.

HaydenWelch
MVP Regular Contributor

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
...
RPGIS
by MVP Regular Contributor
MVP Regular Contributor

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.

0 Kudos