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.
@@ -1,8 +1,8 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: hmr
3
- Version: 0.1.1.1
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.1.1.1"
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
- self._recompute = False
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.track()
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._recompute = True
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
- code = compile(self.__file.read_text("utf-8"), str(self.__file), "exec", dont_inherit=True)
64
- exec(code, self.__namespace, self.__namespace_proxy)
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
- try:
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
- try:
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
- try:
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
- assert Path(entry).is_file(), f"{entry} is not a file"
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.1.1.1"
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
- for computation in _current_computations:
13
- if computation is not self:
14
- self.subscribers.add(computation)
15
- computation.dependencies.add(self)
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
- _batches[-1].callbacks.extend(self.subscribers)
21
+ schedule_callbacks(self.subscribers)
20
22
  else:
21
- for subscriber in {*self.subscribers}:
22
- subscriber.trigger()
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) -> T: ...
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: list[BaseComputation] = []
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
- callbacks = set(self.callbacks)
127
- self.callbacks.clear()
128
- for computation in callbacks:
129
- computation.trigger()
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
- last = _batches.pop()
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