hmr 0.3.3.3__py3-none-any.whl → 0.4.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {hmr-0.3.3.3.dist-info → hmr-0.4.0.dist-info}/METADATA +1 -1
- hmr-0.4.0.dist-info/RECORD +14 -0
- {hmr-0.3.3.3.dist-info → hmr-0.4.0.dist-info}/WHEEL +1 -1
- reactivity/helpers.py +49 -7
- reactivity/hmr/__main__.py +4 -0
- reactivity/hmr/api.py +0 -2
- reactivity/hmr/core.py +56 -31
- reactivity/hmr/utils.py +1 -1
- reactivity/primitives.py +57 -0
- hmr-0.3.3.3.dist-info/RECORD +0 -13
- {hmr-0.3.3.3.dist-info → hmr-0.4.0.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,14 @@
|
|
1
|
+
hmr-0.4.0.dist-info/METADATA,sha256=SrlGz7FGOOGLKRHK7XNuluX_-FkXYvF0DeL12Qaa7DM,258
|
2
|
+
hmr-0.4.0.dist-info/WHEEL,sha256=tSfRZzRHthuv7vxpI4aehrdN9scLjk-dCJkPLzkHxGg,90
|
3
|
+
hmr-0.4.0.dist-info/entry_points.txt,sha256=g_T0uJ43WgsdG14kkkdaBQuIL0HO-m1qvtjXMP6d060,59
|
4
|
+
reactivity/__init__.py,sha256=pX-RUzkezCC1x4eOWGxNhXbwrbvBLP_3pQuZr9eZz1Y,300
|
5
|
+
reactivity/functional.py,sha256=U06vshcVhZ0sb218gcmHtEhfgTNAGtQ7zyvPz2w5qKM,1292
|
6
|
+
reactivity/helpers.py,sha256=1KCpre2HTFZrngEKkI2HwSFMkCmsUCq2aPEbp0y3kKg,5140
|
7
|
+
reactivity/hmr/__init__.py,sha256=S5ZIHqCRpevdzWuhS0aCua_S8F0LkK0YNg6IgeTScFQ,177
|
8
|
+
reactivity/hmr/__main__.py,sha256=uIcyjR5gMFIXH_3hS0B3SD00RirVf7GIct-uItx675o,64
|
9
|
+
reactivity/hmr/api.py,sha256=USwQUtMIdhGRJFGYTp-S23ZelbB39FkV9bB0UEqlAvc,1579
|
10
|
+
reactivity/hmr/core.py,sha256=4bGNcugfk1pKMngw_NMyW8HFogVmorlHyV3utyBHYF0,11488
|
11
|
+
reactivity/hmr/hooks.py,sha256=-yLr5ktiyqPb1nDbHsgv6-c_ZkziBjNqCU-0PCfXGYU,592
|
12
|
+
reactivity/hmr/utils.py,sha256=-PO-LMP4sc3IP-Bn_baq2w9IFWBZ3zGesgRn5wR6bS0,1555
|
13
|
+
reactivity/primitives.py,sha256=mB6cbHKDqtilOfgaEhshtRWJq9s0nPEKqRK0hfCoyFE,5671
|
14
|
+
hmr-0.4.0.dist-info/RECORD,,
|
reactivity/helpers.py
CHANGED
@@ -1,10 +1,9 @@
|
|
1
1
|
from collections import defaultdict
|
2
2
|
from collections.abc import Callable, Mapping, MutableMapping
|
3
|
-
from functools import partial
|
4
3
|
from typing import Self, overload
|
5
4
|
from weakref import WeakKeyDictionary
|
6
5
|
|
7
|
-
from .primitives import BaseComputation, Batch, Signal, Subscribable
|
6
|
+
from .primitives import BaseComputation, Batch, Derived, Signal, Subscribable
|
8
7
|
|
9
8
|
|
10
9
|
class Memoized[T](Subscribable, BaseComputation[T]):
|
@@ -54,7 +53,7 @@ class MemoizedProperty[T, I]:
|
|
54
53
|
return self
|
55
54
|
if func := self.map.get(instance):
|
56
55
|
return func()
|
57
|
-
self.map[instance] = func = Memoized(
|
56
|
+
self.map[instance] = func = Memoized(self.method.__get__(instance, owner))
|
58
57
|
return func()
|
59
58
|
|
60
59
|
|
@@ -74,7 +73,7 @@ class MemoizedMethod[T, I]:
|
|
74
73
|
return self
|
75
74
|
if memo := self.map.get(instance):
|
76
75
|
return memo
|
77
|
-
self.map[instance] = memo = Memoized(
|
76
|
+
self.map[instance] = memo = Memoized(self.method.__get__(instance, owner))
|
78
77
|
return memo
|
79
78
|
|
80
79
|
|
@@ -99,7 +98,7 @@ class Reactive[K, V](Subscribable, MutableMapping[K, V]):
|
|
99
98
|
return value
|
100
99
|
|
101
100
|
def __setitem__(self, key: K, value: V):
|
102
|
-
with Batch():
|
101
|
+
with Batch(force_flush=False):
|
103
102
|
self._signals[key].set(value)
|
104
103
|
self.notify()
|
105
104
|
|
@@ -107,7 +106,7 @@ class Reactive[K, V](Subscribable, MutableMapping[K, V]):
|
|
107
106
|
state = self._signals[key]
|
108
107
|
if state.get(track=False) is self.UNSET:
|
109
108
|
raise KeyError(key)
|
110
|
-
with Batch():
|
109
|
+
with Batch(force_flush=False):
|
111
110
|
state.set(self.UNSET)
|
112
111
|
self.notify()
|
113
112
|
|
@@ -121,8 +120,51 @@ class Reactive[K, V](Subscribable, MutableMapping[K, V]):
|
|
121
120
|
|
122
121
|
def __repr__(self):
|
123
122
|
self.track()
|
124
|
-
|
123
|
+
unset = self.UNSET
|
124
|
+
return repr({k: value for k, v in self._signals.items() if (value := v.get()) is not unset})
|
125
125
|
|
126
126
|
def items(self):
|
127
127
|
self.track()
|
128
128
|
return ({k: v.get() for k, v in self._signals.items()}).items()
|
129
|
+
|
130
|
+
|
131
|
+
class DerivedProperty[T, I]:
|
132
|
+
def __init__(self, method: Callable[[I], T]):
|
133
|
+
super().__init__()
|
134
|
+
self.method = method
|
135
|
+
self.map = WeakKeyDictionary[I, Derived[T]]()
|
136
|
+
|
137
|
+
@overload
|
138
|
+
def __get__(self, instance: None, owner: type[I]) -> Self: ...
|
139
|
+
@overload
|
140
|
+
def __get__(self, instance: I, owner: type[I]) -> T: ...
|
141
|
+
|
142
|
+
def __get__(self, instance: I | None, owner):
|
143
|
+
if instance is None:
|
144
|
+
return self
|
145
|
+
if func := self.map.get(instance):
|
146
|
+
return func()
|
147
|
+
self.map[instance] = func = Derived(self.method.__get__(instance, owner))
|
148
|
+
return func()
|
149
|
+
|
150
|
+
|
151
|
+
class DerivedMethod[T, I]:
|
152
|
+
def __init__(self, method: Callable[[I], T], check_equality=True):
|
153
|
+
super().__init__()
|
154
|
+
self.method = method
|
155
|
+
self.check_equality = check_equality
|
156
|
+
self.map = WeakKeyDictionary[I, Derived[T]]()
|
157
|
+
|
158
|
+
@overload
|
159
|
+
def __get__(self, instance: None, owner: type[I]) -> Self: ...
|
160
|
+
@overload
|
161
|
+
def __get__(self, instance: I, owner: type[I]) -> Derived[T]: ...
|
162
|
+
|
163
|
+
def __get__(self, instance: I | None, owner):
|
164
|
+
if instance is None:
|
165
|
+
return self
|
166
|
+
if func := self.map.get(instance):
|
167
|
+
return func
|
168
|
+
|
169
|
+
self.map[instance] = func = Derived(self.method.__get__(instance, owner), self.check_equality)
|
170
|
+
return func
|
reactivity/hmr/api.py
CHANGED
reactivity/hmr/core.py
CHANGED
@@ -5,14 +5,17 @@ from functools import cached_property
|
|
5
5
|
from importlib.abc import Loader, MetaPathFinder
|
6
6
|
from importlib.machinery import ModuleSpec
|
7
7
|
from importlib.util import spec_from_loader
|
8
|
-
from inspect import currentframe
|
8
|
+
from inspect import currentframe, ismethod
|
9
9
|
from pathlib import Path
|
10
10
|
from site import getsitepackages
|
11
11
|
from types import ModuleType, TracebackType
|
12
12
|
from typing import Any, Self
|
13
13
|
from weakref import WeakValueDictionary
|
14
14
|
|
15
|
-
from .. import Reactive, batch
|
15
|
+
from .. import Reactive, batch
|
16
|
+
from ..functional import create_effect
|
17
|
+
from ..helpers import DerivedMethod
|
18
|
+
from ..primitives import BaseDerived, Derived, Signal
|
16
19
|
from .hooks import call_post_reload_hooks, call_pre_reload_hooks
|
17
20
|
|
18
21
|
|
@@ -29,10 +32,31 @@ def is_called_in_this_file() -> bool:
|
|
29
32
|
return frame.f_globals.get("__file__") == __file__
|
30
33
|
|
31
34
|
|
35
|
+
class Name(Signal, BaseDerived):
|
36
|
+
def __init__(self, initial_value):
|
37
|
+
super().__init__(initial_value)
|
38
|
+
|
39
|
+
|
32
40
|
class NamespaceProxy(Reactive[str, Any]):
|
33
|
-
def __init__(self, initial: MutableMapping, check_equality=True):
|
41
|
+
def __init__(self, initial: MutableMapping, module: "ReactiveModule", check_equality=True):
|
34
42
|
super().__init__(initial, check_equality)
|
35
43
|
self._original = initial
|
44
|
+
self.module = module
|
45
|
+
|
46
|
+
def _null(self):
|
47
|
+
self.module.load.subscribers.add(signal := Name(self.UNSET))
|
48
|
+
signal.dependencies.add(self.module.load)
|
49
|
+
return signal
|
50
|
+
|
51
|
+
def __getitem__(self, key: str):
|
52
|
+
try:
|
53
|
+
return super().__getitem__(key)
|
54
|
+
finally:
|
55
|
+
signal = self._signals[key]
|
56
|
+
if self.module.load in signal.subscribers:
|
57
|
+
# a module's loader shouldn't subscribe its variables
|
58
|
+
signal.subscribers.remove(self.module.load)
|
59
|
+
self.module.load.dependencies.remove(signal)
|
36
60
|
|
37
61
|
def __setitem__(self, key, value):
|
38
62
|
self._original[key] = value
|
@@ -53,7 +77,7 @@ class ReactiveModule(ModuleType):
|
|
53
77
|
self.__is_initialized = True
|
54
78
|
|
55
79
|
self.__namespace = namespace
|
56
|
-
self.__namespace_proxy = NamespaceProxy(namespace)
|
80
|
+
self.__namespace_proxy = NamespaceProxy(namespace, self)
|
57
81
|
self.__file = file
|
58
82
|
|
59
83
|
self.instances[file.resolve()] = self
|
@@ -64,7 +88,7 @@ class ReactiveModule(ModuleType):
|
|
64
88
|
return self.__file
|
65
89
|
raise AttributeError("file")
|
66
90
|
|
67
|
-
@
|
91
|
+
@DerivedMethod
|
68
92
|
def __load(self):
|
69
93
|
try:
|
70
94
|
code = compile(self.__file.read_text("utf-8"), str(self.__file), "exec", dont_inherit=True)
|
@@ -72,6 +96,13 @@ class ReactiveModule(ModuleType):
|
|
72
96
|
sys.excepthook(type(e), e, e.__traceback__)
|
73
97
|
else:
|
74
98
|
exec(code, self.__namespace, self.__namespace_proxy)
|
99
|
+
finally:
|
100
|
+
for dep in list((load := self.__load).dependencies):
|
101
|
+
assert ismethod(load.fn) # for type narrowing
|
102
|
+
if isinstance(dep, Derived) and ismethod(dep.fn) and isinstance(dep.fn.__self__, ReactiveModule) and dep.fn.__func__ is load.fn.__func__:
|
103
|
+
# unsubscribe it because we want invalidation to be fine-grained
|
104
|
+
dep.subscribers.remove(load)
|
105
|
+
load.dependencies.remove(dep)
|
75
106
|
|
76
107
|
@property
|
77
108
|
def load(self):
|
@@ -91,6 +122,8 @@ class ReactiveModule(ModuleType):
|
|
91
122
|
try:
|
92
123
|
return self.__namespace_proxy[name]
|
93
124
|
except KeyError as e:
|
125
|
+
if getattr := self.__namespace_proxy.get("__getattr__"):
|
126
|
+
return getattr(name)
|
94
127
|
raise AttributeError(*e.args) from e
|
95
128
|
|
96
129
|
def __setattr__(self, name: str, value):
|
@@ -200,14 +233,9 @@ class BaseReloader:
|
|
200
233
|
namespace = {"__file__": self.entry, "__name__": "__main__"}
|
201
234
|
return ReactiveModule(Path(self.entry), namespace, "__main__")
|
202
235
|
|
203
|
-
@memoized_method
|
204
236
|
def run_entry_file(self):
|
205
|
-
|
206
|
-
|
207
|
-
self.entry_module.load.invalidate()
|
208
|
-
self.entry_module.load()
|
209
|
-
|
210
|
-
call_post_reload_hooks()
|
237
|
+
with self.error_filter:
|
238
|
+
self.entry_module.load()
|
211
239
|
|
212
240
|
@property
|
213
241
|
def watch_filter(self):
|
@@ -222,22 +250,23 @@ class BaseReloader:
|
|
222
250
|
return
|
223
251
|
|
224
252
|
path2module = get_path_module_map()
|
253
|
+
staled_modules: set[ReactiveModule] = set()
|
254
|
+
|
255
|
+
call_pre_reload_hooks()
|
225
256
|
|
226
257
|
with batch():
|
227
258
|
for type, file in events:
|
228
259
|
if type is not Change.deleted:
|
229
260
|
path = Path(file).resolve()
|
230
|
-
if
|
231
|
-
|
232
|
-
elif module := path2module.get(path):
|
233
|
-
module.load.invalidate()
|
261
|
+
if module := path2module.get(path):
|
262
|
+
staled_modules.add(module)
|
234
263
|
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
264
|
+
for module in staled_modules:
|
265
|
+
with self.error_filter:
|
266
|
+
module.load.invalidate()
|
267
|
+
module.load() # because `module.load` is not pulled by anyone
|
268
|
+
|
269
|
+
call_post_reload_hooks()
|
241
270
|
|
242
271
|
|
243
272
|
class _SimpleEvent:
|
@@ -268,11 +297,10 @@ class SyncReloader(BaseReloader):
|
|
268
297
|
del self._stop_event
|
269
298
|
|
270
299
|
def keep_watching_until_interrupt(self):
|
271
|
-
|
272
|
-
|
273
|
-
|
300
|
+
call_pre_reload_hooks()
|
301
|
+
with suppress(KeyboardInterrupt), create_effect(self.run_entry_file):
|
302
|
+
call_post_reload_hooks()
|
274
303
|
self.start_watching()
|
275
|
-
self.run_entry_file.dispose()
|
276
304
|
|
277
305
|
|
278
306
|
class AsyncReloader(BaseReloader):
|
@@ -294,11 +322,8 @@ class AsyncReloader(BaseReloader):
|
|
294
322
|
del self._stop_event
|
295
323
|
|
296
324
|
async def keep_watching_until_interrupt(self):
|
297
|
-
with suppress(KeyboardInterrupt):
|
298
|
-
with self.error_filter:
|
299
|
-
self.run_entry_file()
|
325
|
+
with suppress(KeyboardInterrupt), create_effect(self.run_entry_file):
|
300
326
|
await self.start_watching()
|
301
|
-
self.run_entry_file.dispose()
|
302
327
|
|
303
328
|
|
304
329
|
def cli():
|
@@ -312,4 +337,4 @@ def cli():
|
|
312
337
|
SyncReloader(entry).keep_watching_until_interrupt()
|
313
338
|
|
314
339
|
|
315
|
-
__version__ = "0.
|
340
|
+
__version__ = "0.4.0"
|
reactivity/hmr/utils.py
CHANGED
reactivity/primitives.py
CHANGED
@@ -156,3 +156,60 @@ _batches: list[Batch] = []
|
|
156
156
|
|
157
157
|
def schedule_callbacks(callbacks: Iterable[BaseComputation]):
|
158
158
|
_batches[-1].callbacks.update(callbacks)
|
159
|
+
|
160
|
+
|
161
|
+
class BaseDerived[T](Subscribable, BaseComputation[T]):
|
162
|
+
def __init__(self):
|
163
|
+
super().__init__()
|
164
|
+
self.dirty = True
|
165
|
+
|
166
|
+
def _sync_dirty_deps(self):
|
167
|
+
for dep in self.dependencies:
|
168
|
+
if isinstance(dep, BaseDerived) and dep.dirty:
|
169
|
+
dep()
|
170
|
+
|
171
|
+
|
172
|
+
class Derived[T](BaseDerived[T]):
|
173
|
+
UNSET: T = object() # type: ignore
|
174
|
+
|
175
|
+
def __init__(self, fn: Callable[[], T], check_equality=True):
|
176
|
+
super().__init__()
|
177
|
+
self.fn = fn
|
178
|
+
self._check_equality = check_equality
|
179
|
+
self._value = self.UNSET
|
180
|
+
|
181
|
+
def recompute(self):
|
182
|
+
self._before()
|
183
|
+
try:
|
184
|
+
value = self.fn()
|
185
|
+
self.dirty = False
|
186
|
+
if self._check_equality:
|
187
|
+
if value == self._value:
|
188
|
+
return
|
189
|
+
elif self._value is self.UNSET: # do not notify on first set
|
190
|
+
self._value = value
|
191
|
+
return
|
192
|
+
self._value = value
|
193
|
+
self.notify()
|
194
|
+
finally:
|
195
|
+
self._after()
|
196
|
+
|
197
|
+
def __call__(self):
|
198
|
+
self.track()
|
199
|
+
self._sync_dirty_deps()
|
200
|
+
if self.dirty:
|
201
|
+
self.recompute()
|
202
|
+
|
203
|
+
return self._value
|
204
|
+
|
205
|
+
def trigger(self):
|
206
|
+
self.dirty = True
|
207
|
+
if _pulled(self):
|
208
|
+
self()
|
209
|
+
|
210
|
+
def invalidate(self):
|
211
|
+
self.trigger()
|
212
|
+
|
213
|
+
|
214
|
+
def _pulled(sub: Subscribable):
|
215
|
+
return any(not isinstance(s, BaseDerived) or _pulled(s) for s in sub.subscribers)
|
hmr-0.3.3.3.dist-info/RECORD
DELETED
@@ -1,13 +0,0 @@
|
|
1
|
-
hmr-0.3.3.3.dist-info/METADATA,sha256=TWN_VystOGF0zdKckjiayJus6XK7GJ-L03xlibtXs2E,260
|
2
|
-
hmr-0.3.3.3.dist-info/WHEEL,sha256=thaaA2w1JzcGC48WYufAs8nrYZjJm8LqNfnXFOFyCC4,90
|
3
|
-
hmr-0.3.3.3.dist-info/entry_points.txt,sha256=g_T0uJ43WgsdG14kkkdaBQuIL0HO-m1qvtjXMP6d060,59
|
4
|
-
reactivity/__init__.py,sha256=pX-RUzkezCC1x4eOWGxNhXbwrbvBLP_3pQuZr9eZz1Y,300
|
5
|
-
reactivity/functional.py,sha256=U06vshcVhZ0sb218gcmHtEhfgTNAGtQ7zyvPz2w5qKM,1292
|
6
|
-
reactivity/helpers.py,sha256=7gwsIKKrjEahSz9G9oR4s1LdYXQTCIMO0k4UGXGla9Y,3714
|
7
|
-
reactivity/hmr/__init__.py,sha256=S5ZIHqCRpevdzWuhS0aCua_S8F0LkK0YNg6IgeTScFQ,177
|
8
|
-
reactivity/hmr/api.py,sha256=-0-6Tn0AVkaDs7_qrCCd9TXxRTPDMDB08-UYfepTdec,1644
|
9
|
-
reactivity/hmr/core.py,sha256=TSbL1AMiel2eKzk23z1Sb8ruCTx9s5m6blbj0L9RGD4,10252
|
10
|
-
reactivity/hmr/hooks.py,sha256=-yLr5ktiyqPb1nDbHsgv6-c_ZkziBjNqCU-0PCfXGYU,592
|
11
|
-
reactivity/hmr/utils.py,sha256=zM7X5I8ywOguOt20uE55INWOGen3FuJZDAvgouu7lI8,1550
|
12
|
-
reactivity/primitives.py,sha256=DR2waJbzhVKOioHXMliE4FIsxQUq7DZA0umPrlvchA4,4217
|
13
|
-
hmr-0.3.3.3.dist-info/RECORD,,
|
File without changes
|