Source code for flexicon.code.System.CheckOperations

#
#   CheckOperations.py
#
#   Class: CheckOperations
#          Consistency check and validation operations for FieldWorks Language
#          Explorer projects via SIL Language and Culture Model (LCM) API.
#
#   Platform: Python.NET
#             FieldWorks Version 9+
#
#   Copyright 2025
#

import logging

logger = logging.getLogger(__name__)

import clr

clr.AddReference("System")
import System
from datetime import datetime

# Import FLEx LCM types
from SIL.LCModel import (
    ICmPossibility,
    ICmPossibilityFactory,
    ICmPossibilityList,
    ILexEntry,
    IWfiWordform,
    IText,
)
from SIL.LCModel.Core.KernelInterfaces import ITsString
from SIL.LCModel.Core.Text import TsStringUtils

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

# Import string utilities
from ..Shared.string_utils import normalize_text, normalize_match_key


[docs] class CheckOperations(BaseOperations): """ This class provides operations for managing consistency checks and validation in a FieldWorks project. Consistency checks help identify data quality issues, missing information, and inconsistencies in lexical entries, texts, and other project data. Check types are managed as possibility list items. This class should be accessed via FLExProject.Checks property. Usage:: from flexlibs2 import FLExProject project = FLExProject() project.OpenProject("my project", writeEnabled=True) # Get all check types for check_type in project.Checks.GetAllCheckTypes(): name = project.Checks.GetName(check_type) enabled = project.Checks.IsEnabled(check_type) print(f"{name}: {'Enabled' if enabled else 'Disabled'}") # Create a custom check type check = project.Checks.CreateCheckType("Missing Gloss Check") project.Checks.SetDescription(check, "Verify all senses have glosses") project.Checks.EnableCheck(check) # Run a check and get results project.Checks.RunCheck(check) errors = project.Checks.GetErrorCount(check) warnings = project.Checks.GetWarningCount(check) print(f"Check found {errors} errors and {warnings} warnings") # Get items with issues items = project.Checks.FindItemsWithIssues(check) for item in items: issues = project.Checks.GetIssuesForObject(check, item) print(f"Issues: {len(issues)}") project.CloseProject() """ def __init__(self, project): """ Initialize CheckOperations with a FLExProject instance. Args: project: The FLExProject instance to operate on. """ super().__init__(project) # Cache for check execution results self._check_results = {} self._check_last_run = {} self._check_enabled = {} # --- Core CRUD Operations --- @OperationsMethod def GetAllCheckTypes(self): """ Get all consistency check types in the project. Returns all check types that are available for validation and consistency checking. Yields: ICmPossibility: Each check type object Example: >>> for check_type in project.Checks.GetAllCheckTypes(): ... name = project.Checks.GetName(check_type) ... desc = project.Checks.GetDescription(check_type) ... enabled = project.Checks.IsEnabled(check_type) ... print(f"{name}: {desc} ({'Enabled' if enabled else 'Disabled'})") Missing Gloss: Check for senses without glosses (Enabled) Duplicate Entries: Find potential duplicate lexical entries (Disabled) Incomplete Paradigm: Verify paradigm completeness (Enabled) Notes: - Check types are stored as possibility list items - Custom check types can be created for project-specific needs - Each check type can be enabled or disabled independently - Check types persist across sessions See Also: CreateCheckType, FindCheckType, GetEnabledChecks """ check_list = self._GetCheckList() if check_list: for check_type in check_list.PossibilitiesOS: yield check_type # Also yield nested check types for check_type in check_list.PossibilitiesOS: for subcheck in check_type.SubPossibilitiesOS: yield subcheck @OperationsMethod def CreateCheckType(self, name, description=None, wsHandle=None): """ Create a new consistency check type. Creates a new check type that can be used to validate project data. The check type is added to the project's check possibility list. Args: name (str): The name of the check type. Must be unique and non-empty. description (str, optional): Description of what the check validates. Defaults to None (empty description). wsHandle: Optional writing system handle. Defaults to analysis WS. Returns: ICmPossibility: The newly created check type object. Raises: FP_ReadOnlyError: If project was not opened with writeEnabled=True. FP_NullParameterError: If name is None or empty. FP_ParameterError: If a check type with this name already exists. Example: >>> # Create a simple check type >>> check = project.Checks.CreateCheckType("Missing Etymology Check") >>> print(project.Checks.GetName(check)) Missing Etymology Check >>> # Create with description >>> check = project.Checks.CreateCheckType( ... "Incomplete Paradigm", ... "Verify all paradigm slots are filled") >>> print(project.Checks.GetDescription(check)) Verify all paradigm slots are filled >>> # Enable the check >>> project.Checks.EnableCheck(check) Notes: - Check is created but not enabled by default - Use EnableCheck() to activate the check - The check type persists across sessions - Custom logic for running the check must be implemented separately See Also: DeleteCheckType, GetAllCheckTypes, EnableCheck """ self._EnsureWriteEnabled() self._ValidateParam(name, "name") name = name.strip() if isinstance(name, str) else "" self._ValidateParam(name, "name") # Check if check type with this name already exists if self.FindCheckType(name): raise FP_ParameterError(f"A check type with the name '{name}' already exists") wsHandle = self.__WSHandle(wsHandle) # Get or create the check possibility list check_list = self._GetOrCreateCheckList() # Create the new check type using the factory factory = self.project.project.ServiceLocator.GetService(ICmPossibilityFactory) with self._TransactionCM(f"Create check type '{name}'"): new_check = factory.Create() # Add to check list (must be done before setting properties) check_list.PossibilitiesOS.Add(new_check) # Set name mkstr = TsStringUtils.MakeString(name, wsHandle) new_check.Name.set_String(wsHandle, mkstr) # Set description if provided if description: desc_str = TsStringUtils.MakeString(description, wsHandle) new_check.Description.set_String(wsHandle, desc_str) # Initialize as disabled by default self._check_enabled[new_check.Guid] = False return new_check @OperationsMethod def DeleteCheckType(self, check_or_hvo): """ Delete a consistency check type from the project. Removes the check type and all associated results and configuration. Args: check_or_hvo: Either an ICmPossibility check type object or its HVO (integer identifier). Raises: FP_ReadOnlyError: If project was not opened with writeEnabled=True. FP_NullParameterError: If check_or_hvo is None. FP_ParameterError: If the check type does not exist or is invalid. Example: >>> # Delete by object >>> check = project.Checks.FindCheckType("Old Check") >>> if check: ... project.Checks.DeleteCheckType(check) >>> # Delete by HVO >>> project.Checks.DeleteCheckType(check_hvo) Warning: - Deletion is permanent and cannot be undone - All check results and configuration are deleted - Only delete custom check types, not system checks Notes: - Removes check from the possibility list - Clears cached results for this check - Cannot be undone See Also: CreateCheckType, GetAllCheckTypes """ self._EnsureWriteEnabled() check_obj = self.__GetCheckObject(check_or_hvo) # Remove from check list check_list = self._GetCheckList() if check_list: # Try to remove from top level if check_obj in check_list.PossibilitiesOS: check_list.PossibilitiesOS.Remove(check_obj) else: # Try to remove from parent's subitems for parent in check_list.PossibilitiesOS: if check_obj in parent.SubPossibilitiesOS: parent.SubPossibilitiesOS.Remove(check_obj) break # Clear cached data guid = check_obj.Guid if guid in self._check_results: del self._check_results[guid] if guid in self._check_last_run: del self._check_last_run[guid] if guid in self._check_enabled: del self._check_enabled[guid] @OperationsMethod def FindCheckType(self, name): """ Find a check type by its name. Searches for a check type with the specified name in the project's check possibility list. Args: name (str): The check type name to search for (case-insensitive). Returns: ICmPossibility or None: The check type object if found, None otherwise. Raises: FP_NullParameterError: If name is None or empty. Example: >>> # Find a check type >>> check = project.Checks.FindCheckType("Missing Gloss") >>> if check: ... status = project.Checks.GetCheckStatus(check) ... print(f"Status: {status}") Status: Ready >>> # Case-insensitive search >>> check = project.Checks.FindCheckType("missing gloss") >>> print(check is not None) True Notes: - Search is case-insensitive - Searches in default analysis writing system - Returns first match only - Returns None if not found (doesn't raise exception) - Searches recursively through nested check types See Also: GetAllCheckTypes, CreateCheckType, GetName """ self._ValidateParam(name, "name") name = name.strip() if isinstance(name, str) else "" self._ValidateParam(name, "name") target = normalize_match_key(name, casefold=True) wsHandle = self.project.project.DefaultAnalWs # Search through all check types for check_type in self.GetAllCheckTypes(): check_name = ITsString(check_type.Name.get_String(wsHandle)).Text if check_name and normalize_match_key(check_name, casefold=True) == target: return check_type return None # --- Property Methods --- @OperationsMethod def GetName(self, check_or_hvo, wsHandle=None): """ Get the name of a check type. Retrieves the check type name in the specified writing system. Args: check_or_hvo: Either an ICmPossibility check type object or its HVO (integer identifier). wsHandle: Optional writing system handle. Defaults to analysis WS. Returns: str: The name of the check type in the specified writing system. Returns empty string if no name is set. Raises: FP_NullParameterError: If check_or_hvo is None. FP_ParameterError: If the check type does not exist or is invalid. Example: >>> check = project.Checks.FindCheckType("Missing Gloss") >>> name = project.Checks.GetName(check) >>> print(name) Missing Gloss >>> # Get name in specific WS >>> ws_handle = project.WSHandle('fr') >>> name_fr = project.Checks.GetName(check, ws_handle) See Also: SetName, GetDescription, FindCheckType """ check_obj = self.__GetCheckObject(check_or_hvo) wsHandle = self.__WSHandle(wsHandle) name = ITsString(check_obj.Name.get_String(wsHandle)).Text return name or "" @OperationsMethod def SetName(self, check_or_hvo, name, wsHandle=None): """ Set the name of a check type. Sets the check type name in the specified writing system. Args: check_or_hvo: Either an ICmPossibility check type object or its HVO (integer identifier). name (str): The new name for the check type. Must be non-empty. wsHandle: Optional writing system handle. Defaults to analysis WS. Raises: FP_ReadOnlyError: If project was not opened with writeEnabled=True. FP_NullParameterError: If check_or_hvo or name is None/empty. FP_ParameterError: If the check type does not exist or is invalid. Example: >>> check = project.Checks.FindCheckType("Old Name") >>> if check: ... project.Checks.SetName(check, "Updated Check Name") ... print(project.Checks.GetName(check)) Updated Check Name >>> # Set name in specific WS >>> ws_handle = project.WSHandle('fr') >>> project.Checks.SetName(check, "Vérification manquante", ws_handle) See Also: GetName, SetDescription """ self._EnsureWriteEnabled() self._ValidateParam(name, "name") name = name.strip() if isinstance(name, str) else "" self._ValidateParam(name, "name") check_obj = self.__GetCheckObject(check_or_hvo) wsHandle = self.__WSHandle(wsHandle) mkstr = TsStringUtils.MakeString(name, wsHandle) check_obj.Name.set_String(wsHandle, mkstr) @OperationsMethod def GetDescription(self, check_or_hvo, wsHandle=None): """ Get the description of a check type. Retrieves the detailed description explaining what the check validates. Args: check_or_hvo: Either an ICmPossibility check type object or its HVO (integer identifier). wsHandle: Optional writing system handle. Defaults to analysis WS. Returns: str: The description of the check type. Returns empty string if no description is set. Raises: FP_NullParameterError: If check_or_hvo is None. FP_ParameterError: If the check type does not exist or is invalid. Example: >>> check = project.Checks.FindCheckType("Missing Gloss") >>> desc = project.Checks.GetDescription(check) >>> print(desc) Identifies lexical senses that are missing glosses in the analysis writing system See Also: SetDescription, GetName """ check_obj = self.__GetCheckObject(check_or_hvo) wsHandle = self.__WSHandle(wsHandle) desc = ITsString(check_obj.Description.get_String(wsHandle)).Text return desc or "" @OperationsMethod def SetDescription(self, check_or_hvo, description, wsHandle=None): """ Set the description of a check type. Sets a detailed description explaining what the check validates. Args: check_or_hvo: Either an ICmPossibility check type object or its HVO (integer identifier). description (str): The new description text. wsHandle: Optional writing system handle. Defaults to analysis WS. Raises: FP_ReadOnlyError: If project was not opened with writeEnabled=True. FP_NullParameterError: If check_or_hvo or description is None. FP_ParameterError: If the check type does not exist or is invalid. Example: >>> check = project.Checks.FindCheckType("Missing Etymology") >>> project.Checks.SetDescription( ... check, ... "Identifies entries without etymology information") >>> print(project.Checks.GetDescription(check)) Identifies entries without etymology information See Also: GetDescription, SetName """ self._EnsureWriteEnabled() self._ValidateParam(description, "description") check_obj = self.__GetCheckObject(check_or_hvo) wsHandle = self.__WSHandle(wsHandle) mkstr = TsStringUtils.MakeString(description, wsHandle) check_obj.Description.set_String(wsHandle, mkstr) # --- Execution Methods --- @OperationsMethod def RunCheck(self, check_or_hvo, target_objects=None): """ Run a consistency check on project data. Executes the specified check and stores the results. The actual validation logic is determined by the check type. Args: check_or_hvo: Either an ICmPossibility check type object or its HVO (integer identifier). target_objects (list, optional): List of specific objects to check. If None, checks all relevant objects in the project. Defaults to None. Returns: dict: Results dictionary containing: - 'errors': list of error items - 'warnings': list of warning items - 'passed': list of items that passed - 'timestamp': datetime when check was run Raises: FP_NullParameterError: If check_or_hvo is None. FP_ParameterError: If the check type does not exist, is invalid, or is not enabled. Example: >>> # Run check on all data >>> check = project.Checks.FindCheckType("Missing Gloss") >>> project.Checks.EnableCheck(check) >>> results = project.Checks.RunCheck(check) >>> print(f"Errors: {len(results['errors'])}") Errors: 15 >>> print(f"Warnings: {len(results['warnings'])}") Warnings: 3 >>> # Run check on specific entries >>> entries = list(project.LexiconGetAllEntries())[:10] >>> results = project.Checks.RunCheck(check, entries) Notes: - Check must be enabled before running - Results are cached until next run - Timestamp records when check was executed - Validation logic determined by check type name - Supports: Missing Gloss, Missing Definition, Missing Example, Missing Grammatical Info, Duplicate Entries, Missing Etymology - Unknown check types will mark all objects as passed See Also: GetCheckStatus, GetCheckResults, GetErrorCount, EnableCheck """ check_obj = self.__GetCheckObject(check_or_hvo) # Verify check is enabled if not self.IsEnabled(check_obj): raise FP_ParameterError("Check must be enabled before running") # Initialize results results = {"errors": [], "warnings": [], "passed": [], "timestamp": datetime.now()} # Get target objects if not specified if target_objects is None: target_objects = self._GetDefaultCheckTargets(check_obj) # Run the check (placeholder - actual implementation would vary by check type) results = self._ExecuteCheck(check_obj, target_objects) # Cache results guid = check_obj.Guid self._check_results[guid] = results self._check_last_run[guid] = results["timestamp"] return results @OperationsMethod def GetCheckStatus(self, check_or_hvo): """ Get the current status of a check type. Returns a status indicating whether the check is ready to run, running, completed, or has issues. Args: check_or_hvo: Either an ICmPossibility check type object or its HVO (integer identifier). Returns: str: Status string, one of: - 'Ready': Check is enabled and ready to run - 'Disabled': Check is not enabled - 'Completed': Check has been run and results are available - 'Never Run': Check has never been executed Raises: FP_NullParameterError: If check_or_hvo is None. FP_ParameterError: If the check type does not exist or is invalid. Example: >>> check = project.Checks.FindCheckType("Missing Gloss") >>> status = project.Checks.GetCheckStatus(check) >>> print(f"Status: {status}") Status: Ready >>> # Run check and check status >>> project.Checks.RunCheck(check) >>> status = project.Checks.GetCheckStatus(check) >>> print(status) Completed See Also: RunCheck, GetLastRun, IsEnabled """ check_obj = self.__GetCheckObject(check_or_hvo) guid = check_obj.Guid if not self.IsEnabled(check_obj): return "Disabled" elif guid in self._check_results: return "Completed" elif guid in self._check_last_run: return "Completed" else: return "Never Run" @OperationsMethod def GetLastRun(self, check_or_hvo): """ Get the timestamp when a check was last executed. Returns the date and time when the check was most recently run. Args: check_or_hvo: Either an ICmPossibility check type object or its HVO (integer identifier). Returns: datetime or None: Timestamp of last execution, or None if the check has never been run. Raises: FP_NullParameterError: If check_or_hvo is None. FP_ParameterError: If the check type does not exist or is invalid. Example: >>> check = project.Checks.FindCheckType("Missing Gloss") >>> last_run = project.Checks.GetLastRun(check) >>> if last_run: ... print(f"Last run: {last_run.strftime('%Y-%m-%d %H:%M:%S')}") ... else: ... print("Never run") Last run: 2025-11-23 14:30:45 See Also: RunCheck, GetCheckStatus """ check_obj = self.__GetCheckObject(check_or_hvo) guid = check_obj.Guid return self._check_last_run.get(guid, None) # --- Results Methods --- @OperationsMethod def GetCheckResults(self, check_or_hvo): """ Get the full results from the last check execution. Retrieves all results including errors, warnings, and passed items from the most recent run of the check. Args: check_or_hvo: Either an ICmPossibility check type object or its HVO (integer identifier). Returns: dict or None: Results dictionary containing: - 'errors': list of error items - 'warnings': list of warning items - 'passed': list of items that passed - 'timestamp': datetime when check was run Returns None if check has never been run. Raises: FP_NullParameterError: If check_or_hvo is None. FP_ParameterError: If the check type does not exist or is invalid. Example: >>> check = project.Checks.FindCheckType("Missing Gloss") >>> project.Checks.RunCheck(check) >>> results = project.Checks.GetCheckResults(check) >>> if results: ... print(f"Errors: {len(results['errors'])}") ... print(f"Warnings: {len(results['warnings'])}") ... print(f"Passed: {len(results['passed'])}") Errors: 15 Warnings: 3 Passed: 142 See Also: RunCheck, GetErrorCount, GetWarningCount """ check_obj = self.__GetCheckObject(check_or_hvo) guid = check_obj.Guid return self._check_results.get(guid, None) @OperationsMethod def GetErrorCount(self, check_or_hvo): """ Get the number of errors found by a check. Returns the count of items that failed the check with errors from the most recent execution. Args: check_or_hvo: Either an ICmPossibility check type object or its HVO (integer identifier). Returns: int: Number of errors found. Returns 0 if check has never been run. Raises: FP_NullParameterError: If check_or_hvo is None. FP_ParameterError: If the check type does not exist or is invalid. Example: >>> check = project.Checks.FindCheckType("Missing Gloss") >>> project.Checks.RunCheck(check) >>> error_count = project.Checks.GetErrorCount(check) >>> print(f"Found {error_count} errors") Found 15 errors See Also: GetWarningCount, GetCheckResults, RunCheck """ check_obj = self.__GetCheckObject(check_or_hvo) guid = check_obj.Guid results = self._check_results.get(guid, None) if results: return len(results.get("errors", [])) return 0 @OperationsMethod def GetWarningCount(self, check_or_hvo): """ Get the number of warnings found by a check. Returns the count of items with warnings from the most recent check execution. Args: check_or_hvo: Either an ICmPossibility check type object or its HVO (integer identifier). Returns: int: Number of warnings found. Returns 0 if check has never been run. Raises: FP_NullParameterError: If check_or_hvo is None. FP_ParameterError: If the check type does not exist or is invalid. Example: >>> check = project.Checks.FindCheckType("Missing Gloss") >>> project.Checks.RunCheck(check) >>> warning_count = project.Checks.GetWarningCount(check) >>> print(f"Found {warning_count} warnings") Found 3 warnings See Also: GetErrorCount, GetCheckResults, RunCheck """ check_obj = self.__GetCheckObject(check_or_hvo) guid = check_obj.Guid results = self._check_results.get(guid, None) if results: return len(results.get("warnings", [])) return 0 # --- Filter Methods --- @OperationsMethod def GetEnabledChecks(self): """ Get all enabled check types. Returns only those check types that are currently enabled and ready to run. Yields: ICmPossibility: Each enabled check type object Example: >>> # List enabled checks >>> for check in project.Checks.GetEnabledChecks(): ... name = project.Checks.GetName(check) ... print(f"Enabled: {name}") Enabled: Missing Gloss Enabled: Incomplete Paradigm Enabled: Duplicate Entries >>> # Count enabled checks >>> enabled_count = len(list(project.Checks.GetEnabledChecks())) >>> print(f"Total enabled: {enabled_count}") Total enabled: 3 Notes: - Only returns checks that have been explicitly enabled - Disabled checks are excluded - Useful for batch operations on active checks See Also: GetAllCheckTypes, EnableCheck, DisableCheck, IsEnabled """ for check_type in self.GetAllCheckTypes(): if self.IsEnabled(check_type): yield check_type @OperationsMethod def EnableCheck(self, check_or_hvo): """ Enable a consistency check type. Activates the check so it can be run on project data. Args: check_or_hvo: Either an ICmPossibility check type object or its HVO (integer identifier). Raises: FP_ReadOnlyError: If project was not opened with writeEnabled=True. FP_NullParameterError: If check_or_hvo is None. FP_ParameterError: If the check type does not exist or is invalid. Example: >>> check = project.Checks.FindCheckType("Missing Gloss") >>> project.Checks.EnableCheck(check) >>> print(project.Checks.IsEnabled(check)) True >>> # Run the enabled check >>> results = project.Checks.RunCheck(check) See Also: DisableCheck, IsEnabled, GetEnabledChecks """ self._EnsureWriteEnabled() check_obj = self.__GetCheckObject(check_or_hvo) guid = check_obj.Guid self._check_enabled[guid] = True @OperationsMethod def DisableCheck(self, check_or_hvo): """ Disable a consistency check type. Deactivates the check so it cannot be run. Previous results are retained. Args: check_or_hvo: Either an ICmPossibility check type object or its HVO (integer identifier). Raises: FP_ReadOnlyError: If project was not opened with writeEnabled=True. FP_NullParameterError: If check_or_hvo is None. FP_ParameterError: If the check type does not exist or is invalid. Example: >>> check = project.Checks.FindCheckType("Missing Gloss") >>> project.Checks.DisableCheck(check) >>> print(project.Checks.IsEnabled(check)) False >>> # Previous results still accessible >>> results = project.Checks.GetCheckResults(check) See Also: EnableCheck, IsEnabled, GetEnabledChecks """ self._EnsureWriteEnabled() check_obj = self.__GetCheckObject(check_or_hvo) guid = check_obj.Guid self._check_enabled[guid] = False @OperationsMethod def IsEnabled(self, check_or_hvo): """ Check if a consistency check type is enabled. Returns whether the check is currently active and can be run. Args: check_or_hvo: Either an ICmPossibility check type object or its HVO (integer identifier). Returns: bool: True if check is enabled, False otherwise. Raises: FP_NullParameterError: If check_or_hvo is None. FP_ParameterError: If the check type does not exist or is invalid. Example: >>> check = project.Checks.FindCheckType("Missing Gloss") >>> if project.Checks.IsEnabled(check): ... print("Check is active") ... else: ... print("Check is disabled") Check is active See Also: EnableCheck, DisableCheck, GetEnabledChecks """ check_obj = self.__GetCheckObject(check_or_hvo) guid = check_obj.Guid return self._check_enabled.get(guid, False) # --- Query Methods --- @OperationsMethod def FindItemsWithIssues(self, check_or_hvo, issue_type="all"): """ Find all items that have issues according to a check. Returns items that failed the check, either with errors, warnings, or both, from the most recent check execution. Args: check_or_hvo: Either an ICmPossibility check type object or its HVO (integer identifier). issue_type (str): Type of issues to return. Options: - 'all': Both errors and warnings (default) - 'errors': Only errors - 'warnings': Only warnings Returns: list: List of objects that have the specified issues. Returns empty list if check has never been run or no issues found. Raises: FP_NullParameterError: If check_or_hvo is None. FP_ParameterError: If the check type does not exist, is invalid, or issue_type is not recognized. Example: >>> check = project.Checks.FindCheckType("Missing Gloss") >>> project.Checks.RunCheck(check) >>> >>> # Get all items with issues >>> items = project.Checks.FindItemsWithIssues(check) >>> print(f"Total items with issues: {len(items)}") Total items with issues: 18 >>> >>> # Get only items with errors >>> error_items = project.Checks.FindItemsWithIssues(check, 'errors') >>> print(f"Items with errors: {len(error_items)}") Items with errors: 15 >>> >>> # Get only items with warnings >>> warning_items = project.Checks.FindItemsWithIssues(check, 'warnings') >>> print(f"Items with warnings: {len(warning_items)}") Items with warnings: 3 Notes: - Returns items from most recent check execution only - Returns empty list if check has never been run - issue_type parameter controls filtering of results See Also: GetIssuesForObject, RunCheck, GetCheckResults """ check_obj = self.__GetCheckObject(check_or_hvo) guid = check_obj.Guid if issue_type not in ["all", "errors", "warnings"]: raise FP_ParameterError("issue_type must be 'all', 'errors', or 'warnings'") results = self._check_results.get(guid, None) if not results: return [] items = [] if issue_type in ["all", "errors"]: items.extend(results.get("errors", [])) if issue_type in ["all", "warnings"]: items.extend(results.get("warnings", [])) return items @OperationsMethod def GetIssuesForObject(self, check_or_hvo, obj): """ Get all issues found for a specific object. Returns detailed information about issues found for the given object during the most recent check execution. Args: check_or_hvo: Either an ICmPossibility check type object or its HVO (integer identifier). obj: The object to get issues for (e.g., ILexEntry, IText, etc.). Returns: list: List of issue descriptions for the object. Returns empty list if object has no issues or check has never been run. Raises: FP_NullParameterError: If check_or_hvo or obj is None. FP_ParameterError: If the check type does not exist or is invalid. Example: >>> check = project.Checks.FindCheckType("Missing Gloss") >>> project.Checks.RunCheck(check) >>> >>> # Get issues for a specific entry >>> entry = list(project.LexiconGetAllEntries())[0] >>> issues = project.Checks.GetIssuesForObject(check, entry) >>> for issue in issues: ... print(f"- {issue}") - Sense 1 is missing gloss in English - Sense 2 is missing gloss in English Notes: - Returns issues from most recent check execution only - Returns empty list if object passed the check - Issue descriptions are human-readable strings - Useful for displaying issues in UI or reports See Also: FindItemsWithIssues, RunCheck, GetCheckResults """ self._ValidateParam(obj, "obj") check_obj = self.__GetCheckObject(check_or_hvo) guid = check_obj.Guid results = self._check_results.get(guid, None) if not results: return [] # Search for object in errors and warnings issues = [] for item in results.get("errors", []): if hasattr(item, "Hvo") and hasattr(obj, "Hvo"): if item.Hvo == obj.Hvo: issues.append(f"Error: {self._GetIssueDescription(item)}") for item in results.get("warnings", []): if hasattr(item, "Hvo") and hasattr(obj, "Hvo"): if item.Hvo == obj.Hvo: issues.append(f"Warning: {self._GetIssueDescription(item)}") return issues # --- Metadata Methods --- @OperationsMethod def GetGuid(self, check_or_hvo): """ Get the GUID of a check type. Returns the globally unique identifier for the check type. Args: check_or_hvo: Either an ICmPossibility check type object or its HVO (integer identifier). Returns: System.Guid: The check type's globally unique identifier. Raises: FP_NullParameterError: If check_or_hvo is None. FP_ParameterError: If the check type does not exist or is invalid. Example: >>> check = project.Checks.FindCheckType("Missing Gloss") >>> guid = project.Checks.GetGuid(check) >>> print(guid) a23b6fcc-654c-4983-a11c-5e4e15e1f6e9 >>> # Use GUID to retrieve check later >>> retrieved = project.Object(guid) >>> name = project.Checks.GetName(retrieved) >>> print(name) Missing Gloss Notes: - GUID is stable across sessions - GUID is unique across all projects - Use GUID for persistent references - GUID can be used with project.Object(guid) to retrieve the check See Also: FindCheckType, GetName """ check_obj = self.__GetCheckObject(check_or_hvo) return check_obj.Guid # --- Private Helper Methods --- def __GetCheckObject(self, check_or_hvo): """ Internal function to get ICmPossibility check object from check_or_hvo. Args: check_or_hvo: Either an ICmPossibility object or its HVO (integer). Returns: ICmPossibility: The check object. Raises: FP_NullParameterError: If check_or_hvo is None. FP_ParameterError: If the object doesn't exist or isn't a valid check. """ self._ValidateParam(check_or_hvo, "check_or_hvo") try: # If it's an integer, get the object if isinstance(check_or_hvo, int): obj = self.project.Object(check_or_hvo) return ICmPossibility(obj) else: # Already a possibility object return ICmPossibility(check_or_hvo) except Exception: raise FP_ParameterError(f"Invalid check type object or HVO: {check_or_hvo}") def __WSHandle(self, wsHandle): """ Get writing system handle, defaulting to analysis WS. Args: wsHandle: Optional writing system handle. Returns: int: The writing system handle. """ if wsHandle is None: return self.project.project.DefaultAnalWs return self.project._FLExProject__WSHandle(wsHandle, self.project.project.DefaultAnalWs) def _GetCheckList(self): """ Get the check possibility list. Returns: ICmPossibilityList or None: The check list if it exists, None otherwise. """ # Check if there's a dedicated check list # In FLEx, this might be stored in a specific location # For now, return None if not found # Implementation would depend on actual FLEx data model return None def _GetOrCreateCheckList(self): """ Get or create the check possibility list. Returns: ICmPossibilityList: The check list. """ check_list = self._GetCheckList() if check_list: return check_list # Create a new check list # Note: This is a simplified implementation # Actual implementation would depend on FLEx data model from SIL.LCModel import ICmPossibilityListFactory factory = self.project.project.ServiceLocator.GetInstance(ICmPossibilityListFactory) new_list = factory.Create() wsHandle = self.project.project.DefaultAnalWs name_str = TsStringUtils.MakeString("Consistency Checks", wsHandle) new_list.Name.set_String(wsHandle, name_str) return new_list def _GetDefaultCheckTargets(self, check_obj): """ Get default target objects for a check. Args: check_obj: The check type ICmPossibility object. Returns: list: List of default objects to check. """ # Default to all lexical entries # Actual implementation would vary based on check type return list(self.project.LexiconGetAllEntries()) def _ExecuteCheck(self, check_obj, target_objects): """ Execute the actual check logic. Args: check_obj: The check type ICmPossibility object. target_objects: List of objects to check. Returns: dict: Results dictionary with errors, warnings, and passed items. Notes: Implements common FLEx consistency checks: - Missing Gloss (ERROR): Senses without glosses - Missing Definition (WARNING): Senses without definitions - Missing Example (WARNING): Senses without examples - Missing Grammatical Info (ERROR): Senses without POS - Duplicate Entries (WARNING): Entries with same lexeme form - Missing Etymology (WARNING): Entries without etymology """ results = {"errors": [], "warnings": [], "passed": [], "timestamp": datetime.now()} # Get check name - use exact matching (Craig's pattern) check_name = self.GetName(check_obj) wsHandle = self.project.project.DefaultAnalWs # Perform check based on exact check type name for obj in target_objects: try: # Ensure we're working with ILexEntry if not isinstance(obj, ILexEntry): try: obj = ILexEntry(obj) except TypeError: # Object is not a lexical entry - skip it results["passed"].append(obj) continue entry = obj has_issue = False # Use exact name matching for check types (Craig's pattern) if check_name == "Missing Gloss": # ERROR: Senses without glosses for sense in entry.SensesOS: gloss_text = ITsString(sense.Gloss.get_String(wsHandle)).Text if not gloss_text or not gloss_text.strip(): results["errors"].append(entry) has_issue = True break elif check_name == "Missing Definition": # WARNING: Senses without definitions for sense in entry.SensesOS: def_text = ITsString(sense.Definition.get_String(wsHandle)).Text if not def_text or not def_text.strip(): results["warnings"].append(entry) has_issue = True break elif check_name == "Missing Example": # WARNING: Senses without examples for sense in entry.SensesOS: if sense.ExamplesOS.Count == 0: results["warnings"].append(entry) has_issue = True break elif check_name == "Missing Grammatical Info" or check_name == "Missing Part of Speech": # ERROR: Senses without POS/MSA for sense in entry.SensesOS: if sense.MorphoSyntaxAnalysisRA is None: results["errors"].append(entry) has_issue = True break elif check_name == "Duplicate Entries": # WARNING: Entries with same lexeme form lacking homograph numbers if hasattr(entry, "HomographNumber") and entry.HomographNumber == 0: results["warnings"].append(entry) has_issue = True elif check_name == "Missing Etymology": # WARNING: Entries without etymology if entry.EtymologyOS.Count == 0: results["warnings"].append(entry) has_issue = True else: # Unknown check type - mark as passed (don't fail on custom checks) pass # If no issue found, mark as passed if not has_issue: results["passed"].append(entry) except (AttributeError, KeyError) as e: # Entry structure incompatible with this check type logger.warning(f"Entry {obj} incompatible with check '{check_name}': {e}") results["passed"].append(obj) except Exception as e: # Unexpected error - this indicates a bug, don't hide it logger.error(f"Unexpected error checking entry {obj} with '{check_name}': {e}") raise return results def _GetIssueDescription(self, obj): """ Get a human-readable description of an issue for an object. Args: obj: The object with issues. Returns: str: Description of the issue. """ # Placeholder implementation # Actual description would be based on check type and object if hasattr(obj, "HeadWord") and obj.HeadWord: return f"Issue with '{normalize_text(obj.HeadWord.Text)}'" return "Issue found" @OperationsMethod def Duplicate(self, item_or_hvo, insert_after=True, deep=True): """ Duplicate a check type, creating a new copy with a new GUID. Args: item_or_hvo: The ICmPossibility check object or HVO to duplicate. insert_after (bool): If True (default), insert after the source check. If False, insert at end of parent's list. deep (bool): If True (default), also duplicate sub-checks. If False, only copy simple properties. Returns: ICmPossibility: The newly created duplicate 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: >>> check = project.Checks.FindCheckType("Missing Gloss") >>> if check: ... dup = project.Checks.Duplicate(check) ... print(f"Duplicate: {project.Checks.GetName(dup)}") Notes: - Factory.Create() automatically generates a new GUID - MultiString properties: Name, Description - Check state (enabled/disabled) is not copied - Check results are not copied See Also: CreateCheckType, DeleteCheckType, GetGuid """ self._EnsureWriteEnabled() check_obj = self.__GetCheckObject(item_or_hvo) # PossibilitiesOS lives on ICmPossibilityList (top-level) and # SubPossibilitiesOS lives on ICmPossibility (sub-check). Disambiguate # by ClassName so pythonnet surfaces the typed collection accessor. raw_parent = check_obj.Owner if raw_parent.ClassName == "CmPossibilityList": parent = ICmPossibilityList(raw_parent) else: parent = ICmPossibility(raw_parent) # Create new check using factory (auto-generates new GUID) factory = self.project.project.ServiceLocator.GetService(ICmPossibilityFactory) with self._TransactionCM("Duplicate check type"): duplicate = factory.Create() # ADD TO PARENT FIRST if insert_after: # Insert after source if hasattr(parent, "PossibilitiesOS"): source_index = parent.PossibilitiesOS.IndexOf(check_obj) parent.PossibilitiesOS.Insert(source_index + 1, duplicate) elif hasattr(parent, "SubPossibilitiesOS"): source_index = parent.SubPossibilitiesOS.IndexOf(check_obj) parent.SubPossibilitiesOS.Insert(source_index + 1, duplicate) else: # Insert at end if hasattr(parent, "PossibilitiesOS"): parent.PossibilitiesOS.Add(duplicate) elif hasattr(parent, "SubPossibilitiesOS"): parent.SubPossibilitiesOS.Add(duplicate) # Copy MultiString properties (AFTER adding to parent) duplicate.Name.CopyAlternatives(check_obj.Name) if hasattr(check_obj, "Description") and check_obj.Description: duplicate.Description.CopyAlternatives(check_obj.Description) # Note: Check state and results are not copied # The duplicate starts as disabled with no results # Deep copy: duplicate sub-checks into the NEW duplicate if deep and hasattr(check_obj, "SubPossibilitiesOS") and check_obj.SubPossibilitiesOS.Count > 0: for sub in check_obj.SubPossibilitiesOS: self._DuplicateSubCheckInto(sub, duplicate, deep=True) return duplicate def _DuplicateSubCheckInto(self, source_check, parent_dup, deep=True): """Duplicate a sub-check into the specified parent's SubPossibilitiesOS.""" factory = self.project.project.ServiceLocator.GetService(ICmPossibilityFactory) dup_check = factory.Create() parent_dup.SubPossibilitiesOS.Add(dup_check) # Copy properties dup_check.Name.CopyAlternatives(source_check.Name) if hasattr(source_check, "Description") and source_check.Description: dup_check.Description.CopyAlternatives(source_check.Description) # Recurse into nested sub-checks if deep and hasattr(source_check, "SubPossibilitiesOS") and source_check.SubPossibilitiesOS.Count > 0: for nested_check in source_check.SubPossibilitiesOS: self._DuplicateSubCheckInto(nested_check, dup_check, deep=True) # ========== SYNC INTEGRATION METHODS ========== @OperationsMethod def GetSyncableProperties(self, item): """Get syncable properties for cross-project synchronization.""" check_obj = self.__GetCheckObject(item) wsHandle = self.__WSHandle(None) props = {} props["Name"] = ITsString(check_obj.Name.get_String(wsHandle)).Text or "" if hasattr(check_obj, "Description") and check_obj.Description: props["Description"] = ITsString(check_obj.Description.get_String(wsHandle)).Text or "" return props @OperationsMethod def CompareTo(self, item1, item2, ops1=None, ops2=None): """Compare two checks and return detailed differences.""" if ops1 is None: ops1 = self if ops2 is None: ops2 = self is_different = False differences = {"properties": {}} props1 = ops1.GetSyncableProperties(item1) props2 = ops2.GetSyncableProperties(item2) for key in set(props1.keys()) | set(props2.keys()): val1 = props1.get(key) val2 = props2.get(key) if val1 != val2: is_different = True differences["properties"][key] = {"source": val1, "target": val2, "type": "modified"} return is_different, differences