#
# WfiMorphBundleOperations.py
#
# Class: WfiMorphBundleOperations
# Morpheme bundle operations for FieldWorks Language Explorer
# projects via SIL Language and Culture Model (LCM) API.
#
# Platform: Python.NET
# FieldWorks Version 9+
#
# Copyright 2025
#
import logging
# Import FLEx LCM types
from SIL.LCModel import (
IWfiMorphBundle,
IWfiMorphBundleFactory,
IWfiAnalysis,
ILexSense,
IMoForm,
IMoMorphSynAnalysis,
IMoInflClass,
)
from SIL.LCModel.Core.KernelInterfaces import ITsString
from SIL.LCModel.Core.Text import TsStringUtils
# Import flexlibs exceptions
from ..FLExProject import (
FP_ParameterError,
)
from ..BaseOperations import BaseOperations, OperationsMethod, wrap_enumerable
from ..lcm_casting import (
cast_to_concrete,
get_inflection_class_from_msa,
set_inflection_class_on_msa,
INFLECTION_CLASS_BEARING_MSA_CLASSES,
)
logger = logging.getLogger(__name__)
[docs]
class WfiMorphBundleOperations(BaseOperations):
"""
This class provides operations for managing morpheme bundles in a FieldWorks project.
Morpheme bundles (IWfiMorphBundle) represent individual morphemes within a wordform
analysis. Each bundle contains the morpheme's form, gloss, and links to lexical
information (sense, MSA, morph type, inflection class).
In FLEx's interlinear text system, a wordform analysis consists of multiple
morph bundles that break down the word into its constituent morphemes. For example,
the word "running" might have two bundles: "run" (stem) + "-ing" (suffix).
Usage::
from flexicon import FLExProject
project = FLExProject()
project.OpenProject("my project", writeEnabled=True)
# Get a wordform and its analysis
wordform = project.Wordforms.Find("running")
analyses = project.Wordforms.GetAnalyses(wordform)
if analyses:
analysis = analyses[0]
# Get all morph bundles for the analysis
bundles = project.MorphBundles.GetAll(analysis)
for bundle in bundles:
form = project.MorphBundles.GetForm(bundle)
gloss = project.MorphBundles.GetGloss(bundle)
print(f"{form} - {gloss}")
# Create a new morph bundle
bundle = project.MorphBundles.Create(analysis)
project.MorphBundles.SetForm(bundle, "run")
project.MorphBundles.SetGloss(bundle, "run")
# Link to lexicon
sense = project.LexiconAllSenses()[0]
project.MorphBundles.SetSense(bundle, sense)
project.CloseProject()
"""
def __init__(self, project):
"""
Initialize WfiMorphBundleOperations with a FLExProject instance.
Args:
project: The FLExProject instance to operate on.
"""
super().__init__(project)
def _GetSequence(self, parent):
"""Specify which sequence to reorder for morph bundles.
``parent`` is an ``IWfiAnalysis``; the ordered sequence of morph
bundles lives on its ``MorphBundlesOS`` property (there is no
``MorphsOS`` on ``IWfiAnalysis``).
"""
return parent.MorphBundlesOS
# ==================== CORE CRUD OPERATIONS ====================
@wrap_enumerable
@OperationsMethod
def GetAll(self, analysis_or_hvo):
"""
Get all morph bundles for a wordform analysis.
Args:
analysis_or_hvo: The IWfiAnalysis object or HVO.
Returns:
EnumerableWrapper[IWfiMorphBundle]: Each morph bundle in the analysis.
Raises:
FP_NullParameterError: If analysis_or_hvo is None.
Example:
>>> morphBundleOps = WfiMorphBundleOperations(project)
>>> analysis = analyses[0]
>>> for bundle in morphBundleOps.GetAll(analysis):
... form = morphBundleOps.GetForm(bundle)
... gloss = morphBundleOps.GetGloss(bundle)
... print(f"{form} - {gloss}")
run - run
-ing - PROG
Notes:
- Returns bundles in the order they appear in the analysis
- Order represents left-to-right morpheme sequence
- Returns empty generator if analysis has no morph bundles
- Each bundle represents a single morpheme in the word
See Also:
Create, Delete, Reorder
"""
self._ValidateParam(analysis_or_hvo, "analysis_or_hvo")
analysis = self.__GetAnalysisObject(analysis_or_hvo)
# Yield all morph bundles in sequence
for bundle in analysis.MorphBundlesOS:
yield bundle
@OperationsMethod
def Create(self, analysis_or_hvo, guid=None):
"""
Create a new morph bundle for a wordform analysis.
Args:
analysis_or_hvo: The IWfiAnalysis object or HVO.
guid (optional): GUID to assign to the new morph bundle, as a
``System.Guid`` or string. Use this when REPRODUCING a
morph bundle from another project so it keeps its original
identity. None (the default) mints a fresh GUID.
Returns:
IWfiMorphBundle: The newly created morph bundle object.
Raises:
FP_ReadOnlyError: If the project is not opened with write enabled.
FP_NullParameterError: If analysis_or_hvo is None.
Example:
>>> morphBundleOps = WfiMorphBundleOperations(project)
>>> analysis = analyses[0]
>>> bundle = morphBundleOps.Create(analysis)
>>> morphBundleOps.SetForm(bundle, "run")
>>> morphBundleOps.SetGloss(bundle, "run")
>>> # Create multiple bundles for complex morphology
>>> stem_bundle = morphBundleOps.Create(analysis)
>>> morphBundleOps.SetForm(stem_bundle, "walk")
>>> affix_bundle = morphBundleOps.Create(analysis)
>>> morphBundleOps.SetForm(affix_bundle, "-ed")
Notes:
- The bundle is added to the end of the analysis's bundle sequence
- New bundles have no form, gloss, or lexical links by default
- Use SetForm(), SetGloss(), SetSense(), etc. to populate
- Use Reorder() to change the sequence if needed
See Also:
Delete, GetAll, SetForm, SetGloss
"""
self._EnsureWriteEnabled()
self._ValidateParam(analysis_or_hvo, "analysis_or_hvo")
analysis = self.__GetAnalysisObject(analysis_or_hvo)
with self._TransactionCM("Create morph bundle"):
# Create the new morph bundle using the factory
factory = self.project.project.ServiceLocator.GetService(IWfiMorphBundleFactory)
bundle = self._CreateWithGuid(factory, guid, "IWfiMorphBundle")
# Add to analysis's morph bundles collection
analysis.MorphBundlesOS.Add(bundle)
return bundle
@OperationsMethod
def Delete(self, bundle_or_hvo):
"""
Delete a morph bundle from its analysis.
Args:
bundle_or_hvo: The IWfiMorphBundle object or HVO to delete.
Raises:
FP_ReadOnlyError: If the project is not opened with write enabled.
FP_NullParameterError: If bundle_or_hvo is None.
Example:
>>> morphBundleOps = WfiMorphBundleOperations(project)
>>> analysis = analyses[0]
>>> bundles = list(morphBundleOps.GetAll(analysis))
>>> if len(bundles) > 1:
... # Delete the last bundle
... morphBundleOps.Delete(bundles[-1])
Warning:
- Deletion is permanent and cannot be undone
- Deleting a bundle changes the morphological analysis
- May affect concordance and interlinear text displays
- Consider whether analysis should be modified or replaced
Notes:
- Removes the bundle from the owning analysis
- Other bundles in the analysis are not affected
- The bundle is removed from the database
See Also:
Create, GetAll, Reorder
"""
self._EnsureWriteEnabled()
self._ValidateParam(bundle_or_hvo, "bundle_or_hvo")
bundle = self.__GetBundleObject(bundle_or_hvo)
# Cast owner to its concrete IWfiAnalysis so MorphBundlesOS is
# reachable. Raw bundle.Owner is typed as ICmObject and
# hasattr(...,"MorphBundlesOS") returns False there, which is why
# the prior implementation silently no-opped.
owner = self._GetTypedOwner(bundle)
if owner is None:
raise FP_ParameterError("Morph bundle has no owning analysis")
with self._TransactionCM("Delete morph bundle"):
owner.MorphBundlesOS.Remove(bundle)
@OperationsMethod
def Duplicate(self, item_or_hvo, insert_after=True, deep=False):
"""
Duplicate a morph bundle, creating a new copy with a new GUID.
Args:
item_or_hvo: The IWfiMorphBundle object or HVO to duplicate.
insert_after (bool): If True (default), insert after the source bundle.
If False, insert at end of analysis's bundle sequence.
deep (bool): Accepted for API uniformity across Operations classes. WfiMorphBundle has no owned objects, so this parameter is ignored.
Returns:
IWfiMorphBundle: The newly created duplicate bundle with a new GUID.
Raises:
FP_ReadOnlyError: If the project is not opened with write enabled.
FP_NullParameterError: If item_or_hvo is None.
Example:
>>> morphBundleOps = WfiMorphBundleOperations(project)
>>> analysis = analyses[0]
>>> bundles = list(morphBundleOps.GetAll(analysis))
>>> if bundles:
... # Duplicate bundle
... dup = morphBundleOps.Duplicate(bundles[0])
... print(f"Original: {morphBundleOps.GetGuid(bundles[0])}")
... print(f"Duplicate: {morphBundleOps.GetGuid(dup)}")
Original: 12345678-1234-1234-1234-123456789abc
Duplicate: 87654321-4321-4321-4321-cba987654321
...
... # Verify content was copied
... print(f"Form: {morphBundleOps.GetForm(dup)}")
... print(f"Gloss: {morphBundleOps.GetGloss(dup)}")
Notes:
- Factory.Create() automatically generates a new GUID
- insert_after=True preserves the original bundle's position in sequence
- Simple properties copied: Form, Gloss (MultiStrings)
- Reference properties copied: SenseRA, MsaRA, MorphRA
- IWfiMorphBundle has no InflClassRA member of its own; the
inflection class lives on the MSA (IMoStemMsa.InflectionClassRA)
and rides along automatically because MsaRA is copied by
reference, not cloned. See GetInflectionClass.
- WfiMorphBundle has no owned objects, so deep parameter has no effect
- Useful for creating similar morpheme analyses or templates
See Also:
Create, Delete, GetGuid
"""
self._EnsureWriteEnabled()
self._ValidateParam(item_or_hvo, "item_or_hvo")
# Get source bundle and parent; MorphBundlesOS is declared on IWfiAnalysis
# so cast directly instead of doing a wasteful Hvo round-trip.
source = self.__GetBundleObject(item_or_hvo)
parent = IWfiAnalysis(source.Owner)
with self._TransactionCM("Duplicate morph bundle"):
# Create new bundle using factory (auto-generates new GUID)
factory = self.project.project.ServiceLocator.GetService(IWfiMorphBundleFactory)
duplicate = factory.Create()
# Determine insertion position
if insert_after:
# Index by HVO (issue #533). MorphBundlesOS can yield bare interface
# views whose Python identity differs from source.
bundle_list = list(parent.MorphBundlesOS)
target_hvo = source.Hvo
source_index = None
for i, b in enumerate(bundle_list):
if b.Hvo == target_hvo:
source_index = i
break
if source_index is None:
insert_index = len(bundle_list)
else:
insert_index = source_index + 1
parent.MorphBundlesOS.Insert(insert_index, duplicate)
else:
# Insert at end
parent.MorphBundlesOS.Add(duplicate)
# Copy MultiString properties. IWfiMorphBundle has Form but
# NOT Gloss -- the displayed gloss comes from SenseRA.Gloss,
# which the SenseRA assignment below preserves automatically.
# The previous duplicate.Gloss.CopyAlternatives line raised
# AttributeError on every call (same root bug as #16 / #108);
# 4319886 fixed GetGloss/SetGloss but missed this site.
# (issue #107)
duplicate.Form.CopyAlternatives(source.Form)
# Copy Reference Atomic (RA) properties
if hasattr(source, "SenseRA") and source.SenseRA:
duplicate.SenseRA = source.SenseRA
if hasattr(source, "MsaRA") and source.MsaRA:
duplicate.MsaRA = source.MsaRA
if hasattr(source, "MorphRA") and source.MorphRA:
duplicate.MorphRA = source.MorphRA
# No separate inflection-class copy is needed: IWfiMorphBundle
# has no InflClassRA member (the "hasattr(source, 'InflClassRA')"
# guard that used to gate this block was always False, and the
# dead body underneath it wrote a nonexistent
# `duplicate.InflClassRA`). The inflection class lives on the
# MSA (IMoStemMsa.InflectionClassRA), and the MsaRA assignment
# just above already copies that MSA REFERENCE (not a clone),
# so the duplicate bundle automatically sees the same
# inflection class its source sees, via the same MSA object.
# See get_inflection_class_from_msa() in lcm_casting.py for the
# read path (issue #259 / lcm-member-truth-sweep C10/T4.2).
# Note: WfiMorphBundle has no owned objects (OS collections), so deep has no effect
return duplicate
# ========== SYNC INTEGRATION METHODS ==========
@OperationsMethod
def GetSyncableProperties(self, item):
"""
Get all syncable properties of a morpheme bundle.
Args:
item: The IWfiMorphBundle object.
Returns:
dict: Dictionary of syncable properties with their values.
Example:
>>> props = project.WfiMorphBundles.GetSyncableProperties(bundle)
>>> print(props['Form'])
{'en': 'run'}
>>> print(props['SenseRA'])
'abc123...' # GUID of linked sense
Notes:
- MultiString properties: Form
(IWfiMorphBundle has no Gloss field of its own; the
displayed gloss is derived from SenseRA.Gloss -- see
GetGloss for the read path.)
- Reference Atomic properties: SenseRA, MsaRA, MorphRA (GUIDs)
- InflClassRA (GUID) is also included, but IWfiMorphBundle has
no InflClassRA member of its own -- the value is read from
the linked MSA (IMoStemMsa.InflectionClassRA) via
get_inflection_class_from_msa(). The "InflClassRA" key name
is kept for sync-format stability even though the LCM
navigation path has changed.
"""
props = {}
# MultiString properties
if hasattr(item, "Form") and item.Form:
props["Form"] = self.project.GetMultiStringDict(item.Form)
# Reference Atomic properties (return GUIDs)
if hasattr(item, "SenseRA") and item.SenseRA:
props["SenseRA"] = str(item.SenseRA.Guid)
if hasattr(item, "MsaRA") and item.MsaRA:
props["MsaRA"] = str(item.MsaRA.Guid)
if hasattr(item, "MorphRA") and item.MorphRA:
props["MorphRA"] = str(item.MorphRA.Guid)
# IWfiMorphBundle has no InflClassRA member of its own -- the
# inflection class lives on the linked MSA
# (IMoStemMsa.InflectionClassRA). The "InflClassRA" key name is
# kept for sync-format stability: existing sync payloads/diff tools
# already key on it, and the field's *meaning* to a sync consumer
# (the bundle's effective inflection class) is unchanged -- only
# the LCM navigation path used to compute the value has changed.
# See get_inflection_class_from_msa() (issue #259 /
# lcm-member-truth-sweep C10/T4.3).
infl_class = get_inflection_class_from_msa(getattr(item, "MsaRA", None))
if infl_class is not None:
props["InflClassRA"] = str(infl_class.Guid)
return props
@OperationsMethod
def CompareTo(self, item1, item2, ops1=None, ops2=None):
"""
Compare two morpheme bundles for differences.
Args:
item1: First bundle object (from project 1)
item2: Second bundle object (from project 2)
ops1: Optional WfiMorphBundleOperations instance for project 1 (defaults to self)
ops2: Optional WfiMorphBundleOperations instance for project 2 (defaults to self)
Returns:
tuple: (is_different, differences_dict)
- is_different (bool): True if bundles differ, False if identical
- differences_dict (dict): Maps property names to (value1, value2) tuples
Example:
>>> is_diff, diffs = ops1.CompareTo(bundle1, bundle2, ops1, ops2)
>>> if is_diff:
... for prop, (val1, val2) in diffs.items():
... print(f"{prop}: {val1} != {val2}")
Notes:
- Compares the Form MultiString (IWfiMorphBundle has no
Gloss field of its own; see GetSyncableProperties)
- Compares reference properties by GUID
- Empty/null values are treated as equivalent
"""
if ops1 is None:
ops1 = self
if ops2 is None:
ops2 = self
props1 = ops1.GetSyncableProperties(item1)
props2 = ops2.GetSyncableProperties(item2)
differences = {}
# Get all property keys from both items
all_keys = set(props1.keys()) | set(props2.keys())
for key in all_keys:
val1 = props1.get(key)
val2 = props2.get(key)
# Compare values inline: FLExProject has no _CompareValues
# member (calling it raised AttributeError on every compare;
# same fix as MediaOperations.CompareTo).
if val1 != val2:
# Values are different
differences[key] = (val1, val2)
is_different = len(differences) > 0
return (is_different, differences)
@OperationsMethod
def Reorder(self, analysis_or_hvo, bundle_list):
"""
Reorder morph bundles within an analysis.
Args:
analysis_or_hvo: The IWfiAnalysis object or HVO.
bundle_list: List of IWfiMorphBundle objects in the desired order.
Raises:
FP_ReadOnlyError: If the project is not opened with write enabled.
FP_NullParameterError: If analysis_or_hvo or bundle_list is None.
FP_ParameterError: If bundle_list is invalid.
Example:
>>> morphBundleOps = WfiMorphBundleOperations(project)
>>> analysis = analyses[0]
>>> bundles = list(morphBundleOps.GetAll(analysis))
>>> # Reverse the order
>>> morphBundleOps.Reorder(analysis, bundles[::-1])
>>> # Move a specific bundle to the front
>>> bundles = list(morphBundleOps.GetAll(analysis))
>>> affix = bundles[1]
>>> stem = bundles[0]
>>> morphBundleOps.Reorder(analysis, [affix, stem])
Notes:
- The bundle_list must contain all bundles from the analysis
- Bundles from other analyses cannot be added this way
- Order affects morpheme sequence in interlinear displays
- Use carefully - incorrect ordering affects linguistic analysis
See Also:
GetAll, Create, Delete
"""
self._EnsureWriteEnabled()
self._ValidateParam(analysis_or_hvo, "analysis_or_hvo")
self._ValidateParam(bundle_list, "bundle_list")
analysis = self.__GetAnalysisObject(analysis_or_hvo)
# Validate that all bundles belong to this analysis
current_bundles = set(analysis.MorphBundlesOS)
new_bundles = set(bundle_list)
if current_bundles != new_bundles:
raise FP_ParameterError("Bundle list must contain exactly the same bundles as the analysis")
with self._TransactionCM("Reorder morph bundles"):
self._ApplySequenceOrder(analysis.MorphBundlesOS, list(bundle_list))
# ==================== FORM & GLOSS OPERATIONS ====================
@OperationsMethod
def GetForm(self, bundle_or_hvo, wsHandle=None):
"""
Get the morpheme form of a bundle.
Args:
bundle_or_hvo: The IWfiMorphBundle object or HVO.
wsHandle: Optional writing system handle. Defaults to vernacular WS.
Returns:
str: The morpheme form, or empty string if not set.
Raises:
FP_NullParameterError: If bundle_or_hvo is None.
Example:
>>> morphBundleOps = WfiMorphBundleOperations(project)
>>> bundles = list(morphBundleOps.GetAll(analysis))
>>> if bundles:
... form = morphBundleOps.GetForm(bundles[0])
... print(form)
run
>>> # Get form in specific writing system
>>> form_ipa = morphBundleOps.GetForm(bundles[0],
... project.WSHandle('en-fonipa'))
>>> print(form_ipa)
rʌn
Notes:
- Form represents the surface morpheme as it appears in text
- Returns empty string if form not set in specified writing system
- Typically uses vernacular writing system
- May include affixation markers like "-", "pre-", etc.
See Also:
SetForm, GetGloss, GetAll
"""
self._ValidateParam(bundle_or_hvo, "bundle_or_hvo")
bundle = self.__GetBundleObject(bundle_or_hvo)
wsHandle = self.__WSHandleVern(wsHandle)
form = ITsString(bundle.Form.get_String(wsHandle)).Text
return form or ""
@OperationsMethod
def SetForm(self, bundle_or_hvo, text, wsHandle=None):
"""
Set the morpheme form of a bundle.
Args:
bundle_or_hvo: The IWfiMorphBundle object or HVO.
text: The morpheme form text to set.
wsHandle: Optional writing system handle. Defaults to vernacular WS.
Raises:
FP_ReadOnlyError: If the project is not opened with write enabled.
FP_NullParameterError: If bundle_or_hvo or text is None.
Example:
>>> morphBundleOps = WfiMorphBundleOperations(project)
>>> bundles = list(morphBundleOps.GetAll(analysis))
>>> if bundles:
... morphBundleOps.SetForm(bundles[0], "walk")
... print(morphBundleOps.GetForm(bundles[0]))
walk
>>> # Set IPA pronunciation form
>>> morphBundleOps.SetForm(bundles[0], "wɔk",
... project.WSHandle('en-fonipa'))
Notes:
- Form represents how the morpheme appears in the text
- Can include affix markers (-, pre-, etc.)
- Should match actual text tokens or morpheme segmentation
- Use different writing systems for pronunciation variants
See Also:
GetForm, SetGloss, Create
"""
self._EnsureWriteEnabled()
self._ValidateParam(bundle_or_hvo, "bundle_or_hvo")
self._ValidateParam(text, "text")
bundle = self.__GetBundleObject(bundle_or_hvo)
wsHandle = self.__WSHandleVern(wsHandle)
with self._TransactionCM(f"Set morph bundle form '{text}'"):
mkstr = TsStringUtils.MakeString(text, wsHandle)
bundle.Form.set_String(wsHandle, mkstr)
@OperationsMethod
def GetGloss(self, bundle_or_hvo, wsHandle=None):
"""
Get the morpheme gloss of a bundle.
Read-only convenience: a morpheme bundle has no free-standing
gloss field on IWfiMorphBundle itself. The displayed gloss
comes from the bundle's linked lexical sense
(``bundle.SenseRA.Gloss``). This method forwards to that path
and, if no sense gloss is found, falls back to the MSA-level
``InterlinearAbbr`` (``bundle.MsaRA.InterlinearAbbr``) which
covers grammatical morphemes whose gloss lives on the MSA.
Args:
bundle_or_hvo: The IWfiMorphBundle object or HVO.
wsHandle: Optional writing system handle. Defaults to analysis WS.
Returns:
str: The linked sense's gloss in the chosen WS; if the sense
gloss is empty or absent, the MSA InterlinearAbbr; or an
empty string if neither is available.
Raises:
FP_NullParameterError: If bundle_or_hvo is None.
Example:
>>> morphBundleOps = WfiMorphBundleOperations(project)
>>> bundles = list(morphBundleOps.GetAll(analysis))
>>> if bundles:
... gloss = morphBundleOps.GetGloss(bundles[0])
... print(gloss)
run
Notes:
- Returns "" only when neither a sense gloss nor an MSA
InterlinearAbbr is available.
- Uses the analysis writing system by default (sense gloss
only; InterlinearAbbr is WS-independent).
- The sense-gloss path honours the wsHandle; the MSA fallback
returns the LCM-computed abbreviation string directly.
See Also:
GetForm, GetSense
"""
self._ValidateParam(bundle_or_hvo, "bundle_or_hvo")
bundle = self.__GetBundleObject(bundle_or_hvo)
wsHandle = self.__WSHandleAnal(wsHandle)
# IWfiMorphBundle has no Gloss field; the displayed gloss comes
# from the linked sense's MultiUnicode Gloss. (issue #16)
sense = bundle.SenseRA
if sense is not None:
gloss = ITsString(sense.Gloss.get_String(wsHandle)).Text
if gloss:
return gloss
# Fallback for grammatical morphemes (issue #110): if the bundle has
# an MsaRA, use InterlinearAbbr which is defined on IMoMorphSynAnalysis
# and covers all four concrete MSA types (MoStemMsa, MoDerivAffMsa,
# MoInflAffMsa, MoUnclassifiedAffixMsa).
msa = bundle.MsaRA
if msa is not None:
abbr = msa.InterlinearAbbr
if abbr:
return abbr
return ""
@OperationsMethod
def SetGloss(self, bundle_or_hvo, text, wsHandle=None):
"""
Setting a gloss on a morpheme bundle is not supported.
IWfiMorphBundle has no Gloss field of its own; the displayed
gloss is derived from the bundle's linked sense
(``bundle.SenseRA.Gloss``). Writing to that path would mutate
the lexical sense for every wordform/analysis that references
it, which is a surprising side effect to attach to a
per-bundle setter. This method therefore refuses with a clear
redirect rather than performing a hidden write.
To change the gloss seen on a bundle, locate the linked sense
and update its gloss directly:
sense = morphBundleOps.GetSense(bundle)
if sense is not None:
project.Senses.SetGloss(sense, text, wsHandle=wsHandle)
Raises:
NotImplementedError: Always, with a message pointing at the
LexSense path. The exception class signals an unsupported
operation (capability refusal), not a bad argument.
"""
raise NotImplementedError(
"WfiMorphBundleOperations.SetGloss is not supported: "
"IWfiMorphBundle has no Gloss field, and writing through "
"SenseRA.Gloss would mutate the shared lexical sense. "
"Use LexSenseOperations.SetGloss on bundle.SenseRA "
"(see GetSense) when you really intend to change the sense's "
"gloss for all bundles that reference it."
)
# ==================== LEXICAL LINK OPERATIONS ====================
@OperationsMethod
def GetSense(self, bundle_or_hvo):
"""
Get the linked lexical sense for a morph bundle.
Args:
bundle_or_hvo: The IWfiMorphBundle object or HVO.
Returns:
ILexSense or None: The linked sense object, or None if not linked.
Raises:
FP_NullParameterError: If bundle_or_hvo is None.
Example:
>>> morphBundleOps = WfiMorphBundleOperations(project)
>>> bundles = list(morphBundleOps.GetAll(analysis))
>>> if bundles:
... sense = morphBundleOps.GetSense(bundles[0])
... if sense:
... # Get sense gloss
... wsHandle = project.GetDefaultAnalysisWSHandle()
... sense_gloss = ITsString(sense.Gloss.get_String(wsHandle)).Text
... print(f"Linked to sense: {sense_gloss}")
Linked to sense: run
Notes:
- Returns None if bundle is not linked to lexicon
- Linking provides richer lexical information
- Linked sense can provide definition, part of speech, etc.
- Bundle gloss may derive from sense gloss
- Important for concordance and lexical queries
See Also:
SetSense, GetMSA, GetGloss
"""
self._ValidateParam(bundle_or_hvo, "bundle_or_hvo")
bundle = self.__GetBundleObject(bundle_or_hvo)
return bundle.SenseRA if bundle.SenseRA else None
@OperationsMethod
def SetSense(self, bundle_or_hvo, sense_or_hvo):
"""
Link a morph bundle to a lexical sense.
Args:
bundle_or_hvo: The IWfiMorphBundle object or HVO.
sense_or_hvo: The ILexSense object or HVO to link, or None to unlink.
Raises:
FP_ReadOnlyError: If the project is not opened with write enabled.
FP_NullParameterError: If bundle_or_hvo is None.
Example:
>>> morphBundleOps = WfiMorphBundleOperations(project)
>>> bundles = list(morphBundleOps.GetAll(analysis))
>>> if bundles:
... # Link to a lexical sense
... entries = list(project.LexiconAllEntries())
... senses = project.LexEntry.GetAllSenses(entries[0]) if entries else []
... if senses:
... morphBundleOps.SetSense(bundles[0], senses[0])
>>> # Unlink from sense
>>> morphBundleOps.SetSense(bundles[0], None)
Notes:
- Linking integrates text analysis with lexicon
- Linked sense provides definition, POS, and other information
- Setting to None breaks the lexical link
- Consider also setting MSA when linking sense
- Link is used in concordance views and lexical queries
See Also:
GetSense, SetMSA, SetGloss
"""
self._EnsureWriteEnabled()
self._ValidateParam(bundle_or_hvo, "bundle_or_hvo")
bundle = self.__GetBundleObject(bundle_or_hvo)
# Resolution stays OUTSIDE the transaction so an unresolvable
# reference raises without opening an empty undo task.
if sense_or_hvo is None:
sense = None
else:
sense = self.__GetSenseObject(sense_or_hvo)
with self._TransactionCM("Set morph bundle sense"):
bundle.SenseRA = sense
@OperationsMethod
def GetMorphType(self, bundle_or_hvo):
"""
Get the morpheme type of a bundle's linked allomorph.
Note this is NOT the same object as :meth:`GetMorph`: the bundle's
``MorphRA`` field holds the specific allomorph (``IMoForm``) it
links to, and the morph *type* (stem, prefix, suffix, etc.) lives
one hop further, at ``MorphRA.MorphTypeRA``. This method resolves
that hop for you.
Args:
bundle_or_hvo: The IWfiMorphBundle object or HVO.
Returns:
IMoMorphType or None: The morpheme type (stem, prefix, suffix,
etc.) of the bundle's linked allomorph, or
None if the allomorph has no type set.
Raises:
FP_NullParameterError: If bundle_or_hvo is None.
Example:
>>> morphBundleOps = WfiMorphBundleOperations(project)
>>> bundles = list(morphBundleOps.GetAll(analysis))
>>> if bundles:
... morphType = morphBundleOps.GetMorphType(bundles[0])
... if morphType:
... type_name = morphType.Name.BestAnalysisAlternative.Text
... print(type_name)
stem
Notes:
- Returns None (silently) if the bundle has a linked allomorph
but that allomorph has no morph type set -- an ordinary,
common state.
- Returns None and logs a warning naming the bundle's Hvo if
the bundle has no linked allomorph at all (MorphRA is None):
a bundle with no linked morph is a structurally incomplete
analysis, so the gap is made discoverable.
- Morpheme types include: stem, root, prefix, suffix, infix,
circumfix, clitic, proclitic, enclitic, simulfix, etc.
- Type indicates the morphological category
- Important for morphological analysis and parsing
See Also:
GetMorph, GetSense, GetMSA
"""
self._ValidateParam(bundle_or_hvo, "bundle_or_hvo")
bundle = self.__GetBundleObject(bundle_or_hvo)
morph = bundle.MorphRA
if morph is None:
logger.warning(
"GetMorphType: bundle Hvo=%s has no linked allomorph "
"(MorphRA is None); cannot resolve a morph type",
getattr(bundle, "Hvo", None),
)
return None
# MorphRA is statically typed IMoForm and MorphTypeRA is declared
# there, so it is visible via bare attribute access without a
# cast. Verified live against Sena 3 on 2026-09-06 (the 4.5.1
# precedent -- attribute visibility follows the static wrapper
# interface returned by the property, not the runtime CLR type,
# see CHANGELOG [4.5.1] -- made this worth checking): the bare
# and `IMoForm(morph)`-cast paths returned an identical Hvo, so
# no cast is required here. See
# specs/254-getmorphtype-allomorph/evidence/live-cycle2-fix.md.
return morph.MorphTypeRA if morph.MorphTypeRA else None
@OperationsMethod
def SetMorphType(self, bundle_or_hvo, morph_type_or_hvo):
"""
RETIRED. Always raises FP_ParameterError.
This method used to write its ``morph_type_or_hvo`` argument
directly into ``bundle.MorphRA``, which holds the bundle's linked
allomorph (``IMoForm``), not its morph type. Every non-None call
raised ``TypeError`` at the .NET boundary (pythonnet rejects an
``IMoMorphType`` where an ``IMoForm`` is declared) before any
write reached the LCM; no caller has ever successfully changed a
bundle's morph type through this method. The ``None`` form
(which nulled ``MorphRA``, i.e. cleared the *allomorph*, not the
type) is retired too, since keeping it would preserve a method
that clears an allomorph under a name saying "type".
Args:
bundle_or_hvo: The IWfiMorphBundle object or HVO. Ignored --
this method always raises before validating or resolving
any argument.
morph_type_or_hvo: Ignored. Kept in the signature only so
existing call sites reach this explanatory error instead
of a TypeError about arity.
Raises:
FP_ParameterError: Always, on every call, regardless of
argument values or the project's write-enabled state.
Example:
>>> # To retype the lexicon allomorph itself:
>>> project.Allomorphs.SetMorphType(allomorph, morph_type)
>>> # To change or clear which allomorph this bundle links to:
>>> morphBundleOps.SetMorph(bundles[1], allomorph_or_None)
Notes:
- This method never mutates a bundle; it raises unconditionally.
- The retirement is unconditional: it raises before checking
write-enabled state, so read-only and write-enabled projects
see the identical message.
See Also:
SetMorph, GetMorphType, AllomorphOperations.SetMorphType
"""
raise FP_ParameterError(
"SetMorphType() has been retired: it wrote its morph-type "
"argument into bundle.MorphRA, which holds the bundle's "
"allomorph (IMoForm), not its morph type -- every non-None "
"call raised TypeError at the .NET boundary and never wrote "
"anything. To retype the lexicon allomorph, use "
"project.Allomorphs.SetMorphType(allomorph, morph_type). To "
"change or clear which allomorph this bundle links, use "
"SetMorph(bundle, allomorph_or_None)."
)
@OperationsMethod
def GetMorph(self, bundle_or_hvo):
"""
Get the specific allomorph linked to a bundle.
Note this is NOT the same object as :meth:`GetMorphType`: this
returns the bundle's linked allomorph itself (``IMoForm`` --
concretely a ``MoStemAllomorph`` or ``MoAffixAllomorph``), not its
morph type. Use ``GetMorphType(bundle)`` (or
``GetMorph(bundle).MorphTypeRA``) for the type.
Args:
bundle_or_hvo: The IWfiMorphBundle object or HVO.
Returns:
IMoForm or None: The bundle's linked allomorph, or None if no
allomorph is linked (a real, non-rare state -- roughly 5%
of bundles in a populated project).
Raises:
FP_NullParameterError: If bundle_or_hvo is None.
Example:
>>> morphBundleOps = WfiMorphBundleOperations(project)
>>> bundles = list(morphBundleOps.GetAll(analysis))
>>> if bundles:
... morph = morphBundleOps.GetMorph(bundles[0])
... if morph:
... print(morph.ClassName)
MoStemAllomorph
Notes:
- Returns None silently if no allomorph is linked -- reporting
"no morph linked" is this getter's job, unlike GetMorphType,
which warns because a missing morph blocks the resolution
it was asked to perform.
- The returned object's concrete type is MoStemAllomorph or
MoAffixAllomorph (both IMoForm subtypes).
See Also:
SetMorph, GetMorphType, GetSense
"""
self._ValidateParam(bundle_or_hvo, "bundle_or_hvo")
bundle = self.__GetBundleObject(bundle_or_hvo)
return bundle.MorphRA if bundle.MorphRA else None
@OperationsMethod
def SetMorph(self, bundle_or_hvo, morph_or_hvo):
"""
Set the specific allomorph linked to a bundle.
Args:
bundle_or_hvo: The IWfiMorphBundle object or HVO.
morph_or_hvo: The IMoForm object or HVO to link, or None to
clear the link.
Raises:
FP_ReadOnlyError: If the project is not opened with write enabled.
FP_NullParameterError: If bundle_or_hvo is None.
FP_ParameterError: If morph_or_hvo resolves to an object that
is not an IMoForm (names the received ClassName), rather
than letting the raw pythonnet TypeError escape.
Example:
>>> morphBundleOps = WfiMorphBundleOperations(project)
>>> bundles = list(morphBundleOps.GetAll(analysis))
>>> allomorphs = list(project.Allomorphs.GetAll(entry))
>>> if bundles and allomorphs:
... morphBundleOps.SetMorph(bundles[0], allomorphs[0])
>>> # Clear the linked allomorph
>>> morphBundleOps.SetMorph(bundles[0], None)
Notes:
- Setting to None clears the linked allomorph.
- This does not change the bundle's morph type directly --
the type follows whatever the newly-linked allomorph's own
MorphTypeRA is set to.
See Also:
GetMorph, GetMorphType, SetSense
"""
self._EnsureWriteEnabled()
self._ValidateParam(bundle_or_hvo, "bundle_or_hvo")
bundle = self.__GetBundleObject(bundle_or_hvo)
# Resolution stays OUTSIDE the transaction so an unresolvable
# reference raises without opening an empty undo task.
if morph_or_hvo is None:
morph = None
else:
morph = self.__GetMorphObject(morph_or_hvo)
class_name = getattr(morph, "ClassName", None)
if not isinstance(morph, IMoForm):
raise FP_ParameterError(
"SetMorph: morph_or_hvo must resolve to an IMoForm "
f"(e.g. MoStemAllomorph, MoAffixAllomorph); received "
f"an object with ClassName={class_name!r}"
)
with self._TransactionCM("Set morph bundle morph"):
bundle.MorphRA = morph
# ==================== MSA OPERATIONS ====================
@OperationsMethod
def GetMSA(self, bundle_or_hvo):
"""
Get the Morpho-Syntactic Analysis (MSA) of a bundle.
Args:
bundle_or_hvo: The IWfiMorphBundle object or HVO.
Returns:
IMoMorphSynAnalysis or None: The MSA object, or None if not set.
Raises:
FP_NullParameterError: If bundle_or_hvo is None.
Example:
>>> morphBundleOps = WfiMorphBundleOperations(project)
>>> bundles = list(morphBundleOps.GetAll(analysis))
>>> if bundles:
... msa = morphBundleOps.GetMSA(bundles[0])
... if msa:
... print(f"MSA type: {msa.ClassName}")
MSA type: MoStemMsa
Notes:
- Returns None if MSA not set
- MSA provides morpho-syntactic information
- Types include: MoStemMsa, MoInflAffMsa, MoDerivAffMsa, etc.
- Contains information like part of speech, features, etc.
- Usually comes from linked lexical entry/sense
- Critical for grammatical analysis
See Also:
SetMSA, GetSense, GetMorphType
"""
self._ValidateParam(bundle_or_hvo, "bundle_or_hvo")
bundle = self.__GetBundleObject(bundle_or_hvo)
return bundle.MsaRA if bundle.MsaRA else None
@OperationsMethod
def SetMSA(self, bundle_or_hvo, msa_or_hvo):
"""
Set the Morpho-Syntactic Analysis (MSA) of a bundle.
Args:
bundle_or_hvo: The IWfiMorphBundle object or HVO.
msa_or_hvo: The IMoMorphSynAnalysis object or HVO, or None to unset.
Raises:
FP_ReadOnlyError: If the project is not opened with write enabled.
FP_NullParameterError: If bundle_or_hvo is None.
Example:
>>> morphBundleOps = WfiMorphBundleOperations(project)
>>> bundles = list(morphBundleOps.GetAll(analysis))
>>> if bundles:
... # Get MSA from a lexical sense
... sense = morphBundleOps.GetSense(bundles[0])
... if sense:
... entry = sense.Owner.Owner # Get owning entry
... if entry.MorphoSyntaxAnalysesOC.Count > 0:
... msa = entry.MorphoSyntaxAnalysesOC[0]
... morphBundleOps.SetMSA(bundles[0], msa)
>>> # Clear MSA
>>> morphBundleOps.SetMSA(bundles[0], None)
Notes:
- MSA provides grammatical category and features
- Setting to None clears the MSA reference
- MSA should be compatible with morpheme type
- Usually set in conjunction with sense
- Affects grammatical parsing and concordance
See Also:
GetMSA, SetSense, SetMorphType
"""
self._EnsureWriteEnabled()
self._ValidateParam(bundle_or_hvo, "bundle_or_hvo")
bundle = self.__GetBundleObject(bundle_or_hvo)
# Resolution stays OUTSIDE the transaction so an unresolvable
# reference raises without opening an empty undo task.
if msa_or_hvo is None:
msa = None
else:
msa = self.__GetMSAObject(msa_or_hvo)
with self._TransactionCM("Set morph bundle MSA"):
bundle.MsaRA = msa
# ==================== INFLECTION OPERATIONS ====================
@OperationsMethod
def GetInflType(self, bundle_or_hvo):
"""
Get the inflection type of a bundle.
Args:
bundle_or_hvo: The IWfiMorphBundle object or HVO.
Returns:
LexEntryInflType or None: The inflection type object, or None if not set.
Raises:
FP_NullParameterError: If bundle_or_hvo is None.
Example:
>>> morphBundleOps = WfiMorphBundleOperations(project)
>>> bundles = list(morphBundleOps.GetAll(analysis))
>>> if bundles:
... inflType = morphBundleOps.GetInflType(bundles[0])
... if inflType:
... wsHandle = project.GetDefaultAnalysisWSHandle()
... type_name = ITsString(inflType.Name.get_String(wsHandle)).Text
... print(f"Inflection type: {type_name}")
Inflection type: past tense
Notes:
- Returns None if inflection type not set
- Inflection type specifies the grammatical category
- Examples: past tense, plural, comparative, etc.
- References an ICmPossibility from the inflection type list
- Used in morphological analysis and parsing
- Complements the inflection class information
See Also:
SetInflType, GetInflectionClass, GetMSA
"""
self._ValidateParam(bundle_or_hvo, "bundle_or_hvo")
bundle = self.__GetBundleObject(bundle_or_hvo)
return bundle.InflTypeRA if bundle.InflTypeRA else None
@OperationsMethod
def SetInflType(self, bundle_or_hvo, infl_type_or_hvo):
"""
Set the inflection type of a bundle.
Args:
bundle_or_hvo: The IWfiMorphBundle object or HVO.
infl_type_or_hvo: The LexEntryInflType object or HVO (from
LexDb.VariantEntryTypesOA), or None to unset.
Raises:
FP_ReadOnlyError: If the project is not opened with write enabled.
FP_NullParameterError: If bundle_or_hvo is None.
Example:
>>> morphBundleOps = WfiMorphBundleOperations(project)
>>> bundles = list(morphBundleOps.GetAll(analysis))
>>> if bundles:
... # Get an inflection type (LexEntryInflType lives in
... # LexDb.VariantEntryTypesOA, accessed via VariantOperations)
... infl_types = [t for t in project.Variants.GetAllTypes()
... if t.ClassName == "LexEntryInflType"]
... if infl_types:
... morphBundleOps.SetInflType(bundles[0], infl_types[0])
>>> # Clear inflection type
>>> morphBundleOps.SetInflType(bundles[0], None)
Notes:
- Inflection type specifies the grammatical inflection
- Setting to None clears the type reference
- Type should match the morpheme's grammatical function
- May be automatically set when linking to lexical entry
- Affects morphological analysis and display
See Also:
GetInflType, SetInflectionClass, SetMSA
"""
self._EnsureWriteEnabled()
self._ValidateParam(bundle_or_hvo, "bundle_or_hvo")
bundle = self.__GetBundleObject(bundle_or_hvo)
# Resolution stays OUTSIDE the transaction so an unresolvable
# reference raises without opening an empty undo task.
if infl_type_or_hvo is None:
infl_type = None
else:
# Resolve to ICmPossibility object
if isinstance(infl_type_or_hvo, int):
infl_type = cast_to_concrete(
self.project.Object(infl_type_or_hvo)
)
else:
infl_type = cast_to_concrete(infl_type_or_hvo)
with self._TransactionCM("Set morph bundle inflection type"):
bundle.InflTypeRA = infl_type
@OperationsMethod
def GetInflectionClass(self, bundle_or_hvo):
"""
Get the inflection class of a bundle.
Args:
bundle_or_hvo: The IWfiMorphBundle object or HVO.
Returns:
IMoInflClass or None: The inflection class object, or None if not set.
Raises:
FP_NullParameterError: If bundle_or_hvo is None.
Example:
>>> morphBundleOps = WfiMorphBundleOperations(project)
>>> bundles = list(morphBundleOps.GetAll(analysis))
>>> if bundles:
... inflClass = morphBundleOps.GetInflectionClass(bundles[0])
... if inflClass:
... wsHandle = project.GetDefaultAnalysisWSHandle()
... class_name = ITsString(inflClass.Name.get_String(wsHandle)).Text
... print(f"Inflection class: {class_name}")
Inflection class: strong verb
Notes:
- IWfiMorphBundle has no InflClassRA member of its own. The
inflection class actually lives on the bundle's MSA
(IMoStemMsa.InflectionClassRA), and this method navigates
bundle.MsaRA -> cast_to_concrete() -> IMoStemMsa ->
InflectionClassRA on the caller's behalf (see
get_inflection_class_from_msa() in lcm_casting.py).
- Returns None if the bundle's MsaRA is null, if the MSA is a
non-stem subtype (MoDerivAffMsa, MoInflAffMsa,
MoUnclassifiedAffixMsa -- none of which carry a plain
InflectionClassRA), or if the stem MSA has no class set.
- Inflection classes categorize inflectional paradigms
- Examples: strong verb, weak verb, irregular, etc.
- Relevant mainly for inflectional morphology
- May affect paradigm generation and analysis
- Not applicable to all morpheme types
See Also:
SetInflectionClass, GetMSA, GetMorphType
"""
self._ValidateParam(bundle_or_hvo, "bundle_or_hvo")
bundle = self.__GetBundleObject(bundle_or_hvo)
# IWfiMorphBundle has no InflClassRA member of its own (issue #259
# / lcm-member-truth-sweep C10). The inflection class lives on the
# bundle's MSA (IMoStemMsa.InflectionClassRA); get_inflection_class_from_msa()
# navigates MsaRA -> cast -> narrow to IMoStemMsa -> InflectionClassRA,
# returning None for a null MsaRA or a non-stem MSA subtype without
# ever raising.
return get_inflection_class_from_msa(bundle.MsaRA)
@OperationsMethod
def SetInflectionClass(self, bundle_or_hvo, infl_class_or_hvo):
"""
Set the inflection class of a bundle.
Args:
bundle_or_hvo: The IWfiMorphBundle object or HVO.
infl_class_or_hvo: The IMoInflClass object or HVO, or None to unset.
Raises:
FP_ReadOnlyError: If the project is not opened with write enabled.
FP_NullParameterError: If bundle_or_hvo is None.
FP_ParameterError: If the bundle's MSA is null, or is not a
MoStemMsa (the only MSA subtype with a plain
InflectionClassRA) -- there is no writable target.
Example:
>>> morphBundleOps = WfiMorphBundleOperations(project)
>>> bundles = list(morphBundleOps.GetAll(analysis))
>>> if bundles:
... # Get an inflection class (owned per-POS, via project.POS)
... verb = project.POS.Find("Verb")
... classes = list(project.POS.GetInflectionClasses(verb)) if verb else []
... if classes:
... morphBundleOps.SetInflectionClass(bundles[0], classes[0])
>>> # Clear inflection class
>>> morphBundleOps.SetInflectionClass(bundles[0], None)
Notes:
- IWfiMorphBundle has no InflClassRA member of its own (issue
#259 / lcm-member-truth-sweep C10/C11). The inflection class
actually lives on the bundle's MSA
(IMoStemMsa.InflectionClassRA), reached via bundle.MsaRA --
this method navigates MsaRA -> cast_to_concrete() ->
IMoStemMsa -> InflectionClassRA on the caller's behalf (see
set_inflection_class_on_msa() in lcm_casting.py, and
GetInflectionClass for the read side of the same
navigation).
- IMPORTANT -- this is a per-MSA write, not a per-bundle write:
MoStemMsa is owned by the ILexEntry and is exactly what
LexSense.MorphoSyntaxAnalysisRA points to as "Grammatical
Info." WfiMorphBundle.MsaRA is a pure reference into that
*same* MSA object. Calling this method therefore changes the
inflection class for the shared MSA, and the new value is
immediately visible to every other morph bundle and lex
sense that references it -- not just bundle_or_hvo. This
matches FLEx's own UI behaviour (editing Inflection Class on
a sense's Grammatical Info Details is a lexicon-level edit),
and a print()-style NOTE is emitted each call to make the
fan-out visible to the caller.
- Raises FP_ParameterError rather than silently no-op'ing when
there is no writable target: a null MsaRA, or an MSA that is
not a MoStemMsa (MoInflAffMsa, MoUnclassifiedAffixMsa have no
inflection-class member at all; MoDerivAffMsa instead carries
From/ToInflectionClassRA -- a different pair of properties
this method deliberately does not touch).
- Inflection class specifies paradigm membership
- Setting to None clears the class reference
- Relevant for stems with inflectional variants
- May be inherited from lexical entry
- Affects paradigm generation and validation
See Also:
GetInflectionClass, SetMSA, SetMorphType
"""
self._EnsureWriteEnabled()
self._ValidateParam(bundle_or_hvo, "bundle_or_hvo")
bundle = self.__GetBundleObject(bundle_or_hvo)
# Resolution stays OUTSIDE the transaction so an unresolvable
# reference raises without opening an empty undo task.
if infl_class_or_hvo is None:
infl_class = None
else:
infl_class = self.__GetInflectionClassObject(infl_class_or_hvo)
# IWfiMorphBundle has no InflClassRA member of its own (issue #259
# / lcm-member-truth-sweep C10/C11). Validate the write target
# OUTSIDE the transaction, same as the reference-resolution above,
# so an unwritable MSA raises without opening an empty undo task.
msa = bundle.MsaRA
msa_class_name = getattr(msa, "ClassName", None)
if msa is None or msa_class_name not in INFLECTION_CLASS_BEARING_MSA_CLASSES:
msa_desc = "null" if msa is None else f"ClassName={msa_class_name}"
message = (
f"Cannot set inflection class: bundle's MSA is {msa_desc}; "
f"only MoStemMsa carries InflectionClassRA."
)
if msa_class_name == "MoDerivAffMsa":
message += (
" MoDerivAffMsa carries From/ToInflectionClassRA "
"instead (a different pair of properties this method "
"deliberately does not touch)."
)
raise FP_ParameterError(message)
# QUALITATIVE warning only -- deliberately not an exact fan-out
# count. Scanning IWfiMorphBundleRepository.AllInstances() per call
# would turn this per-object setter's O(n) usage into O(n^2) (issue
# #259 / lcm-member-truth-sweep C11 ruling, point 4). Uses the
# print()-style precedent (CLAUDE.md "Warn on Type Mismatch, Don't
# Block"), not warnings.warn().
print(
"NOTE: inflection class is stored on the shared MSA, not the "
"bundle; this change will be visible to every other morph "
"bundle and sense referencing the same MSA."
)
with self._TransactionCM("Set morph bundle inflection class"):
set_inflection_class_on_msa(msa, infl_class)
# ==================== UTILITY OPERATIONS ====================
@OperationsMethod
def GetOwningAnalysis(self, bundle_or_hvo):
"""
Get the wordform analysis that owns a morph bundle.
Args:
bundle_or_hvo: The IWfiMorphBundle object or HVO.
Returns:
IWfiAnalysis: The owning analysis object.
Raises:
FP_NullParameterError: If bundle_or_hvo is None.
Example:
>>> morphBundleOps = WfiMorphBundleOperations(project)
>>> bundles = list(morphBundleOps.GetAll(analysis))
>>> if bundles:
... owner = morphBundleOps.GetOwningAnalysis(bundles[0])
... print(f"Owner HVO: {owner.Hvo}")
Owner HVO: 12345
Notes:
- Every morph bundle belongs to exactly one analysis
- The owner is always an IWfiAnalysis object
- Useful for navigating from bundle back to analysis
- Owner relationship is fundamental to data structure
See Also:
GetAll, Create, Delete
"""
self._ValidateParam(bundle_or_hvo, "bundle_or_hvo")
bundle = self.__GetBundleObject(bundle_or_hvo)
# Cast to declared return type IWfiAnalysis. Raw bundle.Owner is typed
# as ICmObject in LCM; pythonnet only surfaces IWfiAnalysis properties
# (e.g. MorphBundlesOS) after the explicit interface cast.
return IWfiAnalysis(bundle.Owner)
@OperationsMethod
def GetGuid(self, bundle_or_hvo):
"""
Get the GUID (Globally Unique Identifier) of a morph bundle.
Args:
bundle_or_hvo: The IWfiMorphBundle object or HVO.
Returns:
str: The GUID as a string.
Raises:
FP_NullParameterError: If bundle_or_hvo is None.
Example:
>>> morphBundleOps = WfiMorphBundleOperations(project)
>>> bundles = list(morphBundleOps.GetAll(analysis))
>>> if bundles:
... guid = morphBundleOps.GetGuid(bundles[0])
... print(f"Bundle GUID: {guid}")
Bundle GUID: 12345678-1234-1234-1234-123456789abc
Notes:
- GUID is a unique identifier for the bundle
- Remains constant even if bundle is modified
- Useful for tracking and referencing across sessions
- Format is standard GUID (128-bit value)
- Used in synchronization and external references
See Also:
GetOwningAnalysis, GetAll
"""
self._ValidateParam(bundle_or_hvo, "bundle_or_hvo")
bundle = self.__GetBundleObject(bundle_or_hvo)
return str(bundle.Guid)
# ==================== PRIVATE HELPER METHODS ====================
def __GetBundleObject(self, bundle_or_hvo):
"""
Resolve HVO or object to IWfiMorphBundle.
Args:
bundle_or_hvo: Either an IWfiMorphBundle object or an HVO (int).
Returns:
IWfiMorphBundle: The resolved bundle object.
"""
if isinstance(bundle_or_hvo, int):
return cast_to_concrete(self.project.Object(bundle_or_hvo))
return cast_to_concrete(bundle_or_hvo)
def __GetAnalysisObject(self, analysis_or_hvo):
"""
Resolve HVO or object to IWfiAnalysis.
Args:
analysis_or_hvo: Either an IWfiAnalysis object or an HVO (int).
Returns:
IWfiAnalysis: The resolved analysis object.
"""
if isinstance(analysis_or_hvo, int):
return cast_to_concrete(self.project.Object(analysis_or_hvo))
return cast_to_concrete(analysis_or_hvo)
def __GetSenseObject(self, sense_or_hvo):
"""
Resolve HVO or object to ILexSense.
Args:
sense_or_hvo: Either an ILexSense object or an HVO (int).
Returns:
ILexSense: The resolved sense object.
"""
if isinstance(sense_or_hvo, int):
return cast_to_concrete(self.project.Object(sense_or_hvo))
return cast_to_concrete(sense_or_hvo)
def __GetMorphObject(self, morph_or_hvo):
"""
Resolve HVO or object to IMoForm.
This resolves the bundle's linked allomorph (IMoForm) for
SetMorph. Resolving a morph type here instead -- conflating the
bundle's allomorph field with its morph-type field -- is exactly
the field-confusion bug issue #254 exists to eliminate.
Args:
morph_or_hvo: Either an IMoForm object, an HVO (int), or an
``Allomorph`` wrapper item from ``GetAll()`` (unwrapped
internally before casting, issue #449 -- pythonnet cannot
cast a Python wrapper instance, and ``cast_to_concrete``
returns a wrapper unchanged rather than casting it).
Returns:
IMoForm: The resolved allomorph object (not type-checked here;
SetMorph performs the IMoForm guard after calling this).
"""
morph_or_hvo = self._UnwrapLcm(morph_or_hvo)
if isinstance(morph_or_hvo, int):
return cast_to_concrete(self.project.Object(morph_or_hvo))
return cast_to_concrete(morph_or_hvo)
def __GetMSAObject(self, msa_or_hvo):
"""
Resolve HVO or object to IMoMorphSynAnalysis.
Args:
msa_or_hvo: Either an IMoMorphSynAnalysis object, an HVO (int),
or a ``MorphosyntaxAnalysis`` wrapper item from ``GetAll()``
(unwrapped internally before casting, issue #449).
Returns:
IMoMorphSynAnalysis: The resolved MSA object.
"""
msa_or_hvo = self._UnwrapLcm(msa_or_hvo)
if isinstance(msa_or_hvo, int):
return cast_to_concrete(self.project.Object(msa_or_hvo))
return cast_to_concrete(msa_or_hvo)
def __GetInflectionClassObject(self, infl_class_or_hvo):
"""
Resolve HVO or object to IMoInflClass.
Args:
infl_class_or_hvo: Either an IMoInflClass object or an HVO (int).
Returns:
IMoInflClass: The resolved inflection class object.
"""
if isinstance(infl_class_or_hvo, int):
return cast_to_concrete(self.project.Object(infl_class_or_hvo))
return cast_to_concrete(infl_class_or_hvo)
def __WSHandleVern(self, wsHandle):
"""
Get writing system handle, defaulting to vernacular WS for morpheme forms.
Args:
wsHandle: Optional writing system handle.
Returns:
int: The writing system handle.
"""
if wsHandle is None:
return self.project.project.DefaultVernWs
return self.project._FLExProject__WSHandle(wsHandle, self.project.project.DefaultVernWs)
def __WSHandleAnal(self, wsHandle):
"""
Get writing system handle, defaulting to analysis WS for glosses.
Args:
wsHandle: Optional writing system handle.
Returns:
int: The writing system handle.
"""
if wsHandle is None:
return self.project.project.DefaultAnalWs
return self.project._FLExProject__WSHandle(wsHandle, self.project.project.DefaultAnalWs)