hmr 0.0.1__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.
- hmr-0.0.1.dist-info/METADATA +8 -0
- hmr-0.0.1.dist-info/RECORD +9 -0
- hmr-0.0.1.dist-info/WHEEL +4 -0
- hmr-0.0.1.dist-info/entry_points.txt +5 -0
- reactivity/__init__.py +5 -0
- reactivity/functional.py +54 -0
- reactivity/helpers.py +110 -0
- reactivity/hmr.py +215 -0
- reactivity/primitives.py +133 -0
@@ -0,0 +1,9 @@
|
|
1
|
+
hmr-0.0.1.dist-info/METADATA,sha256=Z9tnvo1XFCBFioLgAyeAheBfjkCzG2YnWxj8btdlohs,228
|
2
|
+
hmr-0.0.1.dist-info/WHEEL,sha256=thaaA2w1JzcGC48WYufAs8nrYZjJm8LqNfnXFOFyCC4,90
|
3
|
+
hmr-0.0.1.dist-info/entry_points.txt,sha256=g_T0uJ43WgsdG14kkkdaBQuIL0HO-m1qvtjXMP6d060,59
|
4
|
+
reactivity/__init__.py,sha256=Stl2BBvqzutjcmF-O-olFTbSzJxzAl1xNMbu8mAVjlo,320
|
5
|
+
reactivity/functional.py,sha256=KAsqgPQsxIGuqO4BAtH6VF78MigLSBQ2k3aL4o_Vffg,1290
|
6
|
+
reactivity/helpers.py,sha256=xb3UThn7CGqJizCeing-vUBVZFwufadIEdWOgCFmuBo,3251
|
7
|
+
reactivity/hmr.py,sha256=97JT1aFiiL_WoVS7uewIdY_5Vr-dHt6Rz5BKvpP8234,7400
|
8
|
+
reactivity/primitives.py,sha256=7E99QLSblRfO3wPxRDpVqw8-KR2VI2g4QheShBXFv1g,3347
|
9
|
+
hmr-0.0.1.dist-info/RECORD,,
|
reactivity/__init__.py
ADDED
@@ -0,0 +1,5 @@
|
|
1
|
+
from .functional import batch, create_effect, create_memo, create_signal, memoized_method, memoized_property
|
2
|
+
from .helpers import Reactive
|
3
|
+
from .primitives import Derived, State
|
4
|
+
|
5
|
+
__all__ = ["Derived", "Reactive", "State", "batch", "create_effect", "create_memo", "create_signal", "memoized_method", "memoized_property"]
|
reactivity/functional.py
ADDED
@@ -0,0 +1,54 @@
|
|
1
|
+
from collections.abc import Callable
|
2
|
+
from functools import wraps
|
3
|
+
from typing import Protocol, overload
|
4
|
+
|
5
|
+
from .helpers import Memoized, MemoizedMethod, MemoizedProperty
|
6
|
+
from .primitives import Batch, Derived, Signal
|
7
|
+
|
8
|
+
|
9
|
+
class Getter[T](Protocol):
|
10
|
+
def __call__(self, track=True) -> T: ...
|
11
|
+
|
12
|
+
|
13
|
+
class Setter[T](Protocol):
|
14
|
+
def __call__(self, value: T): ...
|
15
|
+
|
16
|
+
|
17
|
+
def create_signal[T](initial_value: T = None, check_equality=True) -> tuple[Getter[T], Setter[T]]:
|
18
|
+
signal = Signal(initial_value, check_equality)
|
19
|
+
return signal.get, signal.set
|
20
|
+
|
21
|
+
|
22
|
+
def create_effect[T](fn: Callable[[], T], auto_run=True):
|
23
|
+
return Derived(fn, auto_run)
|
24
|
+
|
25
|
+
|
26
|
+
def create_memo[T](fn: Callable[[], T]):
|
27
|
+
return Memoized(fn)
|
28
|
+
|
29
|
+
|
30
|
+
def memoized_property[T, Self](method: Callable[[Self], T]):
|
31
|
+
return MemoizedProperty(method)
|
32
|
+
|
33
|
+
|
34
|
+
def memoized_method[T, Self](method: Callable[[Self], T]):
|
35
|
+
return MemoizedMethod(method)
|
36
|
+
|
37
|
+
|
38
|
+
@overload
|
39
|
+
def batch() -> Batch: ...
|
40
|
+
@overload
|
41
|
+
def batch[**P, T](func: Callable[P, T]) -> Callable[P, T]: ...
|
42
|
+
|
43
|
+
|
44
|
+
def batch[**P, T](func: Callable[P, T] | None = None) -> Callable[P, T] | Batch:
|
45
|
+
if func is not None:
|
46
|
+
|
47
|
+
@wraps(func)
|
48
|
+
def wrapped(*args, **kwargs):
|
49
|
+
with Batch():
|
50
|
+
return func(*args, **kwargs)
|
51
|
+
|
52
|
+
return wrapped
|
53
|
+
|
54
|
+
return Batch()
|
reactivity/helpers.py
ADDED
@@ -0,0 +1,110 @@
|
|
1
|
+
from collections.abc import Callable, Mapping, MutableMapping
|
2
|
+
from functools import partial
|
3
|
+
from weakref import WeakKeyDictionary
|
4
|
+
|
5
|
+
from .primitives import BaseComputation, Batch, Signal, Subscribable
|
6
|
+
|
7
|
+
|
8
|
+
class Memoized[T](Subscribable, BaseComputation):
|
9
|
+
def __init__(self, fn: Callable[[], T]):
|
10
|
+
super().__init__()
|
11
|
+
self.fn = fn
|
12
|
+
self.is_stale = True
|
13
|
+
self.cached_value: T
|
14
|
+
self._recompute = False
|
15
|
+
|
16
|
+
def trigger(self):
|
17
|
+
if self._recompute:
|
18
|
+
self._before()
|
19
|
+
self.cached_value = self.fn()
|
20
|
+
self._after()
|
21
|
+
self.is_stale = False
|
22
|
+
elif not self.is_stale:
|
23
|
+
del self.cached_value
|
24
|
+
self.is_stale = True
|
25
|
+
self.track()
|
26
|
+
|
27
|
+
def __call__(self):
|
28
|
+
if self.is_stale:
|
29
|
+
self._recompute = True
|
30
|
+
self.trigger()
|
31
|
+
self._recompute = False
|
32
|
+
return self.cached_value
|
33
|
+
|
34
|
+
def invalidate(self):
|
35
|
+
del self.cached_value
|
36
|
+
self.is_stale = True
|
37
|
+
|
38
|
+
|
39
|
+
class MemoizedProperty[T, Self]:
|
40
|
+
def __init__(self, method: Callable[[Self], T]):
|
41
|
+
super().__init__()
|
42
|
+
self.method = method
|
43
|
+
self.map = WeakKeyDictionary[Self, Memoized]()
|
44
|
+
|
45
|
+
def __get__(self, instance, owner):
|
46
|
+
if func := self.map.get(instance):
|
47
|
+
return func()
|
48
|
+
self.map[instance] = func = Memoized(partial(self.method, instance))
|
49
|
+
return func()
|
50
|
+
|
51
|
+
|
52
|
+
class MemoizedMethod[T, Self]:
|
53
|
+
def __init__(self, method: Callable[[Self], T]):
|
54
|
+
super().__init__()
|
55
|
+
self.method = method
|
56
|
+
|
57
|
+
def __get__(self, instance, owner):
|
58
|
+
return Memoized(partial(self.method, instance))
|
59
|
+
|
60
|
+
|
61
|
+
class Reactive[K, V](Subscribable, MutableMapping[K, V]):
|
62
|
+
UNSET: V = object() # type: ignore
|
63
|
+
|
64
|
+
def __hash__(self):
|
65
|
+
return id(self)
|
66
|
+
|
67
|
+
def __init__(self, initial: Mapping | None = None, check_equality=True):
|
68
|
+
super().__init__()
|
69
|
+
self._signals: dict[K, Signal[V]] = {} if initial is None else {k: Signal(v, check_equality) for k, v in initial.items()}
|
70
|
+
self._check_equality = check_equality
|
71
|
+
|
72
|
+
def __getitem__(self, key: K):
|
73
|
+
value = self._signals.setdefault(key, Signal(self.UNSET, self._check_equality)).get()
|
74
|
+
if value is self.UNSET:
|
75
|
+
raise KeyError(key)
|
76
|
+
return value
|
77
|
+
|
78
|
+
def __setitem__(self, key: K, value: V):
|
79
|
+
with Batch():
|
80
|
+
try:
|
81
|
+
self._signals[key].set(value)
|
82
|
+
except KeyError:
|
83
|
+
self._signals[key] = Signal(value, self._check_equality)
|
84
|
+
self._signals[key].set(value)
|
85
|
+
self.notify()
|
86
|
+
|
87
|
+
def __delitem__(self, key: K):
|
88
|
+
state = self._signals[key]
|
89
|
+
if state.get(track=False) is self.UNSET:
|
90
|
+
raise KeyError(key)
|
91
|
+
with Batch():
|
92
|
+
state.set(self.UNSET)
|
93
|
+
state.notify()
|
94
|
+
self.notify()
|
95
|
+
|
96
|
+
def __iter__(self):
|
97
|
+
self.track()
|
98
|
+
return iter(self._signals)
|
99
|
+
|
100
|
+
def __len__(self):
|
101
|
+
self.track()
|
102
|
+
return len(self._signals)
|
103
|
+
|
104
|
+
def __repr__(self):
|
105
|
+
self.track()
|
106
|
+
return repr({k: v.get() for k, v in self._signals.items()})
|
107
|
+
|
108
|
+
def items(self):
|
109
|
+
self.track()
|
110
|
+
return ({k: v.get() for k, v in self._signals.items()}).items()
|
reactivity/hmr.py
ADDED
@@ -0,0 +1,215 @@
|
|
1
|
+
import sys
|
2
|
+
from collections.abc import Iterable, Sequence
|
3
|
+
from contextlib import suppress
|
4
|
+
from functools import cached_property
|
5
|
+
from importlib.abc import Loader, MetaPathFinder
|
6
|
+
from importlib.machinery import ModuleSpec
|
7
|
+
from importlib.util import spec_from_loader
|
8
|
+
from inspect import currentframe
|
9
|
+
from pathlib import Path
|
10
|
+
from runpy import run_path
|
11
|
+
from types import ModuleType
|
12
|
+
|
13
|
+
from . import Reactive, batch, create_effect, memoized_method
|
14
|
+
|
15
|
+
|
16
|
+
def is_called_in_this_file() -> bool:
|
17
|
+
frame = currentframe() # this function
|
18
|
+
assert frame is not None
|
19
|
+
|
20
|
+
frame = frame.f_back # the function calling this function
|
21
|
+
assert frame is not None
|
22
|
+
|
23
|
+
frame = frame.f_back # the function calling the function calling this function
|
24
|
+
assert frame is not None
|
25
|
+
|
26
|
+
return frame.f_globals.get("__file__") == __file__
|
27
|
+
|
28
|
+
|
29
|
+
class ReactiveModule(ModuleType):
|
30
|
+
def __init__(self, file: Path, namespace: dict, name: str, doc: str | None = None):
|
31
|
+
super().__init__(name, doc)
|
32
|
+
self.__namespace = namespace
|
33
|
+
self.__namespace_proxy = Reactive(namespace)
|
34
|
+
self.__file = file
|
35
|
+
|
36
|
+
@property
|
37
|
+
def file(self):
|
38
|
+
if is_called_in_this_file():
|
39
|
+
return self.__file
|
40
|
+
raise AttributeError("file")
|
41
|
+
|
42
|
+
@property
|
43
|
+
def load(self):
|
44
|
+
if is_called_in_this_file():
|
45
|
+
return lambda: exec(self.__file.read_text("utf-8"), self.__namespace, self.__namespace_proxy)
|
46
|
+
raise AttributeError("load")
|
47
|
+
|
48
|
+
def __getattr__(self, name: str):
|
49
|
+
try:
|
50
|
+
return self.__namespace_proxy[name]
|
51
|
+
except KeyError as e:
|
52
|
+
raise AttributeError(*e.args) from e
|
53
|
+
|
54
|
+
def __setattr__(self, name: str, value):
|
55
|
+
if is_called_in_this_file():
|
56
|
+
return super().__setattr__(name, value)
|
57
|
+
self.__namespace_proxy[name] = value
|
58
|
+
|
59
|
+
|
60
|
+
class ReactiveModuleLoader(Loader):
|
61
|
+
def __init__(self, file: Path, is_package=False):
|
62
|
+
super().__init__()
|
63
|
+
self._file = file
|
64
|
+
self._is_package = is_package
|
65
|
+
|
66
|
+
def create_module(self, spec: ModuleSpec):
|
67
|
+
namespace = {}
|
68
|
+
if self._is_package:
|
69
|
+
assert self._file.name == "__init__.py"
|
70
|
+
namespace["__path__"] = [str(self._file.parent.parent)]
|
71
|
+
namespace["__package__"] = spec.name
|
72
|
+
return ReactiveModule(self._file, namespace, spec.name)
|
73
|
+
|
74
|
+
def exec_module(self, module: ModuleType):
|
75
|
+
assert isinstance(module, ReactiveModule)
|
76
|
+
create_effect(lambda: module.load())
|
77
|
+
|
78
|
+
|
79
|
+
class ReactiveModuleFinder(MetaPathFinder):
|
80
|
+
def __init__(self, includes: Iterable[str] = ".", excludes: Iterable[str] = ()):
|
81
|
+
super().__init__()
|
82
|
+
self.includes = [Path(i).resolve() for i in includes]
|
83
|
+
self.excludes = [Path(e).resolve() for e in excludes]
|
84
|
+
|
85
|
+
def find_spec(self, fullname: str, paths: Sequence[str] | None, _=None):
|
86
|
+
if fullname in sys.modules:
|
87
|
+
return None
|
88
|
+
|
89
|
+
for p in paths or sys.path:
|
90
|
+
directory = Path(p)
|
91
|
+
if directory.is_file():
|
92
|
+
continue
|
93
|
+
if any(directory.is_relative_to(exclude) for exclude in self.excludes):
|
94
|
+
continue
|
95
|
+
if all(not directory.is_relative_to(include) for include in self.includes):
|
96
|
+
continue
|
97
|
+
|
98
|
+
file = directory / f"{fullname.replace('.', '/')}.py"
|
99
|
+
if file.is_file() and all(not file.is_relative_to(exclude) for exclude in self.excludes):
|
100
|
+
return spec_from_loader(fullname, ReactiveModuleLoader(file), origin=str(file))
|
101
|
+
file = directory / f"{fullname.replace('.', '/')}/__init__.py"
|
102
|
+
if file.is_file() and all(not file.is_relative_to(exclude) for exclude in self.excludes):
|
103
|
+
return spec_from_loader(fullname, ReactiveModuleLoader(file, is_package=True), origin=str(file))
|
104
|
+
|
105
|
+
|
106
|
+
def patch_module(name_or_module: str | ModuleType):
|
107
|
+
name = name_or_module if isinstance(name_or_module, str) else name_or_module.__name__
|
108
|
+
module = sys.modules[name_or_module] if isinstance(name_or_module, str) else name_or_module
|
109
|
+
assert isinstance(module.__file__, str), f"{name} is not a file-backed module"
|
110
|
+
m = sys.modules[name] = ReactiveModule(Path(module.__file__), module.__dict__, module.__name__, module.__doc__)
|
111
|
+
return m
|
112
|
+
|
113
|
+
|
114
|
+
def patch_meta_path(includes: Iterable[str] = (".",), excludes: Iterable[str] = (".venv",)):
|
115
|
+
sys.meta_path.insert(0, ReactiveModuleFinder(includes, excludes))
|
116
|
+
|
117
|
+
|
118
|
+
def get_path_module_map():
|
119
|
+
return {module.file.resolve(): module for module in sys.modules.values() if isinstance(module, ReactiveModule)}
|
120
|
+
|
121
|
+
|
122
|
+
class BaseReloader:
|
123
|
+
def __init__(self, entry_file: str, includes: Iterable[str] = (".",), excludes: Iterable[str] = ()):
|
124
|
+
self.entry = entry_file
|
125
|
+
self.includes = includes
|
126
|
+
self.excludes = excludes
|
127
|
+
patch_meta_path(includes, excludes)
|
128
|
+
|
129
|
+
@memoized_method
|
130
|
+
def run_entry_file(self):
|
131
|
+
try:
|
132
|
+
run_path(self.entry, run_name="__main__")
|
133
|
+
except Exception as e:
|
134
|
+
sys.excepthook(e.__class__, e, e.__traceback__)
|
135
|
+
|
136
|
+
@property
|
137
|
+
def watch_filter(self):
|
138
|
+
from watchfiles import PythonFilter
|
139
|
+
|
140
|
+
return PythonFilter(ignore_paths=tuple(self.excludes))
|
141
|
+
|
142
|
+
def on_events(self, events: Iterable[tuple[int, str]]):
|
143
|
+
from watchfiles import Change
|
144
|
+
|
145
|
+
if not events:
|
146
|
+
return
|
147
|
+
|
148
|
+
path2module = get_path_module_map()
|
149
|
+
|
150
|
+
with batch():
|
151
|
+
for type, file in events:
|
152
|
+
if type is Change.modified:
|
153
|
+
path = Path(file).resolve()
|
154
|
+
if path.samefile(self.entry):
|
155
|
+
self.run_entry_file()
|
156
|
+
elif module := path2module.get(path):
|
157
|
+
try:
|
158
|
+
module.load()
|
159
|
+
except Exception as e:
|
160
|
+
sys.excepthook(e.__class__, e, e.__traceback__)
|
161
|
+
|
162
|
+
|
163
|
+
class SyncReloader(BaseReloader):
|
164
|
+
@cached_property
|
165
|
+
def _stop_event(self):
|
166
|
+
from threading import Event
|
167
|
+
|
168
|
+
return Event()
|
169
|
+
|
170
|
+
def stop_watching(self):
|
171
|
+
self._stop_event.set()
|
172
|
+
del self._stop_event
|
173
|
+
|
174
|
+
def start_watching(self):
|
175
|
+
from watchfiles import watch
|
176
|
+
|
177
|
+
for events in watch(self.entry, *self.includes, watch_filter=self.watch_filter, stop_event=self._stop_event):
|
178
|
+
self.on_events(events)
|
179
|
+
|
180
|
+
def keep_watching_until_interrupt(self):
|
181
|
+
with suppress(KeyboardInterrupt):
|
182
|
+
self.run_entry_file()
|
183
|
+
self.start_watching()
|
184
|
+
self.run_entry_file.dispose()
|
185
|
+
|
186
|
+
|
187
|
+
class AsyncReloader(BaseReloader):
|
188
|
+
@cached_property
|
189
|
+
def _stop_event(self):
|
190
|
+
from asyncio import Event
|
191
|
+
|
192
|
+
return Event()
|
193
|
+
|
194
|
+
def stop_watching(self):
|
195
|
+
self._stop_event.set()
|
196
|
+
del self._stop_event
|
197
|
+
|
198
|
+
async def start_watching(self):
|
199
|
+
from watchfiles import awatch
|
200
|
+
|
201
|
+
async for events in awatch(self.entry, *self.includes, watch_filter=self.watch_filter, stop_event=self._stop_event):
|
202
|
+
self.on_events(events)
|
203
|
+
|
204
|
+
async def keep_watching_until_interrupt(self):
|
205
|
+
with suppress(KeyboardInterrupt):
|
206
|
+
self.run_entry_file()
|
207
|
+
await self.start_watching()
|
208
|
+
self.run_entry_file.dispose()
|
209
|
+
|
210
|
+
|
211
|
+
def cli():
|
212
|
+
entry = sys.argv[-1]
|
213
|
+
assert Path(entry).is_file(), f"{entry} is not a file"
|
214
|
+
sys.path.insert(0, ".")
|
215
|
+
SyncReloader(entry, excludes={".venv"}).keep_watching_until_interrupt()
|
reactivity/primitives.py
ADDED
@@ -0,0 +1,133 @@
|
|
1
|
+
from collections.abc import Callable
|
2
|
+
from typing import Any
|
3
|
+
from weakref import WeakKeyDictionary, WeakSet
|
4
|
+
|
5
|
+
|
6
|
+
class Subscribable:
|
7
|
+
def __init__(self):
|
8
|
+
super().__init__()
|
9
|
+
self.subscribers = set[BaseComputation]()
|
10
|
+
|
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)
|
16
|
+
|
17
|
+
def notify(self):
|
18
|
+
if _batches:
|
19
|
+
_batches[-1].callbacks.extend(self.subscribers)
|
20
|
+
else:
|
21
|
+
for subscriber in {*self.subscribers}:
|
22
|
+
subscriber.trigger()
|
23
|
+
|
24
|
+
|
25
|
+
class BaseComputation:
|
26
|
+
def __init__(self):
|
27
|
+
super().__init__()
|
28
|
+
self.dependencies = WeakSet[Subscribable]()
|
29
|
+
|
30
|
+
def dispose(self):
|
31
|
+
for dep in self.dependencies:
|
32
|
+
dep.subscribers.remove(self)
|
33
|
+
self.dependencies.clear()
|
34
|
+
|
35
|
+
def _before(self):
|
36
|
+
self.dispose()
|
37
|
+
_current_computations.append(self)
|
38
|
+
|
39
|
+
def _after(self):
|
40
|
+
last = _current_computations.pop()
|
41
|
+
assert last is self # sanity check
|
42
|
+
|
43
|
+
def __enter__(self):
|
44
|
+
return self
|
45
|
+
|
46
|
+
def __exit__(self, *_):
|
47
|
+
self.dispose()
|
48
|
+
|
49
|
+
def trigger(self) -> Any: ...
|
50
|
+
|
51
|
+
def __call__(self):
|
52
|
+
return self.trigger()
|
53
|
+
|
54
|
+
|
55
|
+
_current_computations: list[BaseComputation] = []
|
56
|
+
|
57
|
+
|
58
|
+
class Signal[T](Subscribable):
|
59
|
+
def __init__(self, initial_value: T = None, check_equality=True):
|
60
|
+
super().__init__()
|
61
|
+
self._value: T = initial_value
|
62
|
+
self._check_equality = check_equality
|
63
|
+
|
64
|
+
def get(self, track=True):
|
65
|
+
if track:
|
66
|
+
self.track()
|
67
|
+
return self._value
|
68
|
+
|
69
|
+
def set(self, value: T):
|
70
|
+
if not self._check_equality or self._value != value:
|
71
|
+
self._value = value
|
72
|
+
self.notify()
|
73
|
+
|
74
|
+
|
75
|
+
class State[T](Signal[T]):
|
76
|
+
def __init__(self, initial_value: T = None, check_equality=True):
|
77
|
+
super().__init__(initial_value, check_equality)
|
78
|
+
self._value = initial_value
|
79
|
+
self._check_equality = check_equality
|
80
|
+
self.map = WeakKeyDictionary[Any, Signal[T]]()
|
81
|
+
|
82
|
+
def __get__(self, instance, owner):
|
83
|
+
try:
|
84
|
+
return self.map[instance].get()
|
85
|
+
except KeyError:
|
86
|
+
self.map[instance] = state = Signal(self._value, self._check_equality)
|
87
|
+
return state.get()
|
88
|
+
|
89
|
+
def __set__(self, instance, value: T):
|
90
|
+
try:
|
91
|
+
state = self.map[instance]
|
92
|
+
except KeyError:
|
93
|
+
self.map[instance] = state = Signal(self._value, self._check_equality)
|
94
|
+
state.set(value)
|
95
|
+
|
96
|
+
|
97
|
+
class Derived[T](BaseComputation):
|
98
|
+
def __init__(self, fn: Callable[[], T], auto_run=True):
|
99
|
+
super().__init__()
|
100
|
+
|
101
|
+
self._fn = fn
|
102
|
+
|
103
|
+
if auto_run:
|
104
|
+
self()
|
105
|
+
|
106
|
+
def trigger(self):
|
107
|
+
self._before()
|
108
|
+
try:
|
109
|
+
return self._fn()
|
110
|
+
finally:
|
111
|
+
self._after()
|
112
|
+
|
113
|
+
|
114
|
+
class Batch:
|
115
|
+
def __init__(self):
|
116
|
+
self.callbacks: list[BaseComputation] = []
|
117
|
+
|
118
|
+
def flush(self):
|
119
|
+
callbacks = set(self.callbacks)
|
120
|
+
self.callbacks.clear()
|
121
|
+
for computation in callbacks:
|
122
|
+
computation.trigger()
|
123
|
+
|
124
|
+
def __enter__(self):
|
125
|
+
_batches.append(self)
|
126
|
+
|
127
|
+
def __exit__(self, *_):
|
128
|
+
last = _batches.pop()
|
129
|
+
assert last is self
|
130
|
+
self.flush()
|
131
|
+
|
132
|
+
|
133
|
+
_batches: list[Batch] = []
|