Source code for flexicon.code.Lexicon.LexEntryOperations

#
#   LexEntryOperations.py
#
#   Class: LexEntryOperations
#          Lexical entry operations for FieldWorks Language Explorer
#          projects via SIL Language and Culture Model (LCM) API.
#
#   Platform: Python.NET
#             FieldWorks Version 9+
#
#   Copyright 2025
#

import logging

logger = logging.getLogger(__name__)

# Import BaseOperations parent class
from ..BaseOperations import BaseOperations, OperationsMethod, wrap_enumerable

# Import FLEx LCM types
from SIL.LCModel import (
    ILexEntry,
    ILexEntryFactory,
    ILexEntryRepository,
    ILexSense,
    ILexSenseFactory,
    IMoMorphType,
    IMoStemAllomorphFactory,
    IMoAffixAllomorphFactory,
    IMoForm,
    MoMorphTypeTags,
    ILexEntryRef,
    ILexEntryRefFactory,
    LexEntryRefTags,
    ILexPronunciationFactory,
    ILexEtymologyFactory,
    ICmFileFactory,
)
from SIL.LCModel.Core.KernelInterfaces import ITsString
from SIL.LCModel.Core.Text import TsStringUtils

# Import flexlibs exceptions
from ..FLExProject import (
    FP_ParameterError,
)

# Import string utilities
from ..Shared.string_utils import normalize_text, normalize_match_key, best_analysis_text, best_vernacular_text


[docs] class LexEntryOperations(BaseOperations): """ This class provides operations for managing lexical entries in a FieldWorks project. Lexical entries are the fundamental units of the lexicon, representing words, morphemes, or other lexical items. Each entry has forms (lexeme, citation, alternate), senses (meanings), and various properties. This class should be accessed via FLExProject.LexEntry property. Usage:: from flexlibs2 import FLExProject project = FLExProject() project.OpenProject("my project", writeEnabled=True) # Get all entries for entry in project.LexEntry.GetAll(): headword = project.LexEntry.GetHeadword(entry) print(headword) # Create a new entry entry = project.LexEntry.Create("run", "stem") # Add a sense project.LexEntry.AddSense(entry, "to move rapidly on foot", "en") # Set citation form project.LexEntry.SetCitationForm(entry, "run") project.CloseProject() """ def __init__(self, project): """ Initialize LexEntryOperations with a FLExProject instance. Args: project: The FLExProject instance to operate on. """ super().__init__(project) # --- Core CRUD Operations --- @wrap_enumerable @OperationsMethod def GetAll(self): """ Get all lexical entries in the project. This method returns an iterator over all ILexEntry objects in the project database, allowing iteration over the complete lexicon. Yields: ILexEntry: Each lexical entry object in the project Example: >>> for entry in project.LexEntry.GetAll(): ... headword = project.LexEntry.GetHeadword(entry) ... senses = project.LexEntry.GetSenseCount(entry) ... print(f"{headword} ({senses} senses)") run (3 senses) walk (2 senses) house (4 senses) Notes: - Returns an iterator for memory efficiency with large lexicons - Entries are returned in database order (not alphabetical) - Use GetHeadword() to access the display form - For sorted entries, use FLExProject.LexiconAllEntriesSorted() See Also: Find, Create, GetHeadword """ return self.project.ObjectsIn(ILexEntryRepository) @OperationsMethod def Create(self, lexeme_form, morph_type_name=None, wsHandle=None, create_blank_sense=True): """ Create a new lexical entry in the FLEx project. Args: lexeme_form (str): The lexeme form (headword) of the entry morph_type_name (str, optional): Name of the morph type ("stem", "root", "prefix", "suffix", etc.). If None (default), uses "stem". Use "prefix", "suffix", "infix" for affixes (creates MoAffixAllomorph). Use "stem", "root", "clitic", etc. for stems (creates MoStemAllomorph). wsHandle: Optional writing system handle. Defaults to vernacular WS. create_blank_sense (bool): If True (default), creates a blank sense automatically, matching FLEx GUI behavior. Set to False to create entry without senses. Returns: ILexEntry: The newly created lexical entry object Raises: FP_ReadOnlyError: If project is not opened with write enabled FP_NullParameterError: If lexeme_form is None FP_ParameterError: If lexeme_form is empty or morph type not found Example: >>> # Create a basic stem entry with blank sense (default - no type needed!) >>> entry = project.LexEntry.Create("run") >>> print(project.LexEntry.GetHeadword(entry)) run >>> print(project.LexEntry.GetSenseCount(entry)) 1 >>> # Create entry without sense >>> entry = project.LexEntry.Create("run", create_blank_sense=False) >>> print(project.LexEntry.GetSenseCount(entry)) 0 >>> # Create an affix entry (auto-creates MoAffixAllomorph) >>> suffix = project.LexEntry.Create("-ing", "suffix") >>> print(suffix.LexemeFormOA.ClassName) MoAffixAllomorph >>> # Create with specific writing system >>> entry = project.LexEntry.Create("maison", "stem", ... project.WSHandle('fr')) Notes: - The entry is added to the lexicon database - Morph type defaults to "stem" if not specified - Correct allomorph type (MoStemAllomorph vs MoAffixAllomorph) is automatically chosen based on morph type - The lexeme form is set as the primary form - By default, a blank sense is created (matches FLEx GUI behavior) - Use create_blank_sense=False for entries without senses - Entry GUID is auto-generated See Also: Delete, Exists, Find, AddSense, SetLexemeForm """ self._EnsureWriteEnabled() self._ValidateParam(lexeme_form, "lexeme_form") self._ValidateStringNotEmpty(lexeme_form, "lexeme_form") wsHandle = self.__WSHandle(wsHandle) # Default to "stem" if no morph type specified if morph_type_name is None: morph_type_name = "stem" # Find the morph type morph_type = self.__FindMorphType(morph_type_name) if morph_type is None: raise FP_ParameterError( f"Morph type '{morph_type_name}' not found. " f"Use one of: stem, root, prefix, suffix, infix, etc." ) with self._TransactionCM(f"Create entry '{lexeme_form}'"): # Create the new entry using the factory factory = self.project.project.ServiceLocator.GetService(ILexEntryFactory) new_entry = factory.Create() # Create the lexeme form allomorph using the appropriate factory # Stems use IMoStemAllomorphFactory, affixes use IMoAffixAllomorphFactory if self.__IsStemType(morph_type): allomorph_factory = self.project.project.ServiceLocator.GetService(IMoStemAllomorphFactory) else: allomorph_factory = self.project.project.ServiceLocator.GetService(IMoAffixAllomorphFactory) lexeme_form_obj = allomorph_factory.Create() # Attach lexeme form to entry FIRST (must be done before setting properties) new_entry.LexemeFormOA = lexeme_form_obj # Set the form text mkstr = TsStringUtils.MakeString(lexeme_form, wsHandle) lexeme_form_obj.Form.set_String(wsHandle, mkstr) # Set the morph type lexeme_form_obj.MorphTypeRA = morph_type # Create a blank sense by default (matches FLEx GUI behavior) if create_blank_sense: sense_factory = self.project.project.ServiceLocator.GetService(ILexSenseFactory) blank_sense = sense_factory.Create() new_entry.SensesOS.Add(blank_sense) # Note: Factory.Create() automatically adds the entry to the repository # No explicit Add() call needed - the entry is already in the database return new_entry @OperationsMethod def Delete(self, entry_or_hvo): """ Delete a lexical entry from the FLEx project. Args: entry_or_hvo: Either an ILexEntry object or its HVO (database ID) Raises: FP_ReadOnlyError: If project is not opened with write enabled FP_NullParameterError: If entry_or_hvo is None FP_ParameterError: If entry doesn't exist Example: >>> entry = project.LexEntry.Find("obsolete") >>> if entry: ... project.LexEntry.Delete(entry) >>> # Delete by HVO >>> project.LexEntry.Delete(12345) Warning: - This is a destructive operation - All senses, forms, and relations will be deleted - References from other entries may become invalid - Cannot be undone - Entry will be removed from all texts and analyses Notes: - Deletion cascades to all owned objects (senses, allomorphs, etc.) - Cross-references and relations are automatically cleaned up - Consider marking entries as "Do Not Publish" instead of deleting See Also: Create, Exists """ self._EnsureWriteEnabled() self._ValidateParam(entry_or_hvo, "entry_or_hvo") # Resolve to entry object entry = self.__ResolveObject(entry_or_hvo) # Delete the entry (LCM handles removal from repository) entry.Delete() @OperationsMethod def Duplicate(self, item_or_hvo, deep=True): """ Duplicate a lexical entry, creating a new entry with the same properties. This method creates a copy of an existing entry. With deep=True (default), all owned objects (senses, allomorphs, pronunciations, etymologies) are recursively duplicated. With deep=False, only the entry shell (lexeme form, citation form, morph type) is duplicated. Entry references (variants, complex forms) are NOT copied since they describe inter-entry relationships that don't apply to a duplicate. Args: item_or_hvo: Either an ILexEntry object or its HVO (database ID) deep (bool): If True (default), recursively duplicate all owned objects (senses, allomorphs, pronunciations, etymologies). If False, only duplicate the entry shell (lexeme form, citation form, morph type). Returns: ILexEntry: The newly created duplicate entry Raises: FP_ReadOnlyError: If project is not opened with write enabled FP_NullParameterError: If item_or_hvo is None FP_ParameterError: If entry doesn't exist Example: >>> # Deep duplicate (default: with all content) >>> entry = project.LexEntry.Find("walk") >>> duplicate = project.LexEntry.Duplicate(entry) # deep=True by default >>> print(project.LexEntry.GetSenseCount(duplicate)) 3 >>> # Shallow duplicate (entry shell only) >>> entry = project.LexEntry.Find("run") >>> duplicate = project.LexEntry.Duplicate(entry, deep=False) >>> print(project.LexEntry.GetLexemeForm(duplicate)) run >>> print(project.LexEntry.GetSenseCount(duplicate)) 0 Warning: - deep=True for LexEntry can be slow for complex entries with many senses, subsenses, and examples - The duplicate will have identical content but a new GUID - Homograph numbers are not automatically assigned - you may need to call SetHomographNumber() to distinguish duplicates - Cross-references to other entries are NOT duplicated (to avoid creating invalid references) Notes: - Duplicated entry is added to the lexicon database - New GUID is auto-generated for the duplicate - Lexeme form and citation form are copied - Morph type is copied - With deep=True, senses (with examples, translations, subsenses, pictures), allomorphs, pronunciations, and etymologies are recursively duplicated - EntryRefsOS (variant/complex form refs) are NOT copied — the duplicate will likely become a homograph, not an actual variant or complex form of the same components - Import residue is copied - Date created/modified are set to current time for duplicate - Lexicon entries are automatically sorted alphabetically by FLEx See Also: Create, Delete, project.Senses.Duplicate, project.Allomorphs.Duplicate """ self._EnsureWriteEnabled() self._ValidateParam(item_or_hvo, "item_or_hvo") # Resolve to entry object source_entry = self.__ResolveObject(item_or_hvo) # Get source properties wsHandle = self.project.project.DefaultVernWs lexeme_form = self.GetLexemeForm(source_entry, wsHandle) morph_type = self.GetMorphType(source_entry) # Determine morph type name for Create() morph_type_name = "stem" # default if morph_type: morph_type_name_ts = morph_type.Name.BestAnalysisAlternative if morph_type_name_ts: morph_type_name = ITsString(morph_type_name_ts).Text or "stem" with self._TransactionCM("Duplicate entry"): # Create the new entry (skip blank sense when deep copying — we copy source senses) new_entry = self.Create(lexeme_form, morph_type_name, wsHandle, create_blank_sense=(not deep)) # Copy lexeme form for all writing systems (Create only sets default vernacular) if source_entry.LexemeFormOA and new_entry.LexemeFormOA: new_entry.LexemeFormOA.Form.CopyAlternatives(source_entry.LexemeFormOA.Form) # Copy citation form (all writing systems) new_entry.CitationForm.CopyAlternatives(source_entry.CitationForm) # Copy entry-level MultiString properties new_entry.Comment.CopyAlternatives(source_entry.Comment) new_entry.Bibliography.CopyAlternatives(source_entry.Bibliography) new_entry.LiteralMeaning.CopyAlternatives(source_entry.LiteralMeaning) new_entry.Restrictions.CopyAlternatives(source_entry.Restrictions) new_entry.SummaryDefinition.CopyAlternatives(source_entry.SummaryDefinition) # Copy boolean properties new_entry.DoNotUseForParsing = source_entry.DoNotUseForParsing if hasattr(source_entry, "ExcludeAsHeadword"): new_entry.ExcludeAsHeadword = source_entry.ExcludeAsHeadword # Copy reference collections for pub in source_entry.DoNotPublishInRC: new_entry.DoNotPublishInRC.Add(pub) for pub in source_entry.DoNotShowMainEntryInRC: new_entry.DoNotShowMainEntryInRC.Add(pub) # Copy import residue if present residue = self.GetImportResidue(source_entry) if residue: self.SetImportResidue(new_entry, residue) # Deep duplication: duplicate all owned objects if deep: # Duplicate senses (with all sub-objects: examples, subsenses, pictures) for sense in source_entry.SensesOS: self.project.Senses._deep_copy_sense_to(sense, new_entry) # Duplicate alternate forms (allomorphs) using correct factory type for allomorph in source_entry.AlternateFormsOS: class_name = allomorph.ClassName if class_name == "MoAffixAllomorph": allomorph_factory = self.project.project.ServiceLocator.GetService(IMoAffixAllomorphFactory) else: allomorph_factory = self.project.project.ServiceLocator.GetService(IMoStemAllomorphFactory) new_allomorph = allomorph_factory.Create() new_entry.AlternateFormsOS.Add(new_allomorph) new_allomorph.Form.CopyAlternatives(allomorph.Form) if hasattr(allomorph, "MorphTypeRA"): new_allomorph.MorphTypeRA = allomorph.MorphTypeRA for env in allomorph.PhoneEnvRC: new_allomorph.PhoneEnvRC.Add(env) # Duplicate pronunciations (with media files) for pronunciation in source_entry.PronunciationsOS: pron_factory = self.project.project.ServiceLocator.GetService(ILexPronunciationFactory) new_pron = pron_factory.Create() new_entry.PronunciationsOS.Add(new_pron) new_pron.Form.CopyAlternatives(pronunciation.Form) if hasattr(pronunciation, "LocationRA"): new_pron.LocationRA = pronunciation.LocationRA # Copy media files if hasattr(pronunciation, "MediaFilesOS"): for media in pronunciation.MediaFilesOS: media_factory = self.project.project.ServiceLocator.GetService(ICmFileFactory) new_media = media_factory.Create() new_pron.MediaFilesOS.Add(new_media) new_media.InternalPath = media.InternalPath new_media.Description.CopyAlternatives(media.Description) if hasattr(media, "Copyright"): new_media.Copyright.CopyAlternatives(media.Copyright) # Duplicate etymologies for etymology in source_entry.EtymologyOS: etym_factory = self.project.project.ServiceLocator.GetService(ILexEtymologyFactory) new_etym = etym_factory.Create() new_entry.EtymologyOS.Add(new_etym) new_etym.Source.CopyAlternatives(etymology.Source) new_etym.Form.CopyAlternatives(etymology.Form) new_etym.Gloss.CopyAlternatives(etymology.Gloss) new_etym.Comment.CopyAlternatives(etymology.Comment) new_etym.Bibliography.CopyAlternatives(etymology.Bibliography) if hasattr(etymology, "LanguageNotesRA") and etymology.LanguageNotesRA: new_etym.LanguageNotesRA = etymology.LanguageNotesRA # Note: EntryRefsOS (variant/complex form references) are NOT copied. # These describe relationships between entries (variant-of, complex-form-of) # which don't apply to a duplicate — the copy will likely become a # homograph, not an actual variant or complex form of the same components. return new_entry # ========== SYNC INTEGRATION METHODS ========== @OperationsMethod def GetSyncableProperties(self, item): """ Get all syncable properties of a lexical entry for comparison. Args: item: The ILexEntry object. Returns: dict: Dictionary mapping property names to their values. """ props = {} # Patched 2026-06-19: GramTrans fork — use WritingSystems.GetAll() # (returns CoreWritingSystemDefinition objects with .Handle and .Id) # instead of the nonexistent GetAllWritingSystems() / GetWritingSystemTag() # methods. Matches the patch applied to the Grammar Operations classes # (see STATUS.md "flexlibs2 fork" section). _ws_defs = list(self.project.WritingSystems.GetAll()) # MultiString properties # LexemeForm - primary lexeme form if hasattr(item, "LexemeFormOA") and item.LexemeFormOA: form_dict = {} for ws_def in _ws_defs: text = normalize_text(ITsString(item.LexemeFormOA.Form.get_String(ws_def.Handle)).Text) if text: form_dict[ws_def.Id] = text props["LexemeForm"] = form_dict else: props["LexemeForm"] = {} # CitationForm - citation form (IMultiString) citation_dict = {} if hasattr(item, "CitationForm"): for ws_def in _ws_defs: text = normalize_text(ITsString(item.CitationForm.get_String(ws_def.Handle)).Text) if text: citation_dict[ws_def.Id] = text props["CitationForm"] = citation_dict # Comment - entry-level comment (IMultiString) comment_dict = {} if hasattr(item, "Comment"): for ws_def in _ws_defs: text = normalize_text(ITsString(item.Comment.get_String(ws_def.Handle)).Text) if text: comment_dict[ws_def.Id] = text props["Comment"] = comment_dict # Bibliography - bibliographic reference (IMultiString) bibliography_dict = {} if hasattr(item, "Bibliography"): for ws_def in _ws_defs: text = normalize_text(ITsString(item.Bibliography.get_String(ws_def.Handle)).Text) if text: bibliography_dict[ws_def.Id] = text props["Bibliography"] = bibliography_dict # LiteralMeaning - literal meaning (IMultiString). (P2: confirmed field name) literal_dict = {} if hasattr(item, "LiteralMeaning"): for ws_def in _ws_defs: text = normalize_text(ITsString(item.LiteralMeaning.get_String(ws_def.Handle)).Text) if text: literal_dict[ws_def.Id] = text props["LiteralMeaning"] = literal_dict # Atomic properties # HomographNumber - homograph number if hasattr(item, "HomographNumber"): props["HomographNumber"] = item.HomographNumber # DoNotPublishInRC - publication exclusion reference collection. # ILexEntry.DoNotPublishInRC is a reference collection of ICmPossibility # (publication objects). Serialize as a frozenset of GUID strings so the # sync layer can compare cross-project without object identity. if hasattr(item, "DoNotPublishInRC"): props["DoNotPublishInRC"] = frozenset( str(pub.Guid) for pub in item.DoNotPublishInRC ) else: props["DoNotPublishInRC"] = frozenset() # DoNotShowMainEntryInRC - main entry display exclusion reference collection. # Same LCM shape as DoNotPublishInRC: reference collection of ICmPossibility. if hasattr(item, "DoNotShowMainEntryInRC"): props["DoNotShowMainEntryInRC"] = frozenset( str(pub.Guid) for pub in item.DoNotShowMainEntryInRC ) else: props["DoNotShowMainEntryInRC"] = frozenset() # ImportResidue - import residue from LIFT files. # ILexEntry.ImportResidue is ITsString (single-string), not a plain str. # Route through _ReadTsString to extract plain Python str. (P0 fix) if hasattr(item, "ImportResidue"): props["ImportResidue"] = self._ReadTsString(item.ImportResidue) else: props["ImportResidue"] = "" # Reference Atomic (RA) properties # MainEntriesOrSensesRS is a Reference Sequence (not included as it's complex) # EntryRefsOS is an Owning Sequence (not included) return props @OperationsMethod def ApplySyncableProperties(self, item, props, ws_map=None, fill_gaps=False): """ Apply a syncable-properties dict onto a LexEntry item. Extends the base implementation to handle the two reference-collection fields (DoNotPublishInRC, DoNotShowMainEntryInRC) which are serialized as frozensets of GUID strings by GetSyncableProperties. All other fields delegate to the base class. Domain ruling: under fill_gaps=True the RC fields are skipped entirely because an empty frozenset is a complete intended value ("publish everywhere"), not a gap. Under fill_gaps=False the existing clear-and-rebuild behavior is unchanged. Args: item: Target ILexEntry object (must already exist in target project). props: dict produced by GetSyncableProperties on a source entry. ws_map: Optional source->target writing-system Id mapping. fill_gaps (bool): When True, skip DoNotPublishInRC and DoNotShowMainEntryInRC; pass through to BaseOperations for all other fields. """ import logging as _logging _log = _logging.getLogger(__name__) self._EnsureWriteEnabled() # Handle reference-collection fields first; remove them so the base # class does not try to setattr a frozenset onto an LCM object. _rc_fields = ("DoNotPublishInRC", "DoNotShowMainEntryInRC") remaining_props = {} rc_props = {} for k, v in props.items(): if k in _rc_fields: rc_props[k] = v else: remaining_props[k] = v # Resolve the target project's publication list once for both RC fields. # Publications live in lexDB.PublicationTypesOA.PossibilitiesOS (flat list; # sub-publications are accessed via SubPossibilitiesOS but publication # exclusion flags only reference top-level publications in practice). def _build_pub_guid_map(): pub_map = {} try: for pub in self.project.lexDB.PublicationTypesOA.PossibilitiesOS: pub_map[str(pub.Guid)] = pub except Exception as exc: _log.warning( "[WARN] ApplySyncableProperties: could not enumerate " "PublicationsOA: %s", exc ) return pub_map with self._TransactionCM("Apply entry properties"): # Apply plain / multistring fields via base class. super().ApplySyncableProperties(item, remaining_props, ws_map=ws_map, fill_gaps=fill_gaps) pub_guid_map = None # Lazy: only built if an RC field is present. for field_name, guid_set in rc_props.items(): # Domain ruling: an empty frozenset means "publish everywhere" -- # it is a complete intended value, not a gap. Skip RC fields # entirely under fill_gaps so they are never cleared or rebuilt. if fill_gaps: continue if not hasattr(item, field_name): continue if not isinstance(guid_set, (frozenset, set)): _log.warning( "[WARN] ApplySyncableProperties: %s expected frozenset, " "got %s -- skipping", field_name, type(guid_set).__name__ ) continue if pub_guid_map is None: pub_guid_map = _build_pub_guid_map() rc_collection = getattr(item, field_name) # Clear-and-rebuild: simpler than diffing, acceptable for v1. rc_collection.Clear() for guid_str in guid_set: pub_obj = pub_guid_map.get(guid_str) if pub_obj is None: _log.warning( "[WARN] ApplySyncableProperties: %s GUID %s not found " "in target project's publication list -- skipped", field_name, guid_str ) else: rc_collection.Add(pub_obj) @OperationsMethod def CompareTo(self, item1, item2, ops1=None, ops2=None): """ Compare two lexical entries and return their differences. Args: item1: The first ILexEntry object. item2: The second ILexEntry object. ops1: Optional LexEntryOperations instance for item1. ops2: Optional LexEntryOperations instance for item2. Returns: tuple: (is_different, differences_dict) """ ops1 = ops1 or self ops2 = ops2 or self props1 = ops1.GetSyncableProperties(item1) props2 = ops2.GetSyncableProperties(item2) differences = {} all_keys = set(props1.keys()) | set(props2.keys()) for key in all_keys: val1 = props1.get(key) val2 = props2.get(key) if val1 != val2: differences[key] = (val1, val2) is_different = len(differences) > 0 return is_different, differences @OperationsMethod def Exists(self, lexeme_form, wsHandle=None): """ Check if a lexical entry with the given lexeme form exists. Args: lexeme_form (str): The lexeme form to search for wsHandle: Optional writing system handle. Defaults to vernacular WS. Returns: bool: True if an entry exists with this lexeme form, False otherwise Raises: FP_NullParameterError: If lexeme_form is None Example: >>> if not project.LexEntry.Exists("run"): ... entry = project.LexEntry.Create("run") >>> >>> # Check in specific writing system >>> if project.LexEntry.Exists("maison", project.WSHandle('fr')): ... print("French entry exists") Notes: - Search is case-sensitive - Search is writing-system specific - Searches lexeme forms only (not citation or alternate forms) - Returns False for empty or whitespace-only forms - Use Find() to get the actual entry object See Also: Find, Create """ self._ValidateParam(lexeme_form, "lexeme_form") if not lexeme_form or not lexeme_form.strip(): return False return self.Find(lexeme_form, wsHandle) is not None @OperationsMethod def Find(self, lexeme_form, wsHandle=None): """ Find a lexical entry by its lexeme form. Args: lexeme_form (str): The lexeme form to search for wsHandle: Optional writing system handle. Defaults to vernacular WS. Returns: ILexEntry or None: The entry object if found, None otherwise Raises: FP_NullParameterError: If lexeme_form is None Example: >>> entry = project.LexEntry.Find("run") >>> if entry: ... headword = project.LexEntry.GetHeadword(entry) ... print(f"Found: {headword}") Found: run >>> # Search in specific writing system >>> entry = project.LexEntry.Find("maison", project.WSHandle('fr')) Notes: - Returns first match only - Search is case-sensitive - Search is writing-system specific - Searches lexeme forms only (not citation or alternate forms) - Returns None if not found (doesn't raise exception) - For headword search, iterate GetAll() and use GetHeadword() See Also: Exists, GetAll, GetLexemeForm """ self._ValidateParam(lexeme_form, "lexeme_form") if not lexeme_form or not lexeme_form.strip(): return None wsHandle = self.__WSHandle(wsHandle) # Search through all entries target = normalize_match_key(lexeme_form, casefold=False) for entry in self.GetAll(): if entry.LexemeFormOA: form = ITsString(entry.LexemeFormOA.Form.get_String(wsHandle)).Text if normalize_match_key(form, casefold=False) == target: return entry return None # --- Headword & Form Management --- @OperationsMethod def GetHeadword(self, entry_or_hvo): """ Get the headword (display form) of a lexical entry. The headword is the primary display form shown in lexicon views, combining the lexeme form with homograph numbers if applicable. Args: entry_or_hvo: Either an ILexEntry object or its HVO Returns: str: The headword string (empty string if not set) Raises: FP_NullParameterError: If entry_or_hvo is None Example: >>> entry = project.LexEntry.Find("run") >>> headword = project.LexEntry.GetHeadword(entry) >>> print(headword) run >>> # Entry with homograph number >>> bank1 = project.LexEntry.Find("bank") # financial institution >>> print(project.LexEntry.GetHeadword(bank1)) bank₁ Notes: - Headword is computed from lexeme form + homograph number - Homograph numbers are subscript (₁, ₂, ₃, etc.) - Headword is read-only - set via SetLexemeForm() and SetHomographNumber() - Returns empty string if entry has no lexeme form See Also: GetLexemeForm, GetHomographNumber, SetHeadword """ self._ValidateParam(entry_or_hvo, "entry_or_hvo") entry = self.__ResolveObject(entry_or_hvo) if entry.HeadWord: return self._NormalizeMultiString(normalize_text(entry.HeadWord.Text)) return "" @OperationsMethod def SetHeadword(self, entry_or_hvo, text, wsHandle=None): """ Set the headword by setting the lexeme form. This is a convenience method equivalent to SetLexemeForm(). The headword display includes homograph numbers automatically. Args: entry_or_hvo: Either an ILexEntry object or its HVO text (str): The new headword text wsHandle: Optional writing system handle. Defaults to vernacular WS. Raises: FP_ReadOnlyError: If project is not opened with write enabled FP_NullParameterError: If entry_or_hvo or text is None FP_ParameterError: If text is empty or entry has no lexeme form Example: >>> entry = project.LexEntry.Find("run") >>> project.LexEntry.SetHeadword(entry, "ran") >>> print(project.LexEntry.GetHeadword(entry)) ran Notes: - This sets the lexeme form, not the citation form - Homograph numbers are managed separately via SetHomographNumber() - Equivalent to calling SetLexemeForm() See Also: GetHeadword, SetLexemeForm, SetHomographNumber """ self._EnsureWriteEnabled() self.SetLexemeForm(entry_or_hvo, text, wsHandle) @OperationsMethod def GetLexemeForm(self, entry_or_hvo, wsHandle=None): """ Get the lexeme form of a lexical entry. The lexeme form is the primary underlying form of the entry. Args: entry_or_hvo: Either an ILexEntry object or its HVO wsHandle: Optional writing system handle. Defaults to vernacular WS. Returns: str: The lexeme form text (empty string if not set) Raises: FP_NullParameterError: If entry_or_hvo is None FP_WritingSystemError: If wsHandle refers to a writing system not configured in the project. Example: >>> entry = project.LexEntry.Find("run") >>> lexeme = project.LexEntry.GetLexemeForm(entry) >>> print(lexeme) run >>> # Get in specific writing system >>> lexeme_fr = project.LexEntry.GetLexemeForm(entry, ... project.WSHandle('fr')) Notes: - Returns empty string if entry has no lexeme form - Returns empty string if form not set in specified writing system - Lexeme form is the base allomorph of the entry - Different from citation form which is used for alphabetization See Also: SetLexemeForm, GetCitationForm, GetHeadword """ self._ValidateParam(entry_or_hvo, "entry_or_hvo") entry = self.__ResolveObject(entry_or_hvo) wsHandle = self.__WSHandle(wsHandle) if not entry.LexemeFormOA: return "" form = ITsString(entry.LexemeFormOA.Form.get_String(wsHandle)).Text return self._NormalizeMultiString(form) @OperationsMethod def SetLexemeForm(self, entry_or_hvo, text, wsHandle=None): """ Set the lexeme form of a lexical entry. Args: entry_or_hvo: Either an ILexEntry object or its HVO text (str): The new lexeme form text wsHandle: Optional writing system handle. Defaults to vernacular WS. Raises: FP_ReadOnlyError: If project is not opened with write enabled FP_NullParameterError: If entry_or_hvo or text is None FP_ParameterError: If text is empty or entry has no lexeme form object Example: >>> entry = project.LexEntry.Find("run") >>> project.LexEntry.SetLexemeForm(entry, "ran") >>> print(project.LexEntry.GetLexemeForm(entry)) ran >>> # Set in specific writing system >>> project.LexEntry.SetLexemeForm(entry, "courir", ... project.WSHandle('fr')) Notes: - Entry must have a lexeme form object (created automatically on Create()) - This updates the primary form of the entry - Does not affect citation form or alternate forms - Changes the headword display See Also: GetLexemeForm, SetCitationForm, Create """ self._EnsureWriteEnabled() self._ValidateParam(entry_or_hvo, "entry_or_hvo") self._ValidateParam(text, "text") self._ValidateStringNotEmpty(text, "text") entry = self.__ResolveObject(entry_or_hvo) wsHandle = self.__WSHandle(wsHandle) if not entry.LexemeFormOA: raise FP_ParameterError("Entry has no lexeme form object") mkstr = TsStringUtils.MakeString(text, wsHandle) entry.LexemeFormOA.Form.set_String(wsHandle, mkstr) @OperationsMethod def GetCitationForm(self, entry_or_hvo, wsHandle=None): """ Get the citation form of a lexical entry. The citation form is used for dictionary ordering and citations. It may differ from the lexeme form (e.g., infinitive vs. stem). Args: entry_or_hvo: Either an ILexEntry object or its HVO wsHandle: Optional writing system handle. Defaults to vernacular WS. Returns: str: The citation form text (empty string if not set) Raises: FP_NullParameterError: If entry_or_hvo is None FP_WritingSystemError: If wsHandle refers to a writing system not configured in the project. Example: >>> entry = project.LexEntry.Find("run") >>> citation = project.LexEntry.GetCitationForm(entry) >>> print(citation) run >>> # For verbs, citation might be infinitive >>> verb = project.LexEntry.Find("am") >>> print(project.LexEntry.GetCitationForm(verb)) be Notes: - Returns empty string if citation form not set - Falls back to lexeme form if citation form is empty - Used for alphabetization in dictionaries - Can differ from lexeme form for irregular forms See Also: SetCitationForm, GetLexemeForm """ self._ValidateParam(entry_or_hvo, "entry_or_hvo") entry = self.__ResolveObject(entry_or_hvo) wsHandle = self.__WSHandle(wsHandle) form = ITsString(entry.CitationForm.get_String(wsHandle)).Text return self._NormalizeMultiString(form) @OperationsMethod def SetCitationForm(self, entry_or_hvo, text, wsHandle=None): """ Set the citation form of a lexical entry. Args: entry_or_hvo: Either an ILexEntry object or its HVO text (str): The new citation form text wsHandle: Optional writing system handle. Defaults to vernacular WS. Raises: FP_ReadOnlyError: If project is not opened with write enabled FP_NullParameterError: If entry_or_hvo or text is None Example: >>> entry = project.LexEntry.Find("am") >>> project.LexEntry.SetCitationForm(entry, "be") >>> print(project.LexEntry.GetCitationForm(entry)) be >>> # Set in specific writing system >>> project.LexEntry.SetCitationForm(entry, "être", ... project.WSHandle('fr')) Notes: - Citation form is used for dictionary ordering - Can be empty - entry will use lexeme form for sorting - Commonly used for irregular forms (e.g., verb infinitives) - Does not affect the lexeme form See Also: GetCitationForm, SetLexemeForm """ self._EnsureWriteEnabled() self._ValidateParam(entry_or_hvo, "entry_or_hvo") self._ValidateParam(text, "text") entry = self.__ResolveObject(entry_or_hvo) wsHandle = self.__WSHandle(wsHandle) mkstr = TsStringUtils.MakeString(text, wsHandle) entry.CitationForm.set_String(wsHandle, mkstr) @OperationsMethod def GetBestVernacularAlternative(self, entry_or_hvo): """ Get best available vernacular form (Pattern 5 - fallback logic). Returns the "best" vernacular form for the entry using FLEx's standard fallback logic: Citation Form → Lexeme Form → Headword. Args: entry_or_hvo: Either an ILexEntry object or its HVO Returns: str: The best available vernacular form Example: >>> entry = project.LexEntry.Find("run") >>> best_form = project.LexEntry.GetBestVernacularAlternative(entry) >>> print(best_form) run >>> # Useful for display when you want the "best" form >>> for entry in project.LexiconAllEntries(): ... form = project.LexEntry.GetBestVernacularAlternative(entry) ... print(form) Notes: - Common FLEx pattern for display purposes - Fallback order: CitationForm → LexemeForm → Headword - Always returns a non-empty string (Headword is last resort) - Based on FLEx LCM BestVernacularAlternative property - Uses default vernacular writing system See Also: GetCitationForm, GetLexemeForm, GetHeadword """ self._ValidateParam(entry_or_hvo, "entry_or_hvo") entry = self.__ResolveObject(entry_or_hvo) ws_handle = self.project.project.DefaultVernWs # Try citation form first citation = ITsString(entry.CitationForm.get_String(ws_handle)).Text if citation: return citation # Try lexeme form if entry.LexemeFormOA: lexeme = ITsString(entry.LexemeFormOA.Form.get_String(ws_handle)).Text if lexeme: return lexeme # Last resort: headword (computed property, always returns something) return self.GetHeadword(entry) # --- Computed Properties (Pattern 2) --- @OperationsMethod def GetShortName(self, entry_or_hvo, wsHandle=None): """ Get short display name for entry (Pattern 2 - computed property). Returns an abbreviated form suitable for UI display, typically the headword without additional formatting. Args: entry_or_hvo: Either an ILexEntry object or its HVO wsHandle: Optional writing system handle. Defaults to vernacular WS. Returns: str: Short name for display Example: >>> entry = project.LexEntry.Find("run") >>> short_name = project.LexEntry.GetShortName(entry) >>> print(short_name) run Notes: - Computed property - not stored in database - Typically same as headword for simple entries - Based on FLEx LCM ShortName property - Useful for abbreviated displays See Also: GetHeadword, GetLongName """ self._ValidateParam(entry_or_hvo, "entry_or_hvo") entry = self.__ResolveObject(entry_or_hvo) # Short name is typically the headword return self.GetHeadword(entry) @OperationsMethod def GetLongName(self, entry_or_hvo, wsHandle=None): """ Get long display name for entry (Pattern 2 - computed property). Returns a more descriptive form including additional context, typically headword + gloss or definition. Args: entry_or_hvo: Either an ILexEntry object or its HVO wsHandle: Optional writing system handle. Defaults to analysis WS. Returns: str: Long name for display Example: >>> entry = project.LexEntry.Find("run") >>> long_name = project.LexEntry.GetLongName(entry) >>> print(long_name) run (to move swiftly) Notes: - Computed property - not stored in database - Includes headword + first sense gloss if available - Based on FLEx LCM LongName property - Useful for choosers and detailed displays See Also: GetShortName, GetHeadword """ self._ValidateParam(entry_or_hvo, "entry_or_hvo") entry = self.__ResolveObject(entry_or_hvo) wsHandle = self.__WSHandleAnalysis(wsHandle) # Start with headword long_name = self.GetHeadword(entry) # Add first sense gloss if available if entry.SensesOS and entry.SensesOS.Count > 0: first_sense = entry.SensesOS[0] gloss = ITsString(first_sense.Gloss.get_String(wsHandle)).Text if gloss: long_name = f"{long_name} ({gloss})" return long_name @OperationsMethod def GetLIFTid(self, entry_or_hvo): """ Get LIFT XML identifier for entry (Pattern 2 - computed property). Returns the identifier used in LIFT (Lexicon Interchange FormaT) export. This is typically based on the entry's GUID. Args: entry_or_hvo: Either an ILexEntry object or its HVO Returns: str: LIFT identifier (GUID as string) Example: >>> entry = project.LexEntry.Find("run") >>> lift_id = project.LexEntry.GetLIFTid(entry) >>> print(lift_id) 12345678-1234-1234-1234-123456789abc Notes: - Computed from entry's GUID - Used for LIFT import/export - Persistent across projects - Based on FLEx LCM LIFTid property See Also: GetGuid """ self._ValidateParam(entry_or_hvo, "entry_or_hvo") entry = self.__ResolveObject(entry_or_hvo) # LIFT id is the GUID as string return str(entry.Guid) # --- Entry Properties --- @OperationsMethod def GetHomographNumber(self, entry_or_hvo): """ Get the homograph number of a lexical entry. Homograph numbers distinguish entries with identical forms (e.g., "bank₁" for river bank vs. "bank₂" for financial institution). Args: entry_or_hvo: Either an ILexEntry object or its HVO Returns: int: The homograph number (0 if not set) Raises: FP_NullParameterError: If entry_or_hvo is None Example: >>> bank1 = project.LexEntry.Find("bank") # first entry >>> num = project.LexEntry.GetHomographNumber(bank1) >>> print(num) 1 >>> # Entries without homographs have number 0 >>> unique = project.LexEntry.Find("unique") >>> print(project.LexEntry.GetHomographNumber(unique)) 0 Notes: - Returns 0 for entries without homographs - Returns 1, 2, 3, etc. for homographs - Homograph numbers are assigned automatically by FLEx - Numbers appear as subscripts in headword display See Also: SetHomographNumber, GetHeadword """ self._ValidateParam(entry_or_hvo, "entry_or_hvo") entry = self.__ResolveObject(entry_or_hvo) return entry.HomographNumber @OperationsMethod def SetHomographNumber(self, entry_or_hvo, number): """ Set the homograph number of a lexical entry. Args: entry_or_hvo: Either an ILexEntry object or its HVO number (int): The homograph number (0 to clear, 1+ for homographs) Raises: FP_ReadOnlyError: If project is not opened with write enabled FP_NullParameterError: If entry_or_hvo or number is None FP_ParameterError: If number is negative Example: >>> entry = project.LexEntry.Find("bank") >>> project.LexEntry.SetHomographNumber(entry, 1) >>> print(project.LexEntry.GetHeadword(entry)) bank₁ >>> # Clear homograph number >>> project.LexEntry.SetHomographNumber(entry, 0) Warning: - Manually setting homograph numbers may cause conflicts - FLEx normally manages homograph numbers automatically - Use with caution - prefer letting FLEx auto-assign Notes: - Set to 0 to indicate no homograph - Set to 1, 2, 3, etc. for multiple homographs - Changes the headword display - Affects dictionary sorting and display See Also: GetHomographNumber, GetHeadword """ self._EnsureWriteEnabled() self._ValidateParam(entry_or_hvo, "entry_or_hvo") self._ValidateParam(number, "number") if number < 0: raise FP_ParameterError("Homograph number cannot be negative") entry = self.__ResolveObject(entry_or_hvo) entry.HomographNumber = number @OperationsMethod def GetDateCreated(self, entry_or_hvo): """ Get the creation date of a lexical entry. Args: entry_or_hvo: Either an ILexEntry object or its HVO Returns: System.DateTime: The date and time the entry was created Raises: FP_NullParameterError: If entry_or_hvo is None Example: >>> entry = project.LexEntry.Find("run") >>> created = project.LexEntry.GetDateCreated(entry) >>> print(f"Created: {created}") Created: 2025-01-15 14:30:22 >>> # Format the date >>> from datetime import datetime >>> dt = datetime(created.Year, created.Month, created.Day) >>> print(dt.strftime("%Y-%m-%d")) 2025-01-15 Notes: - Returns System.DateTime object (not Python datetime) - Automatically set when entry is created - Cannot be modified (read-only property) - Timezone is local to the FLEx project See Also: GetDateModified """ self._ValidateParam(entry_or_hvo, "entry_or_hvo") entry = self.__ResolveObject(entry_or_hvo) return entry.DateCreated @OperationsMethod def GetDateModified(self, entry_or_hvo): """ Get the last modification date of a lexical entry. Args: entry_or_hvo: Either an ILexEntry object or its HVO Returns: System.DateTime: The date and time the entry was last modified Raises: FP_NullParameterError: If entry_or_hvo is None Example: >>> entry = project.LexEntry.Find("run") >>> modified = project.LexEntry.GetDateModified(entry) >>> print(f"Last modified: {modified}") Last modified: 2025-01-20 09:15:43 >>> # Check if recently modified >>> from datetime import datetime, timedelta >>> mod_dt = datetime(modified.Year, modified.Month, modified.Day) >>> if datetime.now() - mod_dt < timedelta(days=7): ... print("Modified in the last week") Notes: - Returns System.DateTime object (not Python datetime) - Automatically updated when entry changes - Cannot be modified directly (read-only property) - Updates on any change to entry or its senses/forms See Also: GetDateCreated """ self._ValidateParam(entry_or_hvo, "entry_or_hvo") entry = self.__ResolveObject(entry_or_hvo) return entry.DateModified @OperationsMethod def GetMorphType(self, entry_or_hvo): """ Get the morph type of a lexical entry's lexeme form. The morph type indicates the morphological category (stem, root, prefix, suffix, etc.). Args: entry_or_hvo: Either an ILexEntry object or its HVO Returns: IMoMorphType or None: The morph type object, or None if not set Raises: FP_NullParameterError: If entry_or_hvo is None Example: >>> entry = project.LexEntry.Find("run") >>> morph_type = project.LexEntry.GetMorphType(entry) >>> if morph_type: ... print(ITsString(morph_type.Name.BestAnalysisAlternative).Text) stem >>> # Check if entry is an affix >>> suffix = project.LexEntry.Find("-ing") >>> mt = project.LexEntry.GetMorphType(suffix) >>> if mt: ... name = ITsString(mt.Name.BestAnalysisAlternative).Text ... if name in ("prefix", "suffix", "infix"): ... print("This is an affix") Notes: - Returns None if entry has no lexeme form - Returns None if lexeme form has no morph type set - Common morph types: stem, root, prefix, suffix, infix, circumfix - Morph type affects parsing and morphological analysis See Also: SetMorphType, Create """ self._ValidateParam(entry_or_hvo, "entry_or_hvo") entry = self.__ResolveObject(entry_or_hvo) if not entry.LexemeFormOA: return None return entry.LexemeFormOA.MorphTypeRA @OperationsMethod def SetMorphType(self, entry_or_hvo, morph_type_or_name): """ Set the morph type of a lexical entry's lexeme form. Args: entry_or_hvo: Either an ILexEntry object or its HVO morph_type_or_name: Either an IMoMorphType object or a morph type name (str) such as "stem", "root", "prefix", "suffix", etc. Raises: FP_ReadOnlyError: If project is not opened with write enabled FP_NullParameterError: If entry_or_hvo or morph_type_or_name is None FP_ParameterError: If entry has no lexeme form, or morph type name not found Example: >>> entry = project.LexEntry.Find("-ing") >>> project.LexEntry.SetMorphType(entry, "suffix") >>> mt = project.LexEntry.GetMorphType(entry) >>> print(ITsString(mt.Name.BestAnalysisAlternative).Text) suffix >>> # Set using morph type object >>> morph_types = project.lp.MorphTypesOA.PossibilitiesOS >>> stem_type = morph_types[0] # assuming first is stem >>> project.LexEntry.SetMorphType(entry, stem_type) Notes: - Entry must have a lexeme form object - Morph type name search is case-insensitive - Common morph types: stem, root, prefix, suffix, infix, circumfix - Affects how entry is analyzed in parsing - Project must have the morph type defined See Also: GetMorphType, Create, GetAvailableMorphTypes, ValidateMorphType """ self._EnsureWriteEnabled() self._ValidateParam(entry_or_hvo, "entry_or_hvo") self._ValidateParam(morph_type_or_name, "morph_type_or_name") entry = self.__ResolveObject(entry_or_hvo) if not entry.LexemeFormOA: raise FP_ParameterError("Entry has no lexeme form object") # Resolve morph type if isinstance(morph_type_or_name, str): morph_type = self.__FindMorphType(morph_type_or_name) if morph_type is None: raise FP_ParameterError(f"Morph type '{morph_type_or_name}' not found") else: morph_type = morph_type_or_name entry.LexemeFormOA.MorphTypeRA = morph_type @OperationsMethod def GetAvailableMorphTypes(self, recursive=True, **kwargs): """ Get a list of all available morph types in the project. Args: recursive (bool): If True (default), includes every morph type in the hierarchy (depth-first, parents before children). If False, returns only top-level morph types. Returns: list: List of tuples (name, IMoMorphType, is_stem_type) where: - name (str): The morph type name - IMoMorphType: The morph type object - is_stem_type (bool): True if stem type, False if affix type Example: >>> morph_types = project.LexEntry.GetAvailableMorphTypes() >>> for name, mt, is_stem in morph_types: ... type_str = "stem" if is_stem else "affix" ... print(f"{name}: {type_str}") stem: stem root: stem prefix: affix ... >>> # Top-level types only >>> top_level = project.LexEntry.GetAvailableMorphTypes(recursive=False) See Also: ValidateMorphType, SetMorphType, Create """ self._RejectLegacyKwargs(kwargs, { "include_subcategories": ( "recursive", "name-only rename; semantics preserved", ), "flat": ( "recursive", "semantics inverted: flat=True is now recursive=True", ), }) morph_types = self.project.lp.LexDbOA.MorphTypesOA if morph_types is None: return [] result = [] def collect_types(possibilities): for mt in possibilities: name = best_analysis_text(mt.Name) if name: is_stem = self.__IsStemType(mt) result.append((name, mt, is_stem)) if recursive and mt.SubPossibilitiesOS.Count > 0: collect_types(mt.SubPossibilitiesOS) collect_types(morph_types.PossibilitiesOS) return result @OperationsMethod def ValidateMorphType(self, morph_type_name): """ Check if a morph type name exists in the project. Args: morph_type_name (str): The morph type name to validate (case-insensitive) Returns: tuple: (is_valid, morph_type_obj, is_stem_type) where: - is_valid (bool): True if morph type exists - morph_type_obj (IMoMorphType or None): The morph type object if found - is_stem_type (bool or None): True if stem type, False if affix, None if not found Example: >>> is_valid, mt, is_stem = project.LexEntry.ValidateMorphType("suffix") >>> if is_valid: ... print(f"Valid morph type: {mt.Name.BestAnalysisAlternative.Text}") ... print(f"Is stem type: {is_stem}") Valid morph type: suffix Is stem type: False >>> is_valid, mt, is_stem = project.LexEntry.ValidateMorphType("invalid") >>> print(f"Valid: {is_valid}") Valid: False Notes: - Search is case-insensitive - Searches through all morph types including subcategories - Useful for validating user input before creating entries See Also: GetAvailableMorphTypes, Create, SetMorphType """ if not morph_type_name: return (False, None, None) morph_type = self.__FindMorphType(morph_type_name) if morph_type: is_stem = self.__IsStemType(morph_type) return (True, morph_type, is_stem) return (False, None, None) @OperationsMethod def GetAllByMorphType(self, name_or_list, match_allomorphs=False): """ Return all entries whose lexeme-form morph type matches the given name(s). Comparison is by GUID (not substring), so affix-family names like 'suffix' never accidentally match 'circumfix-suffix' or similar. Display markers are stripped before lookup, so '=enclitic' resolves the same as 'enclitic'. Args: name_or_list (str | list[str]): Morph type name(s) to match. A single string matches one type; a list uses OR semantics (useful for affix-family grouping such as ['prefix', 'suffix', 'infix']). match_allomorphs (bool): When False (default) only the lexeme-form morph type is checked. When True, entries are also included if any AlternateFormsOS allomorph matches. Returns: list: ILexEntry objects whose morph type matches. Raises: FP_ParameterError: If any name in name_or_list is not found in the project's morph type list. Example: >>> affixes = project.LexEntry.GetAllByMorphType(['prefix', 'suffix', 'infix']) >>> stems = project.LexEntry.GetAllByMorphType('stem') >>> enclitics = project.LexEntry.GetAllByMorphType('=enclitic') # markers OK Notes: - An entry's morph type is on its lexeme form; allomorph morph types may differ and are only checked when match_allomorphs=True. - List input uses OR semantics; chain set() operations for AND/exclusion. See Also: ValidateMorphType, GetMorphType, GetAvailableMorphTypes """ if isinstance(name_or_list, str): name_or_list = [name_or_list] # Resolve all names to morph-type GUIDs up front so errors surface early. target_guids = set() for name in name_or_list: mt = self.__FindMorphType(name) if mt is None: raise FP_ParameterError( f"Morph type '{name}' not found. " "Use GetAvailableMorphTypes() to see valid names." ) target_guids.add(mt.Guid) results = [] for entry in self.project.lp.LexDbOA.Entries: matched = False if entry.LexemeFormOA and entry.LexemeFormOA.MorphTypeRA: if entry.LexemeFormOA.MorphTypeRA.Guid in target_guids: matched = True if not matched and match_allomorphs: for alm in entry.AlternateFormsOS: if alm.MorphTypeRA and alm.MorphTypeRA.Guid in target_guids: matched = True break if matched: results.append(entry) return results # --- Sense Management --- @OperationsMethod def GetSenses(self, entry_or_hvo): """ Get all senses of a lexical entry. Args: entry_or_hvo: Either an ILexEntry object or its HVO Returns: list: List of ILexSense objects (empty list if none) Raises: FP_NullParameterError: If entry_or_hvo is None Example: >>> entry = project.LexEntry.Find("run") >>> senses = project.LexEntry.GetSenses(entry) >>> for sense in senses: ... gloss = project.LexiconGetSenseGloss(sense) ... print(f"Sense: {gloss}") Sense: to move rapidly on foot Sense: to operate or function Sense: a point scored in baseball Notes: - Returns empty list if entry has no senses - Senses are in database order - Each sense represents a distinct meaning - Use FLExProject.LexiconGetSenseGloss() to get gloss text See Also: GetSenseCount, AddSense """ self._ValidateParam(entry_or_hvo, "entry_or_hvo") entry = self.__ResolveObject(entry_or_hvo) return list(entry.SensesOS) @OperationsMethod def GetSenseCount(self, entry_or_hvo): """ Get the count of senses for a lexical entry. Args: entry_or_hvo: Either an ILexEntry object or its HVO Returns: int: The number of senses (0 if none) Raises: FP_NullParameterError: If entry_or_hvo is None Example: >>> entry = project.LexEntry.Find("run") >>> count = project.LexEntry.GetSenseCount(entry) >>> print(f"Entry has {count} senses") Entry has 3 senses >>> # Check if entry has senses >>> if project.LexEntry.GetSenseCount(entry) == 0: ... print("Entry has no senses - add one!") Notes: - Returns 0 if entry has no senses - More efficient than len(GetSenses()) for large entries - Includes all senses (no filtering) See Also: GetSenses, AddSense """ self._ValidateParam(entry_or_hvo, "entry_or_hvo") entry = self.__ResolveObject(entry_or_hvo) return entry.SensesOS.Count @OperationsMethod def AddSense(self, entry_or_hvo, gloss, wsHandle=None): """ Add a new sense to a lexical entry. Args: entry_or_hvo: Either an ILexEntry object or its HVO gloss (str): The gloss text for the new sense wsHandle: Optional writing system handle. Defaults to analysis WS. Returns: ILexSense: The newly created sense object Raises: FP_ReadOnlyError: If project is not opened with write enabled FP_NullParameterError: If entry_or_hvo or gloss is None FP_ParameterError: If gloss is empty Example: >>> entry = project.LexEntry.Find("run") >>> sense = project.LexEntry.AddSense(entry, "to move rapidly on foot") >>> gloss = project.LexiconGetSenseGloss(sense) >>> print(gloss) to move rapidly on foot >>> # Add sense in specific writing system >>> sense_fr = project.LexEntry.AddSense(entry, "courir", ... project.WSHandle('fr')) Notes: - Sense is appended to the end of the sense list - Gloss is stored in the specified writing system - Only gloss is set - use FLExProject methods for other properties - New sense has no definition, examples, or other fields set See Also: GetSenses, GetSenseCount """ self._EnsureWriteEnabled() self._ValidateParam(entry_or_hvo, "entry_or_hvo") self._ValidateParam(gloss, "gloss") self._ValidateStringNotEmpty(gloss, "gloss") entry = self.__ResolveObject(entry_or_hvo) wsHandle = self.__WSHandleAnalysis(wsHandle) with self._TransactionCM("Add sense"): # Create the new sense using the factory factory = self.project.project.ServiceLocator.GetService(ILexSenseFactory) new_sense = factory.Create() # Add to entry's sense list (must be done before setting properties) entry.SensesOS.Add(new_sense) # Set the gloss mkstr = TsStringUtils.MakeString(gloss, wsHandle) new_sense.Gloss.set_String(wsHandle, mkstr) return new_sense # --- Additional Properties --- @OperationsMethod def GetGuid(self, entry_or_hvo): """ Get the GUID (Globally Unique Identifier) of a lexical entry. Args: entry_or_hvo: Either an ILexEntry object or its HVO Returns: System.Guid: The entry's GUID Raises: FP_NullParameterError: If entry_or_hvo is None Example: >>> entry = project.LexEntry.Find("run") >>> guid = project.LexEntry.GetGuid(entry) >>> print(guid) a1b2c3d4-e5f6-7890-abcd-ef1234567890 >>> # Get as string >>> guid_str = str(guid) >>> print(guid_str) a1b2c3d4-e5f6-7890-abcd-ef1234567890 >>> # Use GUID to retrieve entry later >>> entry2 = project.Object(guid) >>> print(project.LexEntry.GetHeadword(entry2)) run Notes: - GUIDs are unique across all FLEx projects - GUIDs are persistent (don't change) - Useful for linking entries across projects - Can be used with FLExProject.Object() to retrieve entry See Also: FLExProject.Object, FLExProject.BuildGotoURL """ self._ValidateParam(entry_or_hvo, "entry_or_hvo") entry = self.__ResolveObject(entry_or_hvo) return entry.Guid @OperationsMethod def GetImportResidue(self, entry_or_hvo): """ Get the import residue of a lexical entry. Import residue stores unparsed data from imports (e.g., LIFT, SFM) that couldn't be mapped to FLEx fields. Args: entry_or_hvo: Either an ILexEntry object or its HVO Returns: str: The import residue text (empty string if not set) Raises: FP_NullParameterError: If entry_or_hvo is None Example: >>> entry = project.LexEntry.Find("run") >>> residue = project.LexEntry.GetImportResidue(entry) >>> if residue: ... print(f"Import residue: {residue}") Notes: - Returns empty string if no import residue - Import residue is stored as XML - Contains data that couldn't be imported to standard fields - Useful for preserving data from external sources See Also: SetImportResidue """ self._ValidateParam(entry_or_hvo, "entry_or_hvo") entry = self.__ResolveObject(entry_or_hvo) # ILexEntry.ImportResidue is ITsString (single-string), not a plain # Python str -- it must be unwrapped via _ReadTsString, not passed # through _NormalizeMultiString. (Issue #115.) return self._ReadTsString(entry.ImportResidue) @OperationsMethod def SetImportResidue(self, entry_or_hvo, residue): """ Set the import residue of a lexical entry. Args: entry_or_hvo: Either an ILexEntry object or its HVO residue (str): The import residue text to set Raises: FP_ReadOnlyError: If project is not opened with write enabled FP_NullParameterError: If entry_or_hvo or residue is None Example: >>> entry = project.LexEntry.Find("run") >>> project.LexEntry.SetImportResidue(entry, ... "<custom><field1>value1</field1></custom>") >>> # Clear import residue >>> project.LexEntry.SetImportResidue(entry, "") Notes: - Import residue is typically XML format - Used to store unparsed import data - Can be empty string to clear - Preserves data that doesn't fit standard FLEx fields See Also: GetImportResidue """ self._EnsureWriteEnabled() self._ValidateParam(entry_or_hvo, "entry_or_hvo") self._ValidateParam(residue, "residue") entry = self.__ResolveObject(entry_or_hvo) # ILexEntry.ImportResidue is ITsString (single-string); assigning a # raw Python str raises TypeError at the pythonnet boundary. Wrap # via _MakeTsString. (Issue #39.) entry.ImportResidue = self._MakeTsString(residue) # --- MultiString/MultiUnicode Properties --- @OperationsMethod def GetBibliography(self, entry_or_hvo, wsHandle=None): """ Get the bibliography of a lexical entry. Args: entry_or_hvo: Either an ILexEntry object or its HVO wsHandle: Optional writing system handle. Defaults to analysis WS. Returns: str: The bibliography text Example: >>> entry = project.LexEntry.Find("anthropology") >>> bib = project.LexEntry.GetBibliography(entry) >>> print(bib) Smith 2015: 42-43 """ self._ValidateParam(entry_or_hvo, "entry_or_hvo") entry = self.__ResolveObject(entry_or_hvo) wsHandle = self.__WSHandleAnalysis(wsHandle) return self._NormalizeMultiString(ITsString(entry.Bibliography.get_String(wsHandle)).Text) @OperationsMethod def SetBibliography(self, entry_or_hvo, text, wsHandle=None): """ Set the bibliography of a lexical entry. Args: entry_or_hvo: Either an ILexEntry object or its HVO text (str): The bibliography text wsHandle: Optional writing system handle. Defaults to analysis WS. """ self._EnsureWriteEnabled() self._ValidateParam(entry_or_hvo, "entry_or_hvo") self._ValidateParam(text, "text") entry = self.__ResolveObject(entry_or_hvo) wsHandle = self.__WSHandleAnalysis(wsHandle) mkstr = TsStringUtils.MakeString(text, wsHandle) entry.Bibliography.set_String(wsHandle, mkstr) @OperationsMethod def GetComment(self, entry_or_hvo, wsHandle=None): """ Get the comment of a lexical entry. Args: entry_or_hvo: Either an ILexEntry object or its HVO wsHandle: Optional writing system handle. Defaults to analysis WS. Returns: str: The comment text """ self._ValidateParam(entry_or_hvo, "entry_or_hvo") entry = self.__ResolveObject(entry_or_hvo) wsHandle = self.__WSHandleAnalysis(wsHandle) return self._NormalizeMultiString(ITsString(entry.Comment.get_String(wsHandle)).Text) @OperationsMethod def SetComment(self, entry_or_hvo, text, wsHandle=None): """ Set the comment of a lexical entry. Args: entry_or_hvo: Either an ILexEntry object or its HVO text (str): The comment text wsHandle: Optional writing system handle. Defaults to analysis WS. """ self._EnsureWriteEnabled() self._ValidateParam(entry_or_hvo, "entry_or_hvo") self._ValidateParam(text, "text") entry = self.__ResolveObject(entry_or_hvo) wsHandle = self.__WSHandleAnalysis(wsHandle) mkstr = TsStringUtils.MakeString(text, wsHandle) entry.Comment.set_String(wsHandle, mkstr) @OperationsMethod def GetLiteralMeaning(self, entry_or_hvo, wsHandle=None): """ Get the literal meaning of a lexical entry. Args: entry_or_hvo: Either an ILexEntry object or its HVO wsHandle: Optional writing system handle. Defaults to analysis WS. Returns: str: The literal meaning text """ self._ValidateParam(entry_or_hvo, "entry_or_hvo") entry = self.__ResolveObject(entry_or_hvo) wsHandle = self.__WSHandleAnalysis(wsHandle) return self._NormalizeMultiString(ITsString(entry.LiteralMeaning.get_String(wsHandle)).Text) @OperationsMethod def SetLiteralMeaning(self, entry_or_hvo, text, wsHandle=None): """ Set the literal meaning of a lexical entry. Args: entry_or_hvo: Either an ILexEntry object or its HVO text (str): The literal meaning text wsHandle: Optional writing system handle. Defaults to analysis WS. """ self._EnsureWriteEnabled() self._ValidateParam(entry_or_hvo, "entry_or_hvo") self._ValidateParam(text, "text") entry = self.__ResolveObject(entry_or_hvo) wsHandle = self.__WSHandleAnalysis(wsHandle) mkstr = TsStringUtils.MakeString(text, wsHandle) entry.LiteralMeaning.set_String(wsHandle, mkstr) @OperationsMethod def GetRestrictions(self, entry_or_hvo, wsHandle=None): """ Get the restrictions of a lexical entry. Args: entry_or_hvo: Either an ILexEntry object or its HVO wsHandle: Optional writing system handle. Defaults to analysis WS. Returns: str: The restrictions text """ self._ValidateParam(entry_or_hvo, "entry_or_hvo") entry = self.__ResolveObject(entry_or_hvo) wsHandle = self.__WSHandleAnalysis(wsHandle) return self._NormalizeMultiString(ITsString(entry.Restrictions.get_String(wsHandle)).Text) @OperationsMethod def SetRestrictions(self, entry_or_hvo, text, wsHandle=None): """ Set the restrictions of a lexical entry. Args: entry_or_hvo: Either an ILexEntry object or its HVO text (str): The restrictions text wsHandle: Optional writing system handle. Defaults to analysis WS. """ self._EnsureWriteEnabled() self._ValidateParam(entry_or_hvo, "entry_or_hvo") self._ValidateParam(text, "text") entry = self.__ResolveObject(entry_or_hvo) wsHandle = self.__WSHandleAnalysis(wsHandle) mkstr = TsStringUtils.MakeString(text, wsHandle) entry.Restrictions.set_String(wsHandle, mkstr) @OperationsMethod def GetSummaryDefinition(self, entry_or_hvo, wsHandle=None): """ Get the summary definition of a lexical entry. Args: entry_or_hvo: Either an ILexEntry object or its HVO wsHandle: Optional writing system handle. Defaults to analysis WS. Returns: str: The summary definition text """ self._ValidateParam(entry_or_hvo, "entry_or_hvo") entry = self.__ResolveObject(entry_or_hvo) wsHandle = self.__WSHandleAnalysis(wsHandle) return self._NormalizeMultiString(ITsString(entry.SummaryDefinition.get_String(wsHandle)).Text) @OperationsMethod def SetSummaryDefinition(self, entry_or_hvo, text, wsHandle=None): """ Set the summary definition of a lexical entry. Args: entry_or_hvo: Either an ILexEntry object or its HVO text (str): The summary definition text wsHandle: Optional writing system handle. Defaults to analysis WS. """ self._EnsureWriteEnabled() self._ValidateParam(entry_or_hvo, "entry_or_hvo") self._ValidateParam(text, "text") entry = self.__ResolveObject(entry_or_hvo) wsHandle = self.__WSHandleAnalysis(wsHandle) mkstr = TsStringUtils.MakeString(text, wsHandle) entry.SummaryDefinition.set_String(wsHandle, mkstr) # --- Boolean Properties --- @OperationsMethod def GetDoNotUseForParsing(self, entry_or_hvo): """ Check if an entry is excluded from parsing. Args: entry_or_hvo: Either an ILexEntry object or its HVO Returns: bool: True if excluded from parsing, False otherwise """ self._ValidateParam(entry_or_hvo, "entry_or_hvo") entry = self.__ResolveObject(entry_or_hvo) return entry.DoNotUseForParsing @OperationsMethod def SetDoNotUseForParsing(self, entry_or_hvo, value): """ Set whether an entry is excluded from parsing. Args: entry_or_hvo: Either an ILexEntry object or its HVO value (bool): True to exclude from parsing, False to include """ self._EnsureWriteEnabled() self._ValidateParam(entry_or_hvo, "entry_or_hvo") self._ValidateParam(value, "value") entry = self.__ResolveObject(entry_or_hvo) entry.DoNotUseForParsing = bool(value) @OperationsMethod def GetExcludeAsHeadword(self, entry_or_hvo): """ Check if an entry is excluded as a headword. Args: entry_or_hvo: Either an ILexEntry object or its HVO Returns: bool: True if excluded as headword, False otherwise """ self._ValidateParam(entry_or_hvo, "entry_or_hvo") entry = self.__ResolveObject(entry_or_hvo) return entry.ExcludeAsHeadword @OperationsMethod def SetExcludeAsHeadword(self, entry_or_hvo, value): """ Set whether an entry is excluded as a headword. Args: entry_or_hvo: Either an ILexEntry object or its HVO value (bool): True to exclude as headword, False to include """ self._EnsureWriteEnabled() self._ValidateParam(entry_or_hvo, "entry_or_hvo") self._ValidateParam(value, "value") entry = self.__ResolveObject(entry_or_hvo) entry.ExcludeAsHeadword = bool(value) # --- Collection Properties --- @OperationsMethod def GetDoNotPublishIn(self, entry_or_hvo): """ Get the publications this entry should not be published in. Args: entry_or_hvo: Either an ILexEntry object or its HVO Returns: list: List of publication names Example: >>> entry = project.LexEntry.Find("obscure") >>> pubs = project.LexEntry.GetDoNotPublishIn(entry) >>> print(pubs) ['Main Dictionary', 'Student Edition'] """ self._ValidateParam(entry_or_hvo, "entry_or_hvo") entry = self.__ResolveObject(entry_or_hvo) result = [] for pub in entry.DoNotPublishInRC: name = best_analysis_text(pub.Name) if pub.Name else str(pub.Guid) result.append(name) return result @OperationsMethod def AddDoNotPublishIn(self, entry_or_hvo, publication): """ Add a publication to exclude this entry from. Args: entry_or_hvo: Either an ILexEntry object or its HVO publication: Publication name (str) or ICmPossibility object """ self._EnsureWriteEnabled() self._ValidateParam(entry_or_hvo, "entry_or_hvo") self._ValidateParam(publication, "publication") entry = self.__ResolveObject(entry_or_hvo) # Find publication object if string provided if isinstance(publication, str): pub_obj = self.project.Publications.Find(publication) if pub_obj is None: raise FP_ParameterError(f"Publication '{publication}' not found") publication = pub_obj if publication not in entry.DoNotPublishInRC: entry.DoNotPublishInRC.Add(publication) @OperationsMethod def RemoveDoNotPublishIn(self, entry_or_hvo, publication): """ Remove a publication from the exclude list. Args: entry_or_hvo: Either an ILexEntry object or its HVO publication: Publication name (str) or ICmPossibility object """ self._EnsureWriteEnabled() self._ValidateParam(entry_or_hvo, "entry_or_hvo") self._ValidateParam(publication, "publication") entry = self.__ResolveObject(entry_or_hvo) # Find publication object if string provided if isinstance(publication, str): pub_obj = self.project.Publications.Find(publication) if pub_obj is None: raise FP_ParameterError(f"Publication '{publication}' not found") publication = pub_obj if publication in entry.DoNotPublishInRC: entry.DoNotPublishInRC.Remove(publication) @OperationsMethod def GetDoNotShowMainEntryIn(self, entry_or_hvo): """ Get the publications where this entry should not be shown as main entry. Args: entry_or_hvo: Either an ILexEntry object or its HVO Returns: list: List of publication names """ self._ValidateParam(entry_or_hvo, "entry_or_hvo") entry = self.__ResolveObject(entry_or_hvo) result = [] for pub in entry.DoNotShowMainEntryInRC: name = best_analysis_text(pub.Name) if pub.Name else str(pub.Guid) result.append(name) return result @OperationsMethod def AddDoNotShowMainEntryIn(self, entry_or_hvo, publication): """ Add a publication to not show this entry as main entry. Args: entry_or_hvo: Either an ILexEntry object or its HVO publication: Publication name (str) or ICmPossibility object """ self._EnsureWriteEnabled() self._ValidateParam(entry_or_hvo, "entry_or_hvo") self._ValidateParam(publication, "publication") entry = self.__ResolveObject(entry_or_hvo) # Find publication object if string provided if isinstance(publication, str): pub_obj = self.project.Publications.Find(publication) if pub_obj is None: raise FP_ParameterError(f"Publication '{publication}' not found") publication = pub_obj if publication not in entry.DoNotShowMainEntryInRC: entry.DoNotShowMainEntryInRC.Add(publication) @OperationsMethod def RemoveDoNotShowMainEntryIn(self, entry_or_hvo, publication): """ Remove a publication from the no-main-entry list. Args: entry_or_hvo: Either an ILexEntry object or its HVO publication: Publication name (str) or ICmPossibility object """ self._EnsureWriteEnabled() self._ValidateParam(entry_or_hvo, "entry_or_hvo") self._ValidateParam(publication, "publication") entry = self.__ResolveObject(entry_or_hvo) # Find publication object if string provided if isinstance(publication, str): pub_obj = self.project.Publications.Find(publication) if pub_obj is None: raise FP_ParameterError(f"Publication '{publication}' not found") publication = pub_obj if publication in entry.DoNotShowMainEntryInRC: entry.DoNotShowMainEntryInRC.Remove(publication) # --- Private Helper Methods --- def __ResolveObject(self, entry_or_hvo): """ Resolve HVO or object to ILexEntry. Args: entry_or_hvo: Either an ILexEntry object or an HVO (int) Returns: ILexEntry: The resolved entry object Raises: FP_ParameterError: If HVO doesn't refer to a lexical entry """ if isinstance(entry_or_hvo, int): obj = self.project.Object(entry_or_hvo) if not isinstance(obj, ILexEntry): raise FP_ParameterError("HVO does not refer to a lexical entry") return obj return entry_or_hvo def __WSHandle(self, wsHandle): """ Get writing system handle, defaulting to vernacular WS. 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) # --- Back-Reference Methods (Pattern 3) --- @OperationsMethod def GetVisibleComplexFormBackRefs(self, entry_or_hvo): """ Get all complex forms that reference this entry. Returns all LexEntryRef objects where this entry appears in ShowComplexFormsIn and RefType is ComplexForm. Args: entry_or_hvo: Either an ILexEntry object or its HVO Returns: list: List of ILexEntryRef objects (complex forms referencing this entry) Example: >>> entry = project.LexEntry.Find("run") >>> complex_forms = project.LexEntry.GetVisibleComplexFormBackRefs(entry) >>> for lex_ref in complex_forms: ... complex_entry = lex_ref.OwningEntry ... print(f"Complex form: {project.LexEntry.GetHeadword(complex_entry)}") Notes: - Returns complex forms (compounds, idioms, phrasal verbs, etc.) - Includes subentries (use GetComplexFormsNotSubentries to exclude) - Based on FLEx LCM VisibleComplexFormBackRefs property See Also: GetComplexFormsNotSubentries, GetAllSenses """ self._ValidateParam(entry_or_hvo, "entry_or_hvo") entry = self.__ResolveObject(entry_or_hvo) # Use the LCM property directly (it handles all the complexity) # This is a virtual property that loads incoming references try: back_refs = list(entry.VisibleComplexFormBackRefs) return back_refs except AttributeError: # Fallback if property not available logger.warning("VisibleComplexFormBackRefs not available, returning empty list") return [] @OperationsMethod def GetComplexFormsNotSubentries(self, entry_or_hvo): """ Get complex forms that reference this entry, excluding subentries. Returns complex forms (compounds, idioms, etc.) but excludes any where this entry appears as a subentry (in PrimaryLexemesRS). Args: entry_or_hvo: Either an ILexEntry object or its HVO Returns: list: List of ILexEntryRef objects (complex forms, excluding subentries) Example: >>> entry = project.LexEntry.Find("run") >>> complex_forms = project.LexEntry.GetComplexFormsNotSubentries(entry) >>> for lex_ref in complex_forms: ... cf_entry = lex_ref.OwningEntry ... print(f"Complex form: {project.LexEntry.GetHeadword(cf_entry)}") Notes: - Filters out subentries from VisibleComplexFormBackRefs - A subentry is where the entry appears in PrimaryLexemesRS, OR where one of the entry's senses appears in SensesRS (sense-level subentry) - Based on FLEx LCM ComplexFormsNotSubentries property See Also: GetVisibleComplexFormBackRefs """ self._ValidateParam(entry_or_hvo, "entry_or_hvo") entry = self.__ResolveObject(entry_or_hvo) # Get all complex form back refs all_complex_forms = self.GetVisibleComplexFormBackRefs(entry) # Collect HVOs of all senses owned by this entry for the sense-level check. try: entry_sense_hvos = {s.Hvo for s in entry.SensesOS} except Exception: entry_sense_hvos = set() # Filter out subentries. An entry is treated as a subentry when: # (a) it appears in PrimaryLexemesRS of the ILexEntryRef (entry-level), OR # (b) one of its senses appears in SensesRS of the ILexEntryRef # (sense-level subentry, e.g. shown via ShowSubentriesIn). result = [] for lex_ref in all_complex_forms: try: # Check entry-level subentry (PrimaryLexemesRS) is_subentry = any(item.Hvo == entry.Hvo for item in lex_ref.PrimaryLexemesRS) # Check sense-level subentry (SensesRS) if not already excluded if not is_subentry and entry_sense_hvos: try: is_subentry = any( item.Hvo in entry_sense_hvos for item in lex_ref.SensesRS ) except Exception: pass if not is_subentry: result.append(lex_ref) except Exception: # If we can't check, include it result.append(lex_ref) return result @OperationsMethod def GetMinimalLexReferences(self, entry_or_hvo): """ Get essential lexical references for this entry. Returns only "minimal" lexical references - those that are multi-target or have specific mapping types (sequence types). Args: entry_or_hvo: Either an ILexEntry object or its HVO Returns: list: List of ILexReference objects (minimal references) Example: >>> entry = project.LexEntry.Find("big") >>> lex_refs = project.LexEntry.GetMinimalLexReferences(entry) >>> for lex_ref in lex_refs: ... ref_type = lex_ref.Owner # ILexRefType ... print(f"Reference type: {ref_type.Name.BestAnalysisAlternative.Text}") ... for target in lex_ref.TargetsRS: ... if target.Hvo != entry.Hvo: ... print(f" -> {project.LexEntry.GetHeadword(target)}") Notes: - Includes multi-target references (synonyms, antonyms, etc.) - Includes sequence-type references - Excludes single-target non-sequence references - Based on FLEx LCM MinimalLexReferences property See Also: GetVisibleComplexFormBackRefs """ self._ValidateParam(entry_or_hvo, "entry_or_hvo") entry = self.__ResolveObject(entry_or_hvo) # Use the LCM property directly try: lex_refs = list(entry.MinimalLexReferences) return lex_refs except AttributeError: # Fallback if property not available logger.warning("MinimalLexReferences not available, returning empty list") return [] @wrap_enumerable @OperationsMethod def GetAllSenses(self, entry_or_hvo): """ Get all senses owned by this entry, including subsenses recursively. Returns all senses in a flattened list, recursively including all subsenses at any depth. Args: entry_or_hvo: Either an ILexEntry object or its HVO Returns: list: List of ILexSense objects (all senses and subsenses) Example: >>> entry = project.LexEntry.Find("run") >>> all_senses = project.LexEntry.GetAllSenses(entry) >>> print(f"Total senses (including subsenses): {len(all_senses)}") >>> for sense in all_senses: ... gloss = project.Senses.GetGloss(sense) ... depth = len(list(sense.PathToRoot)) - 2 # Approximate depth ... indent = " " * depth ... print(f"{indent}{gloss}") Notes: - Recursively collects all subsenses at any depth - Based on FLEx LCM AllSenses property - For counting: use GetSenseCount or len(GetAllSenses(entry)) - Entry.AllSenses does NOT include the entry itself See Also: GetSenseCount, GetSenses """ self._ValidateParam(entry_or_hvo, "entry_or_hvo") entry = self.__ResolveObject(entry_or_hvo) # Use the LCM property directly try: all_senses = list(entry.AllSenses) return all_senses except AttributeError: # Fallback: manually collect recursively result = [] for sense in entry.SensesOS: # Use LexSenseOperations to get all subsenses try: result.extend(list(sense.AllSenses)) except Exception: # Manual recursive collection as last resort result.append(sense) if sense.SensesOS and sense.SensesOS.Count > 0: for subsense in sense.SensesOS: result.extend(self.__CollectSubsenses(subsense)) return result def __CollectSubsenses(self, sense): """ Helper to recursively collect subsenses. Args: sense: ILexSense object Returns: list: All subsenses including the sense itself """ result = [sense] if sense.SensesOS and sense.SensesOS.Count > 0: for subsense in sense.SensesOS: result.extend(self.__CollectSubsenses(subsense)) return result # --- Complex Form Helper Methods (Pattern 4) --- @OperationsMethod def AddComplexFormComponent(self, complex_entry_or_hvo, component_or_hvo): """ Add a component to a complex form (compound, idiom, phrasal verb). This is a convenience method that wraps FLEx's AddComponent logic. If the complex entry doesn't have an EntryRef yet, one is created. Args: complex_entry_or_hvo: The complex form entry (or HVO) component_or_hvo: The component entry or sense (or HVO) Raises: FP_ReadOnlyError: If project not opened with write enabled FP_NullParameterError: If either parameter is None FP_ParameterError: If component is not an entry or sense Example: >>> # Create "kick the bucket" idiom >>> kick = project.LexEntry.Find("kick") >>> bucket = project.LexEntry.Find("bucket") >>> idiom = project.LexEntry.Create("kick the bucket") >>> project.LexEntry.AddComplexFormComponent(idiom, kick) >>> project.LexEntry.AddComplexFormComponent(idiom, bucket) Notes: - Creates LexEntryRef if needed (RefType = krtComplexForm) - Adds to ComponentLexemesRS - First component added becomes Primary (published location) - Based on FLEx ILexEntry.AddComponent() method - Complex forms must be stems, not roots See Also: RemoveComplexFormComponent, GetVisibleComplexFormBackRefs """ self._EnsureWriteEnabled() self._ValidateParam(complex_entry_or_hvo, "complex_entry_or_hvo") self._ValidateParam(component_or_hvo, "component_or_hvo") complex_entry = self.__ResolveObject(complex_entry_or_hvo) component = self.__ResolveObject(component_or_hvo) # Validate component is entry or sense if not isinstance(component, (ILexEntry, ILexSense)): raise FP_ParameterError("Component must be an ILexEntry or ILexSense") # Find or create complex form EntryRef entry_ref = None for ref in complex_entry.EntryRefsOS: if ref.RefType == LexEntryRefTags.krtComplexForm: entry_ref = ref break with self._TransactionCM("Add complex form component"): if entry_ref is None: # Create new LexEntryRef for complex form factory = self.project.project.ServiceLocator.GetInstance(ILexEntryRefFactory) entry_ref = factory.Create() complex_entry.EntryRefsOS.Add(entry_ref) entry_ref.RefType = LexEntryRefTags.krtComplexForm entry_ref.HideMinorEntry = 0 # Show the complex form # Add to ComponentLexemesRS if not already present if not any(item.Hvo == component.Hvo for item in entry_ref.ComponentLexemesRS): entry_ref.ComponentLexemesRS.Add(component) # Add to PrimaryLexemesRS if empty (first component = primary) if entry_ref.PrimaryLexemesRS.Count == 0: entry_ref.PrimaryLexemesRS.Add(component) # Add to ShowComplexFormsInRS for visibility if not any(item.Hvo == component.Hvo for item in entry_ref.ShowComplexFormsInRS): entry_ref.ShowComplexFormsInRS.Add(component) @OperationsMethod def RemoveComplexFormComponent(self, complex_entry_or_hvo, component_or_hvo): """ Remove a component from a complex form. Args: complex_entry_or_hvo: The complex form entry (or HVO) component_or_hvo: The component entry or sense to remove (or HVO) Raises: FP_ReadOnlyError: If project not opened with write enabled FP_NullParameterError: If either parameter is None Example: >>> idiom = project.LexEntry.Find("kick the bucket") >>> bucket = project.LexEntry.Find("bucket") >>> project.LexEntry.RemoveComplexFormComponent(idiom, bucket) Notes: - Removes from ComponentLexemesRS, PrimaryLexemesRS, ShowComplexFormsInRS - If all components removed, EntryRef remains but is empty - Does nothing if component not found See Also: AddComplexFormComponent """ self._EnsureWriteEnabled() self._ValidateParam(complex_entry_or_hvo, "complex_entry_or_hvo") self._ValidateParam(component_or_hvo, "component_or_hvo") complex_entry = self.__ResolveObject(complex_entry_or_hvo) component = self.__ResolveObject(component_or_hvo) with self._TransactionCM("Remove complex form component"): # Find complex form EntryRef for entry_ref in complex_entry.EntryRefsOS: if entry_ref.RefType == LexEntryRefTags.krtComplexForm: # Remove from all collections to_remove = [item for item in entry_ref.ComponentLexemesRS if item.Hvo == component.Hvo] for item in to_remove: entry_ref.ComponentLexemesRS.Remove(item) to_remove = [item for item in entry_ref.PrimaryLexemesRS if item.Hvo == component.Hvo] for item in to_remove: entry_ref.PrimaryLexemesRS.Remove(item) to_remove = [item for item in entry_ref.ShowComplexFormsInRS if item.Hvo == component.Hvo] for item in to_remove: entry_ref.ShowComplexFormsInRS.Remove(item) break @OperationsMethod def GetComplexFormComponents(self, complex_entry_or_hvo): """ Get all components of a complex form. Args: complex_entry_or_hvo: The complex form entry (or HVO) Returns: list: List of ILexEntry or ILexSense objects (components) Example: >>> idiom = project.LexEntry.Find("kick the bucket") >>> components = project.LexEntry.GetComplexFormComponents(idiom) >>> for comp in components: ... if isinstance(comp, ILexEntry): ... print(project.LexEntry.GetHeadword(comp)) Notes: - Returns empty list if not a complex form - Components can be entries OR senses - Order is preserved from ComponentLexemesRS See Also: AddComplexFormComponent, GetVisibleComplexFormBackRefs """ self._ValidateParam(complex_entry_or_hvo, "complex_entry_or_hvo") complex_entry = self.__ResolveObject(complex_entry_or_hvo) # Find complex form EntryRef for entry_ref in complex_entry.EntryRefsOS: if entry_ref.RefType == LexEntryRefTags.krtComplexForm: return list(entry_ref.ComponentLexemesRS) return [] # --- Pattern 7: MergeObject (Entry/Sense merging) --- @OperationsMethod def MergeObject(self, survivor_or_hvo, victim_or_hvo, fLoseNoStringData=True, auto_deduplicate=True): """ Merge one entry into another (IRREVERSIBLE operation). This method delegates to the underlying LibLCM C# implementation, which handles all the complex merge logic. Optionally, it can then deduplicate senses, pronunciations, and allomorphs that result from the merge. LibLCM Handles: - Lexeme form differences (creates alternate forms if needed) - Homograph renumbering - Circular reference prevention (complex forms, variants) - LexEntryRef component replacement (FWR-3535) - Back-reference updating - Per-writing-system MultiString merging - **Alternate form deduplication** (identical form + morph type) - **MSA deduplication** (equivalent grammatical analyses) FlexLibs2 Adds (Optional): - Sense deduplication (identical gloss + definition) - Pronunciation deduplication (identical form per WS) - Allomorph deduplication (identical form + morph type) Args: survivor_or_hvo: Entry that will receive merged data (HVO or ILexEntry) victim_or_hvo: Entry that will be deleted after merge (HVO or ILexEntry) fLoseNoStringData (bool): If True, concatenate strings (preserve both values); If False, overwrite strings (victim overwrites survivor) Default: True (data preservation) auto_deduplicate (bool): If True, automatically deduplicate senses, pronunciations, and allomorphs after merge. Default: True Raises: FP_ReadOnlyError: If project not write-enabled FP_NullParameterError: If either parameter is None FP_ParameterError: If entries are not compatible for merging Example:: >>> # Merge duplicate entry into main entry with auto-deduplication >>> main = project.LexEntry.Find("run") >>> duplicate = project.LexEntry.Find("run") >>> project.LexEntry.MergeObject(main, duplicate) >>> # 'duplicate' is deleted, all data merged into 'main' >>> # Senses, pronunciations, allomorphs are auto-deduplicated >>> # Alternate forms and MSAs are already deduplicated by LibLCM >>> # Merge with deduplication disabled >>> entry1 = project.LexEntry.Find("color") >>> entry2 = project.LexEntry.Find("colour") >>> project.LexEntry.MergeObject(entry1, entry2, auto_deduplicate=False) Notes: - This operation is IRREVERSIBLE (victim is deleted) - Delegates all core merge logic to LibLCM's battle-tested ILexEntry.MergeObject() - FlexLibs2 adds optional deduplication layer for senses/pronunciations/allomorphs - Based on FLEx LexEntry.MergeObject (OverridesLing_Lex.cs:3432-3548) See Also: LexSense.MergeObject - For merging senses """ self._EnsureWriteEnabled() self._ValidateParam(survivor_or_hvo, "survivor_or_hvo") self._ValidateParam(victim_or_hvo, "victim_or_hvo") survivor = self.__ResolveObject(survivor_or_hvo) victim = self.__ResolveObject(victim_or_hvo) # Validate merge compatibility (same class, same concrete type if applicable) from ..lcm_casting import validate_merge_compatibility is_compatible, error_msg = validate_merge_compatibility(survivor, victim) if not is_compatible: raise FP_ParameterError(error_msg) # Don't merge an entry into itself if survivor.Hvo == victim.Hvo: raise FP_ParameterError("Cannot merge entry into itself") with self._TransactionCM("Merge entries"): # Delegate to LibLCM's battle-tested merge implementation logger.info(f"Merging entry (HVO: {victim.Hvo}) into survivor (HVO: {survivor.Hvo})") survivor.MergeObject(victim, fLoseNoStringData) # Optional deduplication layer (NEW value added by FlexLibs2) if auto_deduplicate: logger.debug(f"Running auto-deduplication on merged entry (HVO: {survivor.Hvo})") self.__DeduplicateSensesInEntry(survivor) self.__DeduplicatePronunciationsInEntry(survivor) self.__DeduplicateAllomorphsInEntry(survivor) # --- Private Helper Methods for Deduplication --- def __DeduplicateSensesInEntry(self, entry): """ Detect and merge duplicate senses within an entry. This is called after merging entries to clean up any duplicate senses that resulted from the merge. Duplicates are detected by comparing: - Gloss values (per writing system) - Definition values (per writing system) - Semantic domain references Senses with identical signatures are merged into the first occurrence. Args: entry: The ILexEntry to deduplicate Notes: - Uses project.Senses.MergeObject() for actual merging - Logs all auto-merged duplicates - Stops on error to prevent cascading failures """ if not entry.SensesOS or len(entry.SensesOS) < 2: return # No duplicates possible try: # Get sense operations instance sense_ops = self.project.Senses # Build signature map for all senses in entry sig_map = {} for sense in entry.SensesOS: sig = sense_ops._LexSenseOperations__GetSenseSignature(sense) if sig: if sig not in sig_map: sig_map[sig] = [] sig_map[sig].append(sense) # Find and merge duplicate groups merged_count = 0 for sig, senses in sig_map.items(): if len(senses) > 1: # Keep first sense, merge rest into it master = senses[0] for dupe in senses[1:]: try: logger.info( f"Auto-merging duplicate sense in entry (HVO: {dupe.Hvo}) " f"into master sense (HVO: {master.Hvo})" ) sense_ops.MergeObject(master, dupe, fLoseNoStringData=True) merged_count += 1 except Exception as e: logger.warning(f"Could not auto-merge duplicate sense (HVO: {dupe.Hvo}): {e}") if merged_count > 0: logger.info(f"Auto-deduplicated {merged_count} duplicate sense(s) in entry (HVO: {entry.Hvo})") except Exception as e: logger.warning(f"Error during sense deduplication: {e}") # Don't fail the whole merge operation if deduplication fails def __DeduplicatePronunciationsInEntry(self, entry): """ Detect and remove duplicate pronunciations within an entry. Duplicates are detected by comparing pronunciation forms across all writing systems. Pronunciation form must match exactly in the same writing system to be considered a duplicate. Args: entry: The ILexEntry to deduplicate Notes: - Keeps first occurrence, removes duplicates - Logs all removed duplicates - Continues on error to prevent merge failure """ if not entry.PronunciationsOS or len(entry.PronunciationsOS) < 2: return # No duplicates possible try: # Build signature map for all pronunciations in entry sig_map = {} for pron in entry.PronunciationsOS: # Get pronunciation form across all writing systems pron_dict = {} try: for ws in self.project.project.WritingSystemManager.AllWritingSystems: ws_handle = ws.Handle pron_text = ITsString(pron.Form.get_String(ws_handle)).Text if pron.Form else "" if pron_text: pron_dict[ws_handle] = pron_text except Exception as e: logger.debug(f"Could not get pronunciation form: {e}") continue if pron_dict: sig = frozenset(pron_dict.items()) if sig not in sig_map: sig_map[sig] = [] sig_map[sig].append(pron) # Find and remove duplicate groups removed_count = 0 for sig, prons in sig_map.items(): if len(prons) > 1: # Keep first, remove rest for dupe in prons[1:]: try: logger.info( f"Auto-removing duplicate pronunciation in entry (HVO: {dupe.Hvo}) " f"keeping master (HVO: {prons[0].Hvo})" ) dupe.OwningList.Remove(dupe) removed_count += 1 except Exception as e: logger.warning(f"Could not remove duplicate pronunciation (HVO: {dupe.Hvo}): {e}") if removed_count > 0: logger.info(f"Auto-deduplicated {removed_count} duplicate pronunciation(s) in entry (HVO: {entry.Hvo})") except Exception as e: logger.warning(f"Error during pronunciation deduplication: {e}") def __DeduplicateAllomorphsInEntry(self, entry): """ Detect and remove duplicate allomorphs within an entry. Duplicates are detected by comparing allomorph form and morph type. Two allomorphs with identical form and morph type in the same writing system are considered duplicates. Args: entry: The ILexEntry to deduplicate Notes: - Keeps first occurrence, removes duplicates - Logs all removed duplicates - Continues on error to prevent merge failure """ if not entry.AlternateFormsOS or len(entry.AlternateFormsOS) < 2: return # No duplicates possible try: # Build signature map for all allomorphs in entry sig_map = {} for allomorph in entry.AlternateFormsOS: # Get allomorph form and morph type allomorph_dict = {} morph_type_guid = "" try: # Get form across all writing systems for ws in self.project.project.WritingSystemManager.AllWritingSystems: ws_handle = ws.Handle form_text = ITsString(allomorph.Form.get_String(ws_handle)).Text if allomorph.Form else "" if form_text: allomorph_dict[ws_handle] = form_text # Get morph type if hasattr(allomorph, "MorphTypeRA") and allomorph.MorphTypeRA: morph_type_guid = str(allomorph.MorphTypeRA.Guid) except Exception as e: logger.debug(f"Could not get allomorph form/type: {e}") continue if allomorph_dict: sig = (frozenset(allomorph_dict.items()), morph_type_guid) if sig not in sig_map: sig_map[sig] = [] sig_map[sig].append(allomorph) # Find and remove duplicate groups removed_count = 0 for sig, allomorphs in sig_map.items(): if len(allomorphs) > 1: # Keep first, remove rest for dupe in allomorphs[1:]: try: logger.info( f"Auto-removing duplicate allomorph in entry (HVO: {dupe.Hvo}) " f"keeping master (HVO: {allomorphs[0].Hvo})" ) dupe.OwningList.Remove(dupe) removed_count += 1 except Exception as e: logger.warning(f"Could not remove duplicate allomorph (HVO: {dupe.Hvo}): {e}") if removed_count > 0: logger.info(f"Auto-deduplicated {removed_count} duplicate allomorph(s) in entry (HVO: {entry.Hvo})") except Exception as e: logger.warning(f"Error during allomorph deduplication: {e}") # --- Private Helper Methods --- def __IsStemType(self, morph_type): """ Determine if a morph type should use MoStemAllomorph or MoAffixAllomorph. Args: morph_type: IMoMorphType object Returns: bool: True if stem type (uses MoStemAllomorph), False if affix type Notes: Based on FLEx logic in MorphTypeAtomicLauncher.cs Stem types include: stem, root, bound root/stem, clitics, particles, phrases Affix types include: prefix, suffix, infix, circumfix, etc. """ if morph_type is None: return True # Default to stem # Check GUID against known stem types (from MoMorphTypeTags) stem_guids = { MoMorphTypeTags.kguidMorphStem, MoMorphTypeTags.kguidMorphRoot, MoMorphTypeTags.kguidMorphBoundRoot, MoMorphTypeTags.kguidMorphBoundStem, MoMorphTypeTags.kguidMorphClitic, MoMorphTypeTags.kguidMorphEnclitic, MoMorphTypeTags.kguidMorphProclitic, MoMorphTypeTags.kguidMorphParticle, MoMorphTypeTags.kguidMorphPhrase, MoMorphTypeTags.kguidMorphDiscontiguousPhrase, } return morph_type.Guid in stem_guids def __WSHandleAnalysis(self, wsHandle): """ Get writing system handle, defaulting to analysis WS. 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) def __FindMorphType(self, name): """ Find a morph type by name (case-insensitive). Display markers (leading/trailing -, =, ~, <, >) are stripped before matching so that UI-copied strings like '=enclitic' or '-suffix' resolve to the same canonical type as 'enclitic' or 'suffix'. Args: name (str): The morph type name to search for (bare or with display markers) Returns: IMoMorphType or None: The morph type object if found, None otherwise """ # Strip display markers (prefix '-', '=', '~'; postfix '-', '=', '~', '<', '>') # that FLEx shows in the UI but that are NOT part of the canonical IMoMorphType.Name. bare = name.strip('-=~<>') target = normalize_match_key(bare, casefold=True) wsHandle = self.project.project.DefaultAnalWs morph_types = self.project.lp.LexDbOA.MorphTypesOA if morph_types is None: return None # Search through all morph types (including subcategories) def search_morph_types(possibilities): for mt in possibilities: mt_name = best_analysis_text(mt.Name) if mt_name and normalize_match_key(mt_name, casefold=True) == target: return mt # Search subcategories if mt.SubPossibilitiesOS.Count > 0: found = search_morph_types(mt.SubPossibilitiesOS) if found: return found return None return search_morph_types(morph_types.PossibilitiesOS)