Source code for flexicon.code.Lexicon.EtymologyOperations

#
#   EtymologyOperations.py
#
#   Class: EtymologyOperations
#          Etymology tracking 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

# Import FLEx LCM types
from SIL.LCModel import (
    ILexEtymology,
    ILexEtymologyFactory,
    ILexEntry,
)
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


[docs] class EtymologyOperations(BaseOperations): """ This class provides operations for managing etymological information in a FieldWorks project. Etymology tracking records the historical origin and development of lexical entries. Each etymology can specify the source language, etymological form, gloss, linguistic commentary, and bibliographic references. This class supports historical linguistics workflows including etymology documentation, loan word tracking, and diachronic analysis. Usage:: from flexlibs2 import FLExProject project = FLExProject() project.OpenProject("my project", writeEnabled=True) # Access via FLExProject.Etymology property (if configured) # Or create directly: from flexlibs2.code.EtymologyOperations import EtymologyOperations etymOps = EtymologyOperations(project) # Get an entry entry = project.LexEntry.Find("computer") # Create an etymology etym = etymOps.Create( entry, source="English", form="compute", gloss="to calculate", ws="en" ) # Set additional information etymOps.SetComment(etym, "Borrowed in the 1980s", "en") etymOps.SetBibliography(etym, "Smith 2020:145") # Get all etymologies for etym in etymOps.GetAll(entry): source = etymOps.GetSource(etym) form = etymOps.GetForm(etym) print(f"From {source}: {form}") project.CloseProject() """ def __init__(self, project): """ Initialize EtymologyOperations 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 etymologies. For Etymology, we reorder entry.EtymologyOS """ return parent.EtymologyOS # --- Core CRUD Operations --- @wrap_enumerable @OperationsMethod def GetAll(self, entry_or_hvo=None): """ Get all etymologies for a lexical entry, or all etymologies in the entire project. Args: entry_or_hvo: The ILexEntry object or HVO. If None, iterates all etymologies in the entire project. Yields: ILexEtymology: Each etymology object for the entry (or project). Example: >>> etymOps = EtymologyOperations(project) >>> # Get etymologies for specific entry >>> entry = project.LexEntry.Find("telephone") >>> for etym in etymOps.GetAll(entry): ... source = etymOps.GetSource(etym) ... form = etymOps.GetForm(etym) ... gloss = etymOps.GetGloss(etym) ... print(f"Etymology: {source} '{form}' ({gloss})") Etymology: Greek 'tele' (far) Etymology: Greek 'phone' (sound) >>> # Get ALL etymologies in entire project >>> for etym in etymOps.GetAll(): ... source = etymOps.GetSource(etym) ... print(f"Etymology source: {source}") Notes: - When entry_or_hvo is provided: - Returns etymologies in database order - Returns empty generator if entry has no etymologies - Etymologies can be reordered using Reorder() - Each etymology represents one source or stage in word history - When entry_or_hvo is None: - Iterates ALL entries in the project - For each entry, yields all etymologies - Useful for project-wide etymology operations See Also: Create, Delete, Reorder """ if entry_or_hvo is None: # Iterate ALL etymologies in entire project for entry in self.project.lexDB.Entries: for etymology in entry.EtymologyOS: yield etymology else: # Iterate etymologies for specific entry entry = self.__GetEntryObject(entry_or_hvo) for etymology in entry.EtymologyOS: yield etymology @OperationsMethod def Create(self, entry_or_hvo, source=None, form=None, gloss=None, ws=None): """ Create a new etymology for a lexical entry. Args: entry_or_hvo: The ILexEntry object or HVO. source (str, optional): The source language name. form (str, optional): The etymological form in source language. gloss (str, optional): The meaning in source language. ws: Optional writing system handle. Defaults to analysis WS. Returns: ILexEtymology: The newly created etymology object. Raises: FP_ReadOnlyError: If the project is not opened with write enabled. FP_NullParameterError: If entry_or_hvo is None. Example: >>> etymOps = EtymologyOperations(project) >>> entry = project.LexEntry.Find("telephone") >>> etym = etymOps.Create( ... entry, ... source="Greek", ... form="tele", ... gloss="far" ... ) >>> print(etymOps.GetForm(etym)) tele >>> # Create minimal etymology (add details later) >>> etym2 = etymOps.Create(entry) >>> etymOps.SetSource(etym2, "Greek") >>> etymOps.SetForm(etym2, "phone") Notes: - All text fields are optional at creation - Etymology is added at the end of the entry's etymology list - Use Set methods to add/update information after creation - Multiple etymologies can track compound or complex origins - Default writing system is analysis WS See Also: Delete, SetSource, SetForm, SetGloss """ self._EnsureWriteEnabled() self._ValidateParam(entry_or_hvo, "entry_or_hvo") entry = self.__GetEntryObject(entry_or_hvo) wsHandle = self.__WSHandleAnalysis(ws) with self._TransactionCM("Create etymology"): # Create the new etymology using the factory factory = self.project.project.ServiceLocator.GetService(ILexEtymologyFactory) new_etymology = factory.Create() # Add to entry's etymology collection (must be done before setting properties) entry.EtymologyOS.Add(new_etymology) # Set optional fields if provided if source: mkstr = TsStringUtils.MakeString(source, wsHandle) new_etymology.Source.set_String(wsHandle, mkstr) if form: mkstr = TsStringUtils.MakeString(form, wsHandle) new_etymology.Form.set_String(wsHandle, mkstr) if gloss: mkstr = TsStringUtils.MakeString(gloss, wsHandle) new_etymology.Gloss.set_String(wsHandle, mkstr) return new_etymology @OperationsMethod def Delete(self, etymology_or_hvo): """ Delete an etymology from its owning entry. Args: etymology_or_hvo: The ILexEtymology object or HVO to delete. Raises: FP_ReadOnlyError: If the project is not opened with write enabled. FP_NullParameterError: If etymology_or_hvo is None. Example: >>> etymOps = EtymologyOperations(project) >>> entry = project.LexEntry.Find("telephone") >>> etymologies = list(etymOps.GetAll(entry)) >>> if len(etymologies) > 0: ... # Delete the last etymology ... etymOps.Delete(etymologies[-1]) Warning: - Deletion is permanent and cannot be undone - All etymology data (source, form, gloss, etc.) is lost - Consider archiving data before deletion Notes: - Removes the etymology from the owning entry's collection - Other etymologies in the list are automatically renumbered - No error if entry has no other etymologies See Also: Create, GetAll """ self._EnsureWriteEnabled() self._ValidateParam(etymology_or_hvo, "etymology_or_hvo") etymology = self.__GetEtymologyObject(etymology_or_hvo) # Get the owning entry. etymology.Owner is typed as ICmObject # and does not expose EtymologyOS; cast to ILexEntry so the # collection is reachable (the previous hasattr check silently # no-opped because the typed collection lives on the concrete # owner, not on the base interface). owner = self._GetTypedOwner(etymology) if owner is None: raise FP_ParameterError("Etymology has no owning entry") owner.EtymologyOS.Remove(etymology) @OperationsMethod def Duplicate(self, item_or_hvo, insert_after=True): """ Duplicate an etymology, creating a new copy with a new GUID. Args: item_or_hvo: The ILexEtymology object or HVO to duplicate. insert_after (bool): If True (default), insert after the source etymology. If False, insert at end of entry's etymology list. deep (bool): If True, also duplicate owned objects (if any exist). If False (default), only copy simple properties and references. Note: Etymology has no owned objects, so deep has no effect. Returns: ILexEtymology: The newly created duplicate etymology with a new GUID. Raises: FP_ReadOnlyError: If the project is not opened with write enabled. FP_NullParameterError: If item_or_hvo is None. Example: >>> etymOps = EtymologyOperations(project) >>> entry = project.LexEntry.Find("telephone") >>> etymologies = list(etymOps.GetAll(entry)) >>> if etymologies: ... # Duplicate etymology ... dup = etymOps.Duplicate(etymologies[0]) ... print(f"Original: {etymOps.GetGuid(etymologies[0])}") ... print(f"Duplicate: {etymOps.GetGuid(dup)}") Original: 12345678-1234-1234-1234-123456789abc Duplicate: 87654321-4321-4321-4321-cba987654321 ... ... # Verify content was copied ... print(f"Source: {etymOps.GetSource(dup)}") ... print(f"Form: {etymOps.GetForm(dup)}") Notes: - Factory.Create() automatically generates a new GUID - insert_after=True preserves the original etymology's position - Simple properties copied: Source, Form, Gloss, Comment, Bibliography - Reference properties copied: LanguageNotesRA - Etymology has no owned objects, so deep parameter has no effect See Also: Create, Delete, GetGuid """ self._EnsureWriteEnabled() self._ValidateParam(item_or_hvo, "item_or_hvo") # Get source etymology and parent. source.Owner is typed as # ICmObject; cast to ILexEntry so EtymologyOS is reachable. # Without this cast the previous `hasattr(parent, "EtymologyOS")` # silently returned False and the duplicate was orphaned. source = self.__GetEtymologyObject(item_or_hvo) parent = self._GetTypedOwner(source) if parent is None: raise FP_ParameterError("Etymology has no owning entry") with self._TransactionCM("Duplicate etymology"): # Create new etymology using factory (auto-generates new GUID) factory = self.project.project.ServiceLocator.GetService(ILexEtymologyFactory) duplicate = factory.Create() # Determine insertion position if insert_after: # Insert after source etymology source_index = parent.EtymologyOS.IndexOf(source) parent.EtymologyOS.Insert(source_index + 1, duplicate) else: # Insert at end parent.EtymologyOS.Add(duplicate) # Copy simple MultiString properties (AFTER adding to parent) duplicate.Source.CopyAlternatives(source.Source) duplicate.Form.CopyAlternatives(source.Form) duplicate.Gloss.CopyAlternatives(source.Gloss) duplicate.Comment.CopyAlternatives(source.Comment) duplicate.Bibliography.CopyAlternatives(source.Bibliography) # Copy Reference Atomic (RA) properties if hasattr(source, "LanguageNotesRA") and source.LanguageNotesRA: duplicate.LanguageNotesRA = source.LanguageNotesRA # Note: Etymology has no owned objects (OS collections), so deep has no effect return duplicate # ========== SYNC INTEGRATION METHODS ========== @OperationsMethod def GetSyncableProperties(self, item): """ Get all syncable properties of an etymology for comparison. Args: item: The ILexEtymology object. Returns: dict: Dictionary mapping property names to their values. """ props = {} # MultiString properties # Form - the etymological form form_dict = {} if hasattr(item, "Form"): for ws_def in self.project.WritingSystems.GetAll(): from SIL.LCModel.Core.KernelInterfaces import ITsString text = normalize_text(ITsString(item.Form.get_String(ws_def.Handle)).Text) if text: ws_tag = ws_def.Id form_dict[ws_tag] = text props["Form"] = form_dict # Gloss - meaning of the etymological form gloss_dict = {} if hasattr(item, "Gloss"): for ws_def in self.project.WritingSystems.GetAll(): from SIL.LCModel.Core.KernelInterfaces import ITsString text = normalize_text(ITsString(item.Gloss.get_String(ws_def.Handle)).Text) if text: ws_tag = ws_def.Id gloss_dict[ws_tag] = text props["Gloss"] = gloss_dict # Source - source language or reference source_dict = {} if hasattr(item, "Source"): for ws_def in self.project.WritingSystems.GetAll(): from SIL.LCModel.Core.KernelInterfaces import ITsString text = normalize_text(ITsString(item.Source.get_String(ws_def.Handle)).Text) if text: ws_tag = ws_def.Id source_dict[ws_tag] = text props["Source"] = source_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 # Bibliography - bibliographic reference bibliography_dict = {} if hasattr(item, "Bibliography"): for ws_def in self.project.WritingSystems.GetAll(): from SIL.LCModel.Core.KernelInterfaces import ITsString text = normalize_text(ITsString(item.Bibliography.get_String(ws_def.Handle)).Text) if text: ws_tag = ws_def.Id bibliography_dict[ws_tag] = text props["Bibliography"] = bibliography_dict # Reference Atomic (RA) properties # LanguageRA - source language (ICmPossibility from LangProject.AnalysisWSs # or the languages list). Serialize as GUID string. if hasattr(item, "LanguageRA") and item.LanguageRA: props["LanguageRA"] = str(item.LanguageRA.Guid) else: props["LanguageRA"] = None # LanguageNotesRA - language notes reference (ICmPossibility). (P2) # This is an atomic reference, same serialization pattern as LanguageRA. # [JUDGMENT CALL] The LCM field name is LanguageNotesRA on ILexEtymology; # this resolves against the same general possibility list as LanguageRA. # Verification requested: confirm LanguageNotesRA is the correct field name # on ILexEtymology and that its possibility list is LangProject.AnthroListOA # or similar (not AnalysisWSs). If the field does not exist at runtime, # the hasattr guard makes this a no-op. if hasattr(item, "LanguageNotesRA") and item.LanguageNotesRA: props["LanguageNotesRA"] = str(item.LanguageNotesRA.Guid) else: props["LanguageNotesRA"] = None return props @OperationsMethod def ApplySyncableProperties(self, item, props, ws_map=None, fill_gaps=False): """ Apply a syncable-properties dict onto an ILexEtymology item. Extends the base implementation to handle atomic reference fields (LanguageRA, LanguageNotesRA) which are serialized as GUID strings. Resolution uses the generic project.Object(Guid) lookup which works for any LCM object whose GUID is known. Args: item: Target ILexEtymology (must already exist in target project). props: dict produced by GetSyncableProperties on a source etymology. ws_map: Optional source->target writing-system Id mapping. fill_gaps (bool): When True, only write fields whose current target value is empty/absent; passed through to BaseOperations. Atomic-ref fields (LanguageRA, LanguageNotesRA) are always applied regardless (domain has not ruled on them). """ import logging as _logging _log = _logging.getLogger(__name__) self._EnsureWriteEnabled() _ra_fields = ("LanguageRA", "LanguageNotesRA") remaining_props = {} ra_props = {} for k, v in props.items(): if k in _ra_fields: ra_props[k] = v else: remaining_props[k] = v with self._TransactionCM("Apply etymology properties"): # Apply plain / multistring fields via base class. super().ApplySyncableProperties(item, remaining_props, ws_map=ws_map, fill_gaps=fill_gaps) # Resolve atomic reference fields by GUID. for field_name, guid_str in ra_props.items(): if not hasattr(item, field_name): continue if not guid_str: setattr(item, field_name, None) continue try: import System obj = self.project.Object(System.Guid(guid_str)) setattr(item, field_name, obj) except Exception as exc: _log.warning( "[WARN] ApplySyncableProperties: %s GUID %s not found " "in target project -- skipped (%s)", field_name, guid_str, exc ) @OperationsMethod def CompareTo(self, item1, item2, ops1=None, ops2=None): """ Compare two etymologies and return their differences. Args: item1: The first ILexEtymology object. item2: The second ILexEtymology object. ops1: Optional EtymologyOperations instance for item1. ops2: Optional EtymologyOperations 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 Reorder(self, entry_or_hvo, etymology_list): """ Reorder etymologies for a lexical entry. Args: entry_or_hvo: The ILexEntry object or HVO. etymology_list: List of ILexEtymology objects or HVOs in desired order. Raises: FP_ReadOnlyError: If the project is not opened with write enabled. FP_NullParameterError: If entry_or_hvo or etymology_list is None. FP_ParameterError: If etymology_list doesn't match entry's etymologies. Example: >>> etymOps = EtymologyOperations(project) >>> entry = project.LexEntry.Find("telephone") >>> etymologies = list(etymOps.GetAll(entry)) >>> if len(etymologies) > 1: ... # Reverse the order ... etymOps.Reorder(entry, reversed(etymologies)) ... # Verify new order ... for etym in etymOps.GetAll(entry): ... print(etymOps.GetSource(etym)) Notes: - All etymologies must be provided in the new order - All etymologies must belong to the specified entry - Useful for ordering by chronological sequence - Useful for ordering from ultimate to immediate source See Also: GetAll, Create """ self._EnsureWriteEnabled() self._ValidateParam(entry_or_hvo, "entry_or_hvo") self._ValidateParam(etymology_list, "etymology_list") entry = self.__GetEntryObject(entry_or_hvo) # Convert to list if it's an iterator etymology_list = list(etymology_list) # Resolve all to objects etymologies = [self.__GetEtymologyObject(etym) for etym in etymology_list] # Verify all etymologies belong to this entry current_etymologies = set(entry.EtymologyOS) new_etymologies = set(etymologies) if current_etymologies != new_etymologies: raise FP_ParameterError("Etymology list must contain exactly the same etymologies as the entry") with self._TransactionCM("Reorder etymologies"): # Clear and re-add in new order entry.EtymologyOS.Clear() for etymology in etymologies: entry.EtymologyOS.Add(etymology) # --- Source Language Operations --- @OperationsMethod def GetSource(self, etymology_or_hvo, ws=None): """ Get the source language name for an etymology. Args: etymology_or_hvo: The ILexEtymology object or HVO. ws: Optional writing system handle. Defaults to analysis WS. Returns: str: The source language name, or empty string if not set. Raises: FP_NullParameterError: If etymology_or_hvo is None. Example: >>> etymOps = EtymologyOperations(project) >>> entry = project.LexEntry.Find("telephone") >>> etymologies = list(etymOps.GetAll(entry)) >>> if etymologies: ... source = etymOps.GetSource(etymologies[0]) ... print(f"Source language: {source}") Source language: Greek >>> # Get in specific writing system >>> source_fr = etymOps.GetSource(etymologies[0], "fr") Notes: - Returns empty string if source not set in specified writing system - Source can be a language name, language family, or proto-language - Examples: "Latin", "Proto-Indo-European", "French", "Unknown" - Can be set in multiple writing systems for multilingual display See Also: SetSource, GetForm, GetGloss """ self._ValidateParam(etymology_or_hvo, "etymology_or_hvo") etymology = self.__GetEtymologyObject(etymology_or_hvo) wsHandle = self.__WSHandleAnalysis(ws) source = ITsString(etymology.Source.get_String(wsHandle)).Text return self._NormalizeMultiString(source) @OperationsMethod def SetSource(self, etymology_or_hvo, text, ws=None): """ Set the source language name for an etymology. Args: etymology_or_hvo: The ILexEtymology object or HVO. text (str): The source language name. ws: Optional writing system handle. Defaults to analysis WS. Raises: FP_ReadOnlyError: If the project is not opened with write enabled. FP_NullParameterError: If etymology_or_hvo or text is None. Example: >>> etymOps = EtymologyOperations(project) >>> entry = project.LexEntry.Find("telephone") >>> etymologies = list(etymOps.GetAll(entry)) >>> if etymologies: ... etymOps.SetSource(etymologies[0], "Ancient Greek") ... print(etymOps.GetSource(etymologies[0])) Ancient Greek >>> # Set in multiple writing systems >>> etymOps.SetSource(etymologies[0], "Grec ancien", "fr") >>> etymOps.SetSource(etymologies[0], "Griego antiguo", "es") Notes: - Empty string is allowed (clears the source) - Can be set independently in multiple writing systems - Common formats: language name, ISO code, or proto-language - Be consistent within a project for analysis purposes See Also: GetSource, SetForm, SetGloss """ self._EnsureWriteEnabled() self._ValidateParam(etymology_or_hvo, "etymology_or_hvo") self._ValidateParam(text, "text") etymology = self.__GetEtymologyObject(etymology_or_hvo) wsHandle = self.__WSHandleAnalysis(ws) mkstr = TsStringUtils.MakeString(text, wsHandle) etymology.Source.set_String(wsHandle, mkstr) # --- Form & Gloss Operations --- @OperationsMethod def GetForm(self, etymology_or_hvo, ws=None): """ Get the etymological form (the form in the source language). Args: etymology_or_hvo: The ILexEtymology object or HVO. ws: Optional writing system handle. Defaults to analysis WS. Returns: str: The etymological form, or empty string if not set. Raises: FP_NullParameterError: If etymology_or_hvo is None. Example: >>> etymOps = EtymologyOperations(project) >>> entry = project.LexEntry.Find("telephone") >>> etymologies = list(etymOps.GetAll(entry)) >>> if etymologies: ... form = etymOps.GetForm(etymologies[0]) ... source = etymOps.GetSource(etymologies[0]) ... print(f"{source}: {form}") Greek: τηλε Notes: - Returns empty string if form not set in specified writing system - Form should be in the orthography of the source language - May include phonetic transcription, reconstruction, or native script - For Proto-languages, use asterisk notation (e.g., "*tele") See Also: SetForm, GetSource, GetGloss """ self._ValidateParam(etymology_or_hvo, "etymology_or_hvo") etymology = self.__GetEtymologyObject(etymology_or_hvo) wsHandle = self.__WSHandleAnalysis(ws) form = ITsString(etymology.Form.get_String(wsHandle)).Text return self._NormalizeMultiString(form) @OperationsMethod def SetForm(self, etymology_or_hvo, text, ws=None): """ Set the etymological form (the form in the source language). Args: etymology_or_hvo: The ILexEtymology object or HVO. text (str): The etymological form. ws: Optional writing system handle. Defaults to analysis WS. Raises: FP_ReadOnlyError: If the project is not opened with write enabled. FP_NullParameterError: If etymology_or_hvo or text is None. Example: >>> etymOps = EtymologyOperations(project) >>> entry = project.LexEntry.Find("telephone") >>> etym = etymOps.Create(entry) >>> etymOps.SetForm(etym, "tele") >>> print(etymOps.GetForm(etym)) tele >>> # Set reconstructed Proto-Indo-European form >>> etymOps.SetForm(etym, "*tele-") Notes: - Empty string is allowed (clears the form) - Can be set independently in multiple writing systems - Include diacritics, tone marks, or special characters as needed - For reconstructed forms, use asterisk (*) prefix by convention See Also: GetForm, SetSource, SetGloss """ self._EnsureWriteEnabled() self._ValidateParam(etymology_or_hvo, "etymology_or_hvo") self._ValidateParam(text, "text") etymology = self.__GetEtymologyObject(etymology_or_hvo) wsHandle = self.__WSHandleAnalysis(ws) mkstr = TsStringUtils.MakeString(text, wsHandle) etymology.Form.set_String(wsHandle, mkstr) @OperationsMethod def GetGloss(self, etymology_or_hvo, ws=None): """ Get the gloss (meaning in the source language). Args: etymology_or_hvo: The ILexEtymology object or HVO. ws: Optional writing system handle. Defaults to analysis WS. Returns: str: The gloss text, or empty string if not set. Raises: FP_NullParameterError: If etymology_or_hvo is None. Example: >>> etymOps = EtymologyOperations(project) >>> entry = project.LexEntry.Find("telephone") >>> etymologies = list(etymOps.GetAll(entry)) >>> if etymologies: ... form = etymOps.GetForm(etymologies[0]) ... gloss = etymOps.GetGloss(etymologies[0]) ... print(f"'{form}' means '{gloss}'") 'tele' means 'far, distant' Notes: - Returns empty string if gloss not set in specified writing system - Gloss describes the meaning in the source language - May differ from current meaning due to semantic shift - Use semicolons to separate multiple related meanings See Also: SetGloss, GetForm, GetSource """ self._ValidateParam(etymology_or_hvo, "etymology_or_hvo") etymology = self.__GetEtymologyObject(etymology_or_hvo) wsHandle = self.__WSHandleAnalysis(ws) gloss = ITsString(etymology.Gloss.get_String(wsHandle)).Text return self._NormalizeMultiString(gloss) @OperationsMethod def SetGloss(self, etymology_or_hvo, text, ws=None): """ Set the gloss (meaning in the source language). Args: etymology_or_hvo: The ILexEtymology object or HVO. text (str): The gloss text. ws: Optional writing system handle. Defaults to analysis WS. Raises: FP_ReadOnlyError: If the project is not opened with write enabled. FP_NullParameterError: If etymology_or_hvo or text is None. Example: >>> etymOps = EtymologyOperations(project) >>> entry = project.LexEntry.Find("telephone") >>> etym = etymOps.Create(entry) >>> etymOps.SetGloss(etym, "far, distant") >>> print(etymOps.GetGloss(etym)) far, distant Notes: - Empty string is allowed (clears the gloss) - Can be set independently in multiple writing systems - Gloss explains the original meaning in the source language - Helps track semantic change from source to current language See Also: GetGloss, SetForm, SetSource """ self._EnsureWriteEnabled() self._ValidateParam(etymology_or_hvo, "etymology_or_hvo") self._ValidateParam(text, "text") etymology = self.__GetEtymologyObject(etymology_or_hvo) wsHandle = self.__WSHandleAnalysis(ws) mkstr = TsStringUtils.MakeString(text, wsHandle) etymology.Gloss.set_String(wsHandle, mkstr) # --- Comment & Bibliography Operations --- @OperationsMethod def GetComment(self, etymology_or_hvo, ws=None): """ Get the linguistic comment for an etymology. Args: etymology_or_hvo: The ILexEtymology object or HVO. ws: Optional writing system handle. Defaults to analysis WS. Returns: str: The comment text, or empty string if not set. Raises: FP_NullParameterError: If etymology_or_hvo is None. Example: >>> etymOps = EtymologyOperations(project) >>> entry = project.LexEntry.Find("telephone") >>> etymologies = list(etymOps.GetAll(entry)) >>> if etymologies: ... comment = etymOps.GetComment(etymologies[0]) ... print(f"Note: {comment}") Note: Calque from Greek compound; borrowed in 19th century Notes: - Returns empty string if comment not set in specified writing system - Comment field allows free-form linguistic commentary - Use for notes on borrowing, semantic shift, sound changes, etc. - Can include dates, phonological rules, or comparative notes See Also: SetComment, GetBibliography """ self._ValidateParam(etymology_or_hvo, "etymology_or_hvo") etymology = self.__GetEtymologyObject(etymology_or_hvo) wsHandle = self.__WSHandleAnalysis(ws) comment = ITsString(etymology.Comment.get_String(wsHandle)).Text return self._NormalizeMultiString(comment) @OperationsMethod def SetComment(self, etymology_or_hvo, text, ws=None): """ Set the linguistic comment for an etymology. Args: etymology_or_hvo: The ILexEtymology object or HVO. text (str): The comment text. ws: Optional writing system handle. Defaults to analysis WS. Raises: FP_ReadOnlyError: If the project is not opened with write enabled. FP_NullParameterError: If etymology_or_hvo or text is None. Example: >>> etymOps = EtymologyOperations(project) >>> entry = project.LexEntry.Find("telephone") >>> etym = etymOps.Create(entry, source="Greek", form="tele") >>> etymOps.SetComment( ... etym, ... "Calque from Greek; borrowed in 1830s with invention of technology" ... ) >>> # Add comment in multiple languages >>> etymOps.SetComment( ... etym, ... "Calque du grec; emprunté vers 1830", ... "fr" ... ) Notes: - Empty string is allowed (clears the comment) - Can be set independently in multiple writing systems - Use for documenting borrowing process, sound changes, etc. - Include dates, intermediate forms, or comparative evidence - Can be multi-line for detailed explanations See Also: GetComment, SetBibliography """ self._EnsureWriteEnabled() self._ValidateParam(etymology_or_hvo, "etymology_or_hvo") self._ValidateParam(text, "text") etymology = self.__GetEtymologyObject(etymology_or_hvo) wsHandle = self.__WSHandleAnalysis(ws) mkstr = TsStringUtils.MakeString(text, wsHandle) etymology.Comment.set_String(wsHandle, mkstr) @OperationsMethod def GetBibliography(self, etymology_or_hvo): """ Get the bibliographic reference for an etymology. Args: etymology_or_hvo: The ILexEtymology object or HVO. Returns: str: The bibliographic reference, or empty string if not set. Raises: FP_NullParameterError: If etymology_or_hvo is None. Example: >>> etymOps = EtymologyOperations(project) >>> entry = project.LexEntry.Find("telephone") >>> etymologies = list(etymOps.GetAll(entry)) >>> if etymologies: ... bib = etymOps.GetBibliography(etymologies[0]) ... print(f"Source: {bib}") Source: Smith 2020:145; Jones 2018:234 Notes: - Returns empty string if bibliography not set - Bibliography stores academic references for the etymology - Can contain multiple citations separated by semicolons - Use standard citation format for your field - No specific format enforced - use consistent style See Also: SetBibliography, GetComment """ self._ValidateParam(etymology_or_hvo, "etymology_or_hvo") etymology = self.__GetEtymologyObject(etymology_or_hvo) # Bibliography can be stored as a string property or Unicode accessor # Check both possibilities if hasattr(etymology, "Bibliography"): bib = etymology.Bibliography if bib: # If it's an ITsString, extract text if hasattr(bib, "Text"): return self._NormalizeMultiString(bib.Text) # If it's already a string elif isinstance(bib, str): return bib # Try to cast to ITsString else: try: return self._NormalizeMultiString(ITsString(bib).Text) except Exception: return str(bib) if bib else "" return "" @OperationsMethod def SetBibliography(self, etymology_or_hvo, bibliography_text): """ Set the bibliographic reference for an etymology. Args: etymology_or_hvo: The ILexEtymology object or HVO. bibliography_text (str): The bibliographic reference. Raises: FP_ReadOnlyError: If the project is not opened with write enabled. FP_NullParameterError: If etymology_or_hvo or bibliography_text is None. Example: >>> etymOps = EtymologyOperations(project) >>> entry = project.LexEntry.Find("telephone") >>> etym = etymOps.Create(entry, source="Greek") >>> etymOps.SetBibliography(etym, "Smith 2020:145") >>> print(etymOps.GetBibliography(etym)) Smith 2020:145 >>> # Multiple citations >>> etymOps.SetBibliography( ... etym, ... "Smith 2020:145; Jones 2018:234; Brown 2015:89" ... ) Notes: - Empty string is allowed (clears the bibliography) - Use semicolons to separate multiple citations - Common formats: "Author Year:Page", "Author (Year)", etc. - Be consistent with citation style across your project - Can reference etymological dictionaries, historical sources, etc. See Also: GetBibliography, SetComment """ self._EnsureWriteEnabled() self._ValidateParam(etymology_or_hvo, "etymology_or_hvo") self._ValidateParam(bibliography_text, "bibliography_text") etymology = self.__GetEtymologyObject(etymology_or_hvo) wsHandle = self.project.project.DefaultAnalWs # Bibliography might be stored as string or ITsString # Try to set it appropriately if hasattr(etymology, "Bibliography"): # Check if it's a MultiUnicodeAccessor if hasattr(etymology.Bibliography, "set_String"): mkstr = TsStringUtils.MakeString(bibliography_text, wsHandle) etymology.Bibliography.set_String(wsHandle, mkstr) # Otherwise treat as direct string property else: mkstr = TsStringUtils.MakeString(bibliography_text, wsHandle) etymology.Bibliography = mkstr # --- Utility Operations --- @OperationsMethod def GetOwningEntry(self, etymology_or_hvo): """ Get the lexical entry that owns this etymology. Args: etymology_or_hvo: The ILexEtymology object or HVO. Returns: ILexEntry: The owning entry object. Raises: FP_NullParameterError: If etymology_or_hvo is None. Example: >>> etymOps = EtymologyOperations(project) >>> entry = project.LexEntry.Find("telephone") >>> etymologies = list(etymOps.GetAll(entry)) >>> if etymologies: ... owner = etymOps.GetOwningEntry(etymologies[0]) ... headword = project.LexEntry.GetHeadword(owner) ... print(f"Entry: {headword}") Entry: telephone Notes: - Returns the ILexEntry that contains this etymology - Useful for navigation and context - Etymologies always have exactly one owning entry See Also: GetAll, Create """ self._ValidateParam(etymology_or_hvo, "etymology_or_hvo") etymology = self.__GetEtymologyObject(etymology_or_hvo) return ILexEntry(etymology.Owner) @OperationsMethod def GetGuid(self, etymology_or_hvo): """ Get the GUID (Global Unique Identifier) of an etymology. Args: etymology_or_hvo: The ILexEtymology object or HVO. Returns: System.Guid: The GUID of the etymology. Raises: FP_NullParameterError: If etymology_or_hvo is None. Example: >>> etymOps = EtymologyOperations(project) >>> entry = project.LexEntry.Find("telephone") >>> etymologies = list(etymOps.GetAll(entry)) >>> if etymologies: ... guid = etymOps.GetGuid(etymologies[0]) ... print(f"Etymology GUID: {guid}") Etymology GUID: 12345678-1234-1234-1234-123456789abc Notes: - GUIDs are globally unique identifiers - Persistent across project versions - Use for external references and tracking - Same GUID across different copies of the project - HVO is project-specific, GUID is universal See Also: GetOwningEntry, GetAll """ self._ValidateParam(etymology_or_hvo, "etymology_or_hvo") etymology = self.__GetEtymologyObject(etymology_or_hvo) return etymology.Guid # --- Additional Properties --- @OperationsMethod def GetLanguage(self, etymology_or_hvo): """ Get the source language of an etymology. Args: etymology_or_hvo: Either an ILexEtymology object or its HVO Returns: ICmPossibility: The language possibility object, or None Example: >>> entry = project.LexEntry.Find("loanword") >>> etymologies = project.Etymology.GetAll(entry) >>> if etymologies: ... lang = project.Etymology.GetLanguage(etymologies[0]) ... if lang: ... print(lang.Name.BestAnalysisAlternative.Text) """ self._ValidateParam(etymology_or_hvo, "etymology_or_hvo") etymology = self.__GetEtymologyObject(etymology_or_hvo) return etymology.LanguageRA @OperationsMethod def SetLanguage(self, etymology_or_hvo, language): """ Set the source language of an etymology. Args: etymology_or_hvo: Either an ILexEtymology object or its HVO language: ICmPossibility object (language) or None Raises: FP_ReadOnlyError: If project is not opened with write enabled FP_NullParameterError: If etymology_or_hvo is None Example: >>> entry = project.LexEntry.Find("loanword") >>> etymologies = project.Etymology.GetAll(entry) >>> if etymologies: ... # Find language possibility (implementation depends on your project structure) ... # language_obj = ... (get from languages list) ... # project.Etymology.SetLanguage(etymologies[0], language_obj) ... pass """ self._EnsureWriteEnabled() self._ValidateParam(etymology_or_hvo, "etymology_or_hvo") etymology = self.__GetEtymologyObject(etymology_or_hvo) etymology.LanguageRA = language # --- Private Helper Methods --- def __GetEntryObject(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 __GetEtymologyObject(self, etymology_or_hvo): """ Resolve HVO or object to ILexEtymology. Args: etymology_or_hvo: Either an ILexEtymology object or an HVO (int). Returns: ILexEtymology: The resolved etymology object. Raises: FP_ParameterError: If HVO doesn't refer to an etymology. """ if isinstance(etymology_or_hvo, int): obj = self.project.Object(etymology_or_hvo) if not isinstance(obj, ILexEtymology): raise FP_ParameterError("HVO does not refer to an etymology") return obj return etymology_or_hvo def __WSHandleAnalysis(self, ws): """ Get writing system handle, defaulting to analysis WS. Args: ws: Optional writing system handle or identifier. Returns: int: The writing system handle. """ if ws is None: return self.project.project.DefaultAnalWs return self.project._FLExProject__WSHandle(ws, self.project.project.DefaultAnalWs)