thds.core 1.37.20250604193856__py3-none-any.whl → 1.37.20250609172634__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.
- thds/core/scope.py +122 -34
- {thds_core-1.37.20250604193856.dist-info → thds_core-1.37.20250609172634.dist-info}/METADATA +1 -1
- {thds_core-1.37.20250604193856.dist-info → thds_core-1.37.20250609172634.dist-info}/RECORD +6 -6
- {thds_core-1.37.20250604193856.dist-info → thds_core-1.37.20250609172634.dist-info}/WHEEL +0 -0
- {thds_core-1.37.20250604193856.dist-info → thds_core-1.37.20250609172634.dist-info}/entry_points.txt +0 -0
- {thds_core-1.37.20250604193856.dist-info → thds_core-1.37.20250609172634.dist-info}/top_level.txt +0 -0
thds/core/scope.py
CHANGED
|
@@ -37,11 +37,13 @@ wrapping function, which is higher than a `with` statement.
|
|
|
37
37
|
|
|
38
38
|
"""
|
|
39
39
|
|
|
40
|
+
import asyncio
|
|
40
41
|
import atexit
|
|
41
42
|
import contextlib
|
|
42
43
|
import inspect
|
|
43
44
|
import sys
|
|
44
45
|
import typing as ty
|
|
46
|
+
from collections import defaultdict
|
|
45
47
|
from functools import wraps
|
|
46
48
|
from logging import getLogger
|
|
47
49
|
from uuid import uuid4
|
|
@@ -49,37 +51,59 @@ from uuid import uuid4
|
|
|
49
51
|
from .inspect import get_caller_info
|
|
50
52
|
from .stack_context import StackContext
|
|
51
53
|
|
|
52
|
-
|
|
54
|
+
K = ty.TypeVar("K")
|
|
55
|
+
V = ty.TypeVar("V")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class _keydefaultdict(defaultdict[K, V]):
|
|
59
|
+
def __init__(self, default_factory: ty.Callable[[K], V], *args, **kwargs):
|
|
60
|
+
super().__init__(default_factory, *args, **kwargs) # type: ignore
|
|
61
|
+
self.key_default_factory = default_factory
|
|
62
|
+
|
|
63
|
+
def __missing__(self, key: K) -> V:
|
|
64
|
+
ret = self[key] = self.key_default_factory(key)
|
|
65
|
+
return ret
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
_KEYED_SCOPE_CONTEXTS: dict[str, StackContext[contextlib.ExitStack]] = _keydefaultdict(
|
|
69
|
+
lambda key: StackContext(key, contextlib.ExitStack())
|
|
70
|
+
)
|
|
71
|
+
_KEYED_SCOPE_ASYNC_CONTEXTS: dict[str, StackContext[contextlib.AsyncExitStack]] = _keydefaultdict(
|
|
72
|
+
lambda key: StackContext(key, contextlib.AsyncExitStack())
|
|
73
|
+
)
|
|
53
74
|
# all non-nil ExitStacks will be closed at application exit
|
|
54
75
|
|
|
55
76
|
|
|
56
77
|
def _close_root_scopes_atexit():
|
|
57
78
|
for name, scope_sc in _KEYED_SCOPE_CONTEXTS.items():
|
|
58
|
-
|
|
59
|
-
if
|
|
79
|
+
exit_stack_cm = scope_sc()
|
|
80
|
+
if exit_stack_cm:
|
|
60
81
|
try:
|
|
61
|
-
|
|
62
|
-
except
|
|
63
|
-
print(f"Unable to
|
|
82
|
+
exit_stack_cm.__exit__(None, None, None)
|
|
83
|
+
except Exception as exc:
|
|
84
|
+
print(f"Unable to exit scope '{name}' at exit because {exc}", file=sys.stderr)
|
|
85
|
+
|
|
86
|
+
async def do_async_cleanup():
|
|
87
|
+
for name, scope_sc in _KEYED_SCOPE_ASYNC_CONTEXTS.items():
|
|
88
|
+
exit_stack_cm = scope_sc()
|
|
89
|
+
if exit_stack_cm:
|
|
90
|
+
try:
|
|
91
|
+
await exit_stack_cm.__aexit__(None, None, None)
|
|
92
|
+
except Exception as exc:
|
|
93
|
+
print(f"Unable to exit async scope '{name}' at exit because {exc}", file=sys.stderr)
|
|
94
|
+
|
|
95
|
+
if _KEYED_SCOPE_ASYNC_CONTEXTS:
|
|
96
|
+
loop = asyncio.new_event_loop()
|
|
97
|
+
asyncio.set_event_loop(loop)
|
|
98
|
+
try:
|
|
99
|
+
loop.run_until_complete(do_async_cleanup())
|
|
100
|
+
finally:
|
|
101
|
+
loop.close()
|
|
64
102
|
|
|
65
103
|
|
|
66
104
|
atexit.register(_close_root_scopes_atexit)
|
|
67
105
|
|
|
68
106
|
|
|
69
|
-
def _init_sc(key: str, val: contextlib.ExitStack):
|
|
70
|
-
"""This should only ever be called at the root of/during import of a
|
|
71
|
-
module. It is _not_ threadsafe.
|
|
72
|
-
"""
|
|
73
|
-
# normally you shouldn't create a StackContext except as a
|
|
74
|
-
# global. in this case, we're dynamically storing _in_ a
|
|
75
|
-
# global dict, which is equivalent.
|
|
76
|
-
if key in _KEYED_SCOPE_CONTEXTS:
|
|
77
|
-
getLogger(__name__).warning(
|
|
78
|
-
f"Scope {key} already exists! If this is not importlib.reload, you have a problem."
|
|
79
|
-
)
|
|
80
|
-
_KEYED_SCOPE_CONTEXTS[key] = StackContext(key, val)
|
|
81
|
-
|
|
82
|
-
|
|
83
107
|
F = ty.TypeVar("F", bound=ty.Callable)
|
|
84
108
|
|
|
85
109
|
|
|
@@ -92,9 +116,6 @@ def _bound(key: str, func: F) -> F:
|
|
|
92
116
|
|
|
93
117
|
@wraps(func)
|
|
94
118
|
def __scope_boundary_generator_wrap(*args, **kwargs):
|
|
95
|
-
if key not in _KEYED_SCOPE_CONTEXTS:
|
|
96
|
-
_init_sc(key, contextlib.ExitStack()) # this root stack will probably not get used
|
|
97
|
-
|
|
98
119
|
with _KEYED_SCOPE_CONTEXTS[key].set(contextlib.ExitStack()) as scoped_exit_stack:
|
|
99
120
|
with scoped_exit_stack: # enter and exit the ExitStack itself
|
|
100
121
|
ret = yield from func(*args, **kwargs)
|
|
@@ -106,9 +127,6 @@ def _bound(key: str, func: F) -> F:
|
|
|
106
127
|
|
|
107
128
|
@wraps(func)
|
|
108
129
|
async def __scope_boundary_async_generator_wrap(*args, **kwargs):
|
|
109
|
-
if key not in _KEYED_SCOPE_CONTEXTS:
|
|
110
|
-
_init_sc(key, contextlib.ExitStack())
|
|
111
|
-
|
|
112
130
|
with _KEYED_SCOPE_CONTEXTS[key].set(contextlib.ExitStack()) as scoped_exit_stack:
|
|
113
131
|
with scoped_exit_stack:
|
|
114
132
|
async for ret in func(*args, **kwargs):
|
|
@@ -120,9 +138,6 @@ def _bound(key: str, func: F) -> F:
|
|
|
120
138
|
|
|
121
139
|
@wraps(func)
|
|
122
140
|
async def __scope_boundary_coroutine_wrap(*args, **kwargs):
|
|
123
|
-
if key not in _KEYED_SCOPE_CONTEXTS:
|
|
124
|
-
_init_sc(key, contextlib.ExitStack())
|
|
125
|
-
|
|
126
141
|
with _KEYED_SCOPE_CONTEXTS[key].set(contextlib.ExitStack()) as scoped_exit_stack:
|
|
127
142
|
with scoped_exit_stack:
|
|
128
143
|
return await func(*args, **kwargs)
|
|
@@ -131,9 +146,6 @@ def _bound(key: str, func: F) -> F:
|
|
|
131
146
|
|
|
132
147
|
@wraps(func)
|
|
133
148
|
def __scope_boundary_wrap(*args, **kwargs):
|
|
134
|
-
if key not in _KEYED_SCOPE_CONTEXTS:
|
|
135
|
-
_init_sc(key, contextlib.ExitStack()) # this root stack will probably not get used
|
|
136
|
-
|
|
137
149
|
with _KEYED_SCOPE_CONTEXTS[key].set(contextlib.ExitStack()) as scoped_exit_stack:
|
|
138
150
|
with scoped_exit_stack: # enter and exit the ExitStack itself
|
|
139
151
|
return func(*args, **kwargs)
|
|
@@ -141,6 +153,33 @@ def _bound(key: str, func: F) -> F:
|
|
|
141
153
|
return ty.cast(F, __scope_boundary_wrap)
|
|
142
154
|
|
|
143
155
|
|
|
156
|
+
def _async_bound(key: str, func: F) -> F:
|
|
157
|
+
"""A decorator that establishes a scope boundary for context managers
|
|
158
|
+
that can now be `aenter`ed, and will then be aexited when this
|
|
159
|
+
boundary is returned to.
|
|
160
|
+
"""
|
|
161
|
+
if inspect.isasyncgenfunction(func):
|
|
162
|
+
|
|
163
|
+
@wraps(func)
|
|
164
|
+
async def __scope_boundary_async_generator_wrap(*args, **kwargs):
|
|
165
|
+
with _KEYED_SCOPE_ASYNC_CONTEXTS[key].set(contextlib.AsyncExitStack()) as scoped_exit_stack:
|
|
166
|
+
async with scoped_exit_stack:
|
|
167
|
+
async for ret in func(*args, **kwargs):
|
|
168
|
+
yield ret
|
|
169
|
+
|
|
170
|
+
return ty.cast(F, __scope_boundary_async_generator_wrap)
|
|
171
|
+
|
|
172
|
+
assert inspect.iscoroutinefunction(func), "You should not use async_bound on non-async functions."
|
|
173
|
+
|
|
174
|
+
@wraps(func)
|
|
175
|
+
async def __scope_boundary_coroutine_wrap(*args, **kwargs):
|
|
176
|
+
with _KEYED_SCOPE_ASYNC_CONTEXTS[key].set(contextlib.AsyncExitStack()) as scoped_exit_stack:
|
|
177
|
+
async with scoped_exit_stack:
|
|
178
|
+
return await func(*args, **kwargs)
|
|
179
|
+
|
|
180
|
+
return ty.cast(F, __scope_boundary_coroutine_wrap)
|
|
181
|
+
|
|
182
|
+
|
|
144
183
|
class NoScopeFound(Exception):
|
|
145
184
|
pass
|
|
146
185
|
|
|
@@ -157,7 +196,14 @@ def _enter(key: str, context: ty.ContextManager[M]) -> M:
|
|
|
157
196
|
scope_context = _KEYED_SCOPE_CONTEXTS.get(key)
|
|
158
197
|
if scope_context:
|
|
159
198
|
return scope_context().enter_context(context)
|
|
160
|
-
raise NoScopeFound(f"No scope with the key {key} was found.")
|
|
199
|
+
raise NoScopeFound(f"No scope with the key {key} was found - did you call .bound()?")
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
async def _async_enter(key: str, context: ty.AsyncContextManager[M]) -> M:
|
|
203
|
+
scope_context = _KEYED_SCOPE_ASYNC_CONTEXTS.get(key)
|
|
204
|
+
if scope_context:
|
|
205
|
+
return await scope_context().enter_async_context(context)
|
|
206
|
+
raise NoScopeFound(f"No async scope with the key {key} was found - did you call .async_bound()?")
|
|
161
207
|
|
|
162
208
|
|
|
163
209
|
class Scope:
|
|
@@ -181,7 +227,12 @@ class Scope:
|
|
|
181
227
|
def __init__(self, key: str = ""):
|
|
182
228
|
caller_info = get_caller_info(skip=1)
|
|
183
229
|
self.key = caller_info.module + "+" + (key or uuid4().hex)
|
|
184
|
-
|
|
230
|
+
if self.key in _KEYED_SCOPE_CONTEXTS:
|
|
231
|
+
getLogger(__name__).warning(
|
|
232
|
+
f"Scope with key '{self.key}' already exists! If this is not importlib.reload, you have a problem."
|
|
233
|
+
)
|
|
234
|
+
else:
|
|
235
|
+
_KEYED_SCOPE_CONTEXTS[self.key] = StackContext(self.key, contextlib.ExitStack())
|
|
185
236
|
|
|
186
237
|
def bound(self, func: F) -> F:
|
|
187
238
|
"""Add a boundary to this function which will close all of the
|
|
@@ -194,6 +245,43 @@ class Scope:
|
|
|
194
245
|
return _enter(self.key, context)
|
|
195
246
|
|
|
196
247
|
|
|
248
|
+
class AsyncScope:
|
|
249
|
+
"""See docs for Scope - but this is the one you use when you have async context
|
|
250
|
+
managers.
|
|
251
|
+
|
|
252
|
+
These should be module-level/global objects under all
|
|
253
|
+
circumstances, as they share an internal global namespace.
|
|
254
|
+
|
|
255
|
+
"""
|
|
256
|
+
|
|
257
|
+
def __init__(self, key: str = ""):
|
|
258
|
+
caller_info = get_caller_info(skip=1)
|
|
259
|
+
self.key = caller_info.module + "+" + (key or uuid4().hex)
|
|
260
|
+
if self.key in _KEYED_SCOPE_ASYNC_CONTEXTS:
|
|
261
|
+
getLogger(__name__).warning(
|
|
262
|
+
f"Async scope with key '{self.key}' already exists! If this is not importlib.reload, you have a problem."
|
|
263
|
+
)
|
|
264
|
+
else:
|
|
265
|
+
_KEYED_SCOPE_ASYNC_CONTEXTS[self.key] = StackContext(self.key, contextlib.AsyncExitStack())
|
|
266
|
+
|
|
267
|
+
def async_bound(self, func: F) -> F:
|
|
268
|
+
"""Add an async context management boundary to this function - it will _only_
|
|
269
|
+
use async context managers, i.e. __aenter__ and __aexit__ methods.
|
|
270
|
+
|
|
271
|
+
You can wrap an async function with a synchronous bound and use synchronous
|
|
272
|
+
context managers with it, but there's no point to wrapping your sync function
|
|
273
|
+
with an async_bound because you won't be able to enter the context within it.
|
|
274
|
+
"""
|
|
275
|
+
return _async_bound(self.key, func)
|
|
276
|
+
|
|
277
|
+
async def async_enter(self, context: ty.AsyncContextManager[M]) -> M:
|
|
278
|
+
"""Enter the provided Context with a future exit at the nearest boundary for this Scope."""
|
|
279
|
+
return await _async_enter(self.key, context)
|
|
280
|
+
|
|
281
|
+
|
|
197
282
|
default = Scope("__default_scope_stack")
|
|
198
283
|
bound = default.bound
|
|
199
284
|
enter = default.enter
|
|
285
|
+
default_async = AsyncScope("__default_async_scope_stack")
|
|
286
|
+
async_bound = default_async.async_bound
|
|
287
|
+
async_enter = default_async.async_enter
|
|
@@ -33,7 +33,7 @@ thds/core/progress.py,sha256=4YGbxliDl1i-k-88w4s86uy1E69eQ6xJySGPSkpH1QM,3358
|
|
|
33
33
|
thds/core/protocols.py,sha256=4na2EeWUDWfLn5-SxfMmKegDSndJ5z-vwMhDavhCpEM,409
|
|
34
34
|
thds/core/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
35
35
|
thds/core/scaling.py,sha256=f7CtdgK0sN6nroTq5hLAkG8xwbWhbCZUULstSKjoxO0,1615
|
|
36
|
-
thds/core/scope.py,sha256=
|
|
36
|
+
thds/core/scope.py,sha256=9RWWCFRqsgjTyH6rzRm_WnO69N_sEBRaykarc2PAnBY,10834
|
|
37
37
|
thds/core/source_serde.py,sha256=X4c7LiT3VidejqtTel9YB6dWGB3x-ct39KF9E50Nbx4,139
|
|
38
38
|
thds/core/stack_context.py,sha256=17lPOuYWclUpZ-VXRkPgI4WbiMzq7_ZY6Kj1QK_1oNo,1332
|
|
39
39
|
thds/core/thunks.py,sha256=p1OvMBJ4VGMsD8BVA7zwPeAp0L3y_nxVozBF2E78t3M,1053
|
|
@@ -68,8 +68,8 @@ thds/core/sqlite/structured.py,sha256=SvZ67KcVcVdmpR52JSd52vMTW2ALUXmlHEeD-VrzWV
|
|
|
68
68
|
thds/core/sqlite/types.py,sha256=oUkfoKRYNGDPZRk29s09rc9ha3SCk2SKr_K6WKebBFs,1308
|
|
69
69
|
thds/core/sqlite/upsert.py,sha256=BmKK6fsGVedt43iY-Lp7dnAu8aJ1e9CYlPVEQR2pMj4,5827
|
|
70
70
|
thds/core/sqlite/write.py,sha256=z0219vDkQDCnsV0WLvsj94keItr7H4j7Y_evbcoBrWU,3458
|
|
71
|
-
thds_core-1.37.
|
|
72
|
-
thds_core-1.37.
|
|
73
|
-
thds_core-1.37.
|
|
74
|
-
thds_core-1.37.
|
|
75
|
-
thds_core-1.37.
|
|
71
|
+
thds_core-1.37.20250609172634.dist-info/METADATA,sha256=jiRLDApPyiMil8ra_W3dbIkBNLvJlksrcFpeeFR08u8,2275
|
|
72
|
+
thds_core-1.37.20250609172634.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
73
|
+
thds_core-1.37.20250609172634.dist-info/entry_points.txt,sha256=bOCOVhKZv7azF3FvaWX6uxE6yrjK6FcjqhtxXvLiFY8,161
|
|
74
|
+
thds_core-1.37.20250609172634.dist-info/top_level.txt,sha256=LTZaE5SkWJwv9bwOlMbIhiS-JWQEEIcjVYnJrt-CriY,5
|
|
75
|
+
thds_core-1.37.20250609172634.dist-info/RECORD,,
|
|
File without changes
|
{thds_core-1.37.20250604193856.dist-info → thds_core-1.37.20250609172634.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
{thds_core-1.37.20250604193856.dist-info → thds_core-1.37.20250609172634.dist-info}/top_level.txt
RENAMED
|
File without changes
|