#
# WfiGlossOperations.py
#
# Class: WfiGlossOperations
# Wordform gloss operations for FieldWorks Language Explorer projects
# via SIL Language and Culture Model (LCM) API.
#
# Platform: Python.NET
# FieldWorks Version 9+
#
# Copyright 2025
#
import clr
clr.AddReference("System")
from SIL.LCModel import (
IWfiGloss,
IWfiGlossFactory,
IWfiAnalysis,
)
from SIL.LCModel.Core.KernelInterfaces import ITsString
from SIL.LCModel.Core.Text import TsStringUtils
from ..FLExProject import (
FP_ParameterError,
)
from ..BaseOperations import BaseOperations, OperationsMethod, wrap_enumerable
from ..Shared.string_utils import normalize_match_key
from ..lcm_casting import cast_to_concrete
# --- WfiGlossOperations Class ---
[docs]
class WfiGlossOperations(BaseOperations):
"""
Provides operations for managing wordform glosses (WfiGloss) in a FLEx project.
WfiGloss objects represent glosses (meanings/translations) for wordform analyses.
Each WfiAnalysis can have multiple glosses in different writing systems.
Glosses provide the semantic interpretation of an analyzed wordform.
This class should be accessed via FLExProject.WfiGlosses property.
Example:
>>> project = FLExProject()
>>> project.OpenProject("MyProject", writeEnabled=True)
>>> # Get a wordform and its first analysis
>>> wf = project.Wordforms.Find("running")
>>> analyses = project.Wordforms.GetAnalyses(wf)
>>> if analyses:
... analysis = analyses[0]
... # Get all glosses for the analysis
... for gloss in project.WfiGlosses.GetAll(analysis):
... form = project.WfiGlosses.GetForm(gloss)
... print(f"Gloss: {form}")
... # Create a new gloss
... new_gloss = project.WfiGlosses.Create(analysis, "running", "en")
... # Update gloss form
... project.WfiGlosses.SetForm(new_gloss, "to run", "en")
>>> project.CloseProject()
"""
def __init__(self, project):
"""
Initialize WfiGlossOperations.
Args:
project: FLExProject instance
"""
super().__init__(project)
def __WSHandle(self, wsHandle):
"""
Internal helper to resolve writing system handle.
Args:
wsHandle: Writing system handle (language tag or ID), or None for default
Returns:
int: Resolved writing system handle
"""
if wsHandle is None:
return self.project.project.DefaultAnalWs
return self.project._FLExProject__WSHandle(wsHandle, self.project.project.DefaultAnalWs)
def __ResolveGloss(self, gloss_or_hvo):
"""
Resolve an HVO or object to a concrete IWfiGloss proxy.
Segment analyses (SegmentOperations.GetAnalyses) come back as
polymorphic IAnalysis references. Reading ``.Form`` off an
IAnalysis-typed proxy silently yields '' even when the underlying
object *is* a gloss -- the classic issue #212 trap. Casting with
``IWfiGloss(obj)`` surfaces the real Form regardless of the proxy's
static type. A genuinely non-gloss token raises a clear, actionable
error instead of silently returning ''.
Args:
gloss_or_hvo: An IWfiGloss / IAnalysis object, or an HVO (int).
Returns:
IWfiGloss: The object cast to the concrete gloss interface.
Raises:
FP_ParameterError: If the object is not an IWfiGloss.
"""
if isinstance(gloss_or_hvo, int):
gloss = self.project.Object(gloss_or_hvo)
else:
gloss = gloss_or_hvo
# pythonnet isinstance() against the concrete IWfiGloss interface
# returns False for a base IAnalysis-typed reference even when the
# object IS a gloss -- exactly what SegmentOperations.GetAnalyses hands
# back. Dispatch on ClassName (the cast_to_concrete idiom) instead of
# isinstance so the guard fires only on genuinely non-gloss tokens.
if getattr(gloss, "ClassName", None) != "WfiGloss":
raise FP_ParameterError(
"Expected an IWfiGloss but got {0}. Segment analyses are "
"polymorphic IAnalysis objects; only IWfiGloss tokens carry a "
"gloss form, and reading .Form off another subtype silently "
"returns ''. Cast with IWfiGloss(obj) or use "
"SegmentOperations.GetGloss(analysis, ws).".format(
getattr(gloss, "ClassName", type(gloss).__name__)
)
)
# Explicit cast so pythonnet surfaces IWfiGloss.Form even when the
# caller passed an IAnalysis-typed reference (issue #212).
return IWfiGloss(gloss)
def __ResolveAnalysis(self, analysis_or_hvo):
"""
Resolve HVO or object to IWfiAnalysis.
Casts by ``ClassName`` before returning (issue #502, generalising
#275 / #269). ``self.project.Object(hvo)`` returns a bare
``ICmObject``; ``isinstance(obj, IWfiAnalysis)`` is False even for
a genuine analysis, so the old inline HVO guards rejected every real
HVO. ``cast_to_concrete`` widens to the concrete interface.
Args:
analysis_or_hvo: Either an IWfiAnalysis object or an HVO (int).
Returns:
IWfiAnalysis: The resolved analysis object.
Raises:
FP_ParameterError: If HVO does not refer to a wordform analysis.
"""
if isinstance(analysis_or_hvo, int):
obj = self.project.Object(analysis_or_hvo)
if getattr(obj, "ClassName", None) == "WfiAnalysis":
try:
return cast_to_concrete(obj)
except Exception:
pass
if isinstance(obj, IWfiAnalysis):
return obj
raise FP_ParameterError("HVO does not refer to a wordform analysis")
if getattr(analysis_or_hvo, "ClassName", None) == "WfiAnalysis":
try:
return cast_to_concrete(analysis_or_hvo)
except Exception:
pass
return analysis_or_hvo
# --- Core CRUD Operations ---
@wrap_enumerable
@OperationsMethod
def GetAll(self, analysis_or_hvo):
"""
Retrieve all glosses for a wordform analysis.
This method returns an EnumerableWrapper (subscriptable, len()-able,
lazily materialized) over all IWfiGloss objects associated with the
specified analysis, allowing iteration over all meaning glosses.
Args:
analysis_or_hvo: Either an IWfiAnalysis object or its HVO
Returns:
EnumerableWrapper[IWfiGloss]: Each gloss object for the analysis
Raises:
FP_NullParameterError: If analysis_or_hvo is None
FP_ParameterError: If analysis doesn't exist
Example:
>>> wf = project.Wordforms.Find("running")
>>> analyses = project.Wordforms.GetAnalyses(wf)
>>> if analyses:
... for gloss in project.WfiGlosses.GetAll(analyses[0]):
... form = project.WfiGlosses.GetForm(gloss)
... print(form)
Notes:
- Returns an EnumerableWrapper (subscriptable, len()-able) for memory efficiency; the underlying LCM enumerator is only materialized into a list on first len()/index/iteration access
- Glosses are returned in the order they were added
- Returns empty iterator if analysis has no glosses
- Each gloss can have forms in multiple writing systems
See Also:
Create, Find, GetForm
"""
self._ValidateParam(analysis_or_hvo, "analysis_or_hvo")
analysis = self.__ResolveAnalysis(analysis_or_hvo)
# Yield all glosses from the Meanings collection
for gloss in analysis.MeaningsOC:
yield gloss
@OperationsMethod
def Create(self, analysis_or_hvo, form, wsHandle=None, guid=None):
"""
Create a new gloss for a wordform analysis.
Args:
analysis_or_hvo: Either an IWfiAnalysis object or its HVO
form: The gloss text (meaning/translation)
wsHandle: Optional writing system handle. Defaults to analysis WS.
guid (optional): GUID to assign to the new gloss, as a
``System.Guid`` or string. Use this when REPRODUCING a
gloss from another project so it keeps its original
identity. None (the default) mints a fresh GUID.
Returns:
IWfiGloss: The newly created gloss object
Raises:
FP_ReadOnlyError: If project is not opened with write enabled
FP_NullParameterError: If analysis_or_hvo or form is None
FP_ParameterError: If form is empty or analysis doesn't exist
Example:
>>> wf = project.Wordforms.Find("running")
>>> analyses = project.Wordforms.GetAnalyses(wf)
>>> if analyses:
... gloss = project.WfiGlosses.Create(analyses[0], "to run")
... print(project.WfiGlosses.GetForm(gloss))
to run
>>> # Create with specific writing system
>>> gloss_fr = project.WfiGlosses.Create(analyses[0], "courir", "fr")
Notes:
- New gloss is added to the analysis's Meanings collection
- The form is stored in the specified writing system
- Duplicate glosses are allowed (no uniqueness check)
- Gloss is added at the end of the collection
See Also:
Delete, SetForm, GetAll
"""
self._EnsureWriteEnabled()
self._ValidateParam(analysis_or_hvo, "analysis_or_hvo")
self._ValidateParam(form, "form")
if not form or not form.strip():
raise FP_ParameterError("Gloss form cannot be empty")
analysis = self.__ResolveAnalysis(analysis_or_hvo)
wsHandle = self.__WSHandle(wsHandle)
with self._TransactionCM("Create gloss"):
# Get the factory and create the gloss
factory = self.project.project.ServiceLocator.GetService(IWfiGlossFactory)
new_gloss = self._CreateWithGuid(factory, guid, "IWfiGloss")
# Add to analysis's Meanings collection (must be done before setting properties)
analysis.MeaningsOC.Add(new_gloss)
# Set the form
mkstr = TsStringUtils.MakeString(form, wsHandle)
new_gloss.Form.set_String(wsHandle, mkstr)
return new_gloss
# ========== SYNC INTEGRATION METHODS ==========
@OperationsMethod
def GetSyncableProperties(self, item):
"""
Get all syncable properties of a wordform gloss.
Args:
item: The IWfiGloss object.
Returns:
dict: Dictionary of syncable properties with their values.
Example:
>>> props = project.WfiGlosses.GetSyncableProperties(gloss)
>>> print(props['Form'])
{'en': 'running', 'es': 'corriendo'}
Notes:
- MultiString property: Form
- Form contains gloss text in multiple writing systems
"""
props = {}
# MultiString property - Form
if hasattr(item, "Form") and item.Form:
props["Form"] = self.project.GetMultiStringDict(item.Form)
return props
@OperationsMethod
def CompareTo(self, item1, item2, ops1=None, ops2=None):
"""
Compare two wordform glosses for differences.
Args:
item1: First gloss object (from project 1)
item2: Second gloss object (from project 2)
ops1: Optional WfiGlossOperations instance for project 1 (defaults to self)
ops2: Optional WfiGlossOperations instance for project 2 (defaults to self)
Returns:
tuple: (is_different, differences_dict)
- is_different (bool): True if glosses differ, False if identical
- differences_dict (dict): Maps property names to (value1, value2) tuples
Example:
>>> is_diff, diffs = ops1.CompareTo(gloss1, gloss2, ops1, ops2)
>>> if is_diff:
... for prop, (val1, val2) in diffs.items():
... print(f"{prop}: {val1} != {val2}")
Notes:
- Compares Form MultiString across all writing systems
- Empty/null values are treated as equivalent
"""
if ops1 is None:
ops1 = self
if ops2 is None:
ops2 = self
props1 = ops1.GetSyncableProperties(item1)
props2 = ops2.GetSyncableProperties(item2)
differences = {}
# Get all property keys from both items
all_keys = set(props1.keys()) | set(props2.keys())
for key in all_keys:
val1 = props1.get(key)
val2 = props2.get(key)
# Compare values inline: FLExProject has no _CompareValues
# member (calling it raised AttributeError on every compare;
# same fix as MediaOperations.CompareTo).
if val1 != val2:
# Values are different
differences[key] = (val1, val2)
is_different = len(differences) > 0
return (is_different, differences)
@OperationsMethod
def Delete(self, gloss_or_hvo):
"""
Delete a gloss from its owning analysis.
Args:
gloss_or_hvo: Either an IWfiGloss object or its HVO
Raises:
FP_ReadOnlyError: If project is not opened with write enabled
FP_NullParameterError: If gloss_or_hvo is None
FP_ParameterError: If gloss doesn't exist
Example:
>>> wf = project.Wordforms.Find("running")
>>> analyses = project.Wordforms.GetAnalyses(wf)
>>> if analyses:
... glosses = list(project.WfiGlosses.GetAll(analyses[0]))
... if len(glosses) > 1:
... # Delete the last gloss
... project.WfiGlosses.Delete(glosses[-1])
Warning:
- This is a destructive operation
- Deletion is permanent and cannot be undone
- Consider backing up data before deletion
- Text segments that reference this gloss may be affected
See Also:
Create, GetAll
"""
self._EnsureWriteEnabled()
self._ValidateParam(gloss_or_hvo, "gloss_or_hvo")
# Resolve to a concrete IWfiGloss (casts IAnalysis-typed refs; raises
# on non-gloss subtypes instead of silently returning '' -- issue #212)
gloss = self.__ResolveGloss(gloss_or_hvo)
# Get the owning analysis; MeaningsOC is declared on IWfiAnalysis so
# cast unconditionally to surface the typed collection accessor.
analysis = IWfiAnalysis(gloss.Owner)
# Remove from analysis's Meanings collection
with self._TransactionCM("Delete gloss"):
analysis.MeaningsOC.Remove(gloss)
@OperationsMethod
def Duplicate(self, item_or_hvo, insert_after=False, deep=False):
"""
Duplicate a wordform gloss, creating a new copy with a new GUID.
Args:
item_or_hvo: The IWfiGloss object or HVO to duplicate.
insert_after (bool): Ignored. MeaningsOC is an unordered
ILcmOwningCollection with no Insert() method and no concept of
positional ordering. The duplicate is always appended via Add().
deep (bool): Accepted for API uniformity across Operations classes. WfiGloss has no owned objects, so this parameter is ignored.
Returns:
IWfiGloss: The newly created duplicate gloss with a new GUID.
Raises:
FP_ReadOnlyError: If the project is not opened with write enabled.
FP_NullParameterError: If item_or_hvo is None.
Example:
>>> wf = project.Wordforms.Find("running")
>>> analyses = project.Wordforms.GetAnalyses(wf)
>>> if analyses:
... glosses = list(project.WfiGlosses.GetAll(analyses[0]))
... if glosses:
... # Duplicate gloss
... dup = project.WfiGlosses.Duplicate(glosses[0])
... print(f"Original: {project.WfiGlosses.GetGuid(glosses[0])}")
... print(f"Duplicate: {project.WfiGlosses.GetGuid(dup)}")
Original: 12345678-1234-1234-1234-123456789abc
Duplicate: 87654321-4321-4321-4321-cba987654321
...
... # Verify content was copied
... print(f"Form: {project.WfiGlosses.GetForm(dup)}")
Notes:
- Factory.Create() automatically generates a new GUID
- MeaningsOC is unordered; the duplicate is appended via Add()
- Simple properties copied: Form (MultiString in all writing systems)
- Reference properties: None (WfiGloss has no reference properties)
- WfiGloss 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")
# Resolve to a concrete IWfiGloss (issue #212)
source = self.__ResolveGloss(item_or_hvo)
# MeaningsOC is declared on IWfiAnalysis; the untyped _GetObject()
# round-trip previously returned a bare ICmObject here, silently
# failing on `.MeaningsOC`. Cast directly like Delete() already
# does above (same fix pattern, sibling site).
parent = IWfiAnalysis(source.Owner)
with self._TransactionCM("Duplicate gloss"):
# Create new gloss using factory (auto-generates new GUID)
factory = self.project.project.ServiceLocator.GetService(IWfiGlossFactory)
duplicate = factory.Create()
# MeaningsOC is unordered; always append via Add().
parent.MeaningsOC.Add(duplicate)
# Copy simple MultiString properties
duplicate.Form.CopyAlternatives(source.Form)
# Note: WfiGloss has no owned objects (OS collections), so deep has no effect
return duplicate
# --- Form Management Operations ---
@OperationsMethod
def GetForm(self, gloss_or_hvo, wsHandle=None):
"""
Get the gloss text form.
Args:
gloss_or_hvo: Either an IWfiGloss object or its HVO
wsHandle: Optional writing system handle. Defaults to analysis WS.
Returns:
str: The gloss text (empty string if not set)
Raises:
FP_NullParameterError: If gloss_or_hvo is None
FP_ParameterError: If gloss doesn't exist
Example:
>>> wf = project.Wordforms.Find("running")
>>> analyses = project.Wordforms.GetAnalyses(wf)
>>> if analyses:
... for gloss in project.WfiGlosses.GetAll(analyses[0]):
... form = project.WfiGlosses.GetForm(gloss)
... print(form)
Notes:
- Returns empty string if form not set in specified writing system
- Default writing system is the default analysis WS
- Gloss can have different forms in multiple writing systems
See Also:
SetForm, GetAllForms
"""
self._ValidateParam(gloss_or_hvo, "gloss_or_hvo")
# Resolve to a concrete IWfiGloss (casts IAnalysis-typed refs; raises
# on non-gloss subtypes instead of silently returning '' -- issue #212)
gloss = self.__ResolveGloss(gloss_or_hvo)
wsHandle = self.__WSHandle(wsHandle)
# Get the form string
form = ITsString(gloss.Form.get_String(wsHandle)).Text
return form or ""
@OperationsMethod
def SetForm(self, gloss_or_hvo, text, wsHandle=None):
"""
Set the gloss text form.
Args:
gloss_or_hvo: Either an IWfiGloss object or its HVO
text: The new gloss text
wsHandle: Optional writing system handle. Defaults to analysis WS.
Raises:
FP_ReadOnlyError: If project is not opened with write enabled
FP_NullParameterError: If gloss_or_hvo or text is None
FP_ParameterError: If gloss doesn't exist
Example:
>>> wf = project.Wordforms.Find("running")
>>> analyses = project.Wordforms.GetAnalyses(wf)
>>> if analyses:
... glosses = list(project.WfiGlosses.GetAll(analyses[0]))
... if glosses:
... project.WfiGlosses.SetForm(glosses[0], "to run quickly")
... print(project.WfiGlosses.GetForm(glosses[0]))
to run quickly
>>> # Set form in multiple writing systems
>>> project.WfiGlosses.SetForm(glosses[0], "courir", "fr")
>>> project.WfiGlosses.SetForm(glosses[0], "correr", "es")
Notes:
- Empty string is allowed (clears the gloss in that writing system)
- Gloss can be set independently in multiple writing systems
- Use this to provide translations in different languages
See Also:
GetForm, Create
"""
self._EnsureWriteEnabled()
self._ValidateParam(gloss_or_hvo, "gloss_or_hvo")
self._ValidateParam(text, "text")
# Resolve to a concrete IWfiGloss (casts IAnalysis-typed refs; raises
# on non-gloss subtypes instead of silently returning '' -- issue #212)
gloss = self.__ResolveGloss(gloss_or_hvo)
wsHandle = self.__WSHandle(wsHandle)
# Set the form string
with self._TransactionCM(f"Set gloss form '{text}'"):
mkstr = TsStringUtils.MakeString(text, wsHandle)
gloss.Form.set_String(wsHandle, mkstr)
@OperationsMethod
def GetAllForms(self, gloss_or_hvo):
"""
Get all available forms for a gloss across all writing systems.
Args:
gloss_or_hvo: Either an IWfiGloss object or its HVO
Returns:
dict: Dictionary mapping writing system handles to form text
Raises:
FP_NullParameterError: If gloss_or_hvo is None
FP_ParameterError: If gloss doesn't exist
Example:
>>> wf = project.Wordforms.Find("running")
>>> analyses = project.Wordforms.GetAnalyses(wf)
>>> if analyses:
... glosses = list(project.WfiGlosses.GetAll(analyses[0]))
... if glosses:
... forms = project.WfiGlosses.GetAllForms(glosses[0])
... for ws_handle, form in forms.items():
... print(f"WS {ws_handle}: {form}")
Notes:
- Returns only writing systems where form is set
- Dictionary keys are writing system handles (integers)
- Use project.GetWritingSystemName(ws) to get WS names
- Empty forms are excluded from results
See Also:
GetForm, SetForm
"""
self._ValidateParam(gloss_or_hvo, "gloss_or_hvo")
# Resolve to a concrete IWfiGloss (casts IAnalysis-typed refs; raises
# on non-gloss subtypes instead of silently returning '' -- issue #212)
gloss = self.__ResolveGloss(gloss_or_hvo)
forms = {}
# Iterate through available writing systems
for ws in gloss.Form.AvailableWritingSystemIds:
form_text = ITsString(gloss.Form.get_String(ws)).Text
if form_text:
forms[ws] = form_text
return forms
# --- Utility Operations ---
@OperationsMethod
def GetOwningAnalysis(self, gloss_or_hvo):
"""
Get the wordform analysis that owns this gloss.
Args:
gloss_or_hvo: Either an IWfiGloss object or its HVO
Returns:
IWfiAnalysis: The owning analysis object
Raises:
FP_NullParameterError: If gloss_or_hvo is None
FP_ParameterError: If gloss doesn't exist
Example:
>>> # Find a gloss and get its owning analysis
>>> wf = project.Wordforms.Find("running")
>>> analyses = project.Wordforms.GetAnalyses(wf)
>>> if analyses:
... glosses = list(project.WfiGlosses.GetAll(analyses[0]))
... if glosses:
... analysis = project.WfiGlosses.GetOwningAnalysis(glosses[0])
... print(f"Analysis HVO: {analysis.Hvo}")
Notes:
- Gloss is owned by exactly one analysis
- Use this to navigate from gloss back to analysis
- Useful for accessing other analysis properties
See Also:
GetAll, GetGuid
"""
self._ValidateParam(gloss_or_hvo, "gloss_or_hvo")
# Resolve to a concrete IWfiGloss (casts IAnalysis-typed refs; raises
# on non-gloss subtypes instead of silently returning '' -- issue #212)
gloss = self.__ResolveGloss(gloss_or_hvo)
# Cast to declared return type IWfiAnalysis. Raw gloss.Owner is typed
# as ICmObject in LCM; pythonnet only surfaces IWfiAnalysis properties
# (e.g. MorphBundlesOS) after the explicit interface cast.
return IWfiAnalysis(gloss.Owner)
@OperationsMethod
def GetGuid(self, gloss_or_hvo):
"""
Get the GUID (Global Unique Identifier) for a gloss.
Args:
gloss_or_hvo: Either an IWfiGloss object or its HVO
Returns:
System.Guid: The GUID of the gloss
Raises:
FP_NullParameterError: If gloss_or_hvo is None
FP_ParameterError: If gloss doesn't exist
Example:
>>> wf = project.Wordforms.Find("running")
>>> analyses = project.Wordforms.GetAnalyses(wf)
>>> if analyses:
... glosses = list(project.WfiGlosses.GetAll(analyses[0]))
... if glosses:
... guid = project.WfiGlosses.GetGuid(glosses[0])
... print(f"Gloss GUID: {guid}")
Notes:
- GUID is unique across all projects
- GUID remains constant even if object moves
- Useful for cross-referencing between projects
- HVO is project-specific, GUID is universal
See Also:
GetOwningAnalysis, Find
"""
self._ValidateParam(gloss_or_hvo, "gloss_or_hvo")
# Resolve to a concrete IWfiGloss (casts IAnalysis-typed refs; raises
# on non-gloss subtypes instead of silently returning '' -- issue #212)
gloss = self.__ResolveGloss(gloss_or_hvo)
return gloss.Guid
@OperationsMethod
def Find(self, analysis_or_hvo, form, wsHandle=None):
"""
Find and return a gloss by its form text.
Args:
analysis_or_hvo: Either an IWfiAnalysis object or its HVO
form: The gloss text to find
wsHandle: Optional writing system handle. Defaults to analysis WS.
Returns:
IWfiGloss or None: The gloss object if found, None otherwise
Raises:
FP_NullParameterError: If analysis_or_hvo or form is None
FP_ParameterError: If analysis doesn't exist
Example:
>>> wf = project.Wordforms.Find("running")
>>> analyses = project.Wordforms.GetAnalyses(wf)
>>> if analyses:
... gloss = project.WfiGlosses.Find(analyses[0], "to run")
... if gloss:
... print(f"Found gloss: {gloss.Hvo}")
... else:
... print("Gloss not found")
Notes:
- Search is case-sensitive
- Search is writing-system specific
- Returns None if gloss doesn't exist
- Returns first match if multiple glosses have same form
- Use Exists() for simple existence check
See Also:
Exists, GetAll, Create
"""
self._ValidateParam(analysis_or_hvo, "analysis_or_hvo")
self._ValidateParam(form, "form")
if not form or not form.strip():
return None
analysis = self.__ResolveAnalysis(analysis_or_hvo)
wsHandle = self.__WSHandle(wsHandle)
# Search through all glosses
target = normalize_match_key(form, casefold=False)
for gloss in self.GetAll(analysis):
gloss_form = ITsString(gloss.Form.get_String(wsHandle)).Text
if normalize_match_key(gloss_form, casefold=False) == target:
return gloss
return None
@OperationsMethod
def Exists(self, analysis_or_hvo, form, wsHandle=None):
"""
Check if a gloss with the specified form exists for an analysis.
Args:
analysis_or_hvo: Either an IWfiAnalysis object or its HVO
form: The gloss text to check
wsHandle: Optional writing system handle. Defaults to analysis WS.
Returns:
bool: True if the gloss exists, False otherwise
Raises:
FP_NullParameterError: If analysis_or_hvo or form is None
FP_ParameterError: If analysis doesn't exist
Example:
>>> wf = project.Wordforms.Find("running")
>>> analyses = project.Wordforms.GetAnalyses(wf)
>>> if analyses:
... if project.WfiGlosses.Exists(analyses[0], "to run"):
... gloss = project.WfiGlosses.Find(analyses[0], "to run")
... else:
... gloss = project.WfiGlosses.Create(analyses[0], "to run")
Notes:
- Search is case-sensitive
- Search is writing-system specific
- Returns False for empty or whitespace-only forms
- More efficient than Find() when you only need existence check
See Also:
Find, Create
"""
self._ValidateParam(analysis_or_hvo, "analysis_or_hvo")
self._ValidateParam(form, "form")
if not form or not form.strip():
return False
return self.Find(analysis_or_hvo, form, wsHandle) is not None
@OperationsMethod
def GetCount(self, analysis_or_hvo):
"""
Get the count of glosses for a wordform analysis.
Args:
analysis_or_hvo: Either an IWfiAnalysis object or its HVO
Returns:
int: Number of glosses
Raises:
FP_NullParameterError: If analysis_or_hvo is None
FP_ParameterError: If analysis doesn't exist
Example:
>>> wf = project.Wordforms.Find("running")
>>> analyses = project.Wordforms.GetAnalyses(wf)
>>> if analyses:
... count = project.WfiGlosses.GetCount(analyses[0])
... print(f"This analysis has {count} glosses")
Notes:
- More efficient than len(list(GetAll())) for counting
- Returns 0 if analysis has no glosses
- Each gloss may have forms in multiple writing systems
See Also:
GetAll, Create
"""
self._ValidateParam(analysis_or_hvo, "analysis_or_hvo")
analysis = self.__ResolveAnalysis(analysis_or_hvo)
return analysis.MeaningsOC.Count
@OperationsMethod
def GetBestForm(self, gloss_or_hvo):
"""
Get the best available form for a gloss.
This method attempts to find a suitable gloss form by checking writing
systems in order of preference: default analysis WS, then any available WS.
Args:
gloss_or_hvo: Either an IWfiGloss object or its HVO
Returns:
str: The best available gloss form (empty string if none found)
Raises:
FP_NullParameterError: If gloss_or_hvo is None
FP_ParameterError: If gloss doesn't exist
Example:
>>> wf = project.Wordforms.Find("running")
>>> analyses = project.Wordforms.GetAnalyses(wf)
>>> if analyses:
... for gloss in project.WfiGlosses.GetAll(analyses[0]):
... best = project.WfiGlosses.GetBestForm(gloss)
... print(f"Best form: {best}")
Notes:
- Tries default analysis WS first
- Falls back to first available WS if default not set
- Returns empty string if no forms are set
- Useful when you need any available form
See Also:
GetForm, GetAllForms
"""
self._ValidateParam(gloss_or_hvo, "gloss_or_hvo")
# Resolve to a concrete IWfiGloss (casts IAnalysis-typed refs; raises
# on non-gloss subtypes instead of silently returning '' -- issue #212)
gloss = self.__ResolveGloss(gloss_or_hvo)
# Try default analysis WS first
default_ws = self.project.project.DefaultAnalWs
best_form = ITsString(gloss.Form.get_String(default_ws)).Text
if best_form:
return best_form
# Try any available writing system
for ws in gloss.Form.AvailableWritingSystemIds:
form_text = ITsString(gloss.Form.get_String(ws)).Text
if form_text:
return form_text
return ""
@OperationsMethod
def CopyGloss(self, gloss_or_hvo, target_analysis_or_hvo):
"""
Copy a gloss to another analysis.
Creates a new gloss in the target analysis with all forms from the source gloss.
Args:
gloss_or_hvo: Either an IWfiGloss object or its HVO (source)
target_analysis_or_hvo: Either an IWfiAnalysis object or its HVO (target)
Returns:
IWfiGloss: The newly created gloss in the target analysis
Raises:
FP_ReadOnlyError: If project is not opened with write enabled
FP_NullParameterError: If gloss_or_hvo or target_analysis_or_hvo is None
FP_ParameterError: If gloss or analysis doesn't exist
Example:
>>> wf = project.Wordforms.Find("running")
>>> analyses = project.Wordforms.GetAnalyses(wf)
>>> if len(analyses) > 1:
... glosses = list(project.WfiGlosses.GetAll(analyses[0]))
... if glosses:
... # Copy first gloss from first analysis to second
... new_gloss = project.WfiGlosses.CopyGloss(glosses[0], analyses[1])
... print(f"Copied gloss: {project.WfiGlosses.GetForm(new_gloss)}")
Notes:
- Copies all writing system forms from source to target
- Creates a new gloss object (not a reference)
- Useful for sharing glosses between analyses
See Also:
Create, GetAllForms
"""
self._EnsureWriteEnabled()
self._ValidateParam(gloss_or_hvo, "gloss_or_hvo")
self._ValidateParam(target_analysis_or_hvo, "target_analysis_or_hvo")
# Resolve to a concrete IWfiGloss (issue #212)
source_gloss = self.__ResolveGloss(gloss_or_hvo)
target_analysis = self.__ResolveAnalysis(target_analysis_or_hvo)
with self._TransactionCM("Copy gloss"):
# Create new gloss
factory = self.project.project.ServiceLocator.GetService(IWfiGlossFactory)
new_gloss = factory.Create()
# Add to target analysis (must be done before setting properties)
target_analysis.MeaningsOC.Add(new_gloss)
# Copy all forms
for ws in source_gloss.Form.AvailableWritingSystemIds:
form_text = ITsString(source_gloss.Form.get_String(ws)).Text
if form_text:
mkstr = TsStringUtils.MakeString(form_text, ws)
new_gloss.Form.set_String(ws, mkstr)
return new_gloss
@OperationsMethod
def ClearForm(self, gloss_or_hvo, wsHandle=None):
"""
Clear the form for a gloss in a specific writing system.
Args:
gloss_or_hvo: Either an IWfiGloss object or its HVO
wsHandle: Optional writing system handle. Defaults to analysis WS.
Raises:
FP_ReadOnlyError: If project is not opened with write enabled
FP_NullParameterError: If gloss_or_hvo is None
FP_ParameterError: If gloss doesn't exist
Example:
>>> wf = project.Wordforms.Find("running")
>>> analyses = project.Wordforms.GetAnalyses(wf)
>>> if analyses:
... glosses = list(project.WfiGlosses.GetAll(analyses[0]))
... if glosses:
... # Clear the English form
... project.WfiGlosses.ClearForm(glosses[0], "en")
Notes:
- Only clears the form in the specified writing system
- Forms in other writing systems are not affected
- Equivalent to SetForm(gloss, "", ws)
- Gloss object itself is not deleted
See Also:
SetForm, Delete
"""
self._EnsureWriteEnabled()
self._ValidateParam(gloss_or_hvo, "gloss_or_hvo")
self.SetForm(gloss_or_hvo, "", wsHandle)