Source code for pyfcstm.simulate.context

"""
Read-only execution context for abstract function handlers.

This module provides the :class:`ReadOnlyExecutionContext` class that gives
abstract function handlers read-only access to the current execution state
including the active state path and variable values.

The module contains the following main components:

* :class:`ReadOnlyExecutionContext` - Immutable context passed to abstract handlers.

Example::

    >>> from pyfcstm.simulate import ReadOnlyExecutionContext
    >>> def my_handler(ctx: ReadOnlyExecutionContext):
    ...     print(f"State: {ctx.get_full_state_path()}")
    ...     print(f"Counter: {ctx.get_var('counter')}")
"""

from dataclasses import dataclass
from types import MappingProxyType
from typing import Mapping, Optional, Union, Tuple


[docs] @dataclass(frozen=True) class ReadOnlyExecutionContext: """ Read-only execution context for abstract function handlers. Provides immutable access to current state and variable values without allowing modifications. This context is passed to registered abstract handlers during execution. The existing ``state_path``, ``action_name``, and ``action_stage`` fields keep their public meaning, and the additional metadata fields expose the same callsite information in a fixture-friendly shape. :param state_path: Current execution state path from root to leaf. For ancestor aspect actions this is the active descendant leaf. :type state_path: Tuple[str, ...] :param vars: Snapshot of current variable values (immutable mapping copy) :type vars: Mapping[str, Union[int, float]] :param action_name: Full path name of the resolved abstract action target. :type action_name: str :param action_stage: Lifecycle stage at the current callsite (``'enter'``, ``'during'``, or ``'exit'``). :type action_stage: str :param active_leaf: Current active leaf state path. When omitted, it defaults to ``state_path``. :type active_leaf: Tuple[str, ...], optional :param call_stage: Explicit callsite lifecycle stage. When omitted, it defaults to ``action_stage``. :type call_stage: str, optional :param abstract_target: Explicit resolved abstract target path. When omitted, it defaults to ``action_name``. :type abstract_target: str, optional :param named_ref: Full path of the named ``ref`` callsite, or ``None`` when the action was not invoked through a named reference. :type named_ref: Optional[str], optional Example:: >>> ctx = ReadOnlyExecutionContext( ... state_path=('System', 'Active'), ... vars={'counter': 10, 'temperature': 25.5}, ... action_name='System.Active.Monitor', ... action_stage='during' ... ) >>> ctx.get_state_name() 'Active' >>> ctx.get_var('counter') 10 >>> ctx.active_leaf ('System', 'Active') >>> ctx.abstract_target 'System.Active.Monitor' """ state_path: Tuple[str, ...] vars: Mapping[str, Union[int, float]] action_name: str action_stage: str active_leaf: Optional[Tuple[str, ...]] = None call_stage: Optional[str] = None abstract_target: Optional[str] = None named_ref: Optional[str] = None
[docs] def __post_init__(self) -> None: """ Freeze and normalize the context snapshot. Variable mappings are copied into a read-only proxy. Path-like metadata is normalized to tuples, and optional callsite aliases are derived from the existing four constructor fields when omitted. This preserves the original direct-construction API while exposing richer metadata to runtime-created handler contexts. :return: ``None``. :rtype: None Example:: >>> ctx = ReadOnlyExecutionContext( ... state_path=('Root', 'A'), ... vars={}, ... action_name='Root.A.Touch', ... action_stage='during', ... ) >>> ctx.call_stage 'during' >>> ctx.named_ref is None True """ object.__setattr__(self, "state_path", tuple(self.state_path)) object.__setattr__(self, "vars", MappingProxyType(dict(self.vars))) active_leaf = self.state_path if self.active_leaf is None else self.active_leaf object.__setattr__(self, "active_leaf", tuple(active_leaf)) call_stage = self.action_stage if self.call_stage is None else self.call_stage object.__setattr__(self, "call_stage", call_stage) abstract_target = ( self.action_name if self.abstract_target is None else self.abstract_target ) object.__setattr__(self, "abstract_target", abstract_target)
[docs] def get_var(self, name: str) -> Union[int, float]: """ Get variable value by name. :param name: Variable name :type name: str :return: Variable value :rtype: Union[int, float] :raises KeyError: If variable does not exist Example:: >>> ctx.get_var('counter') 10 """ return self.vars[name]
[docs] def get_state_name(self) -> str: """ Get current state name (last component of path). :return: State name, or empty string if no state is active :rtype: str Example:: >>> ctx.get_state_name() 'Active' """ return self.state_path[-1] if self.state_path else ""
[docs] def get_full_state_path(self) -> str: """ Get full state path as dot-separated string. :return: Full state path :rtype: str Example:: >>> ctx.get_full_state_path() 'System.Active' """ return ".".join(self.state_path)
[docs] def has_var(self, name: str) -> bool: """ Check if a variable exists. :param name: Variable name :type name: str :return: ``True`` if variable exists :rtype: bool Example:: >>> ctx.has_var('counter') True >>> ctx.has_var('nonexistent') False """ return name in self.vars