Source code for flexicon.code.TextsWords.WfiAnalysisOperations

#
#   WfiAnalysisOperations.py
#
#   Class: WfiAnalysisOperations
#          Wordform analysis 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")
import System

from SIL.LCModel import (
    IWfiAnalysis,
    IWfiAnalysisFactory,
    IWfiAnalysisRepository,
    IWfiWordform,
    IWfiGlossFactory,
    IWfiMorphBundleFactory,
    IPartOfSpeech,
    ICmAgentEvaluation,
    ICmAgent,
)

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

# --- Approval Status Enum ---

import enum


[docs] class ApprovalStatusTypes(enum.IntEnum): """Approval status values for wordform analyses. IntEnum so existing callers that compared by the underlying integer keep working: ``ApprovalStatusTypes.APPROVED == 2`` is still True, and ``int(ApprovalStatusTypes.APPROVED) == 2`` for explicit coercion. New callers can also pass ``True`` / ``False`` / ``None`` directly to :meth:`SetApprovalStatus` (more user-centric per CLAUDE.md). (issue #59) """ DISAPPROVED = 0 # Parser or human has disapproved UNAPPROVED = 1 # Not yet approved/reviewed APPROVED = 2 # Parser or human has approved
def _coerce_approval_status(status): """Normalize a user-facing approval value to ApprovalStatusTypes. Accepts: - ApprovalStatusTypes member -> passes through. - True / False / None -> APPROVED / DISAPPROVED / UNAPPROVED. Matches the natural mental model: "I approve this" / "I reject this" / "no opinion." (issue #59) - Bare ints 0/1/2 -> looked up against the enum members for back-compat with the pre-IntEnum constants. Raises FP_ParameterError on anything else so a typo'd value fails loudly instead of silently dispatching to the wrong opinion. """ if isinstance(status, ApprovalStatusTypes): return status if status is True: return ApprovalStatusTypes.APPROVED if status is False: return ApprovalStatusTypes.DISAPPROVED if status is None: return ApprovalStatusTypes.UNAPPROVED if isinstance(status, int): try: return ApprovalStatusTypes(status) except ValueError: pass raise FP_ParameterError( f"status must be ApprovalStatusTypes (or True/False/None for " f"approved/disapproved/unapproved), got {status!r}" ) # --- WfiAnalysisOperations Class ---
[docs] class WfiAnalysisOperations(BaseOperations): """ Provides operations for managing wordform analyses in a FLEx project. Wordform analyses represent linguistic analyses of wordforms, including morphological breakdowns, glosses, grammatical categories, and approval status. Each wordform can have multiple analyses representing different possible interpretations. This class should be accessed via FLExProject.WfiAnalyses property. Example: >>> project = FLExProject() >>> project.OpenProject("MyProject", writeEnabled=True) >>> # Get wordform and its analyses >>> wf = project.Wordforms.Find("running") >>> analyses = project.WfiAnalyses.GetAll(wf) >>> for analysis in analyses: ... glosses = project.WfiAnalyses.GetGlosses(analysis) ... print(f"Analysis: {glosses}") >>> # Create new analysis >>> new_analysis = project.WfiAnalyses.Create(wf) >>> # Add gloss >>> project.WfiAnalyses.AddGloss(new_analysis, "running", "en") >>> # Approve analysis >>> project.WfiAnalyses.ApproveAnalysis(new_analysis) """ def __init__(self, project): """ Initialize WfiAnalysisOperations. Args: project: FLExProject instance """ super().__init__(project) def _GetSequence(self, parent): """Specify which sequence to reorder for wordform analyses.""" return parent.AnalysesOS def __WSHandle(self, wsHandle): """ Internal helper to resolve writing system handle (analysis). 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 __GetWordformObject(self, wordform_or_hvo): """ Internal helper to get wordform object from HVO or object. Args: wordform_or_hvo: IWfiWordform object or HVO integer. Returns: IWfiWordform: The wordform object. Raises: FP_ParameterError: If parameter doesn't refer to a wordform. """ if isinstance(wordform_or_hvo, int): wf = self.project.Object(wordform_or_hvo) if not isinstance(wf, IWfiWordform): raise FP_ParameterError("HVO does not refer to a wordform") return wf return wordform_or_hvo def __GetAnalysisObject(self, analysis_or_hvo): """ Internal helper to get analysis object from HVO or object. Args: analysis_or_hvo: IWfiAnalysis object or HVO integer. Returns: IWfiAnalysis: The analysis object. Raises: FP_ParameterError: If parameter doesn't refer to an analysis. """ if isinstance(analysis_or_hvo, int): analysis = self.project.Object(analysis_or_hvo) if not isinstance(analysis, IWfiAnalysis): raise FP_ParameterError("HVO does not refer to an analysis") return analysis return analysis_or_hvo # --- Core CRUD Operations --- @wrap_enumerable @OperationsMethod def GetAll(self, wordform_or_hvo): """ Retrieve all analyses for a wordform. This method returns all IWfiAnalysis objects associated with a given wordform, representing different possible linguistic interpretations. Args: wordform_or_hvo: Either an IWfiWordform object or its HVO Returns: list: List of IWfiAnalysis objects for this wordform Raises: FP_NullParameterError: If wordform_or_hvo is None FP_ParameterError: If wordform doesn't exist Example: >>> wf = project.Wordforms.Find("running") >>> analyses = project.WfiAnalyses.GetAll(wf) >>> print(f"Found {len(analyses)} analyses") Found 2 analyses >>> for analysis in analyses: ... if project.WfiAnalyses.IsHumanApproved(analysis): ... print("This is the approved analysis") Notes: - Returns empty list if wordform has no analyses - Analyses may be human-created or parser-generated - Each analysis represents a possible linguistic interpretation - Analyses include morphological breakdown and glosses See Also: Create, Exists, Delete """ self._ValidateParam(wordform_or_hvo, "wordform_or_hvo") wordform = self.__GetWordformObject(wordform_or_hvo) return list(wordform.AnalysesOC) @OperationsMethod def Create(self, wordform_or_hvo): """ Create a new analysis for a wordform. Creates an empty IWfiAnalysis object associated with the specified wordform. After creation, use AddGloss, AddMorphBundle, SetCategory, etc. to populate the analysis. Args: wordform_or_hvo: Either an IWfiWordform object or its HVO Returns: IWfiAnalysis: The newly created analysis object Raises: FP_ReadOnlyError: If project is not opened with write enabled FP_NullParameterError: If wordform_or_hvo is None FP_ParameterError: If wordform doesn't exist Example: >>> wf = project.Wordforms.Find("running") >>> analysis = project.WfiAnalyses.Create(wf) >>> # Add gloss to the analysis >>> project.WfiAnalyses.AddGloss(analysis, "running", "en") >>> # Set category >>> verb = project.POS.Find("Verb") >>> if verb: ... project.WfiAnalyses.SetCategory(analysis, verb) >>> # Approve it >>> project.WfiAnalyses.ApproveAnalysis(analysis) Notes: - New analyses start with unapproved status - Empty analyses have no glosses or morph bundles - Use SetApprovalStatus or ApproveAnalysis to mark as approved - Consider using AddGloss immediately after creation See Also: Delete, AddGloss, SetCategory, ApproveAnalysis """ self._EnsureWriteEnabled() self._ValidateParam(wordform_or_hvo, "wordform_or_hvo") wordform = self.__GetWordformObject(wordform_or_hvo) with self._TransactionCM("Create analysis"): # Create the analysis using the factory factory = self.project.project.ServiceLocator.GetService(IWfiAnalysisFactory) new_analysis = factory.Create() # Add to wordform's analysis collection wordform.AnalysesOC.Add(new_analysis) return new_analysis # ========== SYNC INTEGRATION METHODS ========== @OperationsMethod def GetSyncableProperties(self, item): """ Get all syncable properties of a wordform analysis. Args: item: The IWfiAnalysis object. Returns: dict: Dictionary of syncable properties with their values. Example: >>> props = project.WfiAnalyses.GetSyncableProperties(analysis) >>> print(props['CategoryRA']) 'abc123...' # GUID of the category (POS) Notes: - Reference Atomic property: CategoryRA (returns GUID string) - Does NOT include owned collections (glosses, morph bundles) - those are children - Does NOT include approval status (metadata) """ props = {} # Reference Atomic property - CategoryRA (Part of Speech) if hasattr(item, "CategoryRA") and item.CategoryRA: props["CategoryRA"] = str(item.CategoryRA.Guid) return props @OperationsMethod def CompareTo(self, item1, item2, ops1=None, ops2=None): """ Compare two wordform analyses for differences. Args: item1: First analysis object (from project 1) item2: Second analysis object (from project 2) ops1: Optional WfiAnalysisOperations instance for project 1 (defaults to self) ops2: Optional WfiAnalysisOperations instance for project 2 (defaults to self) Returns: tuple: (is_different, differences_dict) - is_different (bool): True if analyses differ, False if identical - differences_dict (dict): Maps property names to (value1, value2) tuples Example: >>> is_diff, diffs = ops1.CompareTo(analysis1, analysis2, ops1, ops2) >>> if is_diff: ... for prop, (val1, val2) in diffs.items(): ... print(f"{prop}: {val1} != {val2}") Notes: - Compares CategoryRA (POS) by GUID - 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 if self.project._CompareValues(val1, val2): # Values are different differences[key] = (val1, val2) is_different = len(differences) > 0 return (is_different, differences) @OperationsMethod def Delete(self, analysis_or_hvo): """ Delete an analysis from its owning wordform. Removes the specified analysis from the wordform's analysis collection. All associated glosses and morph bundles are also deleted. Args: analysis_or_hvo: Either an IWfiAnalysis object or its HVO Raises: FP_ReadOnlyError: If project is not opened with write enabled FP_NullParameterError: If analysis_or_hvo is None FP_ParameterError: If analysis doesn't exist Example: >>> wf = project.Wordforms.Find("running") >>> analyses = project.WfiAnalyses.GetAll(wf) >>> # Delete disapproved analyses >>> for analysis in analyses: ... status = project.WfiAnalyses.GetApprovalStatus(analysis) ... if status == ApprovalStatusTypes.DISAPPROVED: ... project.WfiAnalyses.Delete(analysis) Warning: - This is a destructive operation - All glosses and morph bundles are permanently deleted - Text segments using this analysis may be affected - Cannot be undone See Also: Create, GetAll """ self._EnsureWriteEnabled() self._ValidateParam(analysis_or_hvo, "analysis_or_hvo") analysis = self.__GetAnalysisObject(analysis_or_hvo) # Get the owning wordform and remove the analysis. pythonnet # returns analysis.Owner as the base ICmObject, which has no # AnalysesOC; cast to IWfiWordform first (issue #32). wordform = IWfiWordform(analysis.Owner) wordform.AnalysesOC.Remove(analysis) @OperationsMethod def Duplicate(self, item_or_hvo, insert_after=False, deep=False): """ Duplicate a wordform analysis, creating a new copy with a new GUID. Args: item_or_hvo: The IWfiAnalysis object or HVO to duplicate. insert_after (bool): Ignored. AnalysesOC is an unordered ILcmOwningCollection with no Insert() method and no concept of positional ordering. The duplicate is always appended via Add(). deep (bool): If True, also duplicate owned objects (glosses, morph bundles). If False (default), only copy simple properties and references. Returns: IWfiAnalysis: The newly created duplicate analysis 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.WfiAnalyses.GetAll(wf) >>> if analyses: ... # Shallow duplicate (no glosses/morph bundles) ... dup = project.WfiAnalyses.Duplicate(analyses[0]) ... print(f"Original: {project.WfiAnalyses.GetGuid(analyses[0])}") ... print(f"Duplicate: {project.WfiAnalyses.GetGuid(dup)}") Original: 12345678-1234-1234-1234-123456789abc Duplicate: 87654321-4321-4321-4321-cba987654321 ... ... # Deep duplicate (includes all glosses and morph bundles) ... deep_dup = project.WfiAnalyses.Duplicate(analyses[0], deep=True) ... print(f"Glosses: {project.WfiAnalyses.GetGlossCount(deep_dup)}") ... print(f"Morph bundles: {project.WfiAnalyses.GetMorphBundleCount(deep_dup)}") Notes: - Factory.Create() automatically generates a new GUID - insert_after=True preserves the original analysis's position - Simple properties copied: None (analysis has no simple MultiString properties) - Reference properties copied: CategoryRA (part of speech) - Owned objects (deep=True): MeaningsOC (glosses), MorphBundlesOS (morph bundles) - EvaluationsRC (approval status) is NOT copied - new analysis starts unapproved See Also: Create, Delete, GetGuid """ self._EnsureWriteEnabled() self._ValidateParam(item_or_hvo, "item_or_hvo") # Get source analysis and parent; AnalysesOC is declared on IWfiWordform # so cast directly instead of doing a wasteful Hvo round-trip. source = self.__GetAnalysisObject(item_or_hvo) parent = IWfiWordform(source.Owner) with self._TransactionCM("Duplicate analysis"): # Create new analysis using factory (auto-generates new GUID) factory = self.project.project.ServiceLocator.GetService(IWfiAnalysisFactory) duplicate = factory.Create() # AnalysesOC is an unordered ILcmOwningCollection; insert_after has # no semantic meaning and is ignored. parent.AnalysesOC.Add(duplicate) # Copy Reference Atomic (RA) properties if hasattr(source, "CategoryRA") and source.CategoryRA: duplicate.CategoryRA = source.CategoryRA # Note: We intentionally do NOT copy EvaluationsRC (approval status) # New duplicate should start as unapproved # Handle owned objects if deep=True if deep: # Duplicate glosses (MeaningsOC) for gloss in source.MeaningsOC: # Use WfiGlossOperations.Duplicate if available, otherwise manually copy gloss_factory = self.project.project.ServiceLocator.GetService(IWfiGlossFactory) new_gloss = gloss_factory.Create() duplicate.MeaningsOC.Add(new_gloss) # Copy gloss form new_gloss.Form.CopyAlternatives(gloss.Form) # Duplicate morph bundles (MorphBundlesOS) for bundle in source.MorphBundlesOS: # Use WfiMorphBundleOperations.Duplicate if available, otherwise manually copy bundle_factory = self.project.project.ServiceLocator.GetService(IWfiMorphBundleFactory) new_bundle = bundle_factory.Create() duplicate.MorphBundlesOS.Add(new_bundle) # Copy bundle properties new_bundle.Form.CopyAlternatives(bundle.Form) new_bundle.Gloss.CopyAlternatives(bundle.Gloss) if hasattr(bundle, "SenseRA") and bundle.SenseRA: new_bundle.SenseRA = bundle.SenseRA if hasattr(bundle, "MsaRA") and bundle.MsaRA: new_bundle.MsaRA = bundle.MsaRA if hasattr(bundle, "MorphRA") and bundle.MorphRA: new_bundle.MorphRA = bundle.MorphRA if hasattr(bundle, "InflClassRA") and bundle.InflClassRA: new_bundle.InflClassRA = bundle.InflClassRA return duplicate @OperationsMethod def Exists(self, wordform_or_hvo, analysis_or_hvo): """ Check if a specific analysis exists for a wordform. Verifies that the given analysis belongs to the specified wordform. Args: wordform_or_hvo: Either an IWfiWordform object or its HVO analysis_or_hvo: Either an IWfiAnalysis object or its HVO Returns: bool: True if the analysis exists in the wordform, False otherwise Raises: FP_NullParameterError: If either parameter is None Example: >>> wf = project.Wordforms.Find("running") >>> analyses = project.WfiAnalyses.GetAll(wf) >>> if analyses: ... # Check if first analysis exists ... exists = project.WfiAnalyses.Exists(wf, analyses[0]) ... print(f"Analysis exists: {exists}") Analysis exists: True Notes: - Returns False if wordform has no analyses - Checks by comparing HVO values - Useful for validation before operations See Also: GetAll, Create """ self._ValidateParam(wordform_or_hvo, "wordform_or_hvo") self._ValidateParam(analysis_or_hvo, "analysis_or_hvo") wordform = self.__GetWordformObject(wordform_or_hvo) analysis = self.__GetAnalysisObject(analysis_or_hvo) # Check if analysis is in wordform's analyses for wf_analysis in wordform.AnalysesOC: if wf_analysis.Hvo == analysis.Hvo: return True return False # --- Approval & Status Operations --- @OperationsMethod def GetApprovalStatus(self, analysis_or_hvo): """ Get the approval status of an analysis. Returns the current approval status indicating whether the analysis has been approved, disapproved, or is still unapproved. Args: analysis_or_hvo: Either an IWfiAnalysis object or its HVO Returns: int: The approval status (from ApprovalStatusTypes enum) 0 = DISAPPROVED 1 = UNAPPROVED (default) 2 = APPROVED Raises: FP_NullParameterError: If analysis_or_hvo is None FP_ParameterError: If analysis doesn't exist Example: >>> analysis = analyses[0] >>> status = project.WfiAnalyses.GetApprovalStatus(analysis) >>> if status == ApprovalStatusTypes.APPROVED: ... print("This analysis is approved") ... elif status == ApprovalStatusTypes.UNAPPROVED: ... print("This analysis needs review") ... else: ... print("This analysis is disapproved") Notes: - New analyses default to UNAPPROVED status - Parser-generated analyses may have different initial status - Status affects which analysis is used in interlinear text - Human approval overrides parser approval See Also: SetApprovalStatus, IsHumanApproved, IsComputerApproved, ApproveAnalysis, RejectAnalysis """ self._ValidateParam(analysis_or_hvo, "analysis_or_hvo") analysis = self.__GetAnalysisObject(analysis_or_hvo) # Human evaluations take precedence over parser evaluations. # Within the human evaluations, a single approve outweighs # disapproves -- matches FLEx UI semantics where one human # approval makes the analysis "approved" status. # # ICmAgentEvaluation exposes two flags: Human (true = human # analyst, false = parser) and Approves (true = approves, # false = disapproves). The previous implementation treated # any non-empty EvaluationsRC as APPROVED, which silently # mis-reported DISAPPROVED analyses. (issue #38) found_human_disapprove = False for evaluation in analysis.EvaluationsRC: if not getattr(evaluation, "Human", False): continue if getattr(evaluation, "Approves", False): return ApprovalStatusTypes.APPROVED found_human_disapprove = True if found_human_disapprove: return ApprovalStatusTypes.DISAPPROVED return ApprovalStatusTypes.UNAPPROVED @OperationsMethod def SetApprovalStatus(self, analysis_or_hvo, status): """ Set the approval status of an analysis. Updates the approval status to indicate whether the analysis is approved, disapproved, or unapproved. Args: analysis_or_hvo: Either an IWfiAnalysis object or its HVO status: The approval status (from ApprovalStatusTypes enum) 0 = DISAPPROVED 1 = UNAPPROVED 2 = APPROVED Raises: FP_ReadOnlyError: If project is not opened with write enabled FP_NullParameterError: If analysis_or_hvo is None FP_ParameterError: If analysis doesn't exist, status is invalid, or no human agents exist in the project Example: >>> analysis = project.WfiAnalyses.Create(wf) >>> # Approve the analysis >>> project.WfiAnalyses.SetApprovalStatus( ... analysis, ApprovalStatusTypes.APPROVED) >>> >>> # Or reject it >>> project.WfiAnalyses.SetApprovalStatus( ... analysis, ApprovalStatusTypes.DISAPPROVED) Notes: - Use ApprovalStatusTypes enum for status values - APPROVED analyses are preferred in interlinear text - DISAPPROVED analyses are typically hidden - UNAPPROVED is the default state - Consider using ApproveAnalysis() or RejectAnalysis() shortcuts - Creates/updates CmAgentEvaluation objects to persist approval - Requires at least one human agent to exist in project - Uses first available human agent for evaluation - Setting UNAPPROVED removes the evaluation entirely See Also: GetApprovalStatus, ApproveAnalysis, RejectAnalysis, IsHumanApproved """ self._EnsureWriteEnabled() self._ValidateParam(analysis_or_hvo, "analysis_or_hvo") if status not in ( ApprovalStatusTypes.DISAPPROVED, ApprovalStatusTypes.UNAPPROVED, ApprovalStatusTypes.APPROVED, ): raise FP_ParameterError(f"Invalid approval status: {status}. Must be 0, 1, or 2.") analysis = self.__GetAnalysisObject(analysis_or_hvo) # Get default human agent for evaluations # Import here to avoid circular dependency. AgentOperations lives in # the Lists/ subpackage, not TextsWords/ (issue #24). from ..Lists.AgentOperations import AgentOperations agent_ops = AgentOperations(self.project) # Require user to create human agent explicitly (Craig's pattern) human_agents = list(agent_ops.GetHumanAgents()) if not human_agents: raise FP_ParameterError( "No human agents exist in project. Create one first using " "project.Agents.CreateHumanAgent(name, person) before setting approval status." ) # Prefer ILangProject.DefaultUserAgent (the canonical "the user" # agent) when it is present and human. AnalyzingAgentsOC has no # deterministic ordering, so falling straight back to # human_agents[0] would non-deterministically pick whichever # human agent the runtime happened to list first -- e.g. a # legacy collaborator instead of the actual current user. Fall # back to human_agents[0] only when DefaultUserAgent is # unavailable or somehow not a human agent. (issue #56) agent = None default_user = getattr(self.project.lp, "DefaultUserAgent", None) if default_user is not None and getattr(default_user, "Human", False): for candidate in human_agents: if getattr(candidate, "Hvo", None) == getattr(default_user, "Hvo", None): agent = candidate break if agent is None: # DefaultUserAgent is human but wasn't surfaced by # GetHumanAgents() (shouldn't normally happen). Trust # the LCM-canonical reference anyway. agent = default_user if agent is None: agent = human_agents[0] # Use LCM's purpose-built ICmAgent.SetEvaluation(target, opinion). # The previous implementation built ICmAgentEvaluation manually # and added it to analysis.EvaluationsRC, but EvaluationsRC is a # reference collection (RC) -- adding to it doesn't confer # ownership on the new evaluation. ICmAgentEvaluation has to be # owned somewhere (it lives under CmAgent.ApprovesOA or in a # parallel disapprovals collection), and the manual code set # evaluation.Agent on a free-floating object before any owner # took it, which trips the Phase 2 NPE pattern. (issue #26) # # SetEvaluation handles ownership, opinion bookkeeping, and the # remove/replace cases internally. # # Coerce status from True/False/None (user-centric input) or # bare-int 0/1/2 (back-compat with the pre-IntEnum constants) # to the IntEnum members before dispatching. (issue #59) normalized = _coerce_approval_status(status) from SIL.LCModel import Opinions opinion_map = { ApprovalStatusTypes.APPROVED: Opinions.approves, ApprovalStatusTypes.DISAPPROVED: Opinions.disapproves, ApprovalStatusTypes.UNAPPROVED: Opinions.noopinion, } agent.SetEvaluation(analysis, opinion_map[normalized]) @OperationsMethod def IsHumanApproved(self, analysis_or_hvo): """ Check if an analysis is human-approved. Determines whether a human linguist has explicitly approved this analysis (as opposed to parser/computer approval). Args: analysis_or_hvo: Either an IWfiAnalysis object or its HVO Returns: bool: True if human-approved, False otherwise Raises: FP_NullParameterError: If analysis_or_hvo is None FP_ParameterError: If analysis doesn't exist Example: >>> wf = project.Wordforms.Find("running") >>> for analysis in project.WfiAnalyses.GetAll(wf): ... if project.WfiAnalyses.IsHumanApproved(analysis): ... print("Found human-approved analysis") ... glosses = project.WfiAnalyses.GetGlosses(analysis) ... print(f"Glosses: {glosses}") Notes: - Human approval takes precedence over computer approval - New analyses are not human-approved by default - Parser-generated analyses require human review - Used to distinguish manual vs. automatic analyses - Checks for evaluations with human agents and Accepted=True See Also: IsComputerApproved, ApproveAnalysis, GetApprovalStatus """ self._ValidateParam(analysis_or_hvo, "analysis_or_hvo") analysis = self.__GetAnalysisObject(analysis_or_hvo) # ICmAgentEvaluation always exposes Human (bool) and Approves # (bool) per LCM schema; the previous hasattr guards were dead # weight. GetApprovalStatus uses the bare property access # pattern. (issue #60) return any( e.Human and e.Approves for e in analysis.EvaluationsRC ) @OperationsMethod def IsComputerApproved(self, analysis_or_hvo): """ Check if an analysis is computer/parser-approved. Determines whether the parser has approved this analysis (as opposed to human approval). Args: analysis_or_hvo: Either an IWfiAnalysis object or its HVO Returns: bool: True if computer-approved, False otherwise Raises: FP_NullParameterError: If analysis_or_hvo is None FP_ParameterError: If analysis doesn't exist Example: >>> wf = project.Wordforms.Find("running") >>> for analysis in project.WfiAnalyses.GetAll(wf): ... if project.WfiAnalyses.IsComputerApproved(analysis): ... print("Parser-generated analysis") ... else: ... print("Needs parsing or manual creation") Notes: - Computer approval is from the morphological parser - Human approval typically overrides computer approval - Parser analyses may be approved automatically - Useful for identifying parser-generated content - Checks for evaluations with Human=False and Approves=True See Also: IsHumanApproved, GetAgentEvaluation, GetApprovalStatus """ self._ValidateParam(analysis_or_hvo, "analysis_or_hvo") analysis = self.__GetAnalysisObject(analysis_or_hvo) # ICmAgentEvaluation always exposes Human (bool) and Approves # (bool) per LCM schema; the previous hasattr guards were dead # weight. (issue #60) return any( not e.Human and e.Approves for e in analysis.EvaluationsRC ) @OperationsMethod def ApproveAnalysis(self, analysis_or_hvo): """ Approve an analysis (mark as human-approved). Sets the analysis to approved status, indicating that a human linguist has reviewed and approved this analysis. Args: analysis_or_hvo: Either an IWfiAnalysis object or its HVO Raises: FP_ReadOnlyError: If project is not opened with write enabled FP_NullParameterError: If analysis_or_hvo is None FP_ParameterError: If analysis doesn't exist Example: >>> wf = project.Wordforms.Find("running") >>> analysis = project.WfiAnalyses.Create(wf) >>> project.WfiAnalyses.AddGloss(analysis, "running", "en") >>> # Approve the analysis >>> project.WfiAnalyses.ApproveAnalysis(analysis) >>> # Verify >>> if project.WfiAnalyses.IsHumanApproved(analysis): ... print("Analysis is now approved") Notes: - This is a convenience method for SetApprovalStatus(APPROVED) - Marks the analysis as human-approved - Approved analyses are preferred in interlinear text display - Only one analysis per wordform should typically be approved - Creates CmAgentEvaluation with Accepted=True See Also: RejectAnalysis, SetApprovalStatus, IsHumanApproved """ self._EnsureWriteEnabled() self._ValidateParam(analysis_or_hvo, "analysis_or_hvo") analysis = self.__GetAnalysisObject(analysis_or_hvo) # Set to approved status self.SetApprovalStatus(analysis, ApprovalStatusTypes.APPROVED) @OperationsMethod def RejectAnalysis(self, analysis_or_hvo): """ Reject an analysis (mark as disapproved). Sets the analysis to disapproved status, indicating that this analysis is incorrect and should not be used. Args: analysis_or_hvo: Either an IWfiAnalysis object or its HVO Raises: FP_ReadOnlyError: If project is not opened with write enabled FP_NullParameterError: If analysis_or_hvo is None FP_ParameterError: If analysis doesn't exist Example: >>> wf = project.Wordforms.Find("running") >>> for analysis in project.WfiAnalyses.GetAll(wf): ... # Reject parser analyses that are incorrect ... if project.WfiAnalyses.IsComputerApproved(analysis): ... glosses = project.WfiAnalyses.GetGlosses(analysis) ... if "incorrect_gloss" in glosses: ... project.WfiAnalyses.RejectAnalysis(analysis) Notes: - This is a convenience method for SetApprovalStatus(DISAPPROVED) - Rejected analyses are typically hidden in FLEx UI - Use instead of Delete to preserve analysis history - Useful for marking parser errors - Creates CmAgentEvaluation with Accepted=False See Also: ApproveAnalysis, SetApprovalStatus, Delete """ self._EnsureWriteEnabled() self._ValidateParam(analysis_or_hvo, "analysis_or_hvo") analysis = self.__GetAnalysisObject(analysis_or_hvo) # Set to disapproved status self.SetApprovalStatus(analysis, ApprovalStatusTypes.DISAPPROVED) # --- Gloss Operations --- @wrap_enumerable @OperationsMethod def GetGlosses(self, analysis_or_hvo): """ Get all glosses for an analysis. Retrieves all IWfiGloss objects associated with this analysis, representing the meanings/translations of the analysis. Args: analysis_or_hvo: Either an IWfiAnalysis object or its HVO Returns: list: List of IWfiGloss objects Raises: FP_NullParameterError: If analysis_or_hvo is None FP_ParameterError: If analysis doesn't exist Example: >>> analysis = analyses[0] >>> glosses = project.WfiAnalyses.GetGlosses(analysis) >>> for gloss in glosses: ... # Access gloss properties ... print(f"Gloss HVO: {gloss.Hvo}") Notes: - Returns empty list if analysis has no glosses - Glosses are stored in the MeaningsOC collection - Each gloss can have text in multiple writing systems - Typically one gloss per analysis, but multiple are possible See Also: GetGlossCount, AddGloss """ self._ValidateParam(analysis_or_hvo, "analysis_or_hvo") analysis = self.__GetAnalysisObject(analysis_or_hvo) return list(analysis.MeaningsOC) @OperationsMethod def GetGlossCount(self, analysis_or_hvo): """ Get the count of glosses for an analysis. Args: analysis_or_hvo: Either an IWfiAnalysis object or its HVO Returns: int: Number of glosses in the analysis Raises: FP_NullParameterError: If analysis_or_hvo is None FP_ParameterError: If analysis doesn't exist Example: >>> analysis = analyses[0] >>> count = project.WfiAnalyses.GetGlossCount(analysis) >>> print(f"Analysis has {count} gloss(es)") Analysis has 1 gloss(es) Notes: - Returns 0 if analysis has no glosses - Most analyses have exactly one gloss - Multiple glosses may represent different writing systems See Also: GetGlosses, AddGloss """ self._ValidateParam(analysis_or_hvo, "analysis_or_hvo") analysis = self.__GetAnalysisObject(analysis_or_hvo) return analysis.MeaningsOC.Count @OperationsMethod def AddGloss(self, analysis_or_hvo, gloss_text, wsHandle=None): """ Add a gloss to an analysis. Creates a new IWfiGloss object with the specified text and adds it to the analysis's gloss collection. Args: analysis_or_hvo: Either an IWfiAnalysis object or its HVO gloss_text: The gloss text to add wsHandle: Optional writing system handle. Defaults to analysis WS. 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 gloss_text is None FP_ParameterError: If analysis doesn't exist or gloss_text is empty Example: >>> analysis = project.WfiAnalyses.Create(wf) >>> # Add English gloss >>> gloss = project.WfiAnalyses.AddGloss(analysis, "running", "en") >>> # Add French gloss >>> gloss_fr = project.WfiAnalyses.AddGloss(analysis, "courir", "fr") >>> # Verify >>> count = project.WfiAnalyses.GetGlossCount(analysis) >>> print(f"Analysis now has {count} glosses") Notes: - Gloss text is stored in the specified writing system - Multiple glosses can exist for different writing systems - Empty or whitespace-only gloss text raises error - Glosses are stored in the MeaningsOC collection See Also: GetGlosses, GetGlossCount """ self._EnsureWriteEnabled() self._ValidateParam(analysis_or_hvo, "analysis_or_hvo") self._ValidateParam(gloss_text, "gloss_text") if not gloss_text or not gloss_text.strip(): raise FP_ParameterError("Gloss text cannot be empty") analysis = self.__GetAnalysisObject(analysis_or_hvo) wsHandle = self.__WSHandle(wsHandle) with self._TransactionCM("Add gloss"): # Create the gloss using the factory factory = self.project.project.ServiceLocator.GetService(IWfiGlossFactory) new_gloss = factory.Create() # Add to analysis's meanings collection (must be done before setting properties) analysis.MeaningsOC.Add(new_gloss) # Set the gloss text mkstr = TsStringUtils.MakeString(gloss_text, wsHandle) new_gloss.Form.set_String(wsHandle, mkstr) return new_gloss # --- Morph Bundle Operations --- @wrap_enumerable @OperationsMethod def GetMorphBundles(self, analysis_or_hvo): """ Get all morpheme bundles for an analysis. Retrieves all IWfiMorphBundle objects representing the morphological breakdown of the wordform in this analysis. Args: analysis_or_hvo: Either an IWfiAnalysis object or its HVO Returns: list: List of IWfiMorphBundle objects in sequence order Raises: FP_NullParameterError: If analysis_or_hvo is None FP_ParameterError: If analysis doesn't exist Example: >>> analysis = analyses[0] >>> bundles = project.WfiAnalyses.GetMorphBundles(analysis) >>> for bundle in bundles: ... # Access bundle properties ... print(f"Morph bundle HVO: {bundle.Hvo}") Notes: - Returns empty list if analysis has no morph bundles - Bundles are ordered sequence (MorphBundlesOS) - Each bundle represents one morpheme in the analysis - Bundles include morph form, gloss, and grammatical info See Also: GetMorphBundleCount """ self._ValidateParam(analysis_or_hvo, "analysis_or_hvo") analysis = self.__GetAnalysisObject(analysis_or_hvo) return list(analysis.MorphBundlesOS) @OperationsMethod def GetMorphBundleCount(self, analysis_or_hvo): """ Get the count of morpheme bundles for an analysis. Args: analysis_or_hvo: Either an IWfiAnalysis object or its HVO Returns: int: Number of morph bundles in the analysis Raises: FP_NullParameterError: If analysis_or_hvo is None FP_ParameterError: If analysis doesn't exist Example: >>> analysis = analyses[0] >>> count = project.WfiAnalyses.GetMorphBundleCount(analysis) >>> print(f"Analysis has {count} morpheme(s)") Analysis has 2 morpheme(s) Notes: - Returns 0 if analysis has no morph bundles - Count represents the morphological complexity - Monomorphemic words typically have 1 bundle - Polymorphemic words have multiple bundles See Also: GetMorphBundles """ self._ValidateParam(analysis_or_hvo, "analysis_or_hvo") analysis = self.__GetAnalysisObject(analysis_or_hvo) return analysis.MorphBundlesOS.Count # --- Category Operations --- @OperationsMethod def GetCategory(self, analysis_or_hvo): """ Get the grammatical category (part of speech) for an analysis. Retrieves the IPartOfSpeech object representing the grammatical category assigned to this analysis. Args: analysis_or_hvo: Either an IWfiAnalysis object or its HVO Returns: IPartOfSpeech or None: The category object, or None if not set Raises: FP_NullParameterError: If analysis_or_hvo is None FP_ParameterError: If analysis doesn't exist Example: >>> analysis = analyses[0] >>> category = project.WfiAnalyses.GetCategory(analysis) >>> if category: ... name = project.POS.GetName(category) ... abbr = project.POS.GetAbbreviation(category) ... print(f"Category: {name} ({abbr})") Category: Verb (V) Notes: - Returns None if no category is set - Category represents the overall POS for the wordform - Individual morph bundles may have different categories - Use project.POS methods to access category properties See Also: SetCategory, GetMorphBundles """ self._ValidateParam(analysis_or_hvo, "analysis_or_hvo") analysis = self.__GetAnalysisObject(analysis_or_hvo) if hasattr(analysis, "CategoryRA") and analysis.CategoryRA: return analysis.CategoryRA return None @OperationsMethod def SetCategory(self, analysis_or_hvo, category): """ Set the grammatical category (part of speech) for an analysis. Assigns a grammatical category to the analysis, indicating the overall part of speech for the wordform. Args: analysis_or_hvo: Either an IWfiAnalysis object or its HVO category: IPartOfSpeech object to set as the category Raises: FP_ReadOnlyError: If project is not opened with write enabled FP_NullParameterError: If analysis_or_hvo or category is None FP_ParameterError: If analysis doesn't exist Example: >>> analysis = project.WfiAnalyses.Create(wf) >>> # Find or create a verb category >>> verb = project.POS.Find("Verb") >>> if verb: ... project.WfiAnalyses.SetCategory(analysis, verb) >>> # Verify >>> cat = project.WfiAnalyses.GetCategory(analysis) >>> print(project.POS.GetName(cat)) Verb Notes: - Category must be a valid IPartOfSpeech object - Use project.POS methods to find/create categories - Setting to None will clear the category - Category affects grammatical analysis display See Also: GetCategory, project.POS.Find, project.POS.Create """ self._EnsureWriteEnabled() self._ValidateParam(analysis_or_hvo, "analysis_or_hvo") self._ValidateParam(category, "category") analysis = self.__GetAnalysisObject(analysis_or_hvo) # Verify category is a valid IPartOfSpeech if not isinstance(category, IPartOfSpeech): raise FP_ParameterError("Category must be an IPartOfSpeech object") # Set the category if hasattr(analysis, "CategoryRA"): analysis.CategoryRA = category else: raise FP_ParameterError("Analysis does not support CategoryRA property") # --- Evaluation Operations --- @OperationsMethod def GetAgentEvaluation(self, analysis_or_hvo): """ Get the parser/agent evaluation for an analysis. Retrieves evaluation information created by the parser or other automated agents. Args: analysis_or_hvo: Either an IWfiAnalysis object or its HVO Returns: ICmAgentEvaluation or None: The agent evaluation object, or None if not set Raises: FP_NullParameterError: If analysis_or_hvo is None FP_ParameterError: If analysis doesn't exist Example: >>> analysis = analyses[0] >>> evaluation = project.WfiAnalyses.GetAgentEvaluation(analysis) >>> if evaluation: ... print("Analysis has parser evaluation") ... else: ... print("No parser evaluation") Notes: - Returns None if no agent evaluation exists - Agent evaluations are created by the parser - Multiple evaluations may exist in EvaluationsRC - This returns the first evaluation found See Also: GetHumanEvaluation, IsComputerApproved """ self._ValidateParam(analysis_or_hvo, "analysis_or_hvo") analysis = self.__GetAnalysisObject(analysis_or_hvo) # Return the first PARSER evaluation (Human == False). The # previous implementation returned EvaluationsRC[0], but # EvaluationsRC mixes human and parser evaluations and LCM # ordering is not deterministic -- so that path would # non-deterministically return a human evaluation when one # happened to be listed first. Mirrors the discriminating # iteration pattern used in GetApprovalStatus. (issue #55) for evaluation in analysis.EvaluationsRC: if not getattr(evaluation, "Human", True): return evaluation return None @OperationsMethod def GetHumanEvaluation(self, analysis_or_hvo): """ Get the human evaluation for an analysis. Retrieves evaluation information created by human linguists. Args: analysis_or_hvo: Either an IWfiAnalysis object or its HVO Returns: ICmAgentEvaluation or None: The human evaluation object, or None if not set Raises: FP_NullParameterError: If analysis_or_hvo is None FP_ParameterError: If analysis doesn't exist Example: >>> analysis = analyses[0] >>> evaluation = project.WfiAnalyses.GetHumanEvaluation(analysis) >>> if evaluation: ... print("Analysis has human evaluation") ... else: ... print("Analysis not yet reviewed by human") Notes: - Returns None if no human evaluation exists - Human evaluations indicate manual review - Searches EvaluationsRC for human agent - Human evaluation overrides parser evaluation See Also: GetAgentEvaluation, IsHumanApproved """ self._ValidateParam(analysis_or_hvo, "analysis_or_hvo") analysis = self.__GetAnalysisObject(analysis_or_hvo) # Return the first HUMAN evaluation (Human == True). The # previous implementation accepted any evaluation that exposed # an .Agent attribute, but every ICmAgentEvaluation has an # Agent -- the check did not actually discriminate. Mirrors # the iteration pattern used in GetApprovalStatus. (issue #55) for evaluation in analysis.EvaluationsRC: if getattr(evaluation, "Human", False): return evaluation return None @OperationsMethod def GetEvaluations(self, analysis_or_hvo): """ Get all agent evaluations for an analysis. Retrieves all ICmAgentEvaluation objects from the analysis's EvaluationsRC (Reference Collection). This is a read-only property that returns evaluations created by both human linguists and automated agents (e.g., morphological parser). Args: analysis_or_hvo: Either an IWfiAnalysis object or its HVO Returns: list: List of ICmAgentEvaluation objects. Returns empty list if no evaluations exist. Raises: FP_NullParameterError: If analysis_or_hvo is None FP_ParameterError: If analysis doesn't exist Example: >>> analysis = analyses[0] >>> evaluations = project.WfiAnalyses.GetEvaluations(analysis) >>> for evaluation in evaluations: ... if hasattr(evaluation, 'Agent') and evaluation.Agent: ... agent_name = evaluation.Agent.Name.BestAnalysisAlternative.Text ... is_accepted = evaluation.Accepted if hasattr(evaluation, 'Accepted') else None ... print(f"Agent: {agent_name}, Accepted: {is_accepted}") Notes: - This is a READ-ONLY property (Reference Collection) - Returns all evaluations (human and computer/parser) - Empty list indicates no evaluations have been created - Evaluations track approval/disapproval status - Each evaluation links to an ICmAgent (human or parser) - Use IsHumanApproved() or IsComputerApproved() for simple checks - Evaluations are added/removed via SetApprovalStatus() See Also: GetAgentEvaluation, GetHumanEvaluation, IsHumanApproved, IsComputerApproved, SetApprovalStatus """ self._ValidateParam(analysis_or_hvo, "analysis_or_hvo") analysis = self.__GetAnalysisObject(analysis_or_hvo) return list(analysis.EvaluationsRC) # --- Utility Operations --- @OperationsMethod def GetOwningWordform(self, analysis_or_hvo): """ Get the wordform that owns this analysis. Retrieves the IWfiWordform object that contains this analysis. Args: analysis_or_hvo: Either an IWfiAnalysis object or its HVO Returns: IWfiWordform: The owning wordform object Raises: FP_NullParameterError: If analysis_or_hvo is None FP_ParameterError: If analysis doesn't exist Example: >>> analysis = analyses[0] >>> wordform = project.WfiAnalyses.GetOwningWordform(analysis) >>> form = project.Wordforms.GetForm(wordform) >>> print(f"Analysis belongs to: {form}") Analysis belongs to: running Notes: - Every analysis must have an owning wordform - This is the inverse of GetAll(wordform) - Useful for navigating from analysis back to wordform See Also: GetAll, project.Wordforms.GetForm """ self._ValidateParam(analysis_or_hvo, "analysis_or_hvo") analysis = self.__GetAnalysisObject(analysis_or_hvo) # Cast to declared return type IWfiWordform. Raw analysis.Owner is # typed as ICmObject in LCM; pythonnet only surfaces IWfiWordform # properties (e.g. AnalysesOC, Form) after the explicit interface cast. return IWfiWordform(analysis.Owner) @OperationsMethod def GetGuid(self, analysis_or_hvo): """ Get the GUID (globally unique identifier) of an analysis. Retrieves the unique identifier for this analysis object. Args: analysis_or_hvo: Either an IWfiAnalysis object or its HVO Returns: System.Guid: The GUID of the analysis Raises: FP_NullParameterError: If analysis_or_hvo is None FP_ParameterError: If analysis doesn't exist Example: >>> analysis = analyses[0] >>> guid = project.WfiAnalyses.GetGuid(analysis) >>> print(f"Analysis GUID: {guid}") Analysis GUID: a1b2c3d4-e5f6-7890-abcd-ef1234567890 Notes: - GUID is a globally unique identifier - Persists across sessions - Useful for external references - Different from HVO (database handle) See Also: GetOwningWordform """ self._ValidateParam(analysis_or_hvo, "analysis_or_hvo") analysis = self.__GetAnalysisObject(analysis_or_hvo) return analysis.Guid