Skip to content

Commit 3a489f6

Browse files
feat: add reactivity_loss_strategy option
1 parent e9eb6b2 commit 3a489f6

File tree

4 files changed

+22
-10
lines changed

4 files changed

+22
-10
lines changed

packages/hmr/reactivity/context.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,14 +34,18 @@ def enter(self, computation: BaseComputation):
3434
computation.dependencies.update(old_dependencies)
3535
raise
3636
else:
37-
if not computation.dependencies:
37+
if not computation.dependencies and (strategy := computation.reactivity_loss_strategy) != "ignore":
38+
if strategy == "restore":
39+
for dep in old_dependencies:
40+
dep.subscribers.add(computation)
41+
computation.dependencies.update(old_dependencies)
42+
return
3843
from pathlib import Path
3944
from sysconfig import get_path
4045
from warnings import warn
4146

42-
# in the future this may be a configurable behavior (e.g., restoring the previous dependencies)
4347
msg = "lost all its dependencies" if old_dependencies else "has no dependencies"
44-
warn(f"Computation {computation} {msg} and will never be auto-triggered.", RuntimeWarning, skip_file_prefixes=(str(Path(__file__).parent), str(Path(get_path("stdlib")).resolve())))
48+
warn(f"{computation} {msg} and will never be auto-triggered.", RuntimeWarning, skip_file_prefixes=(str(Path(__file__).parent), str(Path(get_path("stdlib")).resolve())))
4549
finally:
4650
last = self.current_computations.pop()
4751
assert last is computation # sanity check

packages/hmr/reactivity/hmr/utils.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ def wrapper() -> T:
7171
return functions[key]()
7272

7373
memo = Derived(wrapper, context=HMR_CONTEXT)
74+
memo.reactivity_loss_strategy = "ignore" # Manually invalidated on source change, so reactivity loss is safe to ignore
7475
memos[key] = memo, source
7576

7677
return _return(wraps(func)(memo))

packages/hmr/reactivity/primitives.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from collections.abc import Callable
2-
from typing import Any, Self, overload
2+
from typing import Any, Literal, Self, overload
33
from weakref import WeakSet
44

55
from .context import Context, default_context
@@ -75,6 +75,18 @@ def trigger(self) -> Any: ...
7575
def __call__(self) -> T:
7676
return self.trigger()
7777

78+
reactivity_loss_strategy: Literal["ignore", "warn", "restore"] = "warn"
79+
"""
80+
A computation without dependencies usually indicates a code mistake.
81+
---
82+
By default, a warning is issued when a computation completes without collecting any dependencies.
83+
This often happens when signal access is behind non-reactive conditions or caching.
84+
You can set this to `"restore"` to automatically preserve previous dependencies as a **temporary workaround**.
85+
The correct practice is to replace those conditions with reactive ones (e.g. `Signal`) or use `Derived` for caching.
86+
* * *
87+
Consider `"ignore"` only when extending this library and manually managing dependencies. Use with caution.
88+
"""
89+
7890

7991
class Signal[T](Subscribable):
8092
def __init__(self, initial_value: T = None, check_equality=True, *, context: Context | None = None):

tests/py/test_hmr.py

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,7 @@ def f(): pass
189189
assert recwarn.list == []
190190

191191

192-
def test_cache_across_reloads_with_other_decorators(recwarn: pytest.WarningsRecorder):
192+
def test_cache_across_reloads_with_other_decorators():
193193
with environment() as env:
194194
env["main.py"] = """
195195
from reactivity.hmr.utils import cache_across_reloads
@@ -203,11 +203,6 @@ def two(): return 2
203203
assert env.stdout_delta == "3\n3\n1\n" # inner function being called twice, while the outer one only once
204204
assert ns["two"] == 2
205205

206-
warning = recwarn.pop(RuntimeWarning)
207-
assert warning.lineno == 4 # f()
208-
assert warning.filename == "main.py"
209-
assert recwarn.list == []
210-
211206

212207
def test_cache_across_reloads_cache_lifespan():
213208
with environment() as env:

0 commit comments

Comments
 (0)