hmr 0.6.2.1__tar.gz → 0.6.3__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.6.2.1 → hmr-0.6.3}/PKG-INFO +1 -1
- {hmr-0.6.2.1 → hmr-0.6.3}/pyproject.toml +1 -1
- hmr-0.6.3/reactivity/hmr/_experimental.py +23 -0
- {hmr-0.6.2.1 → hmr-0.6.3}/reactivity/hmr/core.py +18 -6
- hmr-0.6.3/reactivity/hmr/exec_hack/__init__.py +7 -0
- hmr-0.6.3/reactivity/hmr/exec_hack/transform.py +61 -0
- {hmr-0.6.2.1 → hmr-0.6.3}/reactivity/hmr/utils.py +11 -0
- {hmr-0.6.2.1 → hmr-0.6.3}/reactivity/primitives.py +2 -2
- {hmr-0.6.2.1 → hmr-0.6.3}/README.md +0 -0
- {hmr-0.6.2.1 → hmr-0.6.3}/reactivity/__init__.py +0 -0
- {hmr-0.6.2.1 → hmr-0.6.3}/reactivity/context.py +0 -0
- {hmr-0.6.2.1 → hmr-0.6.3}/reactivity/functional.py +0 -0
- {hmr-0.6.2.1 → hmr-0.6.3}/reactivity/helpers.py +0 -0
- {hmr-0.6.2.1 → hmr-0.6.3}/reactivity/hmr/__init__.py +0 -0
- {hmr-0.6.2.1 → hmr-0.6.3}/reactivity/hmr/__main__.py +0 -0
- {hmr-0.6.2.1 → hmr-0.6.3}/reactivity/hmr/api.py +0 -0
- {hmr-0.6.2.1 → hmr-0.6.3}/reactivity/hmr/hooks.py +0 -0
- {hmr-0.6.2.1 → hmr-0.6.3}/reactivity/hmr/proxy.py +0 -0
@@ -0,0 +1,23 @@
|
|
1
|
+
from collections.abc import Callable
|
2
|
+
|
3
|
+
from reactivity.context import Context
|
4
|
+
|
5
|
+
from ..primitives import BaseDerived, _equal
|
6
|
+
|
7
|
+
|
8
|
+
class Dirty[T](BaseDerived[T]):
|
9
|
+
UNSET = object()
|
10
|
+
|
11
|
+
def __init__(self, fn: Callable[[], T], *, context: Context | None = None):
|
12
|
+
super().__init__(context=context)
|
13
|
+
self.fn = fn
|
14
|
+
self.value = __class__.UNSET
|
15
|
+
|
16
|
+
def trigger(self):
|
17
|
+
self.track()
|
18
|
+
with self._enter():
|
19
|
+
value = self.fn()
|
20
|
+
if not _equal(value, self.value):
|
21
|
+
self.value = value
|
22
|
+
self.notify() # BUG: b -> a <- (b, c) will cause Dirty a to re-evaluate by c
|
23
|
+
return value
|
@@ -13,8 +13,9 @@ from typing import Self
|
|
13
13
|
from weakref import WeakValueDictionary
|
14
14
|
|
15
15
|
from ..context import Context, new_context
|
16
|
-
from ..helpers import DerivedMethod
|
16
|
+
from ..helpers import DerivedMethod, DerivedProperty
|
17
17
|
from ..primitives import BaseDerived, Derived, Signal
|
18
|
+
from ._experimental import Dirty
|
18
19
|
from .hooks import call_post_reload_hooks, call_pre_reload_hooks
|
19
20
|
from .proxy import Proxy
|
20
21
|
|
@@ -114,7 +115,7 @@ class ReactiveModule(ModuleType):
|
|
114
115
|
|
115
116
|
def __getattr__(self, name: str):
|
116
117
|
try:
|
117
|
-
return self.__namespace_proxy[name]
|
118
|
+
return self.__namespace_proxy[name] if name != "__path__" else self.__namespace[name]
|
118
119
|
except KeyError as e:
|
119
120
|
if name != "__path__" and (getattr := self.__namespace_proxy.get("__getattr__")):
|
120
121
|
return getattr(name)
|
@@ -143,6 +144,11 @@ class ReactiveModuleLoader(Loader):
|
|
143
144
|
module.load()
|
144
145
|
|
145
146
|
|
147
|
+
@Dirty
|
148
|
+
def sys_path(): # TODO: Path(".") may change too
|
149
|
+
return sys.path
|
150
|
+
|
151
|
+
|
146
152
|
class ReactiveModuleFinder(MetaPathFinder):
|
147
153
|
def __init__(self, includes: Iterable[str] = ".", excludes: Iterable[str] = ()):
|
148
154
|
super().__init__()
|
@@ -152,12 +158,18 @@ class ReactiveModuleFinder(MetaPathFinder):
|
|
152
158
|
def _accept(self, path: Path):
|
153
159
|
return path.is_file() and not is_relative_to_any(path, self.excludes) and is_relative_to_any(path, self.includes)
|
154
160
|
|
161
|
+
@DerivedProperty
|
162
|
+
def search_paths(self):
|
163
|
+
# FIXME: Handle case where `includes` contains file paths, not just directories
|
164
|
+
# Currently we assume `includes` never specify individual files
|
165
|
+
# And we assume `includes` and `excludes` never change
|
166
|
+
return [path for path in (Path(p).resolve() for p in sys_path()) if not is_relative_to_any(path, self.excludes) and is_relative_to_any(path, self.includes)]
|
167
|
+
|
155
168
|
def find_spec(self, fullname: str, paths: Sequence[str] | None, _=None):
|
156
169
|
if fullname in sys.modules:
|
157
170
|
return None
|
158
171
|
|
159
|
-
for
|
160
|
-
directory = Path(p).resolve()
|
172
|
+
for directory in self.search_paths:
|
161
173
|
if directory.is_file():
|
162
174
|
continue
|
163
175
|
|
@@ -219,7 +231,7 @@ class BaseReloader:
|
|
219
231
|
self.includes = includes
|
220
232
|
self.excludes = excludes
|
221
233
|
patch_meta_path(includes, excludes)
|
222
|
-
self.error_filter = ErrorFilter(*(str
|
234
|
+
self.error_filter = ErrorFilter(*map(str, Path(__file__, "../..").resolve().glob("**/*.py")))
|
223
235
|
|
224
236
|
@cached_property
|
225
237
|
def entry_module(self):
|
@@ -335,4 +347,4 @@ def cli():
|
|
335
347
|
reloader.keep_watching_until_interrupt()
|
336
348
|
|
337
349
|
|
338
|
-
__version__ = "0.6.
|
350
|
+
__version__ = "0.6.3"
|
@@ -0,0 +1,61 @@
|
|
1
|
+
import ast
|
2
|
+
from typing import override
|
3
|
+
|
4
|
+
|
5
|
+
class ClassTransformer(ast.NodeTransformer):
|
6
|
+
@override
|
7
|
+
def visit_ClassDef(self, node: ast.ClassDef):
|
8
|
+
node.body = [
|
9
|
+
name_lookup_function,
|
10
|
+
*map(ClassBodyTransformer().visit, node.body),
|
11
|
+
ast.Delete(targets=[ast.Name(id="__name_lookup", ctx=ast.Del())]),
|
12
|
+
]
|
13
|
+
return node
|
14
|
+
|
15
|
+
|
16
|
+
class ClassBodyTransformer(ast.NodeTransformer):
|
17
|
+
@override
|
18
|
+
def visit_Name(self, node: ast.Name):
|
19
|
+
if isinstance(node.ctx, ast.Load) and node.id != "__name_lookup":
|
20
|
+
return build_name_lookup(node.id)
|
21
|
+
return node
|
22
|
+
|
23
|
+
@override
|
24
|
+
def visit_FunctionDef(self, node: ast.FunctionDef):
|
25
|
+
node.decorator_list = [self.visit(d) for d in node.decorator_list]
|
26
|
+
self.visit(node.args)
|
27
|
+
if node.returns:
|
28
|
+
node.returns = self.visit(node.returns)
|
29
|
+
return node
|
30
|
+
|
31
|
+
visit_AsyncFunctionDef = visit_FunctionDef # type: ignore # noqa: N815
|
32
|
+
|
33
|
+
@override
|
34
|
+
def visit_Lambda(self, node: ast.Lambda):
|
35
|
+
self.visit(node.args)
|
36
|
+
return node
|
37
|
+
|
38
|
+
|
39
|
+
def build_name_lookup(name: str) -> ast.Call:
|
40
|
+
return ast.Call(func=ast.Name(id="__name_lookup", ctx=ast.Load()), args=[ast.Constant(value=name)], keywords=[])
|
41
|
+
|
42
|
+
|
43
|
+
name_lookup_source = """
|
44
|
+
|
45
|
+
def __name_lookup(name):
|
46
|
+
from collections import ChainMap
|
47
|
+
from inspect import currentframe
|
48
|
+
f = currentframe().f_back
|
49
|
+
c = ChainMap(f.f_locals, f.f_globals, f.f_builtins)
|
50
|
+
f = f.f_back
|
51
|
+
while f is not None and f.f_code.co_name != "<module>":
|
52
|
+
c.maps.insert(1, f.f_locals)
|
53
|
+
f = f.f_back
|
54
|
+
m = object()
|
55
|
+
if (v := c.get(name, m)) is not m:
|
56
|
+
return v
|
57
|
+
raise NameError(name)
|
58
|
+
|
59
|
+
"""
|
60
|
+
|
61
|
+
name_lookup_function = ast.parse(name_lookup_source).body[0]
|
@@ -1,3 +1,4 @@
|
|
1
|
+
from ast import parse
|
1
2
|
from collections import UserDict
|
2
3
|
from collections.abc import Callable
|
3
4
|
from functools import wraps
|
@@ -7,6 +8,7 @@ from types import FunctionType
|
|
7
8
|
|
8
9
|
from ..helpers import Memoized
|
9
10
|
from .core import HMR_CONTEXT, NamespaceProxy, ReactiveModule
|
11
|
+
from .exec_hack import fix_class_name_resolution
|
10
12
|
from .hooks import post_reload, pre_reload
|
11
13
|
|
12
14
|
memos: dict[str, Callable] = {}
|
@@ -27,6 +29,9 @@ def clear_memos():
|
|
27
29
|
del memos[func]
|
28
30
|
|
29
31
|
|
32
|
+
_cache_decorator_phase = False
|
33
|
+
|
34
|
+
|
30
35
|
def cache_across_reloads[T](func: Callable[[], T]) -> Callable[[], T]:
|
31
36
|
file = getsourcefile(func)
|
32
37
|
assert file is not None
|
@@ -41,6 +46,12 @@ def cache_across_reloads[T](func: Callable[[], T]) -> Callable[[], T]:
|
|
41
46
|
|
42
47
|
proxy: NamespaceProxy = module._ReactiveModule__namespace_proxy # type: ignore # noqa: SLF001
|
43
48
|
|
49
|
+
global _cache_decorator_phase
|
50
|
+
_cache_decorator_phase = not _cache_decorator_phase
|
51
|
+
if _cache_decorator_phase: # this function will be called twice: once transforming ast and once re-executing the patched source
|
52
|
+
exec(compile(fix_class_name_resolution(parse(source)), file, "exec"), DictProxy(proxy))
|
53
|
+
return proxy[func.__name__]
|
54
|
+
|
44
55
|
func = FunctionType(func.__code__, DictProxy(proxy), func.__name__, func.__defaults__, func.__closure__)
|
45
56
|
|
46
57
|
functions[source] = func
|
@@ -15,8 +15,8 @@ def _equal(a, b):
|
|
15
15
|
comparison_result = a == b
|
16
16
|
if comparison_result:
|
17
17
|
return True
|
18
|
-
except ValueError as e:
|
19
|
-
if "
|
18
|
+
except (ValueError, RuntimeError) as e:
|
19
|
+
if "is ambiguous" in str(e) and hasattr(comparison_result, "all"): # array-like instances
|
20
20
|
comparison_result = comparison_result.all()
|
21
21
|
else:
|
22
22
|
return False
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|