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

# --- 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) @wrap_enumerable @OperationsMethod def GetAll(self): """ Retrieve all wordforms in the FLEx project. This method returns an iterator over all IWfiWordform objects in the project database, allowing iteration over the complete wordform inventory. Yields: IWfiWordform: Each wordform object in the project Example: >>> for wordform in project.Wordforms.GetAll(): ... form = project.Wordforms.GetForm(wordform) ... print(form) Notes: - Returns an iterator for memory efficiency with large projects - 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): """ 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. 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 = factory.Create() # 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() # Resolve to wordform object if isinstance(wordform_or_hvo, int): wordform = self.project.Object(wordform_or_hvo) if not isinstance(wordform, IWfiWordform): raise FP_ParameterError("HVO does not refer to a wordform") else: wordform = wordform_or_hvo # LCM Delete() removes the object from the repository 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 """ # Resolve to wordform object if isinstance(wordform_or_hvo, int): wordform = self.project.Object(wordform_or_hvo) if not isinstance(wordform, IWfiWordform): raise FP_ParameterError("HVO does not refer to a wordform") else: wordform = 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") # Resolve to wordform object if isinstance(wordform_or_hvo, int): wordform = self.project.Object(wordform_or_hvo) if not isinstance(wordform, IWfiWordform): raise FP_ParameterError("HVO does not refer to a wordform") else: wordform = wordform_or_hvo wsHandle = self.__WSHandle(wsHandle) # Set the form string 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 """ # Resolve to wordform object if isinstance(wordform_or_hvo, int): wordform = self.project.Object(wordform_or_hvo) if not isinstance(wordform, IWfiWordform): raise FP_ParameterError("HVO does not refer to a wordform") else: wordform = 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.") # Resolve to wordform object if isinstance(wordform_or_hvo, int): wordform = self.project.Object(wordform_or_hvo) if not isinstance(wordform, IWfiWordform): raise FP_ParameterError("HVO does not refer to a wordform") else: wordform = wordform_or_hvo 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 """ # Resolve to wordform object if isinstance(wordform_or_hvo, int): wordform = self.project.Object(wordform_or_hvo) if not isinstance(wordform, IWfiWordform): raise FP_ParameterError("HVO does not refer to a wordform") else: wordform = 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 """ # Resolve to wordform object if isinstance(wordform_or_hvo, int): wordform = self.project.Object(wordform_or_hvo) if not isinstance(wordform, IWfiWordform): raise FP_ParameterError("HVO does not refer to a wordform") else: wordform = 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 """ # Resolve to wordform object if isinstance(wordform_or_hvo, int): wordform = self.project.Object(wordform_or_hvo) if not isinstance(wordform, IWfiWordform): raise FP_ParameterError("HVO does not refer to a wordform") else: wordform = 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 """ # Resolve to wordform object if isinstance(wordform_or_hvo, int): wordform = self.project.Object(wordform_or_hvo) if not isinstance(wordform, IWfiWordform): raise FP_ParameterError("HVO does not refer to a wordform") else: wordform = wordform_or_hvo return wordform.Checksum @OperationsMethod def GetAllWithStatus(self, status): """ Retrieve all wordforms with a specific spelling status. Args: status: The spelling status to filter by (from SpellingStatusStates) Yields: 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 iterator for memory efficiency with large projects - 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 @OperationsMethod def GetAllUnapproved(self): """ Retrieve all wordforms that are not approved (not CORRECT status). Yields: 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 iterator for memory efficiency with large projects - 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() # Resolve to wordform object if isinstance(wordform_or_hvo, int): wordform = self.project.Object(wordform_or_hvo) if not isinstance(wordform, IWfiWordform): raise FP_ParameterError("HVO does not refer to a wordform") else: wordform = wordform_or_hvo wordform.SpellingStatus = SpellingStatusStates.CORRECT @OperationsMethod def Duplicate(self, item_or_hvo, deep=False): """ 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. 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, InflClassRA 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") # Resolve to wordform object if isinstance(item_or_hvo, int): source = self.project.Object(item_or_hvo) if not isinstance(source, IWfiWordform): raise FP_ParameterError("HVO does not refer to a wordform") else: source = 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 new_bundle.Form.CopyAlternatives(bundle.Form) new_bundle.Gloss.CopyAlternatives(bundle.Gloss) # 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 if hasattr(bundle, "InflClassRA") and bundle.InflClassRA: new_bundle.InflClassRA = bundle.InflClassRA 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 if self.project._CompareValues(val1, val2): # Values are different differences[key] = (val1, val2) is_different = len(differences) > 0 return (is_different, differences)