Source code for flexicon.code.Shared.wrapper_base

#
#   wrapper_base.py
#
#   Class: LCMObjectWrapper
#          Base class for wrapping LCM objects with unified interface access.
#          Transparently handles casting from base interfaces to concrete types.
#
#   Platform: Python.NET
#             FieldWorks Version 9+
#
#   Copyright 2025
#

"""
Base wrapper class for LCM objects with intelligent property routing.

This module provides LCMObjectWrapper, a base class for creating wrapper objects
that transparently handle the two-layer LCM type system:

- Base Interface: Generic interface typed by pythonnet (e.g., IPhSegmentRule)
- Concrete Type: Actual runtime type identified by ClassName attribute
- Concrete Interface: Type-specific interface with additional properties

The Problem:
    In pythonnet, when you access objects from a collection, they're typed as
    the base interface. Accessing concrete type-specific properties requires
    explicit casting based on the ClassName attribute.

The Solution:
    LCMObjectWrapper stores both the base interface and concrete type, then
    uses __getattr__() to route property access intelligently:
    - Try concrete type first (more specific properties)
    - Fall back to base interface if property doesn't exist
    - Return None for missing properties instead of raising AttributeError

Example::

    from flexicon.code.Shared.wrapper_base import LCMObjectWrapper
    from flexicon.code.lcm_casting import cast_to_concrete

    # Wrap an LCM object
    rule = phonRuleOps.GetAll()[0]  # Typed as IPhSegmentRule
    wrapped = LCMObjectWrapper(rule)

    # Access type-specific properties transparently
    if wrapped.ClassName == 'PhRegularRule':
        rhs_count = wrapped.RightHandSidesOS.Count  # Works without casting!

    # Check what properties are available
    common_props = wrapped.get_property('StrucDescOS')
    if common_props:
        print(f"Rule has {len(common_props)} input contexts")

External Casting (pythonnet interface casts):
    Wrapper instances are plain Python objects, so pythonnet cannot cast
    them directly to a .NET interface (e.g. ``ICmObject(wrapped_obj)``
    raises ``TypeError: object does not implement ICmObject``). External
    code that needs to perform its own interface cast should use the
    public ``lcm_object`` property to retrieve the raw LCM object first::

        from SIL.LCModel import ICmObject

        wrapped = phonRuleOps.GetAll()[0]
        raw = wrapped.lcm_object          # unwrap to the raw C# object
        class_name = ICmObject(raw).ClassName

    For the common ``ICmObject`` cast specifically, ``AsICmObject()`` is
    provided as a convenience shortcut::

        class_name = wrapped.AsICmObject().ClassName

Usage Notes:
    - Wrapper classes inherit from LCMObjectWrapper
    - Never access _obj or _concrete directly in subclasses
    - External code should use the public `lcm_object` property (not
      `_obj`/`_concrete`) to reach the raw LCM object for casting
    - Equality and hashing use LCM ``Hvo`` (see ``lcm_identity_hvo``) so
      ``wrapper == raw_object`` and ``wrapper in sequence`` work when the
      underlying object is the same repository entry
    - Use get_property() for safe access with defaults
    - Use class_type property to check the concrete type
"""

from ..lcm_casting import cast_to_concrete


[docs] def lcm_identity_hvo(obj): """ Return the LCM ``Hvo`` used for wrapper equality and hashing. Accepts ``LCMObjectWrapper``, ``PythonicWrapper``, or a raw LCM object. Returns ``None`` when no Hvo can be resolved (non-LCM values). """ if obj is None: return None if isinstance(obj, LCMObjectWrapper): target = obj.lcm_object elif type(obj).__name__ == "PythonicWrapper": try: target = object.__getattribute__(obj, "_obj") except AttributeError: return None else: target = obj hvo = getattr(target, "Hvo", None) if hvo is None: return None return int(hvo)
[docs] class LCMObjectWrapper: """ Base wrapper for LCM objects providing unified interface access. Stores both the base interface and concrete type, routing property access intelligently to support the two-layer LCM type system transparently. External callers needing to cast the wrapped object to a specific pythonnet/.NET interface (ICmObject, ICmPossibility, ICmMajorObject, IMoInflAffixTemplate, etc.) should use the `lcm_object` property to retrieve the raw LCM object, then cast it themselves; or call `AsICmObject()` for the common ICmObject case. Attributes: _obj: The base interface object (e.g., IPhSegmentRule) _concrete: The concrete type object (e.g., IPhRegularRule) """ def __init__(self, lcm_obj): """ Initialize wrapper with an LCM object. Automatically casts the object to its concrete type using the lcm_casting module. Both base and concrete are stored for flexible property access. Args: lcm_obj: An LCM object with a ClassName attribute. Typically a base interface type (e.g., IPhSegmentRule, IMoMorphSynAnalysis, IMoForm). Example:: rule = phonRuleOps.GetAll()[0] wrapped = LCMObjectWrapper(rule) print(wrapped.class_type) # "PhRegularRule" or similar """ self._obj = lcm_obj self._concrete = cast_to_concrete(lcm_obj) def __getattr__(self, name): """ Route property access intelligently across base and concrete types. When a property is accessed on the wrapper, this method: 1. First tries the concrete type (more specific) 2. Falls back to the base interface if property not found 3. Raises AttributeError only if property doesn't exist on either This allows seamless access to both common properties (on base interface) and type-specific properties (on concrete interface) without manual casting. Args: name: Property or method name being accessed. Returns: The property value or method from whichever type has it. Raises: AttributeError: If the property doesn't exist on either type. Example:: # Access common property on base interface name = wrapped.Name # Access type-specific property on concrete interface if wrapped.ClassName == 'PhRegularRule': rhs = wrapped.RightHandSidesOS # Concrete type only # Calling methods works transparently wrapped.SomeMethod() """ # Prevent infinite recursion when accessing _obj or _concrete if name in ("_obj", "_concrete"): raise AttributeError(f"'{type(self).__name__}' object has no attribute '{name}'") # Try concrete type first (more specific) try: return getattr(self._concrete, name) except AttributeError: pass # Fall back to base interface try: return getattr(self._obj, name) except AttributeError: pass # Property not found on either type raise AttributeError(f"'{type(self).__name__}' object and its wrapped LCM object have no attribute '{name}'") @property def class_type(self): """ Get the concrete class type as a string. Returns the ClassName attribute, which uniquely identifies the concrete type of the wrapped object. This is the primary way to check which concrete interface the object implements. Returns: str: The ClassName (e.g., "PhRegularRule", "PhMetathesisRule", "MoStemMsa", "MoInflAffMsa", etc.) Example:: wrapped = LCMObjectWrapper(rule) if wrapped.class_type == 'PhRegularRule': # Object is a regular phonological rule pass elif wrapped.class_type == 'PhMetathesisRule': # Object is a metathesis rule pass """ return self._obj.ClassName @property def lcm_object(self): """ Get the raw LCM object underlying this wrapper. This is the SUPPORTED public accessor for external code that needs to perform its own pythonnet interface cast (e.g. ``ICmObject``, ``ICmPossibility``, ``ICmMajorObject``, ``IMoInflAffixTemplate``). Wrapper instances are plain Python objects and cannot themselves be passed to a pythonnet interface constructor -- only the raw LCM object stored in ``_obj`` can. Subclasses and external callers should use `lcm_object` rather than reaching into `_obj` directly. Returns: The raw LCM object (the same object passed to `__init__()`). This is typically a base interface type (e.g. IPhSegmentRule, IMoMorphSynAnalysis, IMoForm) rather than the concrete cast. Example:: from SIL.LCModel import ICmObject wrapped = phonRuleOps.GetAll()[0] raw = wrapped.lcm_object print(ICmObject(raw).ClassName) """ return self._obj
[docs] def AsICmObject(self): """ Cast the wrapped object to ICmObject. Convenience method for the common case of needing an ICmObject-typed reference (e.g. to read `.ClassName`, `.Hvo`, `.Guid`, `.Owner`, or other properties defined on the base LCM object interface). This is equivalent to ``ICmObject(wrapped.lcm_object)`` but raises a flexlibs-style exception instead of a raw TypeError when there is no underlying object to cast. Returns: ICmObject: The wrapped object cast to ICmObject. Raises: FP_NullParameterError: If the wrapper has no underlying LCM object (`lcm_object` is None). Example:: wrapped = phonRuleOps.GetAll()[0] class_name = wrapped.AsICmObject().ClassName """ from ..FLExProject import FP_NullParameterError if self._obj is None: raise FP_NullParameterError() from SIL.LCModel import ICmObject return ICmObject(self._obj)
[docs] def get_property(self, prop_name, default=None): """ Safely get a property value with a fallback default. Attempts to access a property on the wrapped object, returning a default value if the property doesn't exist. This is safer than direct property access when you're unsure whether a property exists on the wrapped object's type. Args: prop_name: Name of the property to access. default: Value to return if property doesn't exist. Default: None. Returns: The property value if it exists, otherwise the default value. Example:: # Safe access to type-specific properties rhs = wrapped.get_property('RightHandSidesOS') if rhs: print(f"Rule has {rhs.Count} RHS") # Check for optional properties frequency = wrapped.get_property('Frequency', 0) print(f"Rule frequency: {frequency}") # Provide meaningful defaults context_count = wrapped.get_property('StrucDescOS', []) print(f"Input contexts: {len(context_count)}") """ try: return getattr(self, prop_name) except AttributeError: return default
def __eq__(self, other): if other is None: return False self_hvo = lcm_identity_hvo(self) other_hvo = lcm_identity_hvo(other) if self_hvo is None or other_hvo is None: return NotImplemented return self_hvo == other_hvo def __hash__(self): hvo = lcm_identity_hvo(self) if hvo is None: raise TypeError(f"{type(self).__name__} is not hashable without an Hvo") return hash(hvo) def __repr__(self): """ String representation showing the wrapped object's class type. Returns: str: Representation like "LCMObjectWrapper(PhRegularRule)" """ return f"{type(self).__name__}({self.class_type})" def __str__(self): """ Human-readable string representation. Returns: str: Description like "Wrapped LCM object of type PhRegularRule" """ return f"Wrapped LCM object of type {self.class_type}"