Source code for flexicon.code.Grammar.affix_slot
#
# affix_slot.py
#
# Class: AffixSlot
# Wrapper for inflectional affix slot objects providing unified
# interface access to slot properties and the affixes that fill
# them.
#
# Platform: Python.NET
# FieldWorks Version 9+
#
# Copyright 2026
#
"""
Wrapper class for affix slot objects with unified interface.
This module provides AffixSlot, a wrapper class that transparently handles
inflectional affix slot objects (MoInflAffixSlot), providing convenient
access to a slot's name, whether it is optional, and the affixes that fill
it.
Problem:
``IMoInflAffixSlot.Name`` is an ``IMultiUnicode``, not a string, so
printing it directly (``print(slot.Name)``) does not produce readable
text -- issue #542. Callers had no public way to read a slot's name,
optionality, or the affixes filling it without dropping to raw LCM
access.
Solution:
AffixSlot wrapper provides:
- ``.name`` -- the slot name as a plain string (analysis WS, ``***``
normalized to empty)
- ``.optional`` -- bool
- ``.affixes`` -- the ``IMoInflAffMsa`` objects whose ``SlotsRC``
contains this slot, read via the direct ``IMoInflAffixSlot.Affixes``
back-reference
- ``.owner_pos`` -- the owning ``IPartOfSpeech``, if any
Example::
from flexicon.code.Grammar.affix_slot import AffixSlot
slot = posOps.GetAffixSlots(verb)[0] # already an AffixSlot
print(slot.name)
print(slot.optional)
for affix_msa in slot.affixes:
print(affix_msa.Hvo)
"""
from ..Shared.wrapper_base import LCMObjectWrapper
try:
from SIL.LCModel import IPartOfSpeech, PartOfSpeechTags
except ImportError:
# Mock for testing without FieldWorks installed
IPartOfSpeech = None
class PartOfSpeechTags:
kClassId = 0
[docs]
class AffixSlot(LCMObjectWrapper):
"""
Wrapper for inflectional affix slot objects providing unified
interface access.
Handles MoInflAffixSlot objects transparently, providing common
properties without exposing ClassName or casting.
Attributes:
_obj: The base interface object (IMoInflAffixSlot)
_concrete: The concrete type object (IMoInflAffixSlot)
Example::
slot = posOps.GetAffixSlots(verb)[0]
print(slot.name)
if slot.optional:
print("Slot may be left empty")
"""
def __init__(self, lcm_slot):
"""
Initialize AffixSlot wrapper with a slot object.
Args:
lcm_slot: An IMoInflAffixSlot object. Typically from
``POSOperations.GetAffixSlots()`` or
``AffixTemplate.prefix_slots`` / ``.suffix_slots`` /
``.proclitic_slots`` / ``.enclitic_slots``.
Example::
slot = posOps.GetAffixSlots(verb)[0]
wrapped = AffixSlot(slot)
"""
super().__init__(lcm_slot)
@property
def name(self) -> str:
"""
Get the slot's name.
Returns:
str: The slot name in the analysis writing system, or empty
string if not set (including FLEx's ``***`` null marker).
Example::
print(f"Slot: {slot.name}")
"""
from ..Shared.string_utils import normalize_text
from SIL.LCModel.Core.KernelInterfaces import ITsString
name_multistring = self._obj.Name
if not name_multistring:
return ""
default_ws = self._obj.Cache.DefaultAnalWs
name_text = ITsString(name_multistring.get_String(default_ws)).Text
return normalize_text(name_text)
@property
def optional(self) -> bool:
"""
Check whether the slot may be left empty.
Returns:
bool: True if the slot is optional, False if it is obligatory
(FLEx's default for a newly-created slot).
Example::
if slot.optional:
print("This slot may be left empty")
"""
try:
return bool(self._concrete.Optional)
except Exception:
return False
@property
def affixes(self) -> "list[object]":
"""
Get the inflectional-affix MSAs that fill this slot.
Returns:
list: ``IMoInflAffMsa`` objects whose ``SlotsRC`` contains this
slot, read via the direct ``IMoInflAffixSlot.Affixes``
back-reference. Empty list if none.
Example::
for affix_msa in slot.affixes:
print(affix_msa.Hvo)
Notes:
- Access via the ``Affixes`` back-reference property on the
concrete interface (confirmed by live reflection: returns
``IEnumerable<IMoInflAffMsa>``); this is the single most
direct LCM path, since it is already the inverse of
``IMoInflAffMsa.SlotsRC``.
"""
try:
if hasattr(self._concrete, "Affixes"):
affixes = self._concrete.Affixes
return list(affixes) if affixes else []
return []
except Exception:
return []
@property
def owner_pos(self):
"""
Get the owner Part of Speech for this slot.
Returns:
IPartOfSpeech or None: The owner POS, or None if not available.
Example::
if slot.owner_pos:
print(f"Slot for POS: {slot.owner_pos.Name}")
"""
if not hasattr(self._concrete, "OwnerOfClass"):
return None
pos_lcm = self._concrete.OwnerOfClass(PartOfSpeechTags.kClassId)
if pos_lcm is None:
return None
return IPartOfSpeech(pos_lcm) if IPartOfSpeech is not None else pos_lcm
def __repr__(self):
"""String representation showing slot name."""
return f"AffixSlot({self.name or 'Unnamed'})"
def __str__(self):
"""Human-readable description."""
suffix = " (optional)" if self.optional else ""
if self.name:
return f"Slot '{self.name}'{suffix}"
return f"Unnamed slot{suffix}"