Source code for flexicon.code.PythonicWrapper

#
#   PythonicWrapper.py
#
#   Class: PythonicWrapper
#          Wraps LibLCM objects to provide suffix-free property access.
#          Translates pythonic names like 'Senses' to LibLCM names like 'SensesOS'.
#
#   Platform: Python.NET
#             FieldWorks Version 9+
#
#   Copyright 2025
#

"""
Pythonic Wrapper for LibLCM Objects

LibLCM uses 2-character suffixes to indicate relationship types:
  - OA: Owning Atomic (single owned child)
  - OS: Owning Sequence (ordered collection of owned children)
  - OC: Owning Collection (unordered collection of owned children)
  - RA: Reference Atomic (single reference)
  - RS: Reference Sequence (ordered collection of references)
  - RC: Reference Collection (unordered collection of references)

This wrapper allows Python code to use suffix-free names:
  - entry.Senses instead of entry.SensesOS
  - entry.LexemeForm instead of entry.LexemeFormOA
  - sense.SemanticDomains instead of sense.SemanticDomainsRC

Usage::

    from flexlibs2.code.PythonicWrapper import wrap, unwrap

    # Wrap an LCM object
    entry = wrap(raw_entry)

    # Now use pythonic names
    for sense in entry.Senses:      # Resolves to SensesOS
        print(sense.Gloss)          # Resolves to Gloss (no suffix needed)

    # Unwrap to get original object
    raw = unwrap(entry)

    # Or use the p() shorthand
    from flexlibs2.code.PythonicWrapper import p
    for sense in p(entry).Senses:
        print(p(sense).Gloss)
"""

from .Shared.lcm_constants import SUFFIXES


[docs] class PythonicWrapper: """ Wrapper that provides suffix-free property access to LibLCM objects. Uses __getattr__ to intercept attribute access and try suffixed variants when the base name isn't found. Example:: wrapped = PythonicWrapper(entry) for sense in wrapped.Senses: # Tries SensesOS, SensesOC, etc. text = wrapped.Gloss # Returns Gloss directly if it exists """ __slots__ = ("_obj", "_cache") def __init__(self, obj): """ Initialize wrapper with an LCM object. Args: obj: The LibLCM object to wrap (ILexEntry, ILexSense, etc.) """ object.__setattr__(self, "_obj", obj) object.__setattr__(self, "_cache", {}) def __getattr__(self, name): """ Get attribute, trying suffixed variants if base name not found. Resolution order: 1. Check cache for previously resolved name 2. Try the exact name on the wrapped object 3. Try each suffix variant (OS, OC, OA, RS, RC, RA) 4. Raise AttributeError if nothing found """ # Check cache first cache = object.__getattribute__(self, "_cache") if name in cache: return getattr(object.__getattribute__(self, "_obj"), cache[name]) obj = object.__getattribute__(self, "_obj") # Try exact name first if hasattr(obj, name): cache[name] = name return getattr(obj, name) # Try each suffix for suffix in SUFFIXES: suffixed_name = name + suffix if hasattr(obj, suffixed_name): cache[name] = suffixed_name return getattr(obj, suffixed_name) # Not found raise AttributeError( f"'{type(obj).__name__}' object has no attribute '{name}' " f"(also tried: {', '.join(name + s for s in SUFFIXES)})" ) def __setattr__(self, name, value): """ Set attribute, trying suffixed variants if base name not found. """ if name in ("_obj", "_cache"): object.__setattr__(self, name, value) return obj = object.__getattribute__(self, "_obj") cache = object.__getattribute__(self, "_cache") # Check cache first if name in cache: setattr(obj, cache[name], value) return # Try exact name first if hasattr(obj, name): cache[name] = name setattr(obj, name, value) return # Try each suffix for suffix in SUFFIXES: suffixed_name = name + suffix if hasattr(obj, suffixed_name): cache[name] = suffixed_name setattr(obj, suffixed_name, value) return # Not found - try setting anyway (might be a new attribute) setattr(obj, name, value) def __repr__(self): obj = object.__getattribute__(self, "_obj") return f"PythonicWrapper({type(obj).__name__})" def __str__(self): obj = object.__getattribute__(self, "_obj") return str(obj) def __eq__(self, other): obj = object.__getattribute__(self, "_obj") if isinstance(other, PythonicWrapper): return obj == object.__getattribute__(other, "_obj") return obj == other def __hash__(self): obj = object.__getattribute__(self, "_obj") return hash(obj) def __iter__(self): """Allow iteration if wrapped object is iterable.""" obj = object.__getattribute__(self, "_obj") return iter(obj) def __len__(self): """Return length if wrapped object has Count or __len__.""" obj = object.__getattribute__(self, "_obj") if hasattr(obj, "Count"): return obj.Count return len(obj) def __getitem__(self, key): """Allow indexing if wrapped object supports it.""" obj = object.__getattribute__(self, "_obj") return obj[key] def __contains__(self, item): """Support 'in' operator.""" obj = object.__getattribute__(self, "_obj") if isinstance(item, PythonicWrapper): item = object.__getattribute__(item, "_obj") if hasattr(obj, "Contains"): return obj.Contains(item) return item in obj @property def _unwrap(self): """Get the underlying LCM object.""" return object.__getattribute__(self, "_obj")
[docs] def wrap(obj): """ Wrap an LCM object for pythonic property access. Args: obj: LibLCM object (ILexEntry, ILexSense, etc.) Returns: PythonicWrapper: Wrapped object with suffix-free property access Example:: entry = wrap(raw_entry) for sense in entry.Senses: # Uses SensesOS internally print(sense) """ if obj is None: return None if isinstance(obj, PythonicWrapper): return obj # Already wrapped return PythonicWrapper(obj)
[docs] def unwrap(obj): """ Get the underlying LCM object from a wrapper. Args: obj: PythonicWrapper or raw LCM object Returns: The underlying LCM object Example:: raw = unwrap(wrapped_entry) """ if isinstance(obj, PythonicWrapper): return object.__getattribute__(obj, "_obj") return obj
# Shorthand alias p = wrap