#
# 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 flexicon 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.EnvironmentsOS
``IPhPhonData`` (the ``parent`` here -- ``project.lp.PhonologicalDataOA``)
owns ``EnvironmentsOS`` directly, an ``IPhEnvironment`` owning
sequence. There is no ``EnvironmentsOA`` property on this type and
no intervening possibility list to hop through via
``.PossibilitiesOS`` -- confirmed live by ``.NET`` reflection
(issue #277; see specs/277-nonexistent-property-reads/evidence/
live-277-environments.md). The wrong form here was a copy/paste
of a pattern that IS correct for other sequence-owning types in
this codebase (e.g. ``InflectionFeatureOperations._GetSequence``
legitimately reads ``parent.FeaturesOA.PossibilitiesOS``, because
its parent type really does own an intervening
``CmPossibilityList``) but was never true for ``IPhPhonData``.
"""
return parent.EnvironmentsOS
@wrap_enumerable
@OperationsMethod
def GetAll(self):
"""
Get all phonological environments in the project.
Returns:
EnumerableWrapper[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
with self._TransactionCM("Delete environment"):
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)
with self._TransactionCM(f"Set environment name '{name}'"):
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)
with self._TransactionCM("Set environment notation"):
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 LeftContextRA object if present (Reference
Atomic -- see specs/lcm-member-truth-sweep/spec.md C7;
the historical ``LeftContextOA`` name never existed on
``IPhEnvironment``)
- 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 (Reference Atomic).
return getattr(env, "LeftContextRA", 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 RightContextRA object if present (Reference
Atomic -- see specs/lcm-member-truth-sweep/spec.md C7;
the historical ``RightContextOA`` name never existed on
``IPhEnvironment``)
- 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 (Reference Atomic).
return getattr(env, "RightContextRA", 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): INERT for this method -- kept only because it is
pinned surface (EnvironmentOperations.pyi:18).
LeftContextRA/RightContextRA are Reference Atomic,
not owned, so there is nothing to "deep copy": the
duplicate always references the SAME context
objects as the source, regardless of this flag.
See specs/lcm-member-truth-sweep/spec.md C7.
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, "#_")
>>> copy = envOps.Duplicate(word_initial)
>>> print(envOps.GetName(copy))
Word Initial
>>> # `deep` is INERT for context objects (see Notes) -- this
>>> # produces the same context references as the call above.
>>> 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)
- LeftContextRA/RightContextRA are copied by REFERENCE,
unconditionally -- `deep` is INERT for this method. They are
Reference Atomic (not owned), so the duplicate points at the
SAME IPhPhonContext objects as the source; there is nothing
to clone. See specs/lcm-member-truth-sweep/spec.md C7.
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:
# Index by HVO (issue #548). EnvironmentsOS can yield bare interface
# views whose Python identity differs from source.
env_list = list(phon_data.EnvironmentsOS)
target_hvo = source.Hvo
source_index = None
for i, env in enumerate(env_list):
if env.Hvo == target_hvo:
source_index = i
break
if source_index is None:
insert_index = len(env_list)
else:
insert_index = source_index + 1
phon_data.EnvironmentsOS.Insert(insert_index, 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
# Reference assignment: LeftContextRA/RightContextRA are
# Reference Atomic (specs/lcm-member-truth-sweep/spec.md C7),
# not owned -- there is nothing to clone. A duplicate
# environment points at the SAME IPhPhonContext objects as
# the source. This is UNCONDITIONAL, not gated behind `deep`
# (which is inert for this method -- see the Args/Notes
# above): gating correct behaviour behind a flag is the
# anti-pattern CLAUDE.md's "Don't Add a Flag for Behaviour
# That Should Be Unconditional" section forbids.
if source.LeftContextRA is not None:
duplicate.LeftContextRA = source.LeftContextRA
if source.RightContextRA is not None:
duplicate.RightContextRA = source.RightContextRA
return duplicate
# --- Private Helper Methods ---
def __ResolveObject(self, env_or_hvo):
"""
Resolve HVO or object to IPhEnvironment.
Casts to ``IPhEnvironment`` by ``ClassName`` BEFORE returning
(contract conformance, flexicon#260). ``FLExProject.Object(hvo)``
returns a bare ``ICmObject``; without this cast, EVERY caller in
this class that performs direct Python attribute access on the
resolved object -- ``GetName``/``SetName`` (``env.Name...``),
``GetStringRepresentation``/``SetStringRepresentation``
(``env.StringRepresentation...``), ``GetSyncableProperties``
(``getattr(env, prop_name)``) -- raises ``AttributeError`` on the
int-HVO entry path. (Historically -- before the #283 fix,
specs/lcm-member-truth-sweep/spec.md C7 -- ``GetLeftContextPattern``
/ ``GetRightContextPattern`` also returned ``None`` on a
correctly-cast object, but for an unrelated reason: at the time
they read ``LeftContextOA``/``RightContextOA``, property names
that never existed on ``IPhEnvironment`` under any cast. The real,
now-corrected names are ``LeftContextRA``/``RightContextRA``
(Reference Atomic), confirmed live: P6, P6b, P7; see
specs/260-environment-resolver-cast/evidence/
live-T2-red-p6-p6b-p7.md for the RED-before-fix reproduction of
the missing-cast defect this method fixes.)
Unlike its sibling ``AllomorphOperations.__GetEnvironmentObject``
(uncast too, but a genuine BUG-FREE contract mismatch there --
cycle 1 measured live that both of its callers only ever hand
the resolved object to a strongly-typed .NET reference-collection
method, where the CLR binds on runtime type and a cast is not
needed), this resolver's callers perform direct Python attribute
access, which pythonnet's static wrapper-type gate blocks unless
the object is cast to the concrete interface first.
``IPhEnvironment`` has exactly ONE implementing type in the whole
``SIL.LCModel`` assembly (``SIL.LCModel.DomainImpl.PhEnvironment``,
confirmed live via reflection), so there is nothing to
discriminate between -- a single ``ClassName`` guard, merging the
int and object branches, is the correct and complete shape (no
two-branch subtype dance like ``AllomorphOperations``'
``IMoStemAllomorph``/``IMoAffixAllomorph`` split).
Any ``ClassName`` other than ``"PhEnvironment"`` (or a non-LCM
input with no ``ClassName``) is returned UNCHANGED -- this
resolver never raises on a miss, matching the permissive,
never-raising shape used by the other ``ClassName``-dispatched
resolvers in this codebase (``AllomorphOperations.
__GetAllomorphObject``, ``MSAOperations.__GetMsaObject``,
``POSOperations.__ResolveObject``).
Args:
env_or_hvo: Either an IPhEnvironment object or an HVO (int).
Returns:
IPhEnvironment: The resolved environment, cast to the
concrete interface when its ``ClassName`` is
``"PhEnvironment"``; returned unchanged otherwise.
"""
if isinstance(env_or_hvo, int):
obj = self.project.Object(env_or_hvo)
else:
obj = env_or_hvo
class_name = getattr(obj, "ClassName", None)
if class_name == "PhEnvironment":
return IPhEnvironment(obj)
return obj
# ========== 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 context references (LeftContextRA,
RightContextRA -- see specs/lcm-member-truth-sweep/spec.md C7)
- 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)
if hasattr(prop_obj, "get_String"):
# IMultiUnicode / IMultiString - iterate per writing system
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
else:
# ITsString (mono) - read once, no ws loop
text = ITsString(prop_obj).Text
if text:
props[prop_name] = text
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)