Source code for flexicon.code.Grammar.EnvironmentOperations

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

# Import BaseOperations parent class
from ..BaseOperations import BaseOperations, OperationsMethod, wrap_enumerable

# Import FLEx LCM types
from SIL.LCModel import IPhEnvironmentFactory, IPhEnvironment, ICmObjectRepository
from SIL.LCModel.Core.KernelInterfaces import ITsString
from SIL.LCModel.Core.Text import TsStringUtils

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


[docs] class EnvironmentOperations(BaseOperations): """ This class provides operations for managing phonological environments in a FieldWorks project. Phonological environments specify the context in which phonological rules apply. They describe what precedes and follows a segment, using notation like V_V (between vowels), #_ (word-initially), _# (word-finally), etc. Usage:: from flexlibs2 import FLExProject, EnvironmentOperations project = FLExProject() project.OpenProject("my project", writeEnabled=True) envOps = EnvironmentOperations(project) # Get all environments for env in envOps.GetAll(): name = envOps.GetName(env) notation = envOps.GetStringRepresentation(env) print(f"{name}: {notation}") # Create a new environment word_initial = envOps.Create("Word Initial", "At the beginning of a word") envOps.SetStringRepresentation(word_initial, "#_") # Create more environments between_vowels = envOps.Create("Between Vowels") envOps.SetStringRepresentation(between_vowels, "V_V") project.CloseProject() """ def __init__(self, project): """ Initialize EnvironmentOperations with a FLExProject instance. Args: project: The FLExProject instance to operate on. """ super().__init__(project) def _GetSequence(self, parent): """ Specify which sequence to reorder for environments. For Environment, we reorder parent.EnvironmentsOA.PossibilitiesOS """ return parent.EnvironmentsOA.PossibilitiesOS @wrap_enumerable @OperationsMethod def GetAll(self): """ Get all phonological environments in the project. Yields: IPhEnvironment: Each environment object in the project's environment list. Example: >>> envOps = EnvironmentOperations(project) >>> for env in envOps.GetAll(): ... name = envOps.GetName(env) ... notation = envOps.GetStringRepresentation(env) ... print(f"{name}: {notation}") Word Initial: #_ Word Final: _# Between Vowels: V_V Before Consonant: _C Notes: - Returns environments in their defined order - Environments are reusable across phonological rules - Returns empty if no phonological data defined See Also: Create, GetName, GetStringRepresentation """ phon_data = self.project.lp.PhonologicalDataOA if phon_data is not None: for env in phon_data.EnvironmentsOS: yield env @OperationsMethod def Create(self, name, description=None): """ Create a new phonological environment. Args: name (str): The name of the environment (e.g., "Word Initial", "Between Vowels"). description (str, optional): Optional description of when this environment applies. Defaults to None. Returns: IPhEnvironment: The newly created environment object. Raises: FP_ReadOnlyError: If the project is not opened with write enabled. FP_NullParameterError: If name is None. FP_ParameterError: If name is empty. Example: >>> envOps = EnvironmentOperations(project) >>> word_initial = envOps.Create("Word Initial", "At word start") >>> envOps.SetStringRepresentation(word_initial, "#_") >>> print(envOps.GetName(word_initial)) Word Initial >>> between_vowels = envOps.Create("Between Vowels") >>> envOps.SetStringRepresentation(between_vowels, "V_V") Notes: - Name should be descriptive of the phonological context - Use SetStringRepresentation() to set formal notation (e.g., "V_V") - Description is optional but helpful for documentation - Environment is created in the default analysis writing system See Also: Delete, GetName, SetStringRepresentation """ self._EnsureWriteEnabled() self._ValidateParam(name, "name") if not name or not name.strip(): raise FP_ParameterError("Name cannot be empty") # Get the writing system handle wsHandle = self.project.project.DefaultAnalWs # Create the new environment using the factory factory = self.project.project.ServiceLocator.GetService(IPhEnvironmentFactory) with self._TransactionCM("Create environment"): new_env = factory.Create() # Add to the environments list (must be done before setting properties) phon_data = self.project.lp.PhonologicalDataOA phon_data.EnvironmentsOS.Add(new_env) # Set name mkstr_name = TsStringUtils.MakeString(name, wsHandle) new_env.Name.set_String(wsHandle, mkstr_name) # Set description if provided if description: mkstr_desc = TsStringUtils.MakeString(description, wsHandle) new_env.Description.set_String(wsHandle, mkstr_desc) return new_env @OperationsMethod def Delete(self, env_or_hvo): """ Delete a phonological environment. Args: env_or_hvo: The IPhEnvironment object or HVO to delete. Raises: FP_ReadOnlyError: If the project is not opened with write enabled. FP_NullParameterError: If env_or_hvo is None. FP_ParameterError: If the environment is in use and cannot be deleted. Example: >>> envOps = EnvironmentOperations(project) >>> obsolete = envOps.Create("Obsolete Environment") >>> # ... realize it's not needed >>> envOps.Delete(obsolete) Warning: - Deleting an environment that is in use may raise an error from FLEx - This includes environments used in phonological rules and allomorph conditions - Deletion is permanent and cannot be undone - Consider checking usage before deletion See Also: Create, GetAll """ self._EnsureWriteEnabled() self._ValidateParam(env_or_hvo, "env_or_hvo") # Resolve to environment object env = self.__ResolveObject(env_or_hvo) # Remove from the environments list phon_data = self.project.lp.PhonologicalDataOA phon_data.EnvironmentsOS.Remove(env) @OperationsMethod def GetName(self, env_or_hvo, wsHandle=None): """ Get the name of a phonological environment. Args: env_or_hvo: The IPhEnvironment object or HVO. wsHandle: Optional writing system handle. Defaults to analysis WS. Returns: str: The environment name, or empty string if not set. Raises: FP_NullParameterError: If env_or_hvo is None. Example: >>> envOps = EnvironmentOperations(project) >>> for env in envOps.GetAll(): ... name = envOps.GetName(env) ... print(name) Word Initial Word Final Between Vowels >>> # Get name in a specific writing system >>> env = list(envOps.GetAll())[0] >>> name = envOps.GetName(env, project.WSHandle('en')) See Also: SetName, GetStringRepresentation """ self._ValidateParam(env_or_hvo, "env_or_hvo") env = self.__ResolveObject(env_or_hvo) wsHandle = self.__WSHandle(wsHandle) name = ITsString(env.Name.get_String(wsHandle)).Text return name or "" @OperationsMethod def SetName(self, env_or_hvo, name, wsHandle=None): """ Set the name of a phonological environment. Args: env_or_hvo: The IPhEnvironment object or HVO. name (str): The new name. wsHandle: Optional writing system handle. Defaults to analysis WS. Raises: FP_ReadOnlyError: If the project is not opened with write enabled. FP_NullParameterError: If env_or_hvo or name is None. FP_ParameterError: If name is empty. Example: >>> envOps = EnvironmentOperations(project) >>> env = list(envOps.GetAll())[0] >>> envOps.SetName(env, "Word Initial Position") >>> # Use standard terminology >>> envOps.SetName(env, "Intervocalic") # Better than custom names Notes: - Use clear, descriptive names - Standard terminology aids cross-linguistic comparison See Also: GetName, SetStringRepresentation """ self._EnsureWriteEnabled() self._ValidateParam(env_or_hvo, "env_or_hvo") self._ValidateParam(name, "name") if not name or not name.strip(): raise FP_ParameterError("Name cannot be empty") env = self.__ResolveObject(env_or_hvo) wsHandle = self.__WSHandle(wsHandle) mkstr = TsStringUtils.MakeString(name, wsHandle) env.Name.set_String(wsHandle, mkstr) @OperationsMethod def GetStringRepresentation(self, env_or_hvo, wsHandle=None): """ Get the string representation (notation) of a phonological environment. Args: env_or_hvo: The IPhEnvironment object or HVO. wsHandle: Optional writing system handle. Defaults to analysis WS. Returns: str: The environment's string representation (e.g., "V_V", "#_", "_#"), or empty string if not set. Raises: FP_NullParameterError: If env_or_hvo is None. Example: >>> envOps = EnvironmentOperations(project) >>> for env in envOps.GetAll(): ... name = envOps.GetName(env) ... notation = envOps.GetStringRepresentation(env) ... print(f"{name:20} {notation}") Word Initial #_ Word Final _# Between Vowels V_V After Nasal N_ Before Stop _P >>> # Use in rule description >>> env = list(envOps.GetAll())[0] >>> notation = envOps.GetStringRepresentation(env) >>> print(f"Rule applies in: {notation}") Rule applies in: V_V Notes: - String representation uses formal phonological notation - Underscore (_) marks the position of the target segment - # marks word boundaries - $ marks syllable boundaries - Capital letters (V, C, N, etc.) reference natural classes Common Environment Notation: #_ - Word-initial (e.g., #_at) _# - Word-final (e.g., ca_#) V_V - Between vowels (intervocalic) C_C - Between consonants N_ - After nasal _P - Before stop $._ - Syllable-initial _.$ - Syllable-final See Also: SetStringRepresentation, GetName """ self._ValidateParam(env_or_hvo, "env_or_hvo") env = self.__ResolveObject(env_or_hvo) # Note: StringRepresentation is ITsString, not IMultiUnicode # so we access .Text directly (wsHandle is unused for this property) notation = env.StringRepresentation.Text if env.StringRepresentation else "" return notation or "" @OperationsMethod def SetStringRepresentation(self, env_or_hvo, notation, wsHandle=None): """ Set the string representation (notation) of a phonological environment. Args: env_or_hvo: The IPhEnvironment object or HVO. notation (str): The environment representation (e.g., "V_V", "#_", "_#"). wsHandle: Optional writing system handle. Defaults to analysis WS. Raises: FP_ReadOnlyError: If the project is not opened with write enabled. FP_NullParameterError: If env_or_hvo or notation is None. Example: >>> envOps = EnvironmentOperations(project) >>> # Create and define common environments >>> word_initial = envOps.Create("Word Initial") >>> envOps.SetStringRepresentation(word_initial, "#_") >>> word_final = envOps.Create("Word Final") >>> envOps.SetStringRepresentation(word_final, "_#") >>> between_vowels = envOps.Create("Between Vowels") >>> envOps.SetStringRepresentation(between_vowels, "V_V") >>> # Complex environment: after nasal, before stop >>> env = envOps.Create("Nasal-Stop Cluster") >>> envOps.SetStringRepresentation(env, "N_P") Notes: - Use standard phonological environment notation - Underscore (_) marks the target position - Reference natural classes using their abbreviations (V, C, N, P) - Can specify complex environments with multiple contexts - Empty string is allowed for unrestricted environment Notation Guide: _ - Target position (required) # - Word boundary $ - Syllable boundary V - Vowel (natural class) C - Consonant (natural class) N - Nasal (natural class) P - Stop (natural class) | - Disjunction (or) See Also: GetStringRepresentation, Create """ self._EnsureWriteEnabled() self._ValidateParam(env_or_hvo, "env_or_hvo") self._ValidateParam(notation, "notation") env = self.__ResolveObject(env_or_hvo) wsHandle = self.__WSHandle(wsHandle) # Note: StringRepresentation is ITsString, assign directly (not set_String) mkstr = TsStringUtils.MakeString(notation, wsHandle) env.StringRepresentation = mkstr @OperationsMethod def GetLeftContextPattern(self, env_or_hvo): """ Get the left context pattern of a phonological environment (READ-ONLY). This is a computed property that returns the left context specification of the environment, representing what must precede the target position. Args: env_or_hvo: The IPhEnvironment object or HVO. Returns: object or None: The left context object (IPhPhonContext) if set, None otherwise. Raises: FP_NullParameterError: If env_or_hvo is None. Example: >>> envOps = EnvironmentOperations(project) >>> env = envOps.Create("After Vowel") >>> left_ctx = envOps.GetLeftContextPattern(env) >>> if left_ctx: ... print(f"Has left context: {left_ctx}") >>> else: ... print("No left context constraint") >>> # Check all environments for left context >>> for env in envOps.GetAll(): ... name = envOps.GetName(env) ... left_ctx = envOps.GetLeftContextPattern(env) ... if left_ctx: ... print(f"{name} has left context") Notes: - This is a READ-ONLY property (no setter) - Returns the LeftContextOA object if present - Left context specifies what must precede the target - Returns None if no left context is defined - The returned object is typically an IPhPhonContext - Use this to inspect or analyze environment structure - Not intended for modification (use SetLeftContext for that) See Also: GetRightContextPattern, GetStringRepresentation, GetName """ self._ValidateParam(env_or_hvo, "env_or_hvo") env = self.__ResolveObject(env_or_hvo) # Return the left context if it exists if hasattr(env, "LeftContextOA") and env.LeftContextOA: return env.LeftContextOA return None @OperationsMethod def GetRightContextPattern(self, env_or_hvo): """ Get the right context pattern of a phonological environment (READ-ONLY). This is a computed property that returns the right context specification of the environment, representing what must follow the target position. Args: env_or_hvo: The IPhEnvironment object or HVO. Returns: object or None: The right context object (IPhPhonContext) if set, None otherwise. Raises: FP_NullParameterError: If env_or_hvo is None. Example: >>> envOps = EnvironmentOperations(project) >>> env = envOps.Create("Before Consonant") >>> right_ctx = envOps.GetRightContextPattern(env) >>> if right_ctx: ... print(f"Has right context: {right_ctx}") >>> else: ... print("No right context constraint") >>> # Check all environments for right context >>> for env in envOps.GetAll(): ... name = envOps.GetName(env) ... right_ctx = envOps.GetRightContextPattern(env) ... if right_ctx: ... print(f"{name} has right context") Notes: - This is a READ-ONLY property (no setter) - Returns the RightContextOA object if present - Right context specifies what must follow the target - Returns None if no right context is defined - The returned object is typically an IPhPhonContext - Use this to inspect or analyze environment structure - Not intended for modification (use SetRightContext for that) See Also: GetLeftContextPattern, GetStringRepresentation, GetName """ self._ValidateParam(env_or_hvo, "env_or_hvo") env = self.__ResolveObject(env_or_hvo) # Return the right context if it exists if hasattr(env, "RightContextOA") and env.RightContextOA: return env.RightContextOA return None @OperationsMethod def Duplicate(self, item_or_hvo, insert_after=True, deep=True): """ Duplicate a phonological environment, creating a new copy with a new GUID. Args: item_or_hvo: The IPhEnvironment object or HVO to duplicate. insert_after (bool): If True (default), insert after the source environment. If False, insert at end of environments list. deep (bool): If True (default), deep copy owned context objects (LeftContextOA, RightContextOA). If False, contexts are not copied. Returns: IPhEnvironment: The newly created duplicate environment 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: >>> envOps = EnvironmentOperations(project) >>> word_initial = envOps.Create("Word Initial") >>> envOps.SetStringRepresentation(word_initial, "#_") >>> # Deep copy (default - includes owned context objects) >>> copy = envOps.Duplicate(word_initial) >>> print(envOps.GetName(copy)) Word Initial >>> # Shallow copy (no context objects) >>> between_vowels = envOps.Create("Between Vowels") >>> copy = envOps.Duplicate(between_vowels, deep=False) Notes: - Factory.Create() automatically generates a new GUID - insert_after=True preserves the original environment's position - Simple properties copied: Name, Description, StringRepresentation (MultiString) - deep=True copies owned context objects (LeftContextOA, RightContextOA) - Contexts are complex structures with their own owned objects See Also: Create, Delete """ self._EnsureWriteEnabled() self._ValidateParam(item_or_hvo, "item_or_hvo") # Get source environment source = self.__ResolveObject(item_or_hvo) # Create new environment using factory (auto-generates new GUID) factory = self.project.project.ServiceLocator.GetService(IPhEnvironmentFactory) with self._TransactionCM("Duplicate environment"): duplicate = factory.Create() # Add to environments list phon_data = self.project.lp.PhonologicalDataOA if insert_after: source_index = phon_data.EnvironmentsOS.IndexOf(source) phon_data.EnvironmentsOS.Insert(source_index + 1, duplicate) else: phon_data.EnvironmentsOS.Add(duplicate) # Copy simple MultiString properties (AFTER adding to parent) duplicate.Name.CopyAlternatives(source.Name) duplicate.Description.CopyAlternatives(source.Description) # Copy StringRepresentation (ITsString, not MultiString) if source.StringRepresentation: notation = source.StringRepresentation.Text wsHandle = self.__WSHandle(None) mkstr = TsStringUtils.MakeString(notation, wsHandle) duplicate.StringRepresentation = mkstr # Deep copy: owned context objects if deep: from ..lcm_casting import clone_properties # Copy LeftContextOA if exists if hasattr(source, "LeftContextOA") and source.LeftContextOA: try: # Create new context object of the same type src_context = source.LeftContextOA new_context = self.project.project.ServiceLocator.ObjectRepository.NewObject(src_context.ClassID) # Deep clone all properties clone_properties(src_context, new_context, self.project) duplicate.LeftContextOA = new_context except Exception: pass # Copy RightContextOA if exists if hasattr(source, "RightContextOA") and source.RightContextOA: try: # Create new context object of the same type src_context = source.RightContextOA new_context = self.project.project.ServiceLocator.ObjectRepository.NewObject(src_context.ClassID) # Deep clone all properties clone_properties(src_context, new_context, self.project) duplicate.RightContextOA = new_context except Exception: pass return duplicate # --- Private Helper Methods --- def __ResolveObject(self, env_or_hvo): """ Resolve HVO or object to IPhEnvironment. Args: env_or_hvo: Either an IPhEnvironment object or an HVO (int). Returns: IPhEnvironment: The resolved environment object. """ if isinstance(env_or_hvo, int): return self.project.Object(env_or_hvo) return env_or_hvo # ========== SYNC INTEGRATION METHODS ========== @OperationsMethod def GetSyncableProperties(self, item): """ Get dictionary of syncable properties for cross-project synchronization. Args: item: The IPhEnvironment object. Returns: dict: Dictionary mapping property names to their values. Keys are property names, values are the property values. Example: >>> envOps = EnvironmentOperations(project) >>> env = list(envOps.GetAll())[0] >>> props = envOps.GetSyncableProperties(env) >>> print(props.keys()) dict_keys(['Name', 'Description', 'StringRepresentation']) Notes: - Returns all MultiString properties (all writing systems) - Does not include owned objects (LeftContextOA, RightContextOA) - Does not include GUID or HVO """ env = self.__ResolveObject(item) # Get all writing systems for MultiString properties # Fix: ILgWritingSystemFactory does not expose a .WritingSystems # property; enumerate via the wrapper's WritingSystemOperations.GetAll(), # which returns CoreWritingSystemDefinition objects with .Id / .Handle. all_ws = {ws.Id: ws.Handle for ws in self.project.WritingSystems.GetAll()} props = {} # MultiString properties for prop_name in ["Name", "Description", "StringRepresentation"]: prop_obj = getattr(env, prop_name) ws_values = {} for ws_id, ws_handle in all_ws.items(): text = ITsString(prop_obj.get_String(ws_handle)).Text if text: # Only include non-empty values ws_values[ws_id] = text if ws_values: # Only include property if it has values props[prop_name] = ws_values return props @OperationsMethod def ApplySyncableProperties(self, item, props, ws_map=None, fill_gaps=False): """Apply syncable properties (from GetSyncableProperties) onto an item. Inherited from BaseOperations; declared on the concrete class so static API indexers see it. The base implementation handles every property shape this class's GetSyncableProperties emits. """ return super().ApplySyncableProperties(item, props, ws_map, fill_gaps=fill_gaps) @OperationsMethod def CompareTo(self, item1, item2, ops1=None, ops2=None): """ Compare two environments and return detailed differences. Args: item1: First environment to compare (from source project). item2: Second environment to compare (from target project). ops1: Optional EnvironmentOperations instance for item1's project. Defaults to self. ops2: Optional EnvironmentOperations instance for item2's project. Defaults to self. Returns: tuple: (is_different, differences) where: - is_different (bool): True if items differ - differences (dict): Maps property names to (value1, value2) tuples Example: >>> env1 = project1_envOps.Find("Word Initial") >>> env2 = project2_envOps.Find("Word Initial") >>> is_diff, diffs = project1_envOps.CompareTo( ... env1, env2, ... ops1=project1_envOps, ... ops2=project2_envOps ... ) >>> if is_diff: ... for prop, (val1, val2) in diffs.items(): ... print(f"{prop}: {val1} -> {val2}") Notes: - Compares all MultiString properties across all writing systems - Returns empty dict if items are identical - Handles cross-project comparison via ops1/ops2 """ if ops1 is None: ops1 = self if ops2 is None: ops2 = self # Get syncable properties from both items props1 = ops1.GetSyncableProperties(item1) props2 = ops2.GetSyncableProperties(item2) is_different = False differences = {} # Compare each property all_keys = set(props1.keys()) | set(props2.keys()) for key in all_keys: val1 = props1.get(key) val2 = props2.get(key) # For MultiString properties, compare the dictionaries if val1 != val2: is_different = True differences[key] = (val1, val2) return (is_different, differences) # --- Private Helper Methods --- 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)