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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: hmr
3
- Version: 0.3.3.3
3
+ Version: 0.4.0
4
4
  Summary: Hot Module Reload for Python
5
5
  Project-URL: repository, https://github.com/promplate/pyth-on-line/tree/reactivity
6
6
  Requires-Python: >=3.12
@@ -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,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: pdm-backend (2.4.3)
2
+ Generator: pdm-backend (2.4.4)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
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(partial(self.method, instance))
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(partial(self.method, instance))
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
- return repr({k: v.get() for k, v in self._signals.items()})
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
@@ -0,0 +1,4 @@
1
+ if __name__ == "__main__":
2
+ from .core import cli
3
+
4
+ cli()
reactivity/hmr/api.py CHANGED
@@ -2,8 +2,6 @@ from .core import AsyncReloader, BaseReloader, SyncReloader
2
2
 
3
3
 
4
4
  def _clean_up(r: BaseReloader):
5
- r.run_entry_file.dispose()
6
- r.run_entry_file.invalidate()
7
5
  r.entry_module.load.dispose()
8
6
  r.entry_module.load.invalidate()
9
7
 
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, memoized_method
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
- @memoized_method
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
- call_pre_reload_hooks()
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 path.samefile(self.entry):
231
- self.run_entry_file.invalidate()
232
- elif module := path2module.get(path):
233
- module.load.invalidate()
261
+ if module := path2module.get(path):
262
+ staled_modules.add(module)
234
263
 
235
- with self.error_filter:
236
- for module in path2module.values():
237
- if module.file.samefile(self.entry):
238
- continue
239
- module.load()
240
- self.run_entry_file()
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
- with suppress(KeyboardInterrupt):
272
- with self.error_filter:
273
- self.run_entry_file()
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.3.3.3"
340
+ __version__ = "0.4.0"
reactivity/hmr/utils.py CHANGED
@@ -50,7 +50,7 @@ def cache_across_reloads[T](func: Callable[[], T]) -> Callable[[], T]:
50
50
 
51
51
  @wraps(func)
52
52
  @create_memo
53
- def wrapper():
53
+ def wrapper() -> T:
54
54
  return functions[source]()
55
55
 
56
56
  memos[source] = wrapper
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)
@@ -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,,