hmr 0.1.2__tar.gz → 0.2.1__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.2
3
+ Version: 0.2.1
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.2"
9
+ version = "0.2.1"
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,6 +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 site import getsitepackages
11
12
  from types import ModuleType, TracebackType
12
13
  from typing import Any
13
14
 
@@ -115,7 +116,7 @@ class ReactiveModuleFinder(MetaPathFinder):
115
116
  def __init__(self, includes: Iterable[str] = ".", excludes: Iterable[str] = ()):
116
117
  super().__init__()
117
118
  self.includes = [Path(i).resolve() for i in includes]
118
- self.excludes = [Path(e).resolve() for e in excludes]
119
+ self.excludes = [Path(e).resolve() for e in (*excludes, *getsitepackages())]
119
120
 
120
121
  def _accept(self, path: Path):
121
122
  return path.is_file() and not is_relative_to_any(path, self.excludes) and is_relative_to_any(path, self.includes)
@@ -149,7 +150,7 @@ def patch_module(name_or_module: str | ModuleType):
149
150
  return m
150
151
 
151
152
 
152
- def patch_meta_path(includes: Iterable[str] = (".",), excludes: Iterable[str] = (".venv",)):
153
+ def patch_meta_path(includes: Iterable[str] = (".",), excludes: Iterable[str] = ()):
153
154
  sys.meta_path.insert(0, ReactiveModuleFinder(includes, excludes))
154
155
 
155
156
 
@@ -278,9 +279,10 @@ def cli():
278
279
  print("\n Usage: hmr <entry file>, just like python <entry file>\n")
279
280
  exit(1)
280
281
  entry = sys.argv[-1]
281
- assert Path(entry).is_file(), f"{entry} is not a file"
282
+ if not (path := Path(entry)).is_file():
283
+ raise FileNotFoundError(path.resolve())
282
284
  sys.path.insert(0, ".")
283
- SyncReloader(entry, excludes={".venv"}).keep_watching_until_interrupt()
285
+ SyncReloader(entry).keep_watching_until_interrupt()
284
286
 
285
287
 
286
- __version__ = "0.1.2"
288
+ __version__ = "0.2.1"
@@ -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