Source code for flexicon.code.Reversal.ReversalIndexEntryOperations

#
#   ReversalIndexEntryOperations.py
#
#   Class: ReversalIndexEntryOperations
#          Reversal index entry 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_match_key

# Import FLEx LCM types
from SIL.LCModel import (
    IReversalIndex,
    IReversalIndexEntry,
    IReversalIndexEntryFactory,
    ILexSense,
)
from SIL.LCModel.Core.KernelInterfaces import ITsString
from SIL.LCModel.Core.Text import TsStringUtils

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


[docs] class ReversalIndexEntryOperations(BaseOperations): """ This class provides operations for managing reversal index entries in a FieldWorks project. Reversal index entries are the individual entries within a reversal index. Each entry has a form (the reverse headword) and links to one or more lexical senses. Entries can be organized hierarchically with subentries. Reversal entries enable users to look up words in the analysis language and find corresponding vernacular entries. Usage:: from flexlibs2 import FLExProject project = FLExProject() project.OpenProject("my project", writeEnabled=True) # Get reversal index en_ws = project.WSHandle('en') idx = project.ReversalIndexes.FindByWritingSystem(en_ws) # Create reversal entry entry = project.ReversalEntries.Create(idx, "run") # Link to lexical sense lex_entry = list(project.LexiconAllEntries())[0] sense = list(lex_entry.SensesOS)[0] project.ReversalEntries.AddSense(entry, sense) # Get all entries for rev_entry in project.ReversalEntries.GetAll(idx): form = project.ReversalEntries.GetForm(rev_entry) print(f"Reversal: {form}") project.CloseProject() """ def __init__(self, project): """ Initialize ReversalIndexEntryOperations with a FLExProject instance. Args: project: The FLExProject instance to operate on. """ super().__init__(project) def _GetSequence(self, parent): """ Specify which sequence to reorder for reversal entries. For ReversalIndexEntry, we reorder parent.SubentriesOS """ return parent.SubentriesOS # --- Core CRUD Operations --- @wrap_enumerable @OperationsMethod def GetAll(self, index_or_hvo): """ Get all reversal entries in a reversal index. Args: index_or_hvo: Either an IReversalIndex object or its HVO Yields: IReversalIndexEntry: Each reversal entry in the index Raises: FP_NullParameterError: If index_or_hvo is None Example: >>> idx = project.ReversalIndexes.Find("English") >>> for entry in project.ReversalEntries.GetAll(idx): ... form = project.ReversalEntries.GetForm(entry) ... sense_count = len(list(project.ReversalEntries.GetSenses(entry))) ... print(f"{form}: {sense_count} senses") run: 3 senses walk: 2 senses Notes: - Returns top-level entries only (not subentries) - Use GetSubentries() to access hierarchical subentries - Returns empty iterator if index has no entries - Entries are in database order See Also: Create, GetSubentries, FindByHvo """ self._ValidateParam(index_or_hvo, "index_or_hvo") index = self.__GetIndexObject(index_or_hvo) for entry in index.EntriesOC: yield entry @OperationsMethod def Create(self, index_or_hvo, form, sense=None, wsHandle=None): """ Create a new reversal index entry. Args: index_or_hvo: Either an IReversalIndex object or its HVO form (str): The reversal form (headword in analysis language) sense: Optional ILexSense object to link to wsHandle: Optional writing system handle. Defaults to index's WS. Returns: IReversalIndexEntry: The newly created reversal entry Raises: FP_ReadOnlyError: If project is not opened with write enabled FP_NullParameterError: If index_or_hvo or form is None FP_ParameterError: If form is empty Example: >>> idx = project.ReversalIndexes.Find("English") >>> entry = project.ReversalEntries.Create(idx, "run") >>> print(project.ReversalEntries.GetForm(entry)) run >>> # Create with linked sense >>> lex_entry = project.LexEntry.Find("hlauka") >>> sense = list(lex_entry.SensesOS)[0] >>> entry = project.ReversalEntries.Create(idx, "run", sense) Notes: - Entry is added to the reversal index - If sense provided, it's automatically linked - Form is stored in the index's writing system - Entry can link to multiple senses via AddSense() See Also: Delete, Find, AddSense """ self._EnsureWriteEnabled() self._ValidateParam(index_or_hvo, "index_or_hvo") self._ValidateParam(form, "form") if not form or not form.strip(): raise FP_ParameterError("Reversal entry form cannot be empty") index = self.__GetIndexObject(index_or_hvo) # Get writing system from index if not provided if wsHandle is None: ws_str = index.WritingSystem wsHandle = self.project.WSHandle(ws_str) with self._TransactionCM(f"Create reversal entry '{form}'"): # Create the reversal entry using factory factory = self.project.project.ServiceLocator.GetService(IReversalIndexEntryFactory) new_entry = factory.Create() # Add to index's entries collection index.EntriesOC.Add(new_entry) # Set the reversal form mkstr = TsStringUtils.MakeString(form, wsHandle) new_entry.ReversalForm.set_String(wsHandle, mkstr) # Link to sense if provided if sense: new_entry.SensesRS.Add(sense) return new_entry @OperationsMethod def Delete(self, entry_or_hvo): """ Delete a reversal index entry. Args: entry_or_hvo: Either an IReversalIndexEntry object or its HVO Raises: FP_ReadOnlyError: If project is not opened with write enabled FP_NullParameterError: If entry_or_hvo is None Example: >>> entry = project.ReversalEntries.Find(idx, "obsolete") >>> if entry: ... project.ReversalEntries.Delete(entry) Warning: - This is a destructive operation - All subentries will be deleted - Links to lexical senses will be removed - Cannot be undone Notes: - Deletion cascades to all subentries - Links from lexical senses are automatically cleaned up See Also: Create """ self._EnsureWriteEnabled() self._ValidateParam(entry_or_hvo, "entry_or_hvo") entry = self.__ResolveObject(entry_or_hvo) # Delete the entry (LCM handles removal from collections) entry.Delete() @OperationsMethod def Find(self, index_or_hvo, form, wsHandle=None): """ Find a reversal entry by its form. Args: index_or_hvo: Either an IReversalIndex object or its HVO form (str): The reversal form to search for wsHandle: Optional writing system handle. Defaults to index's WS. Returns: IReversalIndexEntry or None: The entry object if found, None otherwise Raises: FP_NullParameterError: If index_or_hvo or form is None Example: >>> idx = project.ReversalIndexes.Find("English") >>> entry = project.ReversalEntries.Find(idx, "run") >>> if entry: ... senses = list(project.ReversalEntries.GetSenses(entry)) ... print(f"Found 'run' with {len(senses)} senses") Found 'run' with 3 senses Notes: - Returns first match only - Search is case-sensitive - Searches top-level entries only (not subentries) - Returns None if not found See Also: FindByHvo, Create, GetAll """ self._ValidateParam(index_or_hvo, "index_or_hvo") self._ValidateParam(form, "form") if not form or not form.strip(): return None index = self.__GetIndexObject(index_or_hvo) # Get writing system from index if not provided if wsHandle is None: ws_str = index.WritingSystem wsHandle = self.project.WSHandle(ws_str) # Search through all entries target = normalize_match_key(form, casefold=False) for entry in self.GetAll(index): entry_form = ITsString(entry.ReversalForm.get_String(wsHandle)).Text if normalize_match_key(entry_form, casefold=False) == target: return entry return None @OperationsMethod def FindByHvo(self, hvo): """ Find a reversal entry by its HVO (database ID). Args: hvo (int): The HVO of the reversal entry Returns: IReversalIndexEntry or None: The entry object if found, None otherwise Raises: FP_NullParameterError: If hvo is None Example: >>> entry = project.ReversalEntries.FindByHvo(12345) >>> if entry: ... form = project.ReversalEntries.GetForm(entry) ... print(f"Found entry: {form}") Notes: - Direct HVO lookup is faster than searching by form - Returns None if HVO doesn't exist or isn't a reversal entry - HVO is the internal database identifier See Also: Find, GetAll """ self._ValidateParam(hvo, "hvo") try: obj = self.project.Object(hvo) if isinstance(obj, IReversalIndexEntry): return obj except Exception: pass return None # --- Property Access --- @OperationsMethod def GetForm(self, entry_or_hvo, wsHandle=None): """ Get the reversal form of a reversal entry. Args: entry_or_hvo: Either an IReversalIndexEntry object or its HVO wsHandle: Optional writing system handle. Defaults to entry's index WS. Returns: str: The reversal form text (empty string if not set) Raises: FP_NullParameterError: If entry_or_hvo is None Example: >>> entry = project.ReversalEntries.Find(idx, "run") >>> form = project.ReversalEntries.GetForm(entry) >>> print(form) run See Also: SetForm, Find """ self._ValidateParam(entry_or_hvo, "entry_or_hvo") entry = self.__ResolveObject(entry_or_hvo) # Use entry's index writing system if not provided if wsHandle is None: wsHandle = self.__GetEntryWS(entry) form = ITsString(entry.ReversalForm.get_String(wsHandle)).Text return form or "" @OperationsMethod def SetForm(self, entry_or_hvo, text, wsHandle=None): """ Set the reversal form of a reversal entry. Args: entry_or_hvo: Either an IReversalIndexEntry object or its HVO text (str): The new reversal form text wsHandle: Optional writing system handle. Defaults to entry's index 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 Example: >>> entry = project.ReversalEntries.Find(idx, "run") >>> project.ReversalEntries.SetForm(entry, "running") >>> print(project.ReversalEntries.GetForm(entry)) running See Also: GetForm """ self._EnsureWriteEnabled() self._ValidateParam(entry_or_hvo, "entry_or_hvo") self._ValidateParam(text, "text") if not text or not text.strip(): raise FP_ParameterError("Reversal entry form cannot be empty") entry = self.__ResolveObject(entry_or_hvo) # Use entry's index writing system if not provided if wsHandle is None: wsHandle = self.__GetEntryWS(entry) mkstr = TsStringUtils.MakeString(text, wsHandle) entry.ReversalForm.set_String(wsHandle, mkstr) # --- Sense Linking --- @OperationsMethod def GetSenses(self, entry_or_hvo): """ Get all lexical senses linked to this reversal entry. Args: entry_or_hvo: Either an IReversalIndexEntry object or its HVO Returns: list: List of ILexSense objects Raises: FP_NullParameterError: If entry_or_hvo is None Example: >>> entry = project.ReversalEntries.Find(idx, "run") >>> senses = project.ReversalEntries.GetSenses(entry) >>> for sense in senses: ... gloss = project.Senses.GetGloss(sense) ... print(f"Linked sense: {gloss}") Linked sense: to move rapidly Linked sense: to flow Linked sense: to operate Notes: - Returns empty list if no senses linked - One reversal entry can link to multiple senses - Links are bidirectional (sense also knows about reversal entry) See Also: AddSense, RemoveSense """ self._ValidateParam(entry_or_hvo, "entry_or_hvo") entry = self.__ResolveObject(entry_or_hvo) return list(entry.SensesRS) @OperationsMethod def AddSense(self, entry_or_hvo, sense): """ Link a lexical sense to this reversal entry. Args: entry_or_hvo: Either an IReversalIndexEntry object or its HVO sense: ILexSense object to link Raises: FP_ReadOnlyError: If project is not opened with write enabled FP_NullParameterError: If entry_or_hvo or sense is None Example: >>> entry = project.ReversalEntries.Find(idx, "run") >>> lex_entry = project.LexEntry.Find("hlauka") >>> sense = list(lex_entry.SensesOS)[0] >>> project.ReversalEntries.AddSense(entry, sense) Notes: - Multiple senses can be linked to one reversal entry - Duplicate links are automatically prevented - Link is bidirectional See Also: RemoveSense, GetSenses """ self._EnsureWriteEnabled() self._ValidateParam(entry_or_hvo, "entry_or_hvo") self._ValidateParam(sense, "sense") entry = self.__ResolveObject(entry_or_hvo) # Add sense if not already linked if sense not in entry.SensesRS: entry.SensesRS.Add(sense) @OperationsMethod def RemoveSense(self, entry_or_hvo, sense): """ Unlink a lexical sense from this reversal entry. Args: entry_or_hvo: Either an IReversalIndexEntry object or its HVO sense: ILexSense object to unlink Raises: FP_ReadOnlyError: If project is not opened with write enabled FP_NullParameterError: If entry_or_hvo or sense is None Example: >>> entry = project.ReversalEntries.Find(idx, "run") >>> sense = list(project.ReversalEntries.GetSenses(entry))[0] >>> project.ReversalEntries.RemoveSense(entry, sense) Notes: - Safe to call even if sense not linked - Doesn't delete the sense, only removes the link - Link removal is bidirectional See Also: AddSense, GetSenses """ self._EnsureWriteEnabled() self._ValidateParam(entry_or_hvo, "entry_or_hvo") self._ValidateParam(sense, "sense") entry = self.__ResolveObject(entry_or_hvo) # Remove sense if linked if sense in entry.SensesRS: entry.SensesRS.Remove(sense) # --- Hierarchical Structure --- @OperationsMethod def GetSubentries(self, entry_or_hvo): """ Get all subentries of a reversal entry. Args: entry_or_hvo: Either an IReversalIndexEntry object or its HVO Returns: list: List of IReversalIndexEntry objects (subentries) Raises: FP_NullParameterError: If entry_or_hvo is None Example: >>> entry = project.ReversalEntries.Find(idx, "run") >>> subentries = project.ReversalEntries.GetSubentries(entry) >>> for sub in subentries: ... form = project.ReversalEntries.GetForm(sub) ... print(f"Subentry: {form}") Subentry: run away Subentry: run out Notes: - Returns empty list if no subentries - Subentries enable hierarchical organization - Common for phrasal verbs, compounds, etc. - Use _GetSequence() for reordering subentries See Also: Create, GetAll """ self._ValidateParam(entry_or_hvo, "entry_or_hvo") entry = self.__ResolveObject(entry_or_hvo) return list(entry.SubentriesOS) # --- Private Helper Methods --- def __ResolveObject(self, entry_or_hvo): """ Resolve HVO or object to IReversalIndexEntry. Args: entry_or_hvo: Either an IReversalIndexEntry object or an HVO (int) Returns: IReversalIndexEntry: The resolved entry object Raises: FP_ParameterError: If HVO doesn't refer to a reversal entry """ if isinstance(entry_or_hvo, int): obj = self.project.Object(entry_or_hvo) if not isinstance(obj, IReversalIndexEntry): raise FP_ParameterError("HVO does not refer to a reversal index entry") return obj return entry_or_hvo def __GetIndexObject(self, index_or_hvo): """ Resolve HVO or object to IReversalIndex. Args: index_or_hvo: Either an IReversalIndex object or an HVO (int) Returns: IReversalIndex: The resolved index object Raises: FP_ParameterError: If HVO doesn't refer to a reversal index """ if isinstance(index_or_hvo, int): obj = self.project.Object(index_or_hvo) if not isinstance(obj, IReversalIndex): raise FP_ParameterError("HVO does not refer to a reversal index") return obj return index_or_hvo def __GetEntryWS(self, entry): """ Get the writing system handle for a reversal entry. Args: entry: IReversalIndexEntry object Returns: int: Writing system handle from the entry's parent index """ # Get the owning reversal index index = entry.ReversalIndex if index is None: raise FP_ParameterError( f"ReversalIndex is None for entry '{entry.Hvo}' -- " "entry may be orphaned or cascade-deleted." ) ws_str = index.WritingSystem return self.project.WSHandle(ws_str)