Source code for flexicon.code.Lexicon.LexReferenceOperations

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

# Import BaseOperations parent class
from ..BaseOperations import BaseOperations, OperationsMethod, wrap_enumerable
from ..Shared.string_utils import normalize_text, normalize_match_key

# Import FLEx LCM types
from SIL.LCModel import (
    ILexReference,
    ILexReferenceFactory,
    ILexRefType,
    ILexRefTypeFactory,
    ILexRefTypeRepository,
    ILexEntry,
    ILexSense,
    LexRefTypeTags,
)
from SIL.LCModel.Core.KernelInterfaces import ITsString
from SIL.LCModel.Core.Text import TsStringUtils

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

# --- Lexical Reference Mapping Type Constants ---


[docs] class LexRefMappingTypes: """ Lexical reference mapping type constants. These correspond to the LexRefTypeTags.MappingTypes enum in FLEx. The mapping type determines how the lexical relation behaves: - SYMMETRIC: Bidirectional equal relations (A ↔ B) Example: synonym, antonym - ASYMMETRIC: Directional relations with forward and reverse (A → B, B ← A) Example: hypernym/hyponym, part/whole - TREE: Tree/hierarchical relations with parent-child structure Example: taxonomic hierarchies, part-whole hierarchies - SEQUENCE: Ordered sequence relations (A → B → C) Example: temporal sequences, procedural steps """ SYMMETRIC = 1 # krtSym - Symmetric (A ↔ B) ASYMMETRIC = 2 # krtAsym - Asymmetric with forward/reverse (A → B, B ← A) TREE = 3 # krtTree - Tree/hierarchical (parent-child) SEQUENCE = 4 # krtSequence - Ordered sequence (A → B → C)
[docs] class LexReferenceOperations(BaseOperations): """ This class provides operations for managing lexical references and cross-references in a FieldWorks project. Lexical references connect entries or senses through various types of relations such as synonyms, antonyms, hypernyms, part-whole relationships, and complex form relationships. These relations help organize the lexicon semantically and structurally. The class handles different mapping types: - Symmetric (e.g., synonym, antonym) - Asymmetric (e.g., hypernym/hyponym) - Tree (e.g., part-whole hierarchies) - Sequence (ordered relationships) This class should be accessed via FLExProject.LexReferences property. Usage:: from flexlibs2 import FLExProject project = FLExProject() project.OpenProject("my project", writeEnabled=True) # Get all relation types for ref_type in project.LexReferences.GetAllTypes(): name = project.LexReferences.GetTypeName(ref_type) mapping = project.LexReferences.GetMappingType(ref_type) print(f"{name}: {mapping}") # Create a new relation type synonym_type = project.LexReferences.CreateType( "Synonym", "Symmetric", reverse_name=None ) # Create a reference between two senses entry1 = project.LexEntry.Find("run") entry2 = project.LexEntry.Find("jog") if entry1 and entry2: sense1 = list(project.Senses.GetAll(entry1))[0] sense2 = list(project.Senses.GetAll(entry2))[0] ref = project.LexReferences.Create(synonym_type, [sense1, sense2]) # Get all references for a sense for ref in project.LexReferences.GetAll(sense1): targets = project.LexReferences.GetTargets(ref) for target in targets: if target.ClassName == "LexSense": gloss = project.Senses.GetGloss(target) print(f"Related sense: {gloss}") project.CloseProject() """ def __init__(self, project): """ Initialize LexReferenceOperations with a FLExProject instance. Args: project: The FLExProject instance to operate on. """ super().__init__(project) # --- Reference Type Management --- @OperationsMethod def GetAllTypes(self): """ Get all lexical relation types in the project. This method returns an iterator over all ILexRefType objects that define the types of lexical relations available in the project, such as Synonym, Antonym, Part-Whole, etc. Yields: ILexRefType: Each lexical reference type object in the project Example: >>> for ref_type in project.LexReferences.GetAllTypes(): ... name = project.LexReferences.GetTypeName(ref_type) ... mapping = project.LexReferences.GetMappingType(ref_type) ... count = len(list(ref_type.MembersOC)) ... print(f"{name} ({mapping}): {count} references") Synonym (Symmetric): 15 references Antonym (Symmetric): 8 references Part-Whole (Tree): 22 references Notes: - Returns all defined relation types, even if unused - Each type defines constraints on how relations work - Mapping type determines reference behavior (symmetric, etc.) - Types can be hierarchical (with subtypes) See Also: FindType, CreateType, GetTypeName, GetMappingType """ return self.project.ObjectsIn(ILexRefTypeRepository) @OperationsMethod def CreateType(self, name, mapping_type, reverse_name=None, wsHandle=None): """ Create a new lexical relation type. Args: name (str): The name of the relation type (e.g., "Synonym") mapping_type (str): The mapping type - one of: - "Symmetric" - bidirectional equal relations (synonym, antonym) - "Asymmetric" - directional relations (hypernym/hyponym) - "Tree" - hierarchical part-whole relations - "Sequence" - ordered sequence relations reverse_name (str, optional): The reverse name for asymmetric relations (e.g., "Hyponym" when name is "Hypernym") wsHandle: Optional writing system handle. Defaults to analysis WS. Returns: ILexRefType: The newly created reference type object Raises: FP_ReadOnlyError: If project is not opened with write enabled FP_NullParameterError: If name or mapping_type is None FP_ParameterError: If mapping_type is invalid or name is empty Example: >>> # Create symmetric relation (synonym) >>> syn_type = project.LexReferences.CreateType("Synonym", "Symmetric") >>> print(project.LexReferences.GetTypeName(syn_type)) Synonym >>> # Create asymmetric relation (hypernym/hyponym) >>> hyper_type = project.LexReferences.CreateType( ... "Hypernym", ... "Asymmetric", ... reverse_name="Hyponym" ... ) >>> # Create tree relation (part-whole) >>> part_type = project.LexReferences.CreateType( ... "Part-Whole", ... "Tree" ... ) Notes: - Symmetric relations don't need reverse_name - Asymmetric relations should provide reverse_name - Tree relations create hierarchical structures - Sequence relations maintain order of targets - Type is added to project's lexical relation types list See Also: DeleteType, FindType, GetAllTypes, SetTypeName """ self._EnsureWriteEnabled() self._ValidateParam(name, "name") self._ValidateParam(mapping_type, "mapping_type") self._ValidateStringNotEmpty(name, "name") # Validate mapping type mapping_type_upper = mapping_type.upper() mapping_value = None # Map string to LcmReferenceType enumeration value # Using the integer values from the FLEx API if mapping_type_upper in ("SYMMETRIC", "SYM"): mapping_value = LexRefMappingTypes.SYMMETRIC elif mapping_type_upper in ("ASYMMETRIC", "ASYM"): mapping_value = LexRefMappingTypes.ASYMMETRIC elif mapping_type_upper == "TREE": mapping_value = LexRefMappingTypes.TREE elif mapping_type_upper in ("SEQUENCE", "SEQ"): mapping_value = LexRefMappingTypes.SEQUENCE else: raise FP_ParameterError( f"Invalid mapping type '{mapping_type}'. " f"Must be one of: Symmetric, Asymmetric, Tree, Sequence" ) wsHandle = self.__WSHandleAnalysis(wsHandle) with self._TransactionCM(f"Create reference type '{name}'"): # Create the new reference type using the factory factory = self.project.project.ServiceLocator.GetService(ILexRefTypeFactory) new_ref_type = factory.Create() # Add to project's lexical relation types (must be done before setting properties) ref_types_list = self.project.lexDB.ReferencesOA if ref_types_list: ref_types_list.PossibilitiesOS.Add(new_ref_type) # Set the name mkstr = TsStringUtils.MakeString(name, wsHandle) new_ref_type.Name.set_String(wsHandle, mkstr) # Set the mapping type new_ref_type.MappingType = mapping_value # Set reverse name for asymmetric relations if reverse_name and mapping_value == LexRefMappingTypes.ASYMMETRIC: rev_mkstr = TsStringUtils.MakeString(reverse_name, wsHandle) new_ref_type.ReverseName.set_String(wsHandle, rev_mkstr) else: # Create the reference list if it doesn't exist from SIL.LCModel import ICmPossibilityListFactory list_factory = self.project.project.ServiceLocator.GetService(ICmPossibilityListFactory) new_list = list_factory.Create() self.project.lexDB.ReferencesOA = new_list new_list.PossibilitiesOS.Add(new_ref_type) return new_ref_type @OperationsMethod def DeleteType(self, ref_type_or_hvo): """ Delete a lexical relation type. Args: ref_type_or_hvo: Either an ILexRefType object or its HVO Raises: FP_ReadOnlyError: If project is not opened with write enabled FP_NullParameterError: If ref_type_or_hvo is None FP_ParameterError: If type doesn't exist Example: >>> ref_type = project.LexReferences.FindType("Obsolete Relation") >>> if ref_type: ... project.LexReferences.DeleteType(ref_type) Warning: - This is a destructive operation - All references of this type will be deleted - Cannot be undone - Use with caution Notes: - Deletion cascades to all LexReference objects of this type - Related entries/senses are not affected, only the references See Also: CreateType, FindType """ self._EnsureWriteEnabled() self._ValidateParam(ref_type_or_hvo, "ref_type_or_hvo") ref_type = self.__ResolveRefType(ref_type_or_hvo) # Remove from the references list ref_types_list = self.project.lexDB.ReferencesOA if ref_types_list: ref_types_list.PossibilitiesOS.Remove(ref_type) @OperationsMethod def FindType(self, name, wsHandle=None): """ Find a lexical relation type by name. Args: name (str): The name of the relation type to search for wsHandle: Optional writing system handle. Defaults to analysis WS. Returns: ILexRefType or None: The reference type object if found, None otherwise Raises: FP_NullParameterError: If name is None Example: >>> syn_type = project.LexReferences.FindType("Synonym") >>> if syn_type: ... mapping = project.LexReferences.GetMappingType(syn_type) ... print(f"Found: {mapping}") Found: Symmetric >>> # Search with specific writing system >>> type_fr = project.LexReferences.FindType( ... "Synonyme", ... project.WSHandle('fr') ... ) Notes: - Search is case-sensitive - Search is writing-system specific - Returns first match only - Returns None if not found (doesn't raise exception) - Searches top-level types only (not subtypes) See Also: GetAllTypes, CreateType, GetTypeName """ self._ValidateParam(name, "name") if not name or not name.strip(): return None wsHandle = self.__WSHandleAnalysis(wsHandle) # Search through all reference types. NFD-normalize both sides: # FLEx stores Unicode in NFD, Python source is typically NFC; a # diacritic-bearing relation name would silently miss without # this. (issue #125) target = normalize_match_key(name, casefold=False) for ref_type in self.GetAllTypes(): type_name = ITsString(ref_type.Name.get_String(wsHandle)).Text if type_name and normalize_match_key(type_name, casefold=False) == target: return ref_type return None @OperationsMethod def GetTypeName(self, ref_type_or_hvo, wsHandle=None): """ Get the name of a lexical relation type. Args: ref_type_or_hvo: Either an ILexRefType object or its HVO wsHandle: Optional writing system handle. Defaults to analysis WS. Returns: str: The name of the reference type (empty string if not set) Raises: FP_NullParameterError: If ref_type_or_hvo is None Example: >>> for ref_type in project.LexReferences.GetAllTypes(): ... name = project.LexReferences.GetTypeName(ref_type) ... print(f"Relation type: {name}") Relation type: Synonym Relation type: Antonym Relation type: Part-Whole Notes: - Returns empty string if name not set in specified writing system - Different writing systems may have different names - Default writing system is analysis WS See Also: SetTypeName, GetTypeReverseName """ self._ValidateParam(ref_type_or_hvo, "ref_type_or_hvo") ref_type = self.__ResolveRefType(ref_type_or_hvo) wsHandle = self.__WSHandleAnalysis(wsHandle) name = ITsString(ref_type.Name.get_String(wsHandle)).Text return name or "" @OperationsMethod def SetTypeName(self, ref_type_or_hvo, name, wsHandle=None): """ Set the name of a lexical relation type. Args: ref_type_or_hvo: Either an ILexRefType object or its HVO name (str): The new name for the reference type wsHandle: Optional writing system handle. Defaults to analysis WS. Raises: FP_ReadOnlyError: If project is not opened with write enabled FP_NullParameterError: If ref_type_or_hvo or name is None Example: >>> ref_type = project.LexReferences.FindType("Synonm") # typo >>> if ref_type: ... project.LexReferences.SetTypeName(ref_type, "Synonym") ... print(project.LexReferences.GetTypeName(ref_type)) Synonym Notes: - Can be set independently in multiple writing systems - Empty string is allowed (clears the name) - Changing name doesn't affect existing references See Also: GetTypeName, SetTypeReverseName """ self._EnsureWriteEnabled() self._ValidateParam(ref_type_or_hvo, "ref_type_or_hvo") self._ValidateParam(name, "name") ref_type = self.__ResolveRefType(ref_type_or_hvo) wsHandle = self.__WSHandleAnalysis(wsHandle) mkstr = TsStringUtils.MakeString(name, wsHandle) ref_type.Name.set_String(wsHandle, mkstr) @OperationsMethod def GetTypeReverseName(self, ref_type_or_hvo, wsHandle=None): """ Get the reverse name for an asymmetric relation type. For asymmetric relations, the reverse name is used for the opposite direction. For example, if the name is "Hypernym", the reverse name would be "Hyponym". Args: ref_type_or_hvo: Either an ILexRefType object or its HVO wsHandle: Optional writing system handle. Defaults to analysis WS. Returns: str: The reverse name (empty string if not set or not asymmetric) Raises: FP_NullParameterError: If ref_type_or_hvo is None Example: >>> hyper_type = project.LexReferences.FindType("Hypernym") >>> if hyper_type: ... name = project.LexReferences.GetTypeName(hyper_type) ... reverse = project.LexReferences.GetTypeReverseName(hyper_type) ... print(f"{name} <-> {reverse}") Hypernym <-> Hyponym Notes: - Only meaningful for asymmetric relations - Returns empty string for symmetric/tree/sequence relations - Returns empty string if not set See Also: SetTypeReverseName, GetTypeName, GetMappingType """ self._ValidateParam(ref_type_or_hvo, "ref_type_or_hvo") ref_type = self.__ResolveRefType(ref_type_or_hvo) wsHandle = self.__WSHandleAnalysis(wsHandle) # Check if this is an asymmetric relation if ref_type.MappingType == LexRefMappingTypes.ASYMMETRIC: reverse_name = ITsString(ref_type.ReverseName.get_String(wsHandle)).Text return reverse_name or "" else: return "" @OperationsMethod def SetTypeReverseName(self, ref_type_or_hvo, name, wsHandle=None): """ Set the reverse name for an asymmetric relation type. Args: ref_type_or_hvo: Either an ILexRefType object or its HVO name (str): The new reverse name wsHandle: Optional writing system handle. Defaults to analysis WS. Raises: FP_ReadOnlyError: If project is not opened with write enabled FP_NullParameterError: If ref_type_or_hvo or name is None FP_ParameterError: If the relation type is not asymmetric Example: >>> hyper_type = project.LexReferences.FindType("Hypernym") >>> if hyper_type: ... project.LexReferences.SetTypeReverseName(hyper_type, "Hyponym") Notes: - Only applicable to asymmetric relations - Will raise error if used on symmetric/tree/sequence relations - Empty string is allowed (clears the reverse name) See Also: GetTypeReverseName, SetTypeName """ self._EnsureWriteEnabled() self._ValidateParam(ref_type_or_hvo, "ref_type_or_hvo") self._ValidateParam(name, "name") ref_type = self.__ResolveRefType(ref_type_or_hvo) # Verify this is an asymmetric relation if ref_type.MappingType != LexRefMappingTypes.ASYMMETRIC: raise FP_ParameterError("SetTypeReverseName can only be used with asymmetric relations") wsHandle = self.__WSHandleAnalysis(wsHandle) mkstr = TsStringUtils.MakeString(name, wsHandle) ref_type.ReverseName.set_String(wsHandle, mkstr) @OperationsMethod def GetMappingType(self, ref_type_or_hvo): """ Get the mapping type of a lexical relation type. The mapping type determines how the relation behaves: - Symmetric: Bidirectional equal relations (e.g., synonym) - Asymmetric: Directional relations with reverse (e.g., hypernym/hyponym) - Tree: Hierarchical parent-child relations (e.g., part-whole) - Sequence: Ordered sequence relations Args: ref_type_or_hvo: Either an ILexRefType object or its HVO Returns: str: The mapping type as a string - one of: "Symmetric", "Asymmetric", "Tree", "Sequence", or "Unknown" Raises: FP_NullParameterError: If ref_type_or_hvo is None Example: >>> for ref_type in project.LexReferences.GetAllTypes(): ... name = project.LexReferences.GetTypeName(ref_type) ... mapping = project.LexReferences.GetMappingType(ref_type) ... print(f"{name}: {mapping}") Synonym: Symmetric Antonym: Symmetric Hypernym: Asymmetric Part-Whole: Tree Notes: - Mapping type is set when the relation type is created - Cannot be changed after creation - Determines UI behavior and relation constraints in FLEx See Also: CreateType, GetTypeName """ self._ValidateParam(ref_type_or_hvo, "ref_type_or_hvo") ref_type = self.__ResolveRefType(ref_type_or_hvo) # Map integer value to string name mapping_value = ref_type.MappingType if mapping_value == LexRefMappingTypes.SYMMETRIC: return "Symmetric" elif mapping_value == LexRefMappingTypes.ASYMMETRIC: return "Asymmetric" elif mapping_value == LexRefMappingTypes.TREE: return "Tree" elif mapping_value == LexRefMappingTypes.SEQUENCE: return "Sequence" else: return "Unknown" # --- Reference Management --- @wrap_enumerable @OperationsMethod def GetAll(self, sense_or_entry=None): """ Get all lexical references for a sense or entry, or all references in the entire project. This returns all LexReference objects that include the specified sense or entry as one of their targets. Args: sense_or_entry: Either an ILexSense or ILexEntry object (or HVO). If None, iterates all lexical references in the entire project. Yields: ILexReference: Each reference that includes this sense/entry (or project) Example: >>> # Get references for specific sense >>> entry = project.LexEntry.Find("run") >>> if entry: ... senses = list(project.Senses.GetAll(entry)) ... if senses: ... for ref in project.LexReferences.GetAll(senses[0]): ... ref_type = project.LexReferences.GetType(ref) ... type_name = project.LexReferences.GetTypeName(ref_type) ... targets = project.LexReferences.GetTargets(ref) ... print(f"{type_name}: {len(targets)} targets") Synonym: 3 targets Hypernym: 2 targets >>> # Get ALL lexical references in entire project >>> for ref in project.LexReferences.GetAll(): ... ref_type = project.LexReferences.GetType(ref) ... type_name = project.LexReferences.GetTypeName(ref_type) ... print(f"Reference type: {type_name}") Notes: - When sense_or_entry is provided: - Returns references where sense/entry is any of the targets - For entry, checks all senses of the entry - Returns empty generator if no references exist - When sense_or_entry is None: - Iterates ALL entries in the project - For each entry, iterates all senses - For each sense, yields all lexical references - Useful for project-wide reference operations See Also: Create, GetTargets, GetReferencesOfType """ if sense_or_entry is None: # Iterate ALL lexical references in entire project seen_refs = set() for entry in self.project.lexDB.Entries: for sense in entry.SensesOS: for ref in sense.ReferringLexReferences: # Avoid duplicates across different senses if ref.Hvo not in seen_refs: seen_refs.add(ref.Hvo) yield ref else: # Iterate references for specific sense or entry obj = self.__ResolveSenseOrEntry(sense_or_entry) # Determine if this is a sense or entry if hasattr(obj, "ClassName"): if obj.ClassName == "LexSense": # Get references for this sense for ref in obj.ReferringLexReferences: yield ref elif obj.ClassName == "LexEntry": # Get references for all senses of this entry seen_refs = set() for sense in obj.SensesOS: for ref in sense.ReferringLexReferences: # Avoid duplicates if ref.Hvo not in seen_refs: seen_refs.add(ref.Hvo) yield ref @OperationsMethod def Create(self, ref_type_or_name, targets): """ Create a new lexical reference linking senses or entries. Args: ref_type_or_name: Either an ILexRefType object, its HVO, or the name of the reference type (str) targets: List of ILexSense or ILexEntry objects (or HVOs) to link. Must contain at least 2 targets. Returns: ILexReference: The newly created reference object Raises: FP_ReadOnlyError: If project is not opened with write enabled FP_NullParameterError: If ref_type_or_name or targets is None FP_ParameterError: If targets has fewer than 2 items, if reference type not found, or if targets are invalid Example: >>> # Create synonym relation between two senses >>> entry1 = project.LexEntry.Find("run") >>> entry2 = project.LexEntry.Find("jog") >>> if entry1 and entry2: ... sense1 = list(project.Senses.GetAll(entry1))[0] ... sense2 = list(project.Senses.GetAll(entry2))[0] ... ... # Find or create synonym type ... syn_type = project.LexReferences.FindType("Synonym") ... if not syn_type: ... syn_type = project.LexReferences.CreateType("Synonym", "Symmetric") ... ... # Create the reference ... ref = project.LexReferences.Create(syn_type, [sense1, sense2]) >>> # Create using type name >>> ref2 = project.LexReferences.Create("Antonym", [sense1, sense3]) Notes: - All targets must be same type (all senses or all entries) - Targets are added to the reference's TargetsRS collection - For asymmetric relations, order matters (first is source) - Reference is automatically added to the relation type See Also: Delete, AddTarget, RemoveTarget, GetTargets """ self._EnsureWriteEnabled() self._ValidateParam(ref_type_or_name, "ref_type_or_name") self._ValidateParam(targets, "targets") if not targets or len(targets) < 2: raise FP_ParameterError("At least 2 targets required to create a reference") # Resolve reference type if isinstance(ref_type_or_name, str): ref_type = self.FindType(ref_type_or_name) if not ref_type: raise FP_ParameterError(f"Reference type '{ref_type_or_name}' not found") else: ref_type = self.__ResolveRefType(ref_type_or_name) # Resolve all targets resolved_targets = [] for target in targets: obj = self.__ResolveSenseOrEntry(target) resolved_targets.append(obj) # Verify all targets are the same type first_class = resolved_targets[0].ClassName if not all(t.ClassName == first_class for t in resolved_targets): raise FP_ParameterError("All targets must be the same type (all LexSense or all LexEntry)") with self._TransactionCM("Create reference"): # Create the new reference using the factory factory = self.project.project.ServiceLocator.GetService(ILexReferenceFactory) new_ref = factory.Create() # Add reference to the type's members (must be done before setting properties) ref_type.MembersOC.Add(new_ref) # Add targets to the reference for target in resolved_targets: new_ref.TargetsRS.Add(target) return new_ref @OperationsMethod def Delete(self, lex_ref_or_hvo): """ Delete a lexical reference. Args: lex_ref_or_hvo: Either an ILexReference object or its HVO Raises: FP_ReadOnlyError: If project is not opened with write enabled FP_NullParameterError: If lex_ref_or_hvo is None Example: >>> entry = project.LexEntry.Find("run") >>> if entry: ... sense = list(project.Senses.GetAll(entry))[0] ... refs = list(project.LexReferences.GetAll(sense)) ... if refs: ... # Delete first reference ... project.LexReferences.Delete(refs[0]) Warning: - This is a destructive operation - Cannot be undone - The related entries/senses are not affected Notes: - Removes the reference from the type's MembersOC collection - Targets are not deleted, only the relationship See Also: Create, GetAll """ self._EnsureWriteEnabled() self._ValidateParam(lex_ref_or_hvo, "lex_ref_or_hvo") lex_ref = self.__ResolveLexRef(lex_ref_or_hvo) # Cast owner to its concrete ILexRefType so MembersOC is reachable. # Raw lex_ref.Owner is typed as ICmObject and # hasattr(...,"MembersOC") returns False there, which is why the # prior implementation silently no-opped. owner = self._GetTypedOwner(lex_ref) if owner is None: raise FP_ParameterError("Lex reference has no owning type") owner.MembersOC.Remove(lex_ref) @OperationsMethod def GetTargets(self, lex_ref_or_hvo): """ Get all target senses or entries in a lexical reference. Args: lex_ref_or_hvo: Either an ILexReference object or its HVO Returns: list: List of ILexSense or ILexEntry objects that are targets Raises: FP_NullParameterError: If lex_ref_or_hvo is None Example: >>> entry = project.LexEntry.Find("run") >>> if entry: ... sense = list(project.Senses.GetAll(entry))[0] ... for ref in project.LexReferences.GetAll(sense): ... targets = project.LexReferences.GetTargets(ref) ... for target in targets: ... if target.ClassName == "LexSense": ... gloss = project.Senses.GetGloss(target) ... print(f" -> {gloss}") ... elif target.ClassName == "LexEntry": ... headword = project.LexEntry.GetHeadword(target) ... print(f" -> {headword}") -> jog -> sprint Notes: - Returns all targets in the TargetsRS collection - Targets can be either LexSense or LexEntry objects - Order may be significant for sequence relations - All targets in a reference are the same type See Also: AddTarget, RemoveTarget, Create """ self._ValidateParam(lex_ref_or_hvo, "lex_ref_or_hvo") lex_ref = self.__ResolveLexRef(lex_ref_or_hvo) return list(lex_ref.TargetsRS) @OperationsMethod def AddTarget(self, lex_ref_or_hvo, sense_or_entry): """ Add a target sense or entry to an existing lexical reference. Args: lex_ref_or_hvo: Either an ILexReference object or its HVO sense_or_entry: The ILexSense or ILexEntry to add (or HVO) Raises: FP_ReadOnlyError: If project is not opened with write enabled FP_NullParameterError: If lex_ref_or_hvo or sense_or_entry is None FP_ParameterError: If target type doesn't match existing targets Example: >>> # Find synonym reference >>> entry1 = project.LexEntry.Find("run") >>> sense1 = list(project.Senses.GetAll(entry1))[0] >>> refs = list(project.LexReferences.GetAll(sense1)) >>> >>> # Find synonym type reference >>> syn_ref = None >>> for ref in refs: ... ref_type = project.LexReferences.GetType(ref) ... if project.LexReferences.GetTypeName(ref_type) == "Synonym": ... syn_ref = ref ... break >>> >>> if syn_ref: ... # Add another synonym ... entry2 = project.LexEntry.Find("sprint") ... sense2 = list(project.Senses.GetAll(entry2))[0] ... project.LexReferences.AddTarget(syn_ref, sense2) Notes: - Target must be same type as existing targets (sense or entry) - Target can already exist in the reference (creates duplicate) - For symmetric relations, order doesn't matter - For asymmetric relations, new target is added at end See Also: RemoveTarget, GetTargets, Create """ self._EnsureWriteEnabled() self._ValidateParam(lex_ref_or_hvo, "lex_ref_or_hvo") self._ValidateParam(sense_or_entry, "sense_or_entry") lex_ref = self.__ResolveLexRef(lex_ref_or_hvo) target = self.__ResolveSenseOrEntry(sense_or_entry) # Verify target type matches existing targets if lex_ref.TargetsRS.Count > 0: first_target = lex_ref.TargetsRS[0] if target.ClassName != first_target.ClassName: raise FP_ParameterError( f"Target type mismatch: cannot add {target.ClassName} " f"to reference with {first_target.ClassName} targets" ) # Add the target lex_ref.TargetsRS.Add(target) @OperationsMethod def RemoveTarget(self, lex_ref_or_hvo, sense_or_entry): """ Remove a target sense or entry from a lexical reference. Args: lex_ref_or_hvo: Either an ILexReference object or its HVO sense_or_entry: The ILexSense or ILexEntry to remove (or HVO) Raises: FP_ReadOnlyError: If project is not opened with write enabled FP_NullParameterError: If lex_ref_or_hvo or sense_or_entry is None FP_ParameterError: If removing would leave fewer than 2 targets Example: >>> entry = project.LexEntry.Find("run") >>> sense = list(project.Senses.GetAll(entry))[0] >>> refs = list(project.LexReferences.GetAll(sense)) >>> if refs: ... targets = project.LexReferences.GetTargets(refs[0]) ... if len(targets) > 2: ... # Safe to remove one ... project.LexReferences.RemoveTarget(refs[0], targets[-1]) Warning: - Will raise error if removal would leave < 2 targets - Consider using Delete() instead if removing all but one target Notes: - Target must exist in the reference - If target not in reference, this is a no-op (no error) - References must have at least 2 targets - If only 2 targets remain, delete the reference instead See Also: AddTarget, GetTargets, Delete """ self._EnsureWriteEnabled() self._ValidateParam(lex_ref_or_hvo, "lex_ref_or_hvo") self._ValidateParam(sense_or_entry, "sense_or_entry") lex_ref = self.__ResolveLexRef(lex_ref_or_hvo) target = self.__ResolveSenseOrEntry(sense_or_entry) # Verify we won't drop below 2 targets if lex_ref.TargetsRS.Count <= 2: raise FP_ParameterError( "Cannot remove target: reference must have at least 2 targets. " "Use Delete() to remove the entire reference instead." ) # Remove the target if it exists if target in lex_ref.TargetsRS: lex_ref.TargetsRS.Remove(target) @OperationsMethod def GetType(self, lex_ref_or_hvo): """ Get the reference type of a lexical reference. Args: lex_ref_or_hvo: Either an ILexReference object or its HVO Returns: ILexRefType: The reference type object Raises: FP_NullParameterError: If lex_ref_or_hvo is None Example: >>> entry = project.LexEntry.Find("run") >>> if entry: ... sense = list(project.Senses.GetAll(entry))[0] ... for ref in project.LexReferences.GetAll(sense): ... ref_type = project.LexReferences.GetType(ref) ... name = project.LexReferences.GetTypeName(ref_type) ... mapping = project.LexReferences.GetMappingType(ref_type) ... print(f"{name} ({mapping})") Synonym (Symmetric) Hypernym (Asymmetric) Notes: - Every reference has exactly one type - Type determines relationship semantics - Type is set when reference is created See Also: GetTypeName, GetMappingType, Create """ self._ValidateParam(lex_ref_or_hvo, "lex_ref_or_hvo") lex_ref = self.__ResolveLexRef(lex_ref_or_hvo) # Cast to declared return type ILexRefType. Raw lex_ref.Owner is typed # as ICmObject in LCM; pythonnet only surfaces ILexRefType properties # (e.g. MembersOC, MappingType) after the explicit interface cast. # The owner of a LexReference is always a LexRefType. return ILexRefType(lex_ref.Owner) @OperationsMethod def GetReferencesOfType(self, ref_type_or_name): """ Get all lexical references of a specific type. Args: ref_type_or_name: Either an ILexRefType object, its HVO, or the name of the reference type (str) Yields: ILexReference: Each reference of the specified type Raises: FP_NullParameterError: If ref_type_or_name is None FP_ParameterError: If reference type name not found Example: >>> # Get all synonym references >>> for ref in project.LexReferences.GetReferencesOfType("Synonym"): ... targets = project.LexReferences.GetTargets(ref) ... glosses = [] ... for target in targets: ... if target.ClassName == "LexSense": ... glosses.append(project.Senses.GetGloss(target)) ... print(f"Synonyms: {', '.join(glosses)}") Synonyms: run, jog, sprint Synonyms: walk, stroll, amble Notes: - Returns references in database order - Returns empty generator if type has no references - Can search by type name (string) or type object See Also: GetAll, GetType, FindType """ self._ValidateParam(ref_type_or_name, "ref_type_or_name") # Resolve reference type if isinstance(ref_type_or_name, str): ref_type = self.FindType(ref_type_or_name) if not ref_type: raise FP_ParameterError(f"Reference type '{ref_type_or_name}' not found") else: ref_type = self.__ResolveRefType(ref_type_or_name) # Yield all members of this type for ref in ref_type.MembersOC: yield ref # --- Show Complex Forms Operations --- @OperationsMethod def ShowComplexFormsIn(self, entry, complex_entry): """ Show a component entry as a subentry under a complex form entry. This creates a "Show Complex Forms In" relationship, making the component entry appear as a subentry in dictionary views of the complex form. Args: entry: The component entry (ILexEntry or HVO) to show as subentry complex_entry: The complex form entry (ILexEntry or HVO) to show it under Returns: ILexReference: The created reference object Raises: FP_ReadOnlyError: If project is not opened with write enabled FP_NullParameterError: If entry or complex_entry is None FP_ParameterError: If entries are invalid Example: >>> # Show "run" as a component under "running shoe" >>> run_entry = project.LexEntry.Find("run") >>> shoe_entry = project.LexEntry.Find("running shoe") >>> if run_entry and shoe_entry: ... ref = project.LexReferences.ShowComplexFormsIn( ... run_entry, ... shoe_entry ... ) >>> # Show multiple components under one complex form >>> run_entry = project.LexEntry.Find("run") >>> away_entry = project.LexEntry.Find("away") >>> runaway_entry = project.LexEntry.Find("runaway") >>> if run_entry and away_entry and runaway_entry: ... # This would typically be done differently, creating one ... # reference with multiple targets ... pass Notes: - Uses the "Complex Forms" reference type - Creates entry-level references (not sense-level) - Component entry appears as subentry in complex form - Useful for compounds, derivatives, idioms, etc. See Also: GetComplexFormEntries, GetComponentEntries """ self._EnsureWriteEnabled() self._ValidateParam(entry, "entry") self._ValidateParam(complex_entry, "complex_entry") component = self.__ResolveEntry(entry) complex_form = self.__ResolveEntry(complex_entry) # Find "Complex Forms" reference type (read-only lookup) complex_type = self.FindType("Complex Forms") with self._TransactionCM("Show complex forms in"): if not complex_type: # Create it as a tree relation complex_type = self.CreateType("Complex Forms", "Tree", reverse_name="Components") # Create reference with complex form as parent, component as child # In tree relations, first target is typically the parent ref = self.Create(complex_type, [complex_form, component]) return ref @OperationsMethod def GetComplexFormEntries(self, entry): """ Get all complex form entries that show this entry as a component. Returns all complex forms (compounds, derivatives, etc.) that list the given entry as one of their components. Args: entry: The component entry (ILexEntry or HVO) Returns: list: List of ILexEntry objects that are complex forms containing this entry as a component Raises: FP_NullParameterError: If entry is None Example: >>> # Find all complex forms containing "run" >>> run_entry = project.LexEntry.Find("run") >>> if run_entry: ... complex_forms = project.LexReferences.GetComplexFormEntries(run_entry) ... for cf in complex_forms: ... headword = project.LexEntry.GetHeadword(cf) ... print(f"Complex form: {headword}") Complex form: running shoe Complex form: run away Complex form: run-up Notes: - Returns entries from "Complex Forms" or similar reference types - Useful for finding derivatives and compounds - Returns empty list if entry is not a component of any complex form See Also: GetComponentEntries, ShowComplexFormsIn """ self._ValidateParam(entry, "entry") component = self.__ResolveEntry(entry) complex_forms = [] # Find "Complex Forms" reference type complex_type = self.FindType("Complex Forms") if not complex_type: return complex_forms # Get all references of this type for ref in complex_type.MembersOC: targets = list(ref.TargetsRS) # Check if our entry is in the targets for i, target in enumerate(targets): if target.Hvo == component.Hvo: # For tree relations, other targets are related complex forms for j, other_target in enumerate(targets): if i != j and other_target.ClassName == "LexEntry": if other_target not in complex_forms: complex_forms.append(other_target) return complex_forms @OperationsMethod def GetComponentEntries(self, complex_entry): """ Get all component entries that make up a complex form entry. Returns all component entries (roots, stems) that are shown as subentries under the given complex form. Args: complex_entry: The complex form entry (ILexEntry or HVO) Returns: list: List of ILexEntry objects that are components of this complex form Raises: FP_NullParameterError: If complex_entry is None Example: >>> # Find components of "running shoe" >>> shoe_entry = project.LexEntry.Find("running shoe") >>> if shoe_entry: ... components = project.LexReferences.GetComponentEntries(shoe_entry) ... for comp in components: ... headword = project.LexEntry.GetHeadword(comp) ... print(f"Component: {headword}") Component: run Component: shoe Notes: - Returns entries from "Complex Forms" or similar reference types - Useful for analyzing compound structure - Returns empty list if entry has no components defined See Also: GetComplexFormEntries, ShowComplexFormsIn """ self._ValidateParam(complex_entry, "complex_entry") complex_form = self.__ResolveEntry(complex_entry) components = [] # Find "Complex Forms" reference type complex_type = self.FindType("Complex Forms") if not complex_type: return components # Get all references of this type for ref in complex_type.MembersOC: targets = list(ref.TargetsRS) # Check if our entry is in the targets for i, target in enumerate(targets): if target.Hvo == complex_form.Hvo: # For tree relations, other targets are components for j, other_target in enumerate(targets): if i != j and other_target.ClassName == "LexEntry": if other_target not in components: components.append(other_target) return components # ========== SYNC INTEGRATION METHODS ========== @OperationsMethod def GetSyncableProperties(self, item): """ Get all syncable properties of a lexical reference for comparison. Args: item: The ILexReference object. Returns: dict: Dictionary mapping property names to their values. """ props = {} # MultiString properties # Name - optional name of the relationship name_dict = {} if hasattr(item, "Name"): for ws_def in self.project.WritingSystems.GetAll(): from SIL.LCModel.Core.KernelInterfaces import ITsString text = normalize_text(ITsString(item.Name.get_String(ws_def.Handle)).Text) if text: ws_tag = ws_def.Id name_dict[ws_tag] = text props["Name"] = name_dict # Comment - additional notes comment_dict = {} if hasattr(item, "Comment"): for ws_def in self.project.WritingSystems.GetAll(): from SIL.LCModel.Core.KernelInterfaces import ITsString text = normalize_text(ITsString(item.Comment.get_String(ws_def.Handle)).Text) if text: ws_tag = ws_def.Id comment_dict[ws_tag] = text props["Comment"] = comment_dict # Reference Atomic (RA) properties # ReferenceTypeRA - the type of reference relationship if hasattr(item, "ReferenceTypeRA") and item.ReferenceTypeRA: props["ReferenceTypeRA"] = str(item.ReferenceTypeRA.Guid) else: props["ReferenceTypeRA"] = None return props @OperationsMethod def CompareTo(self, item1, item2, ops1=None, ops2=None): """ Compare two lexical references and return their differences. Args: item1: The first ILexReference object. item2: The second ILexReference object. ops1: Optional LexReferenceOperations instance for item1. ops2: Optional LexReferenceOperations 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 # --- Private Helper Methods --- def __ResolveRefType(self, ref_type_or_hvo): """ Resolve HVO or object to ILexRefType. Args: ref_type_or_hvo: Either an ILexRefType object or an HVO (int) Returns: ILexRefType: The resolved reference type object Raises: FP_ParameterError: If HVO doesn't refer to a reference type """ if isinstance(ref_type_or_hvo, int): obj = self.project.Object(ref_type_or_hvo) # Verify it's the right type if not hasattr(obj, "MappingType"): raise FP_ParameterError("HVO does not refer to a LexRefType") return obj return ref_type_or_hvo def __ResolveLexRef(self, lex_ref_or_hvo): """ Resolve HVO or object to ILexReference. Args: lex_ref_or_hvo: Either an ILexReference object or an HVO (int) Returns: ILexReference: The resolved reference object Raises: FP_ParameterError: If HVO doesn't refer to a lexical reference """ if isinstance(lex_ref_or_hvo, int): obj = self.project.Object(lex_ref_or_hvo) # Verify it's the right type if not hasattr(obj, "TargetsRS"): raise FP_ParameterError("HVO does not refer to a LexReference") return obj return lex_ref_or_hvo def __ResolveSenseOrEntry(self, sense_or_entry): """ Resolve HVO or object to ILexSense or ILexEntry. Args: sense_or_entry: Either an ILexSense/ILexEntry object or an HVO (int) Returns: ILexSense or ILexEntry: The resolved object Raises: FP_ParameterError: If HVO doesn't refer to sense or entry """ if isinstance(sense_or_entry, int): obj = self.project.Object(sense_or_entry) if not hasattr(obj, "ClassName"): raise FP_ParameterError("HVO does not refer to a LexSense or LexEntry") if obj.ClassName not in ("LexSense", "LexEntry"): raise FP_ParameterError(f"Object is {obj.ClassName}, not LexSense or LexEntry") return obj return sense_or_entry def __ResolveEntry(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 an entry """ if isinstance(entry_or_hvo, int): obj = self.project.Object(entry_or_hvo) if not hasattr(obj, "ClassName") or obj.ClassName != "LexEntry": raise FP_ParameterError("Object is not a LexEntry") return obj return entry_or_hvo 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)