hmr 0.1.1.1__tar.gz → 0.2.0__tar.gz
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.1.1.1 → hmr-0.2.0}/PKG-INFO +2 -2
- {hmr-0.1.1.1 → hmr-0.2.0}/pyproject.toml +2 -2
- {hmr-0.1.1.1 → hmr-0.2.0}/reactivity/helpers.py +12 -15
- {hmr-0.1.1.1 → hmr-0.2.0}/reactivity/hmr.py +38 -14
- {hmr-0.1.1.1 → hmr-0.2.0}/reactivity/primitives.py +36 -18
- {hmr-0.1.1.1 → hmr-0.2.0}/reactivity/__init__.py +0 -0
- {hmr-0.1.1.1 → hmr-0.2.0}/reactivity/functional.py +0 -0
@@ -1,8 +1,8 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: hmr
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.2.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
|
7
|
-
Requires-Dist: watchfiles<2,>=0.21
|
7
|
+
Requires-Dist: watchfiles<2,>=0.21; sys_platform != "emscripten"
|
8
8
|
|
@@ -4,9 +4,9 @@ dynamic = []
|
|
4
4
|
requires-python = ">=3.12"
|
5
5
|
description = "Hot Module Reload for Python"
|
6
6
|
dependencies = [
|
7
|
-
"watchfiles>=0.21,<2",
|
7
|
+
"watchfiles>=0.21,<2 ; sys_platform != 'emscripten'",
|
8
8
|
]
|
9
|
-
version = "0.
|
9
|
+
version = "0.2.0"
|
10
10
|
|
11
11
|
[project.scripts]
|
12
12
|
hmr = "reactivity.hmr:cli"
|
@@ -13,32 +13,29 @@ class Memoized[T](Subscribable, BaseComputation[T]):
|
|
13
13
|
self.fn = fn
|
14
14
|
self.is_stale = True
|
15
15
|
self.cached_value: T
|
16
|
-
|
16
|
+
|
17
|
+
def recompute(self):
|
18
|
+
self._before()
|
19
|
+
try:
|
20
|
+
self.cached_value = self.fn()
|
21
|
+
self.is_stale = False
|
22
|
+
finally:
|
23
|
+
self._after()
|
17
24
|
|
18
25
|
def trigger(self):
|
19
|
-
self.
|
20
|
-
if self._recompute:
|
21
|
-
self._recompute = False
|
22
|
-
self._before()
|
23
|
-
try:
|
24
|
-
self.cached_value = self.fn()
|
25
|
-
self.is_stale = False
|
26
|
-
finally:
|
27
|
-
self._after()
|
28
|
-
else:
|
29
|
-
self.invalidate()
|
26
|
+
self.invalidate()
|
30
27
|
|
31
28
|
def __call__(self):
|
29
|
+
self.track()
|
32
30
|
if self.is_stale:
|
33
|
-
self.
|
34
|
-
self.trigger()
|
35
|
-
assert not self._recompute
|
31
|
+
self.recompute()
|
36
32
|
return self.cached_value
|
37
33
|
|
38
34
|
def invalidate(self):
|
39
35
|
if not self.is_stale:
|
40
36
|
del self.cached_value
|
41
37
|
self.is_stale = True
|
38
|
+
self.notify()
|
42
39
|
|
43
40
|
|
44
41
|
class MemoizedProperty[T, I]:
|
@@ -8,7 +8,7 @@ from importlib.util import spec_from_loader
|
|
8
8
|
from inspect import currentframe
|
9
9
|
from pathlib import Path
|
10
10
|
from runpy import run_path
|
11
|
-
from types import ModuleType
|
11
|
+
from types import ModuleType, TracebackType
|
12
12
|
from typing import Any
|
13
13
|
|
14
14
|
from . import Reactive, batch, memoized_method
|
@@ -60,8 +60,12 @@ class ReactiveModule(ModuleType):
|
|
60
60
|
|
61
61
|
@memoized_method
|
62
62
|
def __load(self):
|
63
|
-
|
64
|
-
|
63
|
+
try:
|
64
|
+
code = compile(self.__file.read_text("utf-8"), str(self.__file), "exec", dont_inherit=True)
|
65
|
+
except SyntaxError as e:
|
66
|
+
sys.excepthook(type(e), e, e.__traceback__)
|
67
|
+
else:
|
68
|
+
exec(code, self.__namespace, self.__namespace_proxy)
|
65
69
|
|
66
70
|
@property
|
67
71
|
def load(self):
|
@@ -153,6 +157,30 @@ def get_path_module_map():
|
|
153
157
|
return {module.file.resolve(): module for module in sys.modules.values() if isinstance(module, ReactiveModule)}
|
154
158
|
|
155
159
|
|
160
|
+
class ErrorFilter:
|
161
|
+
def __init__(self, *exclude_filenames: str):
|
162
|
+
self.exclude_filenames = set(exclude_filenames)
|
163
|
+
|
164
|
+
def __call__(self, tb: TracebackType):
|
165
|
+
current = tb
|
166
|
+
while current is not None:
|
167
|
+
if current.tb_frame.f_code.co_filename not in self.exclude_filenames:
|
168
|
+
return current
|
169
|
+
current = current.tb_next
|
170
|
+
return tb
|
171
|
+
|
172
|
+
def __enter__(self):
|
173
|
+
return self
|
174
|
+
|
175
|
+
def __exit__(self, exc_type: type[BaseException], exc_value: BaseException, traceback: TracebackType):
|
176
|
+
if exc_value is None:
|
177
|
+
return
|
178
|
+
tb = self(traceback)
|
179
|
+
exc_value = exc_value.with_traceback(tb)
|
180
|
+
sys.excepthook(exc_type, exc_value, tb)
|
181
|
+
return True
|
182
|
+
|
183
|
+
|
156
184
|
class BaseReloader:
|
157
185
|
def __init__(self, entry_file: str, includes: Iterable[str] = (".",), excludes: Iterable[str] = ()):
|
158
186
|
self.entry = entry_file
|
@@ -160,13 +188,12 @@ class BaseReloader:
|
|
160
188
|
self.excludes = excludes
|
161
189
|
patch_meta_path(includes, excludes)
|
162
190
|
self.last_globals = {}
|
191
|
+
self.error_filter = ErrorFilter(__file__, "<frozen runpy>")
|
163
192
|
|
164
193
|
@memoized_method
|
165
194
|
def run_entry_file(self):
|
166
|
-
|
195
|
+
with self.error_filter:
|
167
196
|
self.last_globals = run_path(self.entry, self.last_globals, "__main__")
|
168
|
-
except Exception as e:
|
169
|
-
sys.excepthook(e.__class__, e, e.__traceback__)
|
170
197
|
|
171
198
|
@property
|
172
199
|
def watch_filter(self):
|
@@ -189,16 +216,12 @@ class BaseReloader:
|
|
189
216
|
if path.samefile(self.entry):
|
190
217
|
self.run_entry_file.invalidate()
|
191
218
|
elif module := path2module.get(path):
|
192
|
-
|
219
|
+
with self.error_filter:
|
193
220
|
module.load.invalidate()
|
194
|
-
except Exception as e:
|
195
|
-
sys.excepthook(e.__class__, e, e.__traceback__)
|
196
221
|
|
197
222
|
for module in path2module.values():
|
198
|
-
|
223
|
+
with self.error_filter:
|
199
224
|
module.load()
|
200
|
-
except Exception as e:
|
201
|
-
sys.excepthook(e.__class__, e, e.__traceback__)
|
202
225
|
self.run_entry_file()
|
203
226
|
|
204
227
|
|
@@ -255,9 +278,10 @@ def cli():
|
|
255
278
|
print("\n Usage: hmr <entry file>, just like python <entry file>\n")
|
256
279
|
exit(1)
|
257
280
|
entry = sys.argv[-1]
|
258
|
-
|
281
|
+
if not (path := Path(entry)).is_file():
|
282
|
+
raise FileNotFoundError(path.resolve())
|
259
283
|
sys.path.insert(0, ".")
|
260
284
|
SyncReloader(entry, excludes={".venv"}).keep_watching_until_interrupt()
|
261
285
|
|
262
286
|
|
263
|
-
__version__ = "0.
|
287
|
+
__version__ = "0.2.0"
|
@@ -1,4 +1,4 @@
|
|
1
|
-
from collections.abc import Callable
|
1
|
+
from collections.abc import Callable, Iterable
|
2
2
|
from typing import Any, Self, overload
|
3
3
|
from weakref import WeakKeyDictionary, WeakSet
|
4
4
|
|
@@ -9,17 +9,19 @@ class Subscribable:
|
|
9
9
|
self.subscribers = set[BaseComputation]()
|
10
10
|
|
11
11
|
def track(self):
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
12
|
+
if not _current_computations:
|
13
|
+
return
|
14
|
+
last = _current_computations[-1]
|
15
|
+
if last is not self:
|
16
|
+
self.subscribers.add(last)
|
17
|
+
last.dependencies.add(self)
|
16
18
|
|
17
19
|
def notify(self):
|
18
20
|
if _batches:
|
19
|
-
|
21
|
+
schedule_callbacks(self.subscribers)
|
20
22
|
else:
|
21
|
-
|
22
|
-
|
23
|
+
with Batch(force_flush=False):
|
24
|
+
schedule_callbacks(self.subscribers)
|
23
25
|
|
24
26
|
|
25
27
|
class BaseComputation[T]:
|
@@ -46,9 +48,9 @@ class BaseComputation[T]:
|
|
46
48
|
def __exit__(self, *_):
|
47
49
|
self.dispose()
|
48
50
|
|
49
|
-
def trigger(self) ->
|
51
|
+
def trigger(self) -> Any: ...
|
50
52
|
|
51
|
-
def __call__(self):
|
53
|
+
def __call__(self) -> T:
|
52
54
|
return self.trigger()
|
53
55
|
|
54
56
|
|
@@ -119,22 +121,38 @@ class Effect[T](BaseComputation[T]):
|
|
119
121
|
|
120
122
|
|
121
123
|
class Batch:
|
122
|
-
def __init__(self):
|
123
|
-
self.callbacks
|
124
|
+
def __init__(self, force_flush=True):
|
125
|
+
self.callbacks = set[BaseComputation]()
|
126
|
+
self.force_flush = force_flush
|
124
127
|
|
125
128
|
def flush(self):
|
126
|
-
|
127
|
-
self.callbacks
|
128
|
-
|
129
|
-
|
129
|
+
triggered = set()
|
130
|
+
while self.callbacks:
|
131
|
+
callbacks = self.callbacks - triggered
|
132
|
+
self.callbacks.clear()
|
133
|
+
for computation in callbacks:
|
134
|
+
if computation in self.callbacks:
|
135
|
+
continue # skip if re-added during callback
|
136
|
+
computation.trigger()
|
137
|
+
triggered.add(computation)
|
130
138
|
|
131
139
|
def __enter__(self):
|
132
140
|
_batches.append(self)
|
133
141
|
|
134
142
|
def __exit__(self, *_):
|
135
|
-
|
143
|
+
if self.force_flush or len(_batches) == 1:
|
144
|
+
try:
|
145
|
+
self.flush()
|
146
|
+
finally:
|
147
|
+
last = _batches.pop()
|
148
|
+
else:
|
149
|
+
last = _batches.pop()
|
150
|
+
schedule_callbacks(self.callbacks)
|
136
151
|
assert last is self
|
137
|
-
self.flush()
|
138
152
|
|
139
153
|
|
140
154
|
_batches: list[Batch] = []
|
155
|
+
|
156
|
+
|
157
|
+
def schedule_callbacks(callbacks: Iterable[BaseComputation]):
|
158
|
+
_batches[-1].callbacks.update(callbacks)
|
File without changes
|
File without changes
|