#
# 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):
"""
Reorder operations are not supported for wordform analyses.
``IWfiWordform.AnalysesOC`` is an unordered ``ILcmOwningCollection``;
there is no ``AnalysesOS`` and index-based reorder has no LCM meaning
(issue #301; see ``Duplicate()`` comments in this module).
"""
raise NotImplementedError(
"WfiAnalysisOperations does not support inherited Sort / MoveUp / "
"MoveDown / MoveToIndex: IWfiWordform.AnalysesOC is an unordered "
"ILcmOwningCollection with no user-visible order. Use LCM APIs "
"directly if you need to add or remove analyses."
)
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.
"""
# Casts by ClassName BEFORE returning (issue #275, generalising
# #269's fix): self.project.Object() returns a bare ICmObject, so
# isinstance(wf, IWfiWordform) is False even for a genuine
# wordform. Strict widening over the bare isinstance check.
if isinstance(wordform_or_hvo, int):
wf = self.project.Object(wordform_or_hvo)
if getattr(wf, "ClassName", None) == "WfiWordform":
try:
return IWfiWordform(wf)
except Exception:
pass
if isinstance(wf, IWfiWordform):
return wf
raise FP_ParameterError("HVO does not refer to a wordform")
if getattr(wordform_or_hvo, "ClassName", None) == "WfiWordform":
try:
return IWfiWordform(wordform_or_hvo)
except Exception:
pass
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.
"""
# Casts by ClassName BEFORE returning (issue #275, generalising
# #269's fix): self.project.Object() returns a bare ICmObject, so
# isinstance(analysis, IWfiAnalysis) is False even for a genuine
# analysis. Strict widening over the bare isinstance check.
if isinstance(analysis_or_hvo, int):
analysis = self.project.Object(analysis_or_hvo)
if getattr(analysis, "ClassName", None) == "WfiAnalysis":
try:
return IWfiAnalysis(analysis)
except Exception:
pass
if isinstance(analysis, IWfiAnalysis):
return analysis
raise FP_ParameterError("HVO does not refer to an analysis")
if getattr(analysis_or_hvo, "ClassName", None) == "WfiAnalysis":
try:
return IWfiAnalysis(analysis_or_hvo)
except Exception:
pass
return analysis_or_hvo
def __ResolveOwningAnalysis(self, analysis_or_hvo):
"""
Map any segment analysis token to its owning IWfiAnalysis.
``SegmentOperations.GetAnalyses`` yields polymorphic ``IAnalysis``
tokens. The owner-chasing needed to reach a usable ``IWfiAnalysis``
(for category / morph bundles) differs per concrete type, which is why
interlinear loops kept re-implementing it (issue #212):
- ``IWfiGloss`` -> its ``Owner`` is the ``IWfiAnalysis``.
- ``IWfiAnalysis`` -> itself (cast so typed members surface).
- ``IWfiWordform`` (unanalyzed) / ``IPunctuationForm`` -> no owning
analysis; returns ``None``.
Args:
analysis_or_hvo: An IAnalysis token or its HVO (int).
Returns:
IWfiAnalysis or None: The owning analysis, or None for token types
that have none.
"""
if isinstance(analysis_or_hvo, int):
obj = self.project.Object(analysis_or_hvo)
else:
obj = analysis_or_hvo
# Dispatch on ClassName: pythonnet isinstance() against the concrete
# IWfiGloss/IWfiAnalysis interfaces is False for the base IAnalysis-typed
# tokens GetAnalyses returns (issue #212). WfiGloss is checked first
# because its owner is the analysis we want, not the gloss itself.
class_name = getattr(obj, "ClassName", None)
if class_name == "WfiGloss":
return IWfiAnalysis(obj.Owner)
if class_name == "WfiAnalysis":
return IWfiAnalysis(obj)
return None
# --- 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[IWfiAnalysis]: All analyses 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, guid=None):
"""
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
guid (optional): GUID to assign to the new analysis, as a
``System.Guid`` or string. Use this when REPRODUCING a
analysis from another project so it keeps its original
identity. None (the default) mints a fresh GUID.
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 = self._CreateWithGuid(factory, guid, "IWfiAnalysis")
# 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 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, 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)
with self._TransactionCM("Delete analysis"):
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. IWfiMorphBundle has Form but
# NOT Gloss -- the displayed gloss comes from
# SenseRA.Gloss, preserved by the SenseRA copy below.
# (same root bug as #16/#107/#108, missed in this
# sibling site; WfiMorphBundleOperations.Duplicate
# already carries the fix.)
new_bundle.Form.CopyAlternatives(bundle.Form)
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
# No separate inflection-class copy needed: IWfiMorphBundle
# has no InflClassRA member (the guard above was always
# False; the dead body wrote a nonexistent
# `new_bundle.InflClassRA`). Inflection class lives on
# the MSA (IMoStemMsa.InflectionClassRA), and the MsaRA
# assignment above already copies that MSA REFERENCE, so
# new_bundle sees the same inflection class as bundle via
# the shared MSA. See get_inflection_class_from_msa() in
# lcm_casting.py (issue #259 / lcm-member-truth-sweep
# C10/T4.2).
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,
}
# SetEvaluation is an LCM mutator method (it creates/removes the
# evaluation object and rewires the agent's opinion collections), so
# it needs a unit of work like any other write. The status coercion
# and map lookup stay outside: an invalid status must raise before an
# undo task opens (D5/P3).
with self._TransactionCM("Set analysis approval status"):
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
@OperationsMethod
def GetMorphemeBundles(self, analysis_or_hvo):
"""
Get morpheme bundles for any segment analysis token (owner-resolving).
Unlike :meth:`GetMorphBundles` (which requires a concrete
``IWfiAnalysis``), this accepts the polymorphic ``IAnalysis`` tokens
that ``SegmentOperations.GetAnalyses`` returns and resolves the owning
``IWfiAnalysis`` automatically. This is the traversal helper interlinear
loops kept hand-rolling (issue #212).
Args:
analysis_or_hvo: An IAnalysis token (IWfiGloss / IWfiAnalysis /
IWfiWordform / IPunctuationForm) or its HVO.
Returns:
list: The IWfiMorphBundle objects for the owning analysis, in
sequence order. Empty list for token types with no owning analysis
(unanalyzed wordform, punctuation).
Raises:
FP_NullParameterError: If analysis_or_hvo is None.
Example:
>>> for tok in project.Segments.GetAnalyses(segment):
... for mb in project.WfiAnalyses.GetMorphemeBundles(tok):
... ... # no manual IWfiGloss -> Owner -> WfiAnalysis chase
See Also:
GetMorphBundles, GetCategoryAbbrev, SegmentOperations.GetGloss
"""
self._ValidateParam(analysis_or_hvo, "analysis_or_hvo")
analysis = self.__ResolveOwningAnalysis(analysis_or_hvo)
if analysis is None:
return []
return list(analysis.MorphBundlesOS)
# --- 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 GetCategoryAbbrev(self, analysis_or_hvo, wsHandle=None):
"""
Get the category (POS) abbreviation for any analysis token.
Owner-resolving convenience wrapper over :meth:`GetCategory` +
``project.POS.GetAbbreviation`` that accepts the polymorphic
``IAnalysis`` tokens from ``SegmentOperations.GetAnalyses`` -- an
``IWfiGloss`` resolves to its owning ``IWfiAnalysis`` automatically.
Saves interlinear loops from hand-rolling the
``gloss -> Owner -> WfiAnalysis -> CategoryRA -> Abbreviation`` chase
(issue #212).
Args:
analysis_or_hvo: An IAnalysis token or its HVO.
wsHandle: Optional writing system handle. Defaults to analysis WS.
Returns:
str: The POS abbreviation (e.g. "V", "N"), or '' when the token has
no owning analysis or no category assigned.
Raises:
FP_NullParameterError: If analysis_or_hvo is None.
Example:
>>> for tok in project.Segments.GetAnalyses(segment):
... abbr = project.WfiAnalyses.GetCategoryAbbrev(tok)
... print(abbr)
See Also:
GetCategory, GetMorphemeBundles, SegmentOperations.GetGloss
"""
self._ValidateParam(analysis_or_hvo, "analysis_or_hvo")
analysis = self.__ResolveOwningAnalysis(analysis_or_hvo)
if analysis is None:
return ""
category = analysis.CategoryRA
if category is None:
return ""
return self.project.POS.GetAbbreviation(category, wsHandle)
@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. The capability check stays OUTSIDE the transaction
# so an analysis without CategoryRA raises without opening an empty
# undo task.
if hasattr(analysis, "CategoryRA"):
with self._TransactionCM("Set analysis category"):
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