Coverage for src/lexigram/admin/state/store.py: 95%
86 statements
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-21 14:56 +0800
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-21 14:56 +0800
1"""Reactive state management based on Signals for Lexigram Admin."""
3from __future__ import annotations
5import contextvars
6from typing import TYPE_CHECKING, Generic, TypeVar
8if TYPE_CHECKING:
9 from collections.abc import Callable
11T = TypeVar("T")
13# Context for tracking active computations
14_active_effect: contextvars.ContextVar[Effect | None] = contextvars.ContextVar(
15 "_active_effect",
16 default=None,
17)
20class Signal(Generic[T]):
21 """A reactive value that can be observed/subscribed to."""
23 def __init__(self, value: T) -> None:
24 self._value = value
25 self._subscribers: set[Effect | Computed] = set()
27 def get(self) -> T:
28 """Get the value and track the dependency if an effect is active."""
29 effect = _active_effect.get()
30 if effect:
31 self._subscribers.add(effect)
32 effect.add_dependency(self)
33 return self._value
35 def set(self, value: T) -> None:
36 """Update the value and notify all subscribers."""
37 if self._value == value:
38 return
39 self._value = value
40 # Copy subscribers to avoid modification during iteration
41 for subscriber in list(self._subscribers):
42 subscriber.update()
44 def add_subscriber(self, subscriber: Effect | Computed) -> None:
45 """Add a subscriber to this signal."""
46 self._subscribers.add(subscriber)
48 def remove_subscriber(self, subscriber: Effect | Computed) -> None:
49 """Remove a subscriber from this signal."""
50 self._subscribers.discard(subscriber)
52 def __repr__(self) -> str:
53 return f"Signal({self._value})"
56class Computed(Generic[T]):
57 """A value derived from other signals or computed values."""
59 def __init__(self, fn: Callable[[], T]) -> None:
60 self._fn = fn
61 self._value: T | None = None
62 self._dirty = True
63 self._subscribers: set[Effect | Computed] = set()
64 self._dependencies: set[Signal | Computed] = set()
66 def get(self) -> T:
67 """Get the computed value, re-evaluating if dirty."""
68 effect = _active_effect.get()
69 if effect:
70 self._subscribers.add(effect)
71 effect.add_dependency(self)
73 if self._dirty:
74 self._evaluate()
75 return self._value # type: ignore[return-value]
77 def _evaluate(self) -> None:
78 """Run the computation and track dependencies."""
79 # Unsubscribe from old dependencies
80 for dep in self._dependencies:
81 dep.remove_subscriber(self)
82 self._dependencies.clear()
84 # Run with tracking
85 token = _active_effect.set(self) # type: ignore[arg-type]
86 try:
87 self._value = self._fn()
88 finally:
89 _active_effect.reset(token)
91 self._dirty = False
93 def update(self) -> None:
94 """Mark as dirty and notify subscribers."""
95 if not self._dirty:
96 self._dirty = True
97 for subscriber in list(self._subscribers):
98 subscriber.update()
100 def add_dependency(self, dependency: Signal | Computed) -> None:
101 """Add a dependency to this computed value."""
102 self._dependencies.add(dependency)
104 def remove_subscriber(self, subscriber: Effect | Computed) -> None:
105 """Remove a subscriber from this computed value."""
106 self._subscribers.discard(subscriber)
108 def __repr__(self) -> str:
109 return f"Computed({self._value})"
112class Effect:
113 """A side-effect that runs whenever its dependencies change."""
115 def __init__(self, fn: Callable[[], None]) -> None:
116 self._fn = fn
117 self._dependencies: set[Signal | Computed] = set()
118 self._run()
120 def _run(self) -> None:
121 """Run the effect and track dependencies."""
122 # Unsubscribe from old dependencies
123 for dep in self._dependencies:
124 dep.remove_subscriber(self)
125 self._dependencies.clear()
127 # Run with tracking
128 token = _active_effect.set(self)
129 try:
130 self._fn()
131 finally:
132 _active_effect.reset(token)
134 def update(self) -> None:
135 """Re-run the effect because a dependency changed."""
136 self._run()
138 def add_dependency(self, dependency: Signal | Computed) -> None:
139 """Add a dependency to this effect."""
140 self._dependencies.add(dependency)
143def signal(value: T) -> Signal[T]:
144 """Helper to create a signal."""
145 return Signal(value)
148def computed(fn: Callable[[], T]) -> Computed[T]:
149 """Helper to create a computed value."""
150 return Computed(fn)
153def watch(fn: Callable[[], None]) -> Effect:
154 """Helper to create an effect (watcher)."""
155 return Effect(fn)
158class GlobalStore:
159 """Base class for global state containers."""