#
# lcm_casting.py
#
# Module: Utilities for casting LCM objects to their concrete interfaces
# in pythonnet.
#
# Platform: Python.NET
# FieldWorks Version 9+
#
# Copyright 2025
#
"""
LCM Object Casting Utilities for pythonnet.
import logging
This module provides utilities for casting LCM objects from their base interface
types to their concrete derived interfaces. This is necessary because pythonnet
respects .NET interface typing strictly.
The Problem:
When you iterate over a collection like MorphoSyntaxAnalysesOC, pythonnet
returns objects typed as the base interface (IMoMorphSynAnalysis). Properties
from derived interfaces like IMoStemMsa.PartOfSpeechRA are not accessible
until you explicitly cast the object to its concrete interface type.
For example::
# This will NOT work - msa is typed as IMoMorphSynAnalysis
for msa in entry.MorphoSyntaxAnalysesOC:
pos = msa.PartOfSpeechRA # AttributeError - property not found!
# This WILL work - cast to concrete type first
for msa in entry.MorphoSyntaxAnalysesOC:
concrete_msa = cast_to_concrete(msa)
if hasattr(concrete_msa, 'PartOfSpeechRA'):
pos = concrete_msa.PartOfSpeechRA # Works!
Why This Happens:
In .NET, IMoStemMsa inherits from IMoMorphSynAnalysis. When you access a
collection typed as IEnumerable<IMoMorphSynAnalysis>, the CLR returns objects
as the interface type, not the concrete class. Pythonnet cannot automatically
determine the derived interface type - you must cast explicitly.
Usage::
from flexlibs2.code.lcm_casting import cast_to_concrete, get_pos_from_msa
# Cast any LCM object to its concrete interface
for msa in entry.MorphoSyntaxAnalysesOC:
concrete = cast_to_concrete(msa)
print(f"Class: {msa.ClassName}, Type: {type(concrete)}")
# Convenience function for the common POS lookup pattern
for msa in entry.MorphoSyntaxAnalysesOC:
pos = get_pos_from_msa(msa)
if pos:
print(f"Part of Speech: {pos.Name.BestAnalysisAlternative.Text}")
Supported Types:
- MSA types: MoStemMsa, MoDerivAffMsa, MoInflAffMsa, MoUnclassifiedAffixMsa
- Allomorph types: MoStemAllomorph, MoAffixAllomorph
- Phonological rule types: PhRegularRule, PhMetathesisRule, PhReduplicationRule
- Compound rule types: MoEndoCompound, MoExoCompound
- Morphosyntactic prohibition types: MoAdhocProhibGr, MoAdhocProhibMorph, MoAdhocProhibAllomorph
- Owner / container types (used by .Owner casting paths in Lexicon,
Notebook, and Discourse operations): LexEntry, LexSense, RnGenericRec,
CmPossibility, CmAnthroItem, DsConstChart, Text, StText, StTxtPara
Note:
The interface cache is lazy-loaded on first use to avoid import issues
at module load time. This is important because SIL.LCModel may not be
available until after FLExInit has run.
"""
# Interface cache - populated on first use
_interface_cache = {}
_interfaces_loaded = False
def _ensure_interfaces() -> None:
"""
Load and cache LCM interface types from SIL.LCModel.
This function is called automatically on first use of cast_to_concrete().
It populates the _interface_cache dictionary mapping ClassName strings
to their corresponding interface types.
The lazy loading pattern avoids import issues that can occur if
SIL.LCModel is imported before FLExInit has configured the CLR.
Returns:
None
Side Effects:
Populates _interface_cache with ClassName -> Interface mappings.
Sets _interfaces_loaded to True.
Raises:
ImportError: If SIL.LCModel cannot be imported. This typically means
FLExInit has not been run, or the FieldWorks DLLs are not available.
"""
global _interface_cache, _interfaces_loaded
if _interfaces_loaded:
return
from SIL.LCModel import (
# MSA (MorphoSyntaxAnalysis) interfaces
IMoStemMsa,
IMoDerivAffMsa,
IMoInflAffMsa,
IMoUnclassifiedAffixMsa,
# Allomorph interfaces
IMoStemAllomorph,
IMoAffixAllomorph,
IMoAffixForm,
)
# Phonological rule interfaces - try to import, but don't fail if unavailable
# LCM defines two concrete subclasses of PhSegmentRule:
# - PhRegularRule: Standard phonological rules (most common)
# - PhMetathesisRule: Metathesis rules (swapping segments)
# There is no IPhReduplicationRule interface in LCM (no PhReduplicationRule
# class in MasterLCModel.xml); the slot is kept as None for back-compat
# callers that still pass the legacy key.
try:
from SIL.LCModel import (
IPhRegularRule,
IPhMetathesisRule,
IPhSimpleContextSeg,
IPhSimpleContextNC,
IPhSegRuleRHS,
)
except ImportError:
IPhRegularRule = IPhMetathesisRule = None
IPhSimpleContextSeg = IPhSimpleContextNC = IPhSegRuleRHS = None
IPhReduplicationRule = None
# Compound rule interfaces - try to import, but don't fail if unavailable
# These are the two main compound rule types:
# - MoEndoCompound: Head is internal to the compound
# - MoExoCompound: Head is external to the compound
try:
from SIL.LCModel import (
IMoEndoCompound,
IMoExoCompound,
)
except ImportError:
IMoEndoCompound = IMoExoCompound = None
# Morphosyntactic prohibition interfaces - LCM names diverge from the
# POS-flavored shorthand flexlibs2 historically used:
# - MoAdhocProhibGr : Grammatical feature prohibitions (class 110)
# - MoMorphAdhocProhib: Morpheme co-occurrence prohibitions (class 102)
# - MoAlloAdhocProhib : Allomorph co-occurrence prohibitions (class 101)
# Earlier versions of this module spelled the latter two as
# IMoAdhocProhibMorph / IMoAdhocProhibAllomorph -- which never existed in
# LCM -- so the imports always failed and the cache silently went to None.
# Use the real LCM names and expose them under both keys so existing
# cache callers keep working.
try:
from SIL.LCModel import (
IMoAdhocProhibGr,
IMoMorphAdhocProhib,
IMoAlloAdhocProhib,
)
IMoAdhocProhibMorph = IMoMorphAdhocProhib
IMoAdhocProhibAllomorph = IMoAlloAdhocProhib
except ImportError:
IMoAdhocProhibGr = IMoMorphAdhocProhib = IMoAlloAdhocProhib = None
IMoAdhocProhibMorph = IMoAdhocProhibAllomorph = None
# Affix template interface - try to import, but don't fail if unavailable
# - MoInflAffixTemplate: Inflectional affix template patterns
try:
from SIL.LCModel import (
IMoInflAffixTemplate,
)
except ImportError:
IMoInflAffixTemplate = None
# Owner / container interfaces - the typed parents that own the
# objects manipulated by Lexicon, Notebook, and Discourse operations.
# These are needed because `.Owner` on an owned object returns the
# base ICmObject interface, which does not expose typed collection
# properties (AlternateFormsOS, EtymologyOS, EntryRefsOS, AnnotationsOC,
# SubPossibilitiesOS, RowsOS, ...). cast_to_concrete() routes through
# ClassName -> concrete interface so callers can reach those typed
# collections without doing the cast themselves.
try:
from SIL.LCModel import (
ILexEntry,
ILexSense,
ILexRefType,
IRnGenericRec,
ICmPossibility,
ICmAnthroItem,
IDsConstChart,
IText,
IStText,
IStTxtPara,
IWfiAnalysis,
)
except ImportError:
ILexEntry = ILexSense = ILexRefType = IRnGenericRec = None
ICmPossibility = ICmAnthroItem = None
IDsConstChart = IText = IStText = IStTxtPara = None
IWfiAnalysis = None
_interface_cache = {
# MSA types - used for grammatical category assignment
"MoStemMsa": IMoStemMsa,
"MoDerivAffMsa": IMoDerivAffMsa,
"MoInflAffMsa": IMoInflAffMsa,
"MoUnclassifiedAffixMsa": IMoUnclassifiedAffixMsa,
# Allomorph types - used for morpheme form variants
"MoStemAllomorph": IMoStemAllomorph,
"MoAffixAllomorph": IMoAffixAllomorph,
"MoAffixForm": IMoAffixForm,
}
# Add phonological rule types if imports succeeded
# The 3 main rule types in FLEx phonology:
if IPhRegularRule is not None:
_interface_cache["PhRegularRule"] = IPhRegularRule
if IPhMetathesisRule is not None:
_interface_cache["PhMetathesisRule"] = IPhMetathesisRule
if IPhReduplicationRule is not None:
_interface_cache["PhReduplicationRule"] = IPhReduplicationRule
# Context and RHS types used within rules:
if IPhSimpleContextSeg is not None:
_interface_cache["PhSimpleContextSeg"] = IPhSimpleContextSeg
if IPhSimpleContextNC is not None:
_interface_cache["PhSimpleContextNC"] = IPhSimpleContextNC
if IPhSegRuleRHS is not None:
_interface_cache["PhSegRuleRHS"] = IPhSegRuleRHS
# Add compound rule types if imports succeeded
# The 2 main compound rule types in FLEx morphology:
if IMoEndoCompound is not None:
_interface_cache["MoEndoCompound"] = IMoEndoCompound
if IMoExoCompound is not None:
_interface_cache["MoExoCompound"] = IMoExoCompound
# Add morphosyntactic prohibition types if imports succeeded
# The 3 main ad hoc prohibition types in FLEx morphology:
if IMoAdhocProhibGr is not None:
_interface_cache["MoAdhocProhibGr"] = IMoAdhocProhibGr
if IMoAdhocProhibMorph is not None:
_interface_cache["MoAdhocProhibMorph"] = IMoAdhocProhibMorph
if IMoAdhocProhibAllomorph is not None:
_interface_cache["MoAdhocProhibAllomorph"] = IMoAdhocProhibAllomorph
# Add affix template type if import succeeded
if IMoInflAffixTemplate is not None:
_interface_cache["MoInflAffixTemplate"] = IMoInflAffixTemplate
# Add owner / container types if imports succeeded. Used by .Owner
# casting paths in Lexicon, Notebook, and Discourse operations.
if ILexEntry is not None:
_interface_cache["LexEntry"] = ILexEntry
if ILexSense is not None:
_interface_cache["LexSense"] = ILexSense
if ILexRefType is not None:
_interface_cache["LexRefType"] = ILexRefType
if IRnGenericRec is not None:
_interface_cache["RnGenericRec"] = IRnGenericRec
if ICmPossibility is not None:
_interface_cache["CmPossibility"] = ICmPossibility
if ICmAnthroItem is not None:
_interface_cache["CmAnthroItem"] = ICmAnthroItem
if IDsConstChart is not None:
_interface_cache["DsConstChart"] = IDsConstChart
if IText is not None:
_interface_cache["Text"] = IText
if IStText is not None:
_interface_cache["StText"] = IStText
if IStTxtPara is not None:
_interface_cache["StTxtPara"] = IStTxtPara
if IWfiAnalysis is not None:
_interface_cache["WfiAnalysis"] = IWfiAnalysis
_interfaces_loaded = True
[docs]
def cast_to_concrete(obj):
"""
Cast an LCM object to its concrete interface type based on ClassName.
This function examines the object's ClassName property and casts it to
the appropriate derived interface type. If the ClassName is not in the
known mappings, the original object is returned unchanged.
Args:
obj: An LCM object with a ClassName property (e.g., IMoMorphSynAnalysis,
IMoForm, or any ICmObject).
Returns:
The object cast to its concrete interface type, or the original object
if the ClassName is not recognized or casting fails.
Example::
# Iterate MSAs and access derived properties
for msa in entry.MorphoSyntaxAnalysesOC:
concrete_msa = cast_to_concrete(msa)
# Now we can check for and access derived properties
if hasattr(concrete_msa, 'PartOfSpeechRA'):
pos = concrete_msa.PartOfSpeechRA
if pos:
print(f"POS: {pos.Name.BestAnalysisAlternative.Text}")
# Cast allomorphs to access type-specific properties
for allo in entry.AlternateFormsOS:
concrete_allo = cast_to_concrete(allo)
if hasattr(concrete_allo, 'StemName'):
# This is a stem allomorph
stem_name = concrete_allo.StemName
elif hasattr(concrete_allo, 'InflectionClasses'):
# This is an affix allomorph
infl_classes = concrete_allo.InflectionClasses
Notes:
- Returns the original object if ClassName is not in the mapping
- Returns the original object if casting fails for any reason
- Thread-safe for the interface loading (uses lazy initialization)
- The interface cache is loaded on first call
"""
_ensure_interfaces()
# Get the class name from the object
if not hasattr(obj, "ClassName"):
return obj
class_name = obj.ClassName
# Look up the interface type
interface_type = _interface_cache.get(class_name)
if interface_type is None:
return obj
# Cast to the concrete interface
try:
return interface_type(obj)
except Exception:
# If casting fails for any reason, return original
return obj
[docs]
def get_pos_from_msa(msa):
"""
Get the Part of Speech from any MSA type.
This is a convenience function for the common pattern of extracting
the Part of Speech reference from a MorphoSyntaxAnalysis object.
It handles the casting internally and checks each MSA type for its
POS property.
Different MSA types store POS in different properties:
- MoStemMsa: PartOfSpeechRA (main POS for stems)
- MoDerivAffMsa: ToPartOfSpeechRA (output POS after derivation)
- MoInflAffMsa: PartOfSpeechRA (POS this affix attaches to)
- MoUnclassifiedAffixMsa: PartOfSpeechRA
Args:
msa: An MSA object (IMoMorphSynAnalysis or derived type).
Returns:
IPartOfSpeech: The Part of Speech reference, or None if:
- The MSA type doesn't have a POS property
- The POS property is not set (null reference)
- The object cannot be cast to a known MSA type
Example::
# Get POS for all MSAs on an entry
for msa in entry.MorphoSyntaxAnalysesOC:
pos = get_pos_from_msa(msa)
if pos:
pos_name = pos.Name.BestAnalysisAlternative.Text
pos_abbr = pos.Abbreviation.BestAnalysisAlternative.Text
print(f"{pos_name} ({pos_abbr})")
# Check if entry has a specific POS
target_pos_guid = some_guid
has_target_pos = any(
get_pos_from_msa(msa) and
str(get_pos_from_msa(msa).Guid) == str(target_pos_guid)
for msa in entry.MorphoSyntaxAnalysesOC
)
Notes:
- For MoDerivAffMsa, this returns ToPartOfSpeechRA (the output POS),
not FromPartOfSpeechRA (the input POS). Use cast_to_concrete()
directly if you need to access FromPartOfSpeechRA.
- Returns None rather than raising exceptions for robustness
- Handles all common MSA types found in typical FLEx projects
"""
_ensure_interfaces()
if not hasattr(msa, "ClassName"):
return None
class_name = msa.ClassName
try:
if class_name == "MoStemMsa":
interface_type = _interface_cache.get("MoStemMsa")
if interface_type:
concrete = interface_type(msa)
return concrete.PartOfSpeechRA
elif class_name == "MoDerivAffMsa":
interface_type = _interface_cache.get("MoDerivAffMsa")
if interface_type:
concrete = interface_type(msa)
# Return the "to" POS (output of derivation)
return concrete.ToPartOfSpeechRA
elif class_name == "MoInflAffMsa":
interface_type = _interface_cache.get("MoInflAffMsa")
if interface_type:
concrete = interface_type(msa)
return concrete.PartOfSpeechRA
elif class_name == "MoUnclassifiedAffixMsa":
interface_type = _interface_cache.get("MoUnclassifiedAffixMsa")
if interface_type:
concrete = interface_type(msa)
return concrete.PartOfSpeechRA
except Exception:
# If anything fails, return None rather than crashing
pass
return None
[docs]
def clone_properties(source_obj, dest_obj, project=None):
"""
Deep clone all properties from source object to destination object.
This is a Python equivalent of ICloneableCmObject.SetCloneProperties() from C#.
It copies all properties recursively, handling:
- Simple properties (names, descriptions, etc.)
- Reference properties (RA)
- Owned objects (OA) - creates new objects with cloned properties
- Owned collections (OS/OC) - creates new objects for each item
Args:
source_obj: The source LCM object to clone from.
dest_obj: The destination LCM object to clone to.
project: Optional FLExProject instance for factory access. If not provided,
extracted from the destination object's owner.
Returns:
None. The destination object is modified in place.
Example::
from flexlibs2.code.lcm_casting import clone_properties
# Clone a rule
source_rule = phonRuleOps.GetAll()[0]
new_rule = factory.Create()
clone_properties(source_rule, new_rule, project)
Notes:
- Recursively clones owned objects
- Shares reference objects (doesn't create copies of referenced objects)
- Handles collections by adding cloned items to the destination collection
- Silently skips any properties that cannot be cloned
"""
if not hasattr(source_obj, "ClassName") or not hasattr(dest_obj, "ClassName"):
return
# Cast both to concrete types for full property access
source = cast_to_concrete(source_obj)
dest = cast_to_concrete(dest_obj)
# If project not provided, resolve via Cache.LanguageProject (canonical accessor)
if project is None and hasattr(dest, "Cache"):
try:
project = dest.Cache.LanguageProject
except Exception:
pass
# Get all properties from the source object
for attr_name in dir(source):
# Skip private, special, and known method attributes
if attr_name.startswith("_") or attr_name in ["Clone", "PostClone"]:
continue
try:
attr_value = getattr(source, attr_name, None)
# Skip methods and special attributes
if callable(attr_value) or attr_name in ["Hvo", "ClassID", "ClassName", "Guid", "Owner", "OwningFlid"]:
continue
# Try to set the property on destination
if hasattr(dest, attr_name):
try:
# Check if it's a collection (OS/OC) - these need special handling
if hasattr(attr_value, "Count") and hasattr(attr_value, "Add"):
# This is a collection - clone each item
dest_collection = getattr(dest, attr_name)
try:
dest_collection.Clear()
except Exception as e:
logging.debug(f"Failed to clear collection: {e}")
# Add cloned items
for item in attr_value:
try:
# Get factory based on item class name
if project:
factory = _get_factory_for_class(item.ClassName, project.project)
if factory:
cloned_item = factory.Create()
dest_collection.Add(cloned_item)
clone_properties(item, cloned_item, project)
except Exception as e:
# If we can't clone an item, just skip it
logging.debug(f"Failed to clone item: {e}")
else:
# Simple property or reference - copy directly
setattr(dest, attr_name, attr_value)
except Exception as e:
# If we can't set a property, skip it silently
logging.debug(f"Failed to set property {attr_name}: {e}")
except Exception as e:
# If we can't read a property, skip it
logging.debug(f"Failed to read property: {e}")
def _get_factory_for_class(class_name: str, project: object) -> "Optional[object]":
"""
Get the factory for creating an object of the given class.
Args:
class_name: String like 'PhRegularRule', 'PhSegRuleRHS', etc.
project: The FLExProject instance.
Returns:
The factory object, or None if not found.
"""
try:
from SIL.LCModel import (
IPhRegularRuleFactory,
IPhMetathesisRuleFactory,
IPhSegRuleRHSFactory,
IPhSimpleContextSegFactory,
IPhSimpleContextNCFactory,
)
# LCM has no PhReduplicationRule class -- PhSegmentRule only branches
# into PhRegularRule (129) and PhMetathesisRule (130). The factory map
# therefore omits any reduplication entry; callers that pass that key
# fall through to None.
factory_map = {
# The 2 concrete PhSegmentRule subclasses
"PhRegularRule": IPhRegularRuleFactory,
"PhMetathesisRule": IPhMetathesisRuleFactory,
# Context and RHS types
"PhSegRuleRHS": IPhSegRuleRHSFactory,
"PhSimpleContextSeg": IPhSimpleContextSegFactory,
"PhSimpleContextNC": IPhSimpleContextNCFactory,
}
factory_type = factory_map.get(class_name)
if factory_type:
return project.ServiceLocator.GetService(factory_type)
except Exception as e:
logging.debug(f"Failed to get factory for class {class_name}: {e}")
return None
[docs]
def cast_phonological_rule(rule_obj):
"""
Cast a phonological rule to its concrete interface type.
Phonological rules come back from GetAll() typed as IPhSegmentRule (base interface).
This function casts to the concrete interface based on ClassName:
- PhRegularRule -> IPhRegularRule
- PhMetathesisRule -> IPhMetathesisRule
- PhReduplicationRule -> IPhReduplicationRule
Args:
rule_obj: A phonological rule object (typed as IPhSegmentRule or similar).
Returns:
The rule cast to its concrete interface, or the original object if not recognized.
Example::
from flexlibs2.code.lcm_casting import cast_phonological_rule
# Get rules and cast them
for rule in phonRuleOps.GetAll():
concrete_rule = cast_phonological_rule(rule)
# Now can access type-specific properties
if concrete_rule.ClassName == 'PhRegularRule':
rhs_count = concrete_rule.RightHandSidesOS.Count
"""
_ensure_interfaces()
if not hasattr(rule_obj, "ClassName"):
return rule_obj
class_name = rule_obj.ClassName
# Look up the interface for this rule type
interface_type = _interface_cache.get(class_name)
if interface_type is None:
# Not a recognized rule type, return original
return rule_obj
try:
return interface_type(rule_obj)
except Exception:
# If casting fails, return original
return rule_obj
[docs]
def validate_merge_compatibility(survivor_obj, victim_obj):
"""
Validate that two objects can be safely merged.
Checks that both objects are of the same class and, for objects with multiple
concrete implementations, that they have the same concrete type. This prevents
merging incompatible types (e.g., PhRegularRule into PhMetathesisRule).
Args:
survivor_obj: The object that will receive merged data.
victim_obj: The object that will be deleted/merged into survivor.
Returns:
tuple: (is_compatible, error_message)
- (True, "") if merge is safe
- (False, error_message) if merge should be blocked
Example::
from flexlibs2.code.lcm_casting import validate_merge_compatibility
# Validate before merging
is_ok, msg = validate_merge_compatibility(entry1, entry2)
if not is_ok:
raise FP_ParameterError(msg)
# Works for all object types
is_ok, msg = validate_merge_compatibility(rule1, rule2)
is_ok, msg = validate_merge_compatibility(sense1, sense2)
Notes:
- Both objects must have a ClassName attribute
- For types with multiple concrete implementations (like phonological rules),
both must have the same ClassName (e.g., both PhRegularRule)
- For other types, same ClassName is sufficient
- Prevents data corruption from merging incompatible object types
"""
# Check that both objects exist and have ClassName
if not hasattr(survivor_obj, "ClassName"):
return False, "Survivor object has no ClassName attribute"
if not hasattr(victim_obj, "ClassName"):
return False, "Victim object has no ClassName attribute"
survivor_class = survivor_obj.ClassName
victim_class = victim_obj.ClassName
# Classes must match exactly
if survivor_class != victim_class:
return False, (
f"Cannot merge different classes: {victim_class} into {survivor_class}. "
f"Objects must be of the same type."
)
# For types with multiple concrete implementations, additional checks
# could be added here. Currently, ClassName uniquely identifies the concrete type.
return True, ""
[docs]
def get_from_pos_from_msa(msa):
"""
Get the source Part of Speech from a derivational MSA.
Only IMoDerivAffMsa has a FromPartOfSpeechRA property indicating
the POS before derivation. This function returns None for all
other MSA types.
Args:
msa: An MSA object (IMoMorphSynAnalysis or derived type).
Returns:
IPartOfSpeech: The source Part of Speech for derivational affixes,
or None if not a derivational MSA or no source POS is set.
Example::
for msa in entry.MorphoSyntaxAnalysesOC:
from_pos = get_from_pos_from_msa(msa)
to_pos = get_pos_from_msa(msa)
if from_pos and to_pos:
from_name = from_pos.Name.BestAnalysisAlternative.Text
to_name = to_pos.Name.BestAnalysisAlternative.Text
print(f"Derives: {from_name} -> {to_name}")
Notes:
- Only meaningful for MoDerivAffMsa objects
- Returns None for MoStemMsa, MoInflAffMsa, MoUnclassifiedAffixMsa
- Use in combination with get_pos_from_msa() to get both ends
of a derivational relationship
"""
_ensure_interfaces()
if not hasattr(msa, "ClassName"):
return None
if msa.ClassName != "MoDerivAffMsa":
return None
try:
interface_type = _interface_cache.get("MoDerivAffMsa")
if interface_type:
concrete = interface_type(msa)
return concrete.FromPartOfSpeechRA
except Exception:
pass
return None
[docs]
def get_common_properties(objects):
"""
Find properties that are available on ALL objects in a list.
When working with collections of objects that may have different concrete
types (e.g., mixed phonological rules), this function identifies which
properties are safely accessible on all objects without type checking.
This is useful for implementing filtering or display logic that works
uniformly across all types.
Args:
objects: Iterable of LCM objects (all should have ClassName attribute).
Returns:
set: Property names that exist on ALL objects. Empty set if no common
properties or if input is empty.
Example::
from flexlibs2.code.lcm_casting import get_common_properties
# Find properties available on all rule types
rules = phonRuleOps.GetAll()
common = get_common_properties(rules)
print(common) # {'Name', 'Direction', 'StrucDescOS', ...}
# These properties can be accessed safely on any rule
for rule in rules:
name = rule.Name
direction = rule.Direction
Notes:
- Properties starting with '_' are excluded
- Callable attributes (methods) are excluded
- Returns intersection of properties across all objects
- Empty list returns empty set (no intersection with all)
- Useful before implementing collection-wide filters
"""
if not objects:
return set()
# Convert to list to allow multiple iterations
obj_list = list(objects)
if not obj_list:
return set()
# Get cast versions of all objects for comprehensive property access
cast_objects = [cast_to_concrete(obj) for obj in obj_list]
# Start with all properties from first object
first_obj = cast_objects[0]
common = set()
for attr_name in dir(first_obj):
# Skip private attributes and methods
if attr_name.startswith("_"):
continue
# Check if this attribute exists on all other objects
try:
first_attr = getattr(first_obj, attr_name)
# Skip callable attributes (methods)
if callable(first_attr):
continue
# Check if all other objects have this property
if all(hasattr(obj, attr_name) for obj in cast_objects[1:]):
common.add(attr_name)
except Exception:
# Skip properties that fail to access
pass
return common
[docs]
def get_concrete_type_properties(lcm_obj):
"""
Get properties that are unique to an object's concrete type.
When you have an LCM object typed as a base interface (e.g., IPhSegmentRule),
this function identifies which properties are specific to its concrete type
(e.g., RightHandSidesOS for PhRegularRule).
This is useful for introspection and for determining what type-specific
capabilities an object has.
Args:
lcm_obj: An LCM object with a ClassName attribute.
Returns:
dict: Mapping of property names to their values, containing only
properties on the concrete type that don't exist on a simple
interface comparison. Empty dict if no unique properties.
Example::
from flexlibs2.code.lcm_casting import get_concrete_type_properties
# Get type-specific properties for a rule
rule = phonRuleOps.GetAll()[0]
unique_props = get_concrete_type_properties(rule)
if rule.ClassName == 'PhRegularRule':
print('RightHandSidesOS' in unique_props) # True
print(unique_props['RightHandSidesOS']) # The actual RHS collection
if rule.ClassName == 'PhMetathesisRule':
print('LeftPartOfMetathesisOS' in unique_props) # True
Notes:
- Returns empty dict if object has no ClassName attribute
- Properties are returned as property_name -> value mappings
- Private attributes (starting with _) are excluded
- Callable attributes (methods) are excluded
- Use with get_common_properties() to understand type diversity
"""
if not hasattr(lcm_obj, "ClassName"):
return {}
# Get the concrete type
concrete = cast_to_concrete(lcm_obj)
concrete_props = {}
# Get all public, non-callable attributes from concrete type
for attr_name in dir(concrete):
# Skip private attributes
if attr_name.startswith("_"):
continue
# Skip known system attributes
if attr_name in ["ClassName", "Guid", "Hvo", "ClassID", "Owner", "OwningFlid"]:
continue
try:
attr_value = getattr(concrete, attr_name)
# Skip callable attributes (methods)
if callable(attr_value):
continue
# Add this property
concrete_props[attr_name] = attr_value
except Exception:
# Skip properties that fail to access
pass
return concrete_props