Source code for modelly.time.signals.computed

"""Dependency bookkeeping for computed properties.

These helpers keep a ComputedState's subscriptions in step with the observables
it reads. They hold no tracking state themselves: the currently evaluating
computed lives in ``modelly.time.signals.core``.
"""

from __future__ import annotations

import weakref
from collections.abc import Callable, Iterable
from typing import TYPE_CHECKING, Any

from modelly.time.signals.types import ObservableSignals
from modelly.time.signals.util import ALL, SignalType

if TYPE_CHECKING:
    from modelly.time.signals.emitters import HasEmitters

# A dependency spec is (observable_name, *signal_types); one element means ALL.
_NAME_AND_SIGNALS = 2


[docs] class ComputedProperty(property): """A custom property class to identify computed properties.""" signal_types = ObservableSignals
[docs] class ComputedStateBase: """The per-instance data of a computed property; core.ComputedState adds evaluation.""" __slots__ = [ "__weakref__", "dependencies", "func", "is_dirty", "name", "owner", "parents", "value", ] def __init__( self, owner: HasEmitters, name: str, func: Callable, dependencies: Iterable[tuple[str, str | SignalType]] | None = None, ): """Initialize a ComputedState. Args: owner: the HasEmitters instance on which this state is defined name: the name of the computed property func: the computation function dependencies: optional iterable of explicit dependencies to track """ self.owner = owner self.name = name self.func = func self.value = None self.is_dirty = True self.parents: weakref.WeakKeyDictionary[HasEmitters, dict[str, Any]] = ( weakref.WeakKeyDictionary() ) self.dependencies: list[tuple[str, SignalType]] = [] if dependencies: _subscribe_dependencies(self, owner, dependencies) def _set_dirty(self, signal): if not self.is_dirty: self.is_dirty = True self.owner.notify(self.name, ObservableSignals.CHANGED, old=self.value) def _record_access( self, parent: HasEmitters, name: str, current_value: Any ) -> None: """Records access to a parent Observable. Args: parent: the HasEmitters instance to which the Observable belongs name: the public name of the Observable current_value: the current value of the Observable """ try: self.parents[parent][name] = current_value except KeyError: self.parents[parent] = {name: current_value}
def _subscribe_dependencies( state: ComputedStateBase, owner: HasEmitters, dependencies: Iterable[tuple[str, str | SignalType]], ) -> None: """Subscribe state to the explicitly declared dependencies on owner.""" for dep_spec in dependencies: if isinstance(dep_spec, tuple) and len(dep_spec) >= _NAME_AND_SIGNALS: obs_name = dep_spec[0] signal_types = dep_spec[1:] for signal_type in signal_types: owner.observe(obs_name, signal_type, state._set_dirty) state.dependencies.append((obs_name, signal_type)) elif isinstance(dep_spec, tuple) and len(dep_spec) == 1: obs_name = dep_spec[0] owner.observe(obs_name, ALL, state._set_dirty) state.dependencies.append((obs_name, ALL)) def _resubscribe(state: ComputedStateBase, old_parents: Any) -> None: """Diffing engine: subscribe/unsubscribe as dependencies come and go.""" old_deps = {(p, attr) for p, attrs in old_parents.items() for attr in attrs} new_deps = {(p, attr) for p, attrs in state.parents.items() for attr in attrs} for p, attr in old_deps - new_deps: p.unobserve(attr, ALL, state._set_dirty) for p, attr in new_deps - old_deps: p.observe(attr, ALL, state._set_dirty) def _parents_changed(parents: Any) -> bool: """Return whether any recorded parent value differs from its current value.""" if not parents: return True for parent, observations in parents.items(): if parent is None: return True for attr, old_val in observations.items(): current_val = getattr(parent, attr) if current_val != old_val: return True return False