omlish 0.0.0.dev5__py3-none-any.whl → 0.0.0.dev6__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.

Potentially problematic release.


This version of omlish might be problematic. Click here for more details.

Files changed (100) hide show
  1. omlish/__about__.py +1 -1
  2. omlish/asyncs/__init__.py +9 -0
  3. omlish/asyncs/anyio.py +83 -19
  4. omlish/asyncs/asyncio.py +23 -0
  5. omlish/asyncs/asyncs.py +9 -6
  6. omlish/asyncs/bridge.py +316 -0
  7. omlish/asyncs/trio_asyncio.py +7 -3
  8. omlish/collections/__init__.py +1 -0
  9. omlish/collections/identity.py +7 -0
  10. omlish/configs/strings.py +94 -0
  11. omlish/dataclasses/__init__.py +9 -0
  12. omlish/dataclasses/impl/copy.py +30 -0
  13. omlish/dataclasses/impl/exceptions.py +6 -0
  14. omlish/dataclasses/impl/fields.py +24 -25
  15. omlish/dataclasses/impl/init.py +4 -2
  16. omlish/dataclasses/impl/main.py +2 -0
  17. omlish/dataclasses/utils.py +44 -0
  18. omlish/diag/__init__.py +4 -0
  19. omlish/diag/procfs.py +2 -2
  20. omlish/{testing → diag}/pydevd.py +35 -0
  21. omlish/dispatch/_dispatch2.py +65 -0
  22. omlish/dispatch/_dispatch3.py +104 -0
  23. omlish/docker.py +1 -1
  24. omlish/fnpairs.py +11 -0
  25. omlish/http/asgi.py +2 -1
  26. omlish/http/collections.py +15 -0
  27. omlish/http/consts.py +16 -1
  28. omlish/http/sessions.py +10 -3
  29. omlish/inject/__init__.py +45 -17
  30. omlish/inject/binder.py +185 -5
  31. omlish/inject/bindings.py +3 -36
  32. omlish/inject/eagers.py +2 -8
  33. omlish/inject/elements.py +30 -9
  34. omlish/inject/exceptions.py +1 -1
  35. omlish/inject/impl/elements.py +37 -12
  36. omlish/inject/impl/injector.py +19 -2
  37. omlish/inject/impl/inspect.py +33 -5
  38. omlish/inject/impl/origins.py +75 -0
  39. omlish/inject/impl/{private.py → privates.py} +2 -2
  40. omlish/inject/impl/scopes.py +6 -2
  41. omlish/inject/injector.py +8 -4
  42. omlish/inject/inspect.py +18 -0
  43. omlish/inject/keys.py +8 -14
  44. omlish/inject/listeners.py +26 -0
  45. omlish/inject/managed.py +76 -10
  46. omlish/inject/multis.py +68 -18
  47. omlish/inject/origins.py +27 -0
  48. omlish/inject/overrides.py +5 -4
  49. omlish/inject/{private.py → privates.py} +6 -10
  50. omlish/inject/providers.py +12 -85
  51. omlish/inject/scopes.py +13 -6
  52. omlish/inject/types.py +3 -1
  53. omlish/lang/__init__.py +8 -2
  54. omlish/lang/cached.py +2 -2
  55. omlish/lang/classes/restrict.py +2 -1
  56. omlish/lang/classes/simple.py +18 -8
  57. omlish/lang/contextmanagers.py +12 -3
  58. omlish/lang/descriptors.py +131 -0
  59. omlish/lang/functions.py +8 -28
  60. omlish/lang/iterables.py +20 -1
  61. omlish/lang/typing.py +5 -0
  62. omlish/lifecycles/__init__.py +34 -0
  63. omlish/lifecycles/abstract.py +43 -0
  64. omlish/lifecycles/base.py +51 -0
  65. omlish/lifecycles/contextmanagers.py +74 -0
  66. omlish/lifecycles/controller.py +116 -0
  67. omlish/lifecycles/manager.py +161 -0
  68. omlish/lifecycles/states.py +43 -0
  69. omlish/lifecycles/transitions.py +64 -0
  70. omlish/logs/formatters.py +1 -1
  71. omlish/marshal/__init__.py +4 -0
  72. omlish/marshal/naming.py +4 -0
  73. omlish/marshal/objects.py +1 -0
  74. omlish/marshal/polymorphism.py +4 -4
  75. omlish/reflect.py +134 -19
  76. omlish/secrets/__init__.py +7 -0
  77. omlish/secrets/marshal.py +41 -0
  78. omlish/secrets/passwords.py +120 -0
  79. omlish/secrets/secrets.py +47 -0
  80. omlish/serde/__init__.py +0 -0
  81. omlish/{configs → serde}/dotenv.py +12 -24
  82. omlish/{json.py → serde/json.py} +2 -1
  83. omlish/serde/yaml.py +223 -0
  84. omlish/sql/dbs.py +1 -1
  85. omlish/sql/duckdb.py +136 -0
  86. omlish/sql/sqlean.py +17 -0
  87. omlish/term.py +1 -1
  88. omlish/testing/pytest/__init__.py +3 -2
  89. omlish/testing/pytest/inject/harness.py +3 -3
  90. omlish/testing/pytest/marks.py +4 -7
  91. omlish/testing/pytest/plugins/__init__.py +1 -0
  92. omlish/testing/pytest/plugins/asyncs.py +136 -0
  93. omlish/testing/pytest/plugins/pydevd.py +1 -1
  94. omlish/text/glyphsplit.py +92 -0
  95. {omlish-0.0.0.dev5.dist-info → omlish-0.0.0.dev6.dist-info}/METADATA +1 -1
  96. {omlish-0.0.0.dev5.dist-info → omlish-0.0.0.dev6.dist-info}/RECORD +100 -72
  97. /omlish/{configs → serde}/props.py +0 -0
  98. {omlish-0.0.0.dev5.dist-info → omlish-0.0.0.dev6.dist-info}/LICENSE +0 -0
  99. {omlish-0.0.0.dev5.dist-info → omlish-0.0.0.dev6.dist-info}/WHEEL +0 -0
  100. {omlish-0.0.0.dev5.dist-info → omlish-0.0.0.dev6.dist-info}/top_level.txt +0 -0
omlish/__about__.py CHANGED
@@ -3,4 +3,4 @@ __url__ = 'https://github.com/wrmsr/omlish'
3
3
  __license__ = 'BSD-3-Clause'
4
4
  __requires_python__ = '>=3.12'
5
5
 
6
- __version__ = '0.0.0.dev5'
6
+ __version__ = '0.0.0.dev6'
omlish/asyncs/__init__.py CHANGED
@@ -6,6 +6,15 @@ from .asyncs import ( # noqa
6
6
  syncable_iterable,
7
7
  )
8
8
 
9
+ from .bridge import ( # noqa
10
+ a_to_s,
11
+ is_in_bridge,
12
+ s_to_a,
13
+ s_to_a_await,
14
+ trivial_a_to_s,
15
+ trivial_s_to_a,
16
+ )
17
+
9
18
  from .flavors import ( # noqa
10
19
  ContextManagerAdapter,
11
20
  Flavor,
omlish/asyncs/anyio.py CHANGED
@@ -18,6 +18,7 @@ import typing as ta
18
18
 
19
19
  import anyio.streams.memory
20
20
  import anyio.streams.stapled
21
+ import sniffio
21
22
 
22
23
  from .. import check
23
24
  from .. import lang
@@ -26,6 +27,9 @@ from .. import lang
26
27
  T = ta.TypeVar('T')
27
28
 
28
29
 
30
+ ##
31
+
32
+
29
33
  async def anyio_eof_to_empty(fn: ta.Callable[..., ta.Awaitable[T]], *args: ta.Any, **kwargs: ta.Any) -> T | bytes:
30
34
  try:
31
35
  return await fn(*args, **kwargs)
@@ -33,6 +37,85 @@ async def anyio_eof_to_empty(fn: ta.Callable[..., ta.Awaitable[T]], *args: ta.An
33
37
  return b''
34
38
 
35
39
 
40
+ async def gather(*fns: ta.Callable[..., ta.Awaitable[T]], take_first: bool = False) -> list[lang.Maybe[T]]:
41
+ results: list[lang.Maybe[T]] = [lang.empty()] * len(fns)
42
+
43
+ async def inner(fn, i):
44
+ results[i] = lang.just(await fn())
45
+ if take_first:
46
+ tg.cancel_scope.cancel()
47
+
48
+ async with anyio.create_task_group() as tg:
49
+ for i, fn in enumerate(fns):
50
+ tg.start_soon(inner, fn, i)
51
+
52
+ return results
53
+
54
+
55
+ async def first(*fns: ta.Callable[..., ta.Awaitable[T]], **kwargs: ta.Any) -> list[lang.Maybe[T]]:
56
+ return await gather(*fns, take_first=True, **kwargs)
57
+
58
+
59
+ ##
60
+
61
+
62
+ def get_current_task() -> anyio.TaskInfo | None:
63
+ try:
64
+ return anyio.get_current_task()
65
+ except sniffio.AsyncLibraryNotFoundError:
66
+ return None
67
+
68
+
69
+ #
70
+
71
+
72
+ BackendTask: ta.TypeAlias = ta.Union[ # noqa
73
+ # asyncio.tasks.Task,
74
+ # trio.lowlevel.Task,
75
+ ta.Any,
76
+ ]
77
+
78
+
79
+ def _is_class_named(obj: ta.Any, m: str, n: str) -> bool:
80
+ cls = obj.__class__
81
+ return cls.__module__ == m and cls.__name__ == n
82
+
83
+
84
+ def get_backend_task(at: anyio.TaskInfo) -> BackendTask | None:
85
+ if _is_class_named(at, 'anyio._backends._asyncio', 'AsyncIOTaskInfo'):
86
+ # https://github.com/agronholm/anyio/blob/8907964926a24461840eee0925d3f355e729f15d/src/anyio/_backends/_asyncio.py#L1846 # noqa
87
+ # weakref.ref
88
+ obj = at._task() # type: ignore # noqa
89
+ if obj is not None and not (
90
+ _is_class_named(obj, '_asyncio', 'Task') or
91
+ _is_class_named(obj, 'asyncio.tasks', 'Task')
92
+ ):
93
+ raise TypeError(obj)
94
+ return obj
95
+
96
+ elif _is_class_named(at, 'anyio._backends._trio', 'TrioTaskInfo'):
97
+ # https://github.com/agronholm/anyio/blob/8907964926a24461840eee0925d3f355e729f15d/src/anyio/_backends/_trio.py#L850 # noqa
98
+ # weakref.proxy
99
+ # https://stackoverflow.com/a/62144308 :|
100
+ obj = at._task.__repr__.__self__ # type: ignore # noqa
101
+ if obj is not None and not _is_class_named(obj, 'trio.lowlevel', 'Task'):
102
+ raise TypeError(obj)
103
+ return obj
104
+
105
+ else:
106
+ raise TypeError(at)
107
+
108
+
109
+ def get_current_backend_task() -> BackendTask | None:
110
+ if (at := get_current_task()) is not None:
111
+ return get_backend_task(at)
112
+ else:
113
+ return None
114
+
115
+
116
+ ##
117
+
118
+
36
119
  def split_memory_object_streams(
37
120
  *args: anyio.create_memory_object_stream[T],
38
121
  ) -> tuple[
@@ -72,25 +155,6 @@ def staple_memory_object_stream2[T](max_buffer_size: float = 0) -> anyio.streams
72
155
  )
73
156
 
74
157
 
75
- async def gather(*fns: ta.Callable[..., ta.Awaitable[T]], take_first: bool = False) -> list[lang.Maybe[T]]:
76
- results: list[lang.Maybe[T]] = [lang.empty()] * len(fns)
77
-
78
- async def inner(fn, i):
79
- results[i] = lang.just(await fn())
80
- if take_first:
81
- tg.cancel_scope.cancel()
82
-
83
- async with anyio.create_task_group() as tg:
84
- for i, fn in enumerate(fns):
85
- tg.start_soon(inner, fn, i)
86
-
87
- return results
88
-
89
-
90
- async def first(*fns: ta.Callable[..., ta.Awaitable[T]], **kwargs: ta.Any) -> list[lang.Maybe[T]]:
91
- return await gather(*fns, take_first=True, **kwargs)
92
-
93
-
94
158
  ##
95
159
 
96
160
 
omlish/asyncs/asyncio.py CHANGED
@@ -1,4 +1,5 @@
1
1
  import asyncio
2
+ import contextlib
2
3
  import functools
3
4
  import typing as ta
4
5
 
@@ -17,3 +18,25 @@ def asyncio_once(fn: CallableT) -> CallableT:
17
18
  return await future
18
19
 
19
20
  return ta.cast(CallableT, inner)
21
+
22
+
23
+ def get_real_current_loop() -> asyncio.AbstractEventLoop | None:
24
+ return asyncio.get_event_loop_policy()._local._loop # type: ignore # noqa
25
+
26
+
27
+ def drain_tasks(loop=None):
28
+ if loop is None:
29
+ loop = get_real_current_loop()
30
+
31
+ while loop._ready or loop._scheduled: # noqa
32
+ loop._run_once() # noqa
33
+
34
+
35
+ @contextlib.contextmanager
36
+ def draining_asyncio_tasks() -> ta.Iterator[None]:
37
+ loop = get_real_current_loop()
38
+ try:
39
+ yield
40
+ finally:
41
+ if loop is not None:
42
+ drain_tasks(loop) # noqa
omlish/asyncs/asyncs.py CHANGED
@@ -5,10 +5,10 @@ TODO:
5
5
  517 ns ± 13.2 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
6
6
  - injected io provider - sync vs greenlet aio trampolined
7
7
  - push/pull bridge?
8
+ - move to lang
8
9
 
9
10
  https://github.com/sqlalchemy/sqlalchemy/blob/1e75c189da721395bc8c2d899c722a5b9a170404/lib/sqlalchemy/util/_concurrency_py3k.py#L83
10
11
  """
11
- import contextlib
12
12
  import functools
13
13
  import typing as ta
14
14
 
@@ -16,8 +16,7 @@ import typing as ta
16
16
  T = ta.TypeVar('T')
17
17
 
18
18
 
19
- def sync_await(fn: ta.Callable[..., T], *args, **kwargs) -> T:
20
- ret: ta.Any
19
+ def sync_await(fn: ta.Callable[..., T], *args: ta.Any, **kwargs: ta.Any) -> T:
21
20
  ret = missing = object()
22
21
 
23
22
  async def gate():
@@ -25,13 +24,17 @@ def sync_await(fn: ta.Callable[..., T], *args, **kwargs) -> T:
25
24
  ret = await fn(*args, **kwargs) # type: ignore
26
25
 
27
26
  cr = gate()
28
- with contextlib.closing(cr):
29
- with contextlib.suppress(StopIteration):
27
+ try:
28
+ try:
30
29
  cr.send(None)
30
+ except StopIteration:
31
+ pass
31
32
  if ret is missing or cr.cr_await is not None or cr.cr_running:
32
33
  raise TypeError('Not terminated')
34
+ finally:
35
+ cr.close()
33
36
 
34
- return ta.cast(T, ret)
37
+ return ret # type: ignore
35
38
 
36
39
 
37
40
  def sync_list(fn: ta.Callable[..., ta.AsyncIterator[T]], *args, **kwargs) -> list[T]:
@@ -0,0 +1,316 @@
1
+ """
2
+ https://github.com/kubernetes/kubernetes/blob/60c4c2b2521fb454ce69dee737e3eb91a25e0535/pkg/controller/volume/persistentvolume/pv_controller.go#L60-L63
3
+
4
+ ==================================================================
5
+ PLEASE DO NOT ATTEMPT TO SIMPLIFY THIS CODE.
6
+ KEEP THE SPACE SHUTTLE FLYING.
7
+ ==================================================================
8
+
9
+ TODO:
10
+ - reuse greenlet if nested somehow?
11
+ """
12
+ import itertools
13
+ import sys
14
+ import types
15
+ import typing as ta
16
+ import weakref
17
+
18
+ from .. import lang
19
+ from .asyncs import sync_await
20
+
21
+
22
+ if ta.TYPE_CHECKING:
23
+ import asyncio
24
+
25
+ import greenlet
26
+
27
+ from . import anyio as aiu
28
+
29
+ else:
30
+ asyncio = lang.proxy_import('asyncio')
31
+
32
+ greenlet = lang.proxy_import('greenlet')
33
+
34
+ aiu = lang.proxy_import('.anyio', __package__)
35
+
36
+
37
+ T = ta.TypeVar('T')
38
+
39
+
40
+ ##
41
+
42
+
43
+ def trivial_s_to_a(fn):
44
+ async def inner(*args, **kwargs):
45
+ return fn(*args, **kwargs)
46
+ return inner
47
+
48
+
49
+ def trivial_a_to_s(fn):
50
+ def inner(*args, **kwargs):
51
+ return sync_await(fn, *args, **kwargs)
52
+ return inner
53
+
54
+
55
+ ##
56
+ # https://gist.github.com/snaury/202bf4f22c41ca34e56297bae5f33fef
57
+
58
+
59
+ class BridgeAwaitRequiredError(Exception):
60
+ pass
61
+
62
+
63
+ class MissingBridgeGreenletError(Exception):
64
+ pass
65
+
66
+
67
+ class UnexpectedBridgeNestingError(Exception):
68
+ def __init__(self, *args, **kwargs):
69
+ super().__init__(*args, **kwargs)
70
+ # breakpoint()
71
+
72
+
73
+ #
74
+
75
+
76
+ _DEBUG_PRINT: ta.Callable[..., None] | None = None
77
+ # _DEBUG_PRINT = print # noqa
78
+
79
+ _TRACK_TRANSITION_OBJS = False
80
+
81
+
82
+ #
83
+
84
+
85
+ _BRIDGE_TRANSITIONS_SEQ = itertools.count()
86
+
87
+
88
+ class _BridgeTransition(ta.NamedTuple):
89
+ seq: int
90
+ a_to_s: bool
91
+
92
+ obj_cls: type
93
+ obj_id: int
94
+
95
+ obj: ta.Any
96
+
97
+
98
+ def _make_transition(seq: int, a_to_s: bool, obj: ta.Any) -> _BridgeTransition:
99
+ return _BridgeTransition(seq, a_to_s, obj.__class__, id(obj), (obj if _TRACK_TRANSITION_OBJS else None))
100
+
101
+
102
+ _BRIDGED_TASKS: ta.MutableMapping[ta.Any, list[_BridgeTransition]] = weakref.WeakKeyDictionary()
103
+
104
+ _BRIDGE_GREENLET_ATTR = f'__{__package__.replace(".", "__")}__bridge_greenlet__'
105
+
106
+
107
+ def _push_transition(a_to_s: bool, l: list[_BridgeTransition], t: _BridgeTransition) -> _BridgeTransition:
108
+ l.append(t)
109
+ if _DEBUG_PRINT:
110
+ _DEBUG_PRINT(f'_push_transition: {a_to_s=} {id(l)=} {t=}')
111
+ return t
112
+
113
+
114
+ def _pop_transition(a_to_s: bool, l: list[_BridgeTransition]) -> _BridgeTransition:
115
+ t = l.pop()
116
+ if _DEBUG_PRINT:
117
+ _DEBUG_PRINT(f'_pop_transition: {a_to_s=} {id(l)=} {t=}')
118
+ return t
119
+
120
+
121
+ def _get_transitions() -> list[_BridgeTransition]:
122
+ l: list[_BridgeTransition] = []
123
+
124
+ if (t := aiu.get_current_backend_task()) is not None:
125
+ try:
126
+ tl = _BRIDGED_TASKS[t]
127
+ except KeyError:
128
+ pass
129
+ else:
130
+ l.extend(tl)
131
+
132
+ g = greenlet.getcurrent()
133
+ try:
134
+ gl = getattr(g, _BRIDGE_GREENLET_ATTR)
135
+ except AttributeError:
136
+ pass
137
+ else:
138
+ l.extend(gl)
139
+
140
+ l.sort(key=lambda t: (t.seq, t.a_to_s))
141
+ return l
142
+
143
+
144
+ def is_in_bridge() -> bool:
145
+ if _DEBUG_PRINT:
146
+ _DEBUG_PRINT(_get_transitions())
147
+
148
+ if (t := aiu.get_current_backend_task()) is not None:
149
+ try:
150
+ tl = _BRIDGED_TASKS[t]
151
+ except KeyError:
152
+ last_t = None
153
+ else:
154
+ if tl:
155
+ last_t = tl[-1]
156
+ else:
157
+ last_t = None
158
+ else:
159
+ last_t = None
160
+
161
+ g = greenlet.getcurrent()
162
+ try:
163
+ gl = getattr(g, _BRIDGE_GREENLET_ATTR)
164
+ except AttributeError:
165
+ last_g = None
166
+ else:
167
+ if gl:
168
+ last_g = gl[-1]
169
+ else:
170
+ last_g = None
171
+
172
+ if last_t is None:
173
+ if last_g is None:
174
+ return False
175
+ o = last_g
176
+ else: # noqa
177
+ if last_g is None or last_g.seq < last_t.seq:
178
+ o = last_t
179
+ else:
180
+ o = last_g
181
+
182
+ in_a = (t is not None)
183
+
184
+ if _DEBUG_PRINT:
185
+ _DEBUG_PRINT(f'{o.a_to_s=} {in_a=}')
186
+
187
+ return in_a != o.a_to_s
188
+
189
+
190
+ def _safe_cancel_awaitable(awaitable: ta.Awaitable[ta.Any]) -> None:
191
+ # https://docs.python.org/3/reference/datamodel.html#coroutine.close
192
+ if asyncio.iscoroutine(awaitable):
193
+ awaitable.close() # noqa
194
+
195
+
196
+ def s_to_a_await(awaitable: ta.Awaitable[T]) -> T:
197
+ g = greenlet.getcurrent()
198
+
199
+ if not getattr(g, _BRIDGE_GREENLET_ATTR, False):
200
+ _safe_cancel_awaitable(awaitable)
201
+ raise MissingBridgeGreenletError
202
+
203
+ return g.parent.switch(awaitable)
204
+
205
+
206
+ def s_to_a(fn, *, require_await=False):
207
+ @types.coroutine
208
+ def outer(*args, **kwargs):
209
+ def inner():
210
+ try:
211
+ return fn(*args, **kwargs)
212
+ finally:
213
+ if (gl2 := getattr(g, _BRIDGE_GREENLET_ATTR)) is not gl: # noqa
214
+ raise UnexpectedBridgeNestingError
215
+ if (cur_g := _pop_transition(False, gl)) is not added_g: # noqa
216
+ raise UnexpectedBridgeNestingError
217
+ if gl:
218
+ raise UnexpectedBridgeNestingError
219
+
220
+ seq = next(_BRIDGE_TRANSITIONS_SEQ)
221
+
222
+ g = greenlet.greenlet(inner)
223
+ setattr(g, _BRIDGE_GREENLET_ATTR, gl := []) # type: ignore
224
+ added_g = _push_transition(False, gl, _make_transition(seq, False, g))
225
+
226
+ if (t := aiu.get_current_backend_task()) is not None:
227
+ try:
228
+ tl = _BRIDGED_TASKS[t]
229
+ except KeyError:
230
+ tl = _BRIDGED_TASKS[t] = []
231
+ added_t = _push_transition(False, tl, _make_transition(seq, False, g))
232
+
233
+ try:
234
+ result: ta.Any = g.switch()
235
+ switch_occurred = False
236
+ while not g.dead:
237
+ switch_occurred = True
238
+ try:
239
+ value = yield result
240
+ except BaseException: # noqa
241
+ result = g.throw(*sys.exc_info())
242
+ else:
243
+ result = g.switch(value)
244
+
245
+ if require_await and not switch_occurred:
246
+ raise BridgeAwaitRequiredError
247
+
248
+ return result
249
+
250
+ finally:
251
+ if t is not None:
252
+ if (tl2 := _BRIDGED_TASKS[t]) is not tl: # noqa
253
+ raise UnexpectedBridgeNestingError
254
+ if (cur_t := _pop_transition(False, tl)) is not added_t: # noqa
255
+ raise UnexpectedBridgeNestingError
256
+
257
+ return outer
258
+
259
+
260
+ def a_to_s(fn):
261
+ def inner(*args, **kwargs):
262
+ seq = next(_BRIDGE_TRANSITIONS_SEQ)
263
+
264
+ if (t := aiu.get_current_backend_task()) is not None:
265
+ try:
266
+ tl = _BRIDGED_TASKS[t]
267
+ except KeyError:
268
+ tl = _BRIDGED_TASKS[t] = []
269
+ added_t = _push_transition(True, tl, _make_transition(seq, True, t))
270
+ else:
271
+ added_t = None
272
+
273
+ g = greenlet.getcurrent()
274
+ try:
275
+ gl = getattr(g, _BRIDGE_GREENLET_ATTR)
276
+ except AttributeError:
277
+ setattr(g, _BRIDGE_GREENLET_ATTR, gl := [])
278
+ added_g = _push_transition(True, gl, _make_transition(seq, True, g))
279
+
280
+ try:
281
+ ret = missing = object()
282
+
283
+ async def gate():
284
+ nonlocal ret
285
+ ret = await fn(*args, **kwargs)
286
+
287
+ cr = gate()
288
+ sv = None
289
+ try:
290
+ while True:
291
+ try:
292
+ sv = cr.send(sv)
293
+ except StopIteration:
294
+ break
295
+
296
+ if ret is missing or cr.cr_await is not None or cr.cr_running:
297
+ sv = s_to_a_await(sv)
298
+
299
+ finally:
300
+ cr.close()
301
+
302
+ finally:
303
+ if t is not None:
304
+ if (tl2 := _BRIDGED_TASKS[t]) is not tl: # noqa
305
+ raise UnexpectedBridgeNestingError
306
+ if (cur_t := _pop_transition(True, tl)) is not added_t: # noqa
307
+ raise UnexpectedBridgeNestingError
308
+
309
+ if (gl2 := getattr(g, _BRIDGE_GREENLET_ATTR)) is not gl: # noqa
310
+ raise UnexpectedBridgeNestingError
311
+ if (cur_g := _pop_transition(True, gl)) is not added_g: # noqa
312
+ raise UnexpectedBridgeNestingError
313
+
314
+ return ret
315
+
316
+ return inner
@@ -9,6 +9,7 @@ if ta.TYPE_CHECKING:
9
9
 
10
10
  import sniffio
11
11
  import trio_asyncio
12
+
12
13
  else:
13
14
  asyncio = lang.proxy_import('asyncio')
14
15
 
@@ -21,7 +22,7 @@ def check_trio_asyncio() -> None:
21
22
  raise RuntimeError('trio_asyncio loop not running')
22
23
 
23
24
 
24
- def with_trio_asyncio_loop(*, wait=False):
25
+ def with_trio_asyncio_loop(*, wait=False, strict=False):
25
26
  def outer(fn):
26
27
  @functools.wraps(fn)
27
28
  async def inner(*args, **kwargs):
@@ -30,7 +31,10 @@ def with_trio_asyncio_loop(*, wait=False):
30
31
  return
31
32
 
32
33
  if sniffio.current_async_library() != 'trio':
33
- raise RuntimeError('trio loop not running')
34
+ if strict:
35
+ raise RuntimeError('trio loop not running')
36
+ await fn(*args, **kwargs)
37
+ return
34
38
 
35
39
  loop: asyncio.BaseEventLoop
36
40
  async with trio_asyncio.open_loop() as loop:
@@ -40,7 +44,7 @@ def with_trio_asyncio_loop(*, wait=False):
40
44
  if wait:
41
45
  # FIXME: lol
42
46
  while asyncio.all_tasks(loop):
43
- await asyncio.sleep(.2)
47
+ await asyncio.sleep(.1)
44
48
 
45
49
  return inner
46
50
 
@@ -37,6 +37,7 @@ from .identity import ( # noqa
37
37
  IdentityKeyDict,
38
38
  IdentitySet,
39
39
  IdentityWrapper,
40
+ IdentityWeakSet,
40
41
  )
41
42
 
42
43
  from .indexed import ( # noqa
@@ -1,6 +1,7 @@
1
1
  import contextlib
2
2
  import operator as op
3
3
  import typing as ta
4
+ import weakref
4
5
 
5
6
  from .. import lang
6
7
  from .mappings import yield_dict_init
@@ -103,3 +104,9 @@ class IdentitySet(ta.MutableSet[T]):
103
104
 
104
105
  def __iter__(self) -> ta.Iterator[T]:
105
106
  return iter(self._dict.values())
107
+
108
+
109
+ class IdentityWeakSet(weakref.WeakSet):
110
+ def __init__(self, init=None):
111
+ super().__init__()
112
+ self.data = IdentitySet(init) # type: ignore
@@ -0,0 +1,94 @@
1
+ """
2
+ TODO:
3
+ - reflecty generalized rewriter, obviously..
4
+ """
5
+ import collections.abc
6
+ import typing as ta
7
+
8
+ from .. import dataclasses as dc
9
+ from .. import lang
10
+ from ..text import glyphsplit
11
+
12
+
13
+ T = ta.TypeVar('T')
14
+
15
+
16
+ class InterpolateStringsMetadata(lang.Marker):
17
+ pass
18
+
19
+
20
+ @dc.field_modifier
21
+ def secret_or_key_field(f: dc.Field) -> dc.Field:
22
+ return dc.update_field_metadata(f, {
23
+ InterpolateStringsMetadata: True,
24
+ })
25
+
26
+
27
+ @dc.field_modifier
28
+ def interpolate_field(f: dc.Field) -> dc.Field:
29
+ return dc.update_field_metadata(f, {InterpolateStringsMetadata: True})
30
+
31
+
32
+ class StringRewriter:
33
+ def __init__(self, fn: ta.Callable[[str], str]) -> None:
34
+ super().__init__()
35
+ self._fn = fn
36
+
37
+ def __call__(self, v: T, *, _soft: bool = False) -> T:
38
+ if v is None:
39
+ return None # type: ignore
40
+
41
+ if dc.is_dataclass(v):
42
+ kw = {}
43
+ for f in dc.fields(v):
44
+ fv = getattr(v, f.name)
45
+ nfv = self(fv, _soft=not f.metadata.get(InterpolateStringsMetadata))
46
+ if fv is not nfv:
47
+ kw[f.name] = nfv
48
+ if not kw:
49
+ return v # type: ignore
50
+ return dc.replace(v, **kw)
51
+
52
+ if isinstance(v, str):
53
+ if not _soft:
54
+ v = self._fn(v) # type: ignore
55
+ return v # type: ignore
56
+
57
+ if isinstance(v, lang.BUILTIN_SCALAR_ITERABLE_TYPES):
58
+ return v # type: ignore
59
+
60
+ if isinstance(v, collections.abc.Mapping):
61
+ nm = []
62
+ b = False
63
+ for mk, mv in v.items():
64
+ nk, nv = self(mk, _soft=_soft), self(mv, _soft=_soft)
65
+ nm.append((nk, nv))
66
+ b |= nk is not mk or nv is not mv
67
+ if not b:
68
+ return v # type: ignore
69
+ return v.__class__(nm) # type: ignore
70
+
71
+ if isinstance(v, (collections.abc.Sequence, collections.abc.Set)):
72
+ nl = []
73
+ b = False
74
+ for le in v:
75
+ ne = self(le, _soft=_soft)
76
+ nl.append(ne)
77
+ b |= ne is not le
78
+ if not b:
79
+ return v # type: ignore
80
+ return v.__class__(nl) # type: ignore
81
+
82
+ return v
83
+
84
+
85
+ def interpolate_strings(v: T, rpl: ta.Mapping[str, str]) -> T:
86
+ def fn(v):
87
+ if not v:
88
+ return v
89
+ sps = glyphsplit.split_braces(v)
90
+ if len(sps) == 1 and isinstance(sps[0], str):
91
+ return sps[0]
92
+ return ''.join(rpl[p.s] if isinstance(p, glyphsplit.GlyphMatch) else p for p in sps)
93
+
94
+ return StringRewriter(fn)(v)