Source code for flexicon.code.TextsWords.WordformOperations

#
#   WordformOperations.py
#
#   Class: WordformOperations
#          Wordform operations for FieldWorks Language Explorer projects
#          via SIL Language and Culture Model (LCM) API.
#
#   Platform: Python.NET
#             FieldWorks Version 9+
#
#   Copyright Craig Farrow, 2008 - 2024
#

import logging

logger = logging.getLogger(__name__)

import clr

clr.AddReference("System")

from SIL.LCModel import (
    IWfiWordformRepository,
    IWfiWordformFactory,
    IWfiWordform,
    IWfiAnalysisFactory,
    IWfiGlossFactory,
    IWfiMorphBundleFactory,
)

from SIL.LCModel.Core.KernelInterfaces import ITsString
from SIL.LCModel.Core.Text import TsStringUtils

from ..FLExProject import (
    FP_ParameterError,
)
from ..BaseOperations import BaseOperations, OperationsMethod, wrap_enumerable
from ..Shared.string_utils import normalize_match_key
from ..lcm_casting import cast_to_concrete

# --- Spelling Status Enum ---


[docs] class SpellingStatusStates: """Spelling status values for wordforms.""" UNDECIDED = 0 INCORRECT = 1 CORRECT = 2
# --- WordformOperations Class ---
[docs] class WordformOperations(BaseOperations): """ Provides operations for managing wordforms in a FLEx project. Wordforms represent surface forms of words as they appear in texts. Each wordform can have multiple analyses and a spelling status. This class should be accessed via FLExProject.Wordforms property. Example: >>> project = FLExProject() >>> project.OpenProject("MyProject") >>> # Get all wordforms >>> for wf in project.Wordforms.GetAll(): ... print(project.Wordforms.GetForm(wf)) >>> # Create a new wordform >>> wf = project.Wordforms.Create("running") >>> # Set spelling status >>> project.Wordforms.SetSpellingStatus(wf, SpellingStatusStates.CORRECT) Known Limitations: - Deep copy of wordform analyses (deep=True in Duplicate) is not yet implemented. See Duplicate() method for details and workaround. """ def __init__(self, project): """ Initialize WordformOperations. Args: project: FLExProject instance """ super().__init__(project) def __WSHandle(self, wsHandle): """ Internal helper to resolve writing system handle. Args: wsHandle: Writing system handle (language tag or ID), or None for default Returns: int: Resolved writing system handle """ if wsHandle is None: return self.project.project.DefaultVernWs return self.project._FLExProject__WSHandle(wsHandle, self.project.project.DefaultVernWs) def __ResolveWordform(self, wordform_or_hvo): """ Resolve HVO or object to IWfiWordform. Casts by ``ClassName`` before returning (issue #500, generalising #275 / #269). ``self.project.Object(hvo)`` returns a bare ``ICmObject``; ``isinstance(obj, IWfiWordform)`` is False even for a genuine wordform, so the old inline HVO guards rejected every real HVO. ``cast_to_concrete`` widens to the concrete interface. Args: wordform_or_hvo: Either an IWfiWordform object or an HVO (int). Returns: IWfiWordform: The resolved wordform object. Raises: FP_ParameterError: If HVO does not refer to a wordform. """ if isinstance(wordform_or_hvo, int): obj = self.project.Object(wordform_or_hvo) if getattr(obj, "ClassName", None) == "WfiWordform": try: return cast_to_concrete(obj) except Exception: pass if isinstance(obj, IWfiWordform): return obj raise FP_ParameterError("HVO does not refer to a wordform") if getattr(wordform_or_hvo, "ClassName", None) == "WfiWordform": try: return cast_to_concrete(wordform_or_hvo) except Exception: pass return wordform_or_hvo @wrap_enumerable @OperationsMethod def GetAll(self): """ Retrieve all wordforms in the FLEx project. This method returns an EnumerableWrapper (subscriptable, len()-able, lazily materialized) over all IWfiWordform objects in the project database, allowing iteration over the complete wordform inventory. Returns: EnumerableWrapper[IWfiWordform]: Each wordform object in the project Example: >>> for wordform in project.Wordforms.GetAll(): ... form = project.Wordforms.GetForm(wordform) ... print(form) Notes: - Returns an EnumerableWrapper (subscriptable, len()-able) for memory efficiency with large projects; the underlying LCM enumerator is only materialized into a list on first len()/index/iteration access - Wordforms are returned in database order (not alphabetical) - Use GetForm() to access the surface text See Also: Find, Create, GetForm """ return self.project.ObjectsIn(IWfiWordformRepository) @OperationsMethod def Create(self, form, wsHandle=None, guid=None): """ Create a new wordform in the FLEx project. Args: form: The surface text form of the wordform wsHandle: Optional writing system handle. Defaults to vernacular WS. guid (optional): GUID to assign to the new wordform, as a ``System.Guid`` or string. Use this when REPRODUCING a wordform from another project so it keeps its original identity. None (the default) mints a fresh GUID. Returns: IWfiWordform: The newly created wordform object Raises: FP_ReadOnlyError: If project is not opened with write enabled FP_NullParameterError: If form is empty or None Example: >>> wf = project.Wordforms.Create("running") >>> print(project.Wordforms.GetForm(wf)) running >>> # Create in specific writing system >>> wf_fr = project.Wordforms.Create("courir", "fr") Notes: - New wordforms have UNDECIDED spelling status by default - The form is stored in the specified writing system - Does not check if wordform already exists - use Find() or Exists() first See Also: Exists, Find, Delete """ self._EnsureWriteEnabled() self._ValidateParam(form, "form") wsHandle = self.__WSHandle(wsHandle) with self._TransactionCM("Create wordform"): # Factory.Create() owns the wordform automatically in FLEx 9 LCM factory = self.project.project.ServiceLocator.GetService(IWfiWordformFactory) new_wf = self._CreateWithGuid(factory, guid, "IWfiWordform") # Set the form mkstr = TsStringUtils.MakeString(form, wsHandle) new_wf.Form.set_String(wsHandle, mkstr) return new_wf @OperationsMethod def Delete(self, wordform_or_hvo): """ Delete a wordform from the FLEx project. Args: wordform_or_hvo: Either an IWfiWordform object or its HVO (database ID) Raises: FP_ReadOnlyError: If project is not opened with write enabled FP_ParameterError: If wordform doesn't exist Example: >>> wf = project.Wordforms.Find("obsolete") >>> if wf: ... project.Wordforms.Delete(wf) Warning: - This is a destructive operation - All analyses associated with the wordform will also be deleted - Consider setting spelling status to INCORRECT instead of deleting See Also: Create, SetSpellingStatus """ self._EnsureWriteEnabled() wordform = self.__ResolveWordform(wordform_or_hvo) # LCM Delete() removes the object from the repository with self._TransactionCM("Delete wordform"): wordform.Delete() @OperationsMethod def Exists(self, form, wsHandle=None): """ Check if a wordform exists in the FLEx project. Args: form: The surface text form to check wsHandle: Optional writing system handle. Defaults to vernacular WS. Returns: bool: True if the wordform exists, False otherwise Example: >>> if project.Wordforms.Exists("running"): ... wf = project.Wordforms.Find("running") ... else: ... wf = project.Wordforms.Create("running") Notes: - Search is case-sensitive - Search is writing-system specific - Returns False for empty or whitespace-only forms See Also: Find, Create """ if not form or not form.strip(): return False return self.Find(form, wsHandle) is not None @OperationsMethod def Find(self, form, wsHandle=None): """ Find and return a wordform by its surface form. Args: form: The surface text form to find wsHandle: Optional writing system handle. Defaults to vernacular WS. Returns: IWfiWordform or None: The wordform object if found, None otherwise Example: >>> wf = project.Wordforms.Find("running") >>> if wf: ... status = project.Wordforms.GetSpellingStatus(wf) ... print(f"Spelling status: {status}") Notes: - Search is case-sensitive - Search is writing-system specific - Returns None if wordform doesn't exist - Use Exists() for simple existence check See Also: Exists, GetAll, Create """ if not form or not form.strip(): return None wsHandle = self.__WSHandle(wsHandle) # Search through all wordforms target = normalize_match_key(form, casefold=False) for wf in self.GetAll(): wf_form = ITsString(wf.Form.get_String(wsHandle)).Text if normalize_match_key(wf_form, casefold=False) == target: return wf return None @OperationsMethod def GetForm(self, wordform_or_hvo, wsHandle=None): """ Get the surface text form of a wordform. Args: wordform_or_hvo: Either an IWfiWordform object or its HVO wsHandle: Optional writing system handle. Defaults to vernacular WS. Returns: str: The text form of the wordform (empty string if not set) Raises: FP_ParameterError: If wordform doesn't exist Example: >>> for wf in project.Wordforms.GetAll(): ... form = project.Wordforms.GetForm(wf) ... print(form) Notes: - Returns empty string if form not set in specified writing system - Default writing system is the default vernacular WS - Multi-string forms may exist in multiple writing systems See Also: SetForm, Find """ wordform = self.__ResolveWordform(wordform_or_hvo) wsHandle = self.__WSHandle(wsHandle) # Get the form string form = ITsString(wordform.Form.get_String(wsHandle)).Text return form or "" @OperationsMethod def SetForm(self, wordform_or_hvo, form, wsHandle=None): """ Set the surface text form of a wordform. Args: wordform_or_hvo: Either an IWfiWordform object or its HVO form: The new text form to set wsHandle: Optional writing system handle. Defaults to vernacular WS. Raises: FP_ReadOnlyError: If project is not opened with write enabled FP_NullParameterError: If form is empty or None FP_ParameterError: If wordform doesn't exist Example: >>> wf = project.Wordforms.Find("runing") # misspelled >>> if wf: ... project.Wordforms.SetForm(wf, "running") # correct it Warning: - Changing a wordform's text may affect text parsing - May create duplicate if another wordform has same form - Use carefully - consider creating new wordform instead See Also: GetForm, Create """ self._EnsureWriteEnabled() self._ValidateParam(wordform_or_hvo, "wordform_or_hvo") self._ValidateParam(form, "form") wordform = self.__ResolveWordform(wordform_or_hvo) wsHandle = self.__WSHandle(wsHandle) # Set the form string with self._TransactionCM(f"Set wordform form '{form}'"): mkstr = TsStringUtils.MakeString(form, wsHandle) wordform.Form.set_String(wsHandle, mkstr) @OperationsMethod def GetSpellingStatus(self, wordform_or_hvo): """ Get the spelling status of a wordform. Args: wordform_or_hvo: Either an IWfiWordform object or its HVO Returns: int: The current spelling status (0=UNDECIDED, 1=INCORRECT, 2=CORRECT) Raises: FP_ParameterError: If wordform doesn't exist Example: >>> wf = project.Wordforms.Find("running") >>> status = project.Wordforms.GetSpellingStatus(wf) >>> if status == SpellingStatusStates.UNDECIDED: ... project.Wordforms.SetSpellingStatus(wf, SpellingStatusStates.CORRECT) Notes: - New wordforms default to UNDECIDED status (0) - Status affects spell-checking and analysis suggestions - CORRECT status (2) indicates approved wordforms - INCORRECT status (1) marks known misspellings See Also: SetSpellingStatus, SpellingStatusStates """ wordform = self.__ResolveWordform(wordform_or_hvo) return wordform.SpellingStatus @OperationsMethod def SetSpellingStatus(self, wordform_or_hvo, status): """ Set the spelling status of a wordform. Args: wordform_or_hvo: Either an IWfiWordform object or its HVO status: The new spelling status (0, 1, or 2 from SpellingStatusStates) Raises: FP_ReadOnlyError: If project is not opened with write enabled FP_ParameterError: If wordform doesn't exist or status is invalid Example: >>> wf = project.Wordforms.Find("colour") >>> project.Wordforms.SetSpellingStatus(wf, SpellingStatusStates.CORRECT) >>> >>> typo = project.Wordforms.Find("teh") >>> project.Wordforms.SetSpellingStatus(typo, SpellingStatusStates.INCORRECT) Notes: - Use SpellingStatusStates enum for status values: - UNDECIDED (0): Not yet reviewed - INCORRECT (1): Known misspelling - CORRECT (2): Approved spelling See Also: GetSpellingStatus, SpellingStatusStates """ self._EnsureWriteEnabled() if status not in (SpellingStatusStates.UNDECIDED, SpellingStatusStates.INCORRECT, SpellingStatusStates.CORRECT): raise FP_ParameterError(f"Invalid spelling status: {status}. Must be 0, 1, or 2.") wordform = self.__ResolveWordform(wordform_or_hvo) with self._TransactionCM("Set wordform spelling status"): wordform.SpellingStatus = status @OperationsMethod def GetAnalyses(self, wordform_or_hvo): """ Get all analyses associated with a wordform. Args: wordform_or_hvo: Either an IWfiWordform object or its HVO Returns: list: List of IWfiAnalysis objects for this wordform Raises: FP_ParameterError: If wordform doesn't exist Example: >>> wf = project.Wordforms.Find("running") >>> analyses = project.Wordforms.GetAnalyses(wf) >>> for analysis in analyses: ... # Process each analysis ... print(f"Analysis HVO: {analysis.Hvo}") Notes: - Returns empty list if wordform has no analyses - Analyses may be human-created or parser-generated - Each analysis represents a possible linguistic interpretation See Also: GetAll, Find """ wordform = self.__ResolveWordform(wordform_or_hvo) return list(wordform.AnalysesOC) @OperationsMethod def GetOccurrenceCount(self, wordform_or_hvo): """ Get the count of occurrences of a wordform in texts. Args: wordform_or_hvo: Either an IWfiWordform object or its HVO Returns: int: Number of times this wordform appears in project texts Raises: FP_ParameterError: If wordform doesn't exist Example: >>> wf = project.Wordforms.Find("running") >>> count = project.Wordforms.GetOccurrenceCount(wf) >>> print(f"'running' appears {count} times in texts") Notes: - Counts segment occurrences across all texts in the project - Returns 0 if wordform has no occurrences - Each occurrence represents a token in a text segment See Also: GetOccurrences, GetAll """ wordform = self.__ResolveWordform(wordform_or_hvo) # OccurrencesInTexts is IEnumerable[ISegment] (no .Count). The sibling # GetOccurrences method already materializes via list() -- match it # by counting via the iterator. See issue #16. return sum(1 for _ in wordform.OccurrencesInTexts) @OperationsMethod def GetOccurrences(self, wordform_or_hvo): """ Get all segment occurrences of a wordform in texts. Args: wordform_or_hvo: Either an IWfiWordform object or its HVO Returns: list: List of segment objects where this wordform appears Raises: FP_ParameterError: If wordform doesn't exist Example: >>> wf = project.Wordforms.Find("running") >>> occurrences = project.Wordforms.GetOccurrences(wf) >>> for segment in occurrences: ... # Process each text segment containing this wordform ... print(f"Segment HVO: {segment.Hvo}") Notes: - Returns empty list if wordform has no occurrences - Each segment represents a location where the wordform appears - Segments belong to texts (paragraphs/sentences) See Also: GetOccurrenceCount, GetAll """ wordform = self.__ResolveWordform(wordform_or_hvo) return list(wordform.OccurrencesInTexts) @OperationsMethod def GetChecksum(self, wordform_or_hvo): """ Get the checksum of a wordform. Args: wordform_or_hvo: Either an IWfiWordform object or its HVO Returns: int: The wordform's checksum value Raises: FP_ParameterError: If wordform doesn't exist Example: >>> wf = project.Wordforms.Find("running") >>> checksum = project.Wordforms.GetChecksum(wf) >>> print(f"Checksum: {checksum}") Notes: - Checksum is used for data integrity and change detection - The checksum value changes when wordform properties are modified - Useful for tracking modifications and synchronization See Also: GetForm, GetSpellingStatus """ wordform = self.__ResolveWordform(wordform_or_hvo) return wordform.Checksum @wrap_enumerable @OperationsMethod def GetAllWithStatus(self, status): """ Retrieve all wordforms with a specific spelling status. Args: status: The spelling status to filter by (from SpellingStatusStates) Returns: EnumerableWrapper[IWfiWordform]: Each wordform object with the specified status Raises: FP_ParameterError: If status is invalid Example: >>> # Get all correctly spelled wordforms >>> correct_wfs = list(project.Wordforms.GetAllWithStatus(SpellingStatusStates.CORRECT)) >>> print(f"Found {len(correct_wfs)} approved wordforms") >>> >>> # Get all misspellings >>> for wf in project.Wordforms.GetAllWithStatus(SpellingStatusStates.INCORRECT): ... form = project.Wordforms.GetForm(wf) ... print(f"Misspelling: {form}") Notes: - Returns an EnumerableWrapper (subscriptable, len()-able) for memory efficiency with large projects; the underlying LCM enumerator is only materialized into a list on first len()/index/iteration access - Valid status values (from SpellingStatusStates): - UNDECIDED (0): Not yet reviewed - INCORRECT (1): Known misspelling - CORRECT (2): Approved spelling - Use list() to convert to a list if needed See Also: GetAllUnapproved, GetSpellingStatus, SpellingStatusStates """ if status not in (SpellingStatusStates.UNDECIDED, SpellingStatusStates.INCORRECT, SpellingStatusStates.CORRECT): raise FP_ParameterError(f"Invalid spelling status: {status}. Must be 0, 1, or 2.") for wf in self.GetAll(): if wf.SpellingStatus == status: yield wf @wrap_enumerable @OperationsMethod def GetAllUnapproved(self): """ Retrieve all wordforms that are not approved (not CORRECT status). Returns: EnumerableWrapper[IWfiWordform]: Each wordform object with UNDECIDED or INCORRECT status Example: >>> # Get all unapproved wordforms >>> for wf in project.Wordforms.GetAllUnapproved(): ... form = project.Wordforms.GetForm(wf) ... status = project.Wordforms.GetSpellingStatus(wf) ... print(f"{form}: status={status}") >>> >>> # Count unapproved wordforms >>> unapproved_count = sum(1 for _ in project.Wordforms.GetAllUnapproved()) >>> print(f"Total unapproved: {unapproved_count}") Notes: - Returns an EnumerableWrapper (subscriptable, len()-able) for memory efficiency with large projects; the underlying LCM enumerator is only materialized into a list on first len()/index/iteration access - Includes both UNDECIDED (0) and INCORRECT (1) statuses - Excludes only CORRECT (2) status wordforms - Useful for spell-checking and wordform review workflows See Also: GetAllWithStatus, ApproveSpelling, GetSpellingStatus """ for wf in self.GetAll(): if wf.SpellingStatus != SpellingStatusStates.CORRECT: yield wf @OperationsMethod def ApproveSpelling(self, wordform_or_hvo): """ Approve the spelling of a wordform by setting status to CORRECT. Args: wordform_or_hvo: Either an IWfiWordform object or its HVO Raises: FP_ReadOnlyError: If project is not opened with write enabled FP_ParameterError: If wordform doesn't exist Example: >>> wf = project.Wordforms.Find("colour") >>> project.Wordforms.ApproveSpelling(wf) >>> >>> # Approve multiple wordforms >>> for wf in project.Wordforms.GetAllWithStatus(SpellingStatusStates.UNDECIDED): ... if should_approve(wf): ... project.Wordforms.ApproveSpelling(wf) Notes: - This is equivalent to SetSpellingStatus(wf, SpellingStatusStates.CORRECT) - Approved wordforms are marked as correctly spelled - Affects spell-checking and analysis suggestions - Convenience method for common approval workflow See Also: SetSpellingStatus, GetAllUnapproved, GetSpellingStatus """ self._EnsureWriteEnabled() wordform = self.__ResolveWordform(wordform_or_hvo) with self._TransactionCM("Approve wordform spelling"): wordform.SpellingStatus = SpellingStatusStates.CORRECT @OperationsMethod def Duplicate(self, item_or_hvo, deep=False, *, insert_after=True): """ Duplicate a wordform, creating a new copy with a new GUID. Args: item_or_hvo: The IWfiWordform object or HVO to duplicate. deep (bool): If True, also duplicate analyses. If False (default), only copy wordform properties. insert_after (bool): Accepted for API uniformity across Operations classes. Wordforms live in the wordform repository, not a positionally ordered owning sequence, so this parameter is ignored. Returns: IWfiWordform: The newly created duplicate 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: >>> wf = project.Wordforms.Find("running") >>> if wf: ... dup = project.Wordforms.Duplicate(wf) ... # Change the form to create a variant ... project.Wordforms.SetForm(dup, "runnin'") ... print(f"Duplicate: {project.Wordforms.GetForm(dup)}") Notes: - Factory.Create() automatically generates a new GUID - MultiString property: Form - Simple property: SpellingStatus - Analyses duplicated with full nested structure when deep=True - Occurrences are NOT copied (they reference specific text segments) Analysis Deep Copy (deep=True): When deep=True, all analyses are duplicated with their nested structures: What's Copied: - IWfiAnalysis objects (with new GUIDs) - CategoryRA reference (part of speech) - WfiGloss objects with Form MultiString (word-level translations) - WfiMorphBundle objects with Form MultiString (IWfiMorphBundle has no Gloss field of its own; the displayed gloss comes from SenseRA.Gloss) - Morph bundle references: 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) What's NOT Copied: - EvaluationsRC (approval status) - duplicates start unapproved - OccurrencesRS (text segment references) - tied to original contexts Use Cases: - Creating variant wordforms with full linguistic analysis - Duplicating analyzed forms for comparative study - Preserving complex analyses when creating spelling variants Example: >>> wf = project.Wordforms.Find("running") >>> dup = project.Wordforms.Duplicate(wf, deep=True) >>> len(project.Wordforms.GetAnalyses(wf)) == len(project.Wordforms.GetAnalyses(dup)) True See Also: Create, Delete, GetAnalyses """ self._EnsureWriteEnabled() self._ValidateParam(item_or_hvo, "item_or_hvo") source = self.__ResolveWordform(item_or_hvo) with self._TransactionCM("Duplicate wordform"): # Create new wordform using factory (auto-generates new GUID) factory = self.project.project.ServiceLocator.GetService(IWfiWordformFactory) duplicate = factory.Create() # Copy MultiString properties duplicate.Form.CopyAlternatives(source.Form) # Copy simple properties duplicate.SpellingStatus = source.SpellingStatus # Deep copy: duplicate analyses if deep and hasattr(source, "AnalysesOC") and source.AnalysesOC.Count > 0: # Get factories (retrieve once, reuse for all objects) analysis_factory = self.project.project.ServiceLocator.GetService(IWfiAnalysisFactory) gloss_factory = self.project.project.ServiceLocator.GetService(IWfiGlossFactory) bundle_factory = self.project.project.ServiceLocator.GetService(IWfiMorphBundleFactory) # Iterate through all analyses in the source wordform for source_analysis in source.AnalysesOC: # Create new analysis with auto-generated GUID new_analysis = analysis_factory.Create() # Add to duplicate wordform BEFORE copying properties (critical for MultiStrings) duplicate.AnalysesOC.Add(new_analysis) # Copy Reference Atomic (RA) properties if hasattr(source_analysis, "CategoryRA") and source_analysis.CategoryRA: new_analysis.CategoryRA = source_analysis.CategoryRA # Note: We intentionally do NOT copy EvaluationsRC (approval status) # New duplicate analysis should start as unapproved # Deep copy glosses (MeaningsOC - owned collection) if hasattr(source_analysis, "MeaningsOC"): for gloss in source_analysis.MeaningsOC: new_gloss = gloss_factory.Create() new_analysis.MeaningsOC.Add(new_gloss) # Copy MultiString form new_gloss.Form.CopyAlternatives(gloss.Form) # Deep copy morph bundles (MorphBundlesOS - owned sequence) if hasattr(source_analysis, "MorphBundlesOS"): for bundle in source_analysis.MorphBundlesOS: new_bundle = bundle_factory.Create() new_analysis.MorphBundlesOS.Add(new_bundle) # Copy MultiString properties. IWfiMorphBundle # has Form but NOT Gloss -- the displayed gloss # comes from SenseRA.Gloss, preserved by the # SenseRA copy below. (same root bug as # #16/#107/#108, sibling site) new_bundle.Form.CopyAlternatives(bundle.Form) # Copy Reference Atomic (RA) properties if hasattr(bundle, "SenseRA") and bundle.SenseRA: new_bundle.SenseRA = bundle.SenseRA if hasattr(bundle, "MsaRA") and bundle.MsaRA: new_bundle.MsaRA = bundle.MsaRA if hasattr(bundle, "MorphRA") and bundle.MorphRA: new_bundle.MorphRA = bundle.MorphRA # No separate inflection-class copy needed: # IWfiMorphBundle has no InflClassRA member (the # guard above was always False; the dead body # wrote a nonexistent `new_bundle.InflClassRA`). # Inflection class lives on the MSA # (IMoStemMsa.InflectionClassRA), and the MsaRA # assignment above already copies that MSA # REFERENCE, so new_bundle sees the same # inflection class as bundle via the shared MSA. # See get_inflection_class_from_msa() in # lcm_casting.py (issue #259 / # lcm-member-truth-sweep C10/T4.2). logger.info(f"Deep copied {source.AnalysesOC.Count} analyses with nested structures") # Note: Occurrences (OccurrencesRS) are NOT copied as they reference # specific text segments in the corpus return duplicate # ========== SYNC INTEGRATION METHODS ========== @OperationsMethod def GetSyncableProperties(self, item): """ Get all syncable properties of a wordform. Args: item: The IWfiWordform object. Returns: dict: Dictionary of syncable properties with their values. Example: >>> props = project.Wordforms.GetSyncableProperties(wordform) >>> print(props['Form']) {'en': 'running'} >>> print(props['SpellingStatus']) 2 # CORRECT Notes: - MultiString property: Form - Integer property: SpellingStatus (0=UNDECIDED, 1=INCORRECT, 2=CORRECT) - Does NOT include owned collections (analyses) - those are children """ props = {} # MultiString property - Form if hasattr(item, "Form") and item.Form: props["Form"] = self.project.GetMultiStringDict(item.Form) # Integer property - SpellingStatus if hasattr(item, "SpellingStatus"): props["SpellingStatus"] = int(item.SpellingStatus) return props @OperationsMethod def CompareTo(self, item1, item2, ops1=None, ops2=None): """ Compare two wordforms for differences. Args: item1: First wordform object (from project 1) item2: Second wordform object (from project 2) ops1: Optional WordformOperations instance for project 1 (defaults to self) ops2: Optional WordformOperations instance for project 2 (defaults to self) Returns: tuple: (is_different, differences_dict) - is_different (bool): True if wordforms differ, False if identical - differences_dict (dict): Maps property names to (value1, value2) tuples Example: >>> is_diff, diffs = ops1.CompareTo(wf1, wf2, ops1, ops2) >>> if is_diff: ... for prop, (val1, val2) in diffs.items(): ... print(f"{prop}: {val1} != {val2}") Notes: - Compares Form MultiString across all writing systems - Compares SpellingStatus integer value - 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)