omlish 0.0.0.dev408__py3-none-any.whl → 0.0.0.dev410__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.
omlish/lite/maysyncs.py CHANGED
@@ -1,67 +1,224 @@
1
- # ruff: noqa: UP006 UP045
1
+ # ruff: noqa: UP006 UP043 UP045 UP046 UP047
2
2
  # @omlish-lite
3
3
  """
4
+ A system for writing a python function once which can then be effectively used in both sync and async contexts -
5
+ including IO, under any (or no) event loop.
6
+
7
+ Where an 'async fn' returns an 'awaitable', a 'maysync fn' returns a 'maywaitable', which is an object with two
8
+ nullary methods:
9
+
10
+ - `def s()` - to be called in sync contexts
11
+ - `async def a()` - to be called in async and maysync contexts
12
+
13
+ For example, a maysync function `m_inc_int(x: int) -> int` would be used as such:
14
+
15
+ - `assert m_inc_int(5).s() == 6` in sync contexts
16
+ - `assert await m_inc_int(5).a() == 6` in async and maysync contexts
17
+
18
+ Maysync fns may be constructed in two ways: either using `make_maysync`, providing an equivalent pair of sync and async
19
+ functions, or using the `@maysync` decorator to wrap a 'maysync flavored' async function. 'Maysync flavored' async
20
+ functions are ones which only call other maysync functions through their 'maysync context' - that is, they use the 'a'
21
+ methods on maywaitables - for example: `await m_foo().a()` - and the maysync machinery will ultimately call the
22
+ appropriate 'leaf' sync or async functions. Being regular python functions they are free to call whatever they like -
23
+ for example doing sync IO - but the point is to, ideally, route all IO through maysync functions such that the maysync
24
+ code is fully efficiently usable in any context.
25
+
26
+ Internally, it's not really correct to say that there is 'no event loop' in the maysync context - rather, each
27
+ 'entrypoint' call to a maysync fn runs within its own tiny event loop.
28
+
29
+ ===
30
+
4
31
  TODO:
5
32
  - __del__
33
+ - (test) maysync context managers
34
+ - CancelledError
35
+ - for debug, mask any running eventloop while running maysync code
36
+ - `[CO_ASYNC_GENERATOR] = {k for k, v in dis.COMPILER_FLAG_NAMES.items() if v == 'ASYNC_GENERATOR'}` ? inspect is big..
37
+ - works down to 3.8
6
38
  """
7
39
  import abc
8
40
  import functools
41
+ import inspect
42
+ import sys
43
+ import threading
9
44
  import typing as ta
10
45
 
11
46
 
12
47
  T = ta.TypeVar('T')
13
48
  T_co = ta.TypeVar('T_co', covariant=True)
14
49
 
50
+ O = ta.TypeVar('O')
51
+ O_co = ta.TypeVar('O_co', covariant=True)
52
+
53
+ I = ta.TypeVar('I')
54
+ I_contra = ta.TypeVar('I_contra', contravariant=True)
55
+
15
56
  _MaysyncX = ta.TypeVar('_MaysyncX')
16
57
 
17
- _MaysyncGen = ta.Generator['_MaysyncOp', ta.Any, T] # ta.TypeAlias
58
+ _MaysyncRS = ta.TypeVar('_MaysyncRS')
59
+ _MaysyncRA = ta.TypeVar('_MaysyncRA')
18
60
 
19
61
 
20
62
  ##
21
63
 
22
64
 
23
65
  class Maywaitable(ta.Protocol[T_co]):
66
+ """
67
+ The maysync version of `Awaitable[T]`. Non-generator maysync functions return a `Maywaitable`, with the following
68
+ nullary methods:
69
+
70
+ - `def s()` - to be called in sync contexts
71
+ - `async def a()` - to be called in async and maysync contexts
72
+
73
+ Only the proper method should be called in each context.
74
+ """
75
+
24
76
  def s(self) -> T_co:
25
77
  ...
26
78
 
27
79
  def a(self) -> ta.Awaitable[T_co]:
28
80
  ...
29
81
 
30
- def m(self) -> ta.Awaitable[T_co]:
82
+
83
+ class MaysyncGenerator(ta.Protocol[O_co, I_contra]):
84
+ """
85
+ The maysync version of `AsyncGenerator[O, I]`. Generator maysync functions return a `MaysyncGenerator`, with the
86
+ following methods:
87
+
88
+ - `def s()` - to be called in sync contexts
89
+ - `async def a()` - to be called in async and maysync contexts
90
+
91
+ Only the proper method should be called in each context.
92
+ """
93
+
94
+ def s(self) -> ta.Generator[O_co, I_contra, None]:
31
95
  ...
32
96
 
97
+ def a(self) -> ta.AsyncGenerator[O_co, I_contra]:
98
+ ...
33
99
 
34
- Maysync = ta.Callable[..., Maywaitable[T]] # ta.TypeAlias # omlish-amalg-typing-no-move
35
100
 
101
+ # The maysync equivalent of an async function
102
+ MaysyncFn = ta.Callable[..., Maywaitable[T]] # ta.TypeAlias # omlish-amalg-typing-no-move
36
103
 
37
- class Maysync_(abc.ABC, ta.Generic[T]): # noqa
38
- @ta.final
39
- def cast(self) -> Maysync[T]:
40
- return ta.cast('Maysync[T]', self)
104
+ # The maysync equivalent of an async generator function
105
+ MaysyncGeneratorFn = ta.Callable[..., MaysyncGenerator[O, I]] # ta.TypeAlias # omlish-amalg-typing-no-move
106
+
107
+
108
+ class Maysync_(abc.ABC): # noqa
109
+ """
110
+ Abstract base class for maysync objects - either MaysyncFn's or MaysyncGeneratorFn's.
111
+
112
+ The concrete implementations are module-level implementation detail, and in general users should make a point to
113
+ only interact with the protocols defined above, but introspection can be necessary at times.
114
+ """
115
+
116
+ def __init_subclass__(cls, **kwargs):
117
+ if Maysync_ in cls.__bases__ and abc.ABC not in cls.__bases__:
118
+ raise TypeError(cls)
119
+
120
+ super().__init_subclass__(**kwargs)
41
121
 
42
122
  class FnPair(ta.NamedTuple):
43
123
  s: ta.Callable[..., ta.Any]
44
- a: ta.Callable[..., ta.Awaitable[ta.Any]]
124
+ a: ta.Callable[..., ta.Any]
45
125
 
46
126
  @abc.abstractmethod
47
127
  def fn_pair(self) -> ta.Optional[FnPair]:
128
+ """If this maysync object is backed by a (sync, async) pair of functions, returns the pair."""
129
+
48
130
  raise NotImplementedError
49
131
 
50
132
  @abc.abstractmethod
51
- def __call__(self, *args, **kwargs): # -> Maywaitable[T]
133
+ def cast(self):
134
+ pass
135
+
136
+ @abc.abstractmethod
137
+ def __call__(self, *args, **kwargs):
52
138
  raise NotImplementedError
53
139
 
54
140
 
141
+ class MaysyncFn_(Maysync_, abc.ABC, ta.Generic[T]): # noqa
142
+ @ta.final
143
+ def cast(self) -> MaysyncFn[T]:
144
+ return ta.cast('MaysyncFn[T]', self)
145
+
146
+
147
+ class MaysyncGeneratorFn_(Maysync_, abc.ABC, ta.Generic[O, I]): # noqa
148
+ @ta.final
149
+ def cast(self) -> MaysyncGenerator[O, I]:
150
+ return ta.cast('MaysyncGenerator[O, I]', self)
151
+
152
+
153
+ ##
154
+
155
+
156
+ class _MaysyncThreadLocal(threading.local):
157
+ context: ta.Optional['_MaysyncContext'] = None
158
+
159
+
160
+ class _MaysyncContext(abc.ABC):
161
+ mode: ta.ClassVar[ta.Literal['s', 'a']]
162
+
163
+ @classmethod
164
+ def current(cls) -> ta.Optional['_MaysyncContext']:
165
+ return _MaysyncThreadLocal.context
166
+
167
+ @abc.abstractmethod
168
+ def run(self, fn: ta.Callable[..., T], *args: ta.Any, **kwargs: ta.Any) -> T:
169
+ raise NotImplementedError
170
+
171
+
172
+ @ta.final
173
+ class _SyncMaysyncContext(_MaysyncContext):
174
+ mode: ta.ClassVar[ta.Literal['s']] = 's'
175
+
176
+ def run(self, fn: ta.Callable[..., T], *args: ta.Any, **kwargs: ta.Any) -> T:
177
+ prev = _MaysyncThreadLocal.context
178
+ _MaysyncThreadLocal.context = self
179
+
180
+ ph = sys.get_asyncgen_hooks()
181
+ sys.set_asyncgen_hooks(firstiter=None, finalizer=None)
182
+
183
+ try:
184
+ return fn(*args, **kwargs)
185
+
186
+ finally:
187
+ sys.set_asyncgen_hooks(*ph)
188
+
189
+ _MaysyncThreadLocal.context = prev
190
+
191
+
192
+ @ta.final
193
+ class _AsyncMaysyncContext(_MaysyncContext):
194
+ mode: ta.ClassVar[ta.Literal['a']] = 'a'
195
+
196
+ def run(self, fn: ta.Callable[..., T], *args: ta.Any, **kwargs: ta.Any) -> T:
197
+ prev = _MaysyncThreadLocal.context
198
+ _MaysyncThreadLocal.context = self
199
+
200
+ try:
201
+ return fn(*args, **kwargs)
202
+
203
+ finally:
204
+ _MaysyncThreadLocal.context = prev
205
+
206
+
55
207
  ##
56
208
 
57
209
 
58
- class _Maywaitable(abc.ABC, ta.Generic[_MaysyncX, T]):
210
+ class _MaywaitableLike(
211
+ abc.ABC,
212
+ ta.Generic[_MaysyncX],
213
+ ):
214
+ """Abstract base class for the maysync versions of `Awaitable[T]` and `AsyncGenerator[O, I]`."""
215
+
59
216
  @ta.final
60
217
  def __init__(
61
218
  self,
62
219
  x: _MaysyncX,
63
- *args: ta.Any,
64
- **kwargs: ta.Any,
220
+ args: ta.Tuple[ta.Any, ...],
221
+ kwargs: ta.Mapping[str, ta.Any],
65
222
  ) -> None:
66
223
  self._x = x
67
224
  self._args = args
@@ -70,24 +227,36 @@ class _Maywaitable(abc.ABC, ta.Generic[_MaysyncX, T]):
70
227
  def __repr__(self) -> str:
71
228
  return f'{self.__class__.__name__}@{id(self):x}({self._x!r})'
72
229
 
73
- @ta.final
74
- def m(self) -> ta.Awaitable[T]:
75
- return _MaysyncFuture(_MaysyncOp(
76
- ta.cast(ta.Any, self._x),
77
- self._args,
78
- self._kwargs,
79
- ))
230
+
231
+ class _Maywaitable(
232
+ _MaywaitableLike[_MaysyncX],
233
+ abc.ABC,
234
+ ta.Generic[_MaysyncX, T],
235
+ ):
236
+ pass
237
+
238
+
239
+ class _MaysyncGenerator(
240
+ _MaywaitableLike[_MaysyncX],
241
+ abc.ABC,
242
+ ta.Generic[_MaysyncX, O, I],
243
+ ):
244
+ pass
80
245
 
81
246
 
82
247
  ##
83
248
 
84
249
 
85
- @ta.final
86
- class _FnMaysync(Maysync_, ta.Generic[T]):
250
+ class _FpMaysyncFnLike(
251
+ abc.ABC,
252
+ ta.Generic[_MaysyncRS, _MaysyncRA],
253
+ ):
254
+ """A maysync object backed by an underlying (sync, async) function pair."""
255
+
87
256
  def __init__(
88
257
  self,
89
- s: ta.Callable[..., T],
90
- a: ta.Callable[..., ta.Awaitable[T]],
258
+ s: ta.Callable[..., _MaysyncRS],
259
+ a: ta.Callable[..., _MaysyncRA],
91
260
  ) -> None:
92
261
  if s is None:
93
262
  raise TypeError(s)
@@ -106,39 +275,150 @@ class _FnMaysync(Maysync_, ta.Generic[T]):
106
275
  )
107
276
 
108
277
  def __get__(self, instance, owner=None):
109
- return _FnMaysync(
278
+ return self.__class__(
110
279
  self._s.__get__(instance, owner), # noqa
111
280
  self._a.__get__(instance, owner), # noqa
112
281
  )
113
282
 
283
+
284
+ @ta.final
285
+ class _FpMaysyncFn(
286
+ _FpMaysyncFnLike[T, ta.Awaitable[T]],
287
+ MaysyncFn_[T],
288
+ ta.Generic[T],
289
+ ):
114
290
  def __call__(self, *args, **kwargs):
115
- return _FnMaywaitable(self, *args, **kwargs)
291
+ return _FpMaywaitable(self, args, kwargs)
116
292
 
117
293
 
118
294
  @ta.final
119
- class _FnMaywaitable(_Maywaitable[_FnMaysync[T], T]):
295
+ class _FpMaywaitable(
296
+ _Maywaitable[_FpMaysyncFn[T], T],
297
+ ):
120
298
  def s(self) -> T:
121
299
  return self._x._s(*self._args, **self._kwargs) # noqa
122
300
 
123
- async def a(self) -> T:
124
- return await self._x._a(*self._args, **self._kwargs) # noqa
301
+ def a(self) -> ta.Awaitable[T]:
302
+ if _MaysyncContext.current() is not None:
303
+ return _MaysyncFuture(self._x, self._args, self._kwargs)
304
+
305
+ return self._x._a(*self._args, **self._kwargs) # noqa
306
+
307
+
308
+ def make_maysync_fn(
309
+ s: ta.Callable[..., T],
310
+ a: ta.Callable[..., ta.Awaitable[T]],
311
+ ) -> MaysyncFn[T]:
312
+ """Constructs a MaysyncFn from a (sync, async) function pair."""
313
+
314
+ return _FpMaysyncFn(s, a)
315
+
316
+
317
+ @ta.final
318
+ class _FpMaysyncGeneratorFn(
319
+ _FpMaysyncFnLike[ta.Generator[O, I, None], ta.AsyncGenerator[O, I]],
320
+ MaysyncGeneratorFn_[O, I],
321
+ ta.Generic[O, I],
322
+ ):
323
+ def __call__(self, *args, **kwargs):
324
+ return _FpMaysyncGenerator(self, args, kwargs)
325
+
326
+
327
+ @ta.final
328
+ class _FpMaysyncGenerator(
329
+ _MaysyncGenerator[_FpMaysyncGeneratorFn[O, I], O, I],
330
+ ):
331
+ def s(self) -> ta.Generator[O, I, None]:
332
+ return self._x._s(*self._args, **self._kwargs) # noqa
333
+
334
+ def a(self) -> ta.AsyncGenerator[O, I]:
335
+ if (ctx := _MaysyncContext.current()) is not None and ctx.mode == 's':
336
+ async def inner():
337
+ g = self._x._s(*self._args, **self._kwargs) # noqa
338
+
339
+ i: ta.Any = None
340
+ e: ta.Any = None
341
+
342
+ while True:
343
+ try:
344
+ if e is not None:
345
+ o = g.throw(e)
346
+ else:
347
+ o = g.send(i)
348
+ except StopIteration as ex:
349
+ if ex.value is not None:
350
+ raise TypeError(ex) from None
351
+ return
352
+
353
+ i = None
354
+ e = None
355
+
356
+ try:
357
+ i = yield o
358
+ except StopIteration as ex: # noqa
359
+ raise NotImplementedError # noqa
360
+ except StopAsyncIteration as ex: # noqa
361
+ raise NotImplementedError # noqa
362
+ except BaseException as ex: # noqa
363
+ e = ex
364
+
365
+ return inner()
366
+
367
+ return self._x._a(*self._args, **self._kwargs) # noqa
368
+
125
369
 
370
+ def make_maysync_generator_fn(
371
+ s: ta.Callable[..., ta.Generator[O, I, None]],
372
+ a: ta.Callable[..., ta.AsyncGenerator[O, I]],
373
+ ) -> MaysyncGeneratorFn[O, I]:
374
+ """Constructs a MaysyncGeneratorFn from a (sync, async) generator function pair."""
126
375
 
376
+ return _FpMaysyncGeneratorFn(s, a)
377
+
378
+
379
+ @ta.overload
127
380
  def make_maysync(
128
381
  s: ta.Callable[..., T],
129
382
  a: ta.Callable[..., ta.Awaitable[T]],
130
- ) -> Maysync[T]:
131
- return _FnMaysync(s, a)
383
+ ) -> MaysyncFn[T]:
384
+ ...
385
+
386
+
387
+ @ta.overload
388
+ def make_maysync(
389
+ s: ta.Callable[..., ta.Generator[O, I, None]],
390
+ a: ta.Callable[..., ta.AsyncGenerator[O, I]],
391
+ ) -> MaysyncGeneratorFn[O, I]:
392
+ ...
393
+
394
+
395
+ def make_maysync(s, a):
396
+ """
397
+ Constructs a MaysyncFn or MaysyncGeneratorFn from a (sync, async) function pair, using `inspect.isasyncgenfunction`
398
+ to determine the type.
399
+ """
400
+
401
+ if inspect.isasyncgenfunction(a):
402
+ return make_maysync_generator_fn(s, a)
403
+ else:
404
+ return make_maysync_fn(s, a)
132
405
 
133
406
 
134
407
  ##
135
408
 
136
409
 
137
- @ta.final
138
- class _MgMaysync(Maysync_, ta.Generic[T]):
410
+ class _MgMaysyncFnLike(
411
+ abc.ABC,
412
+ ta.Generic[T],
413
+ ):
414
+ """
415
+ A maysync object backed by an underlying generator yielding _MaysyncOp's. The _MgDriver and _MgGeneratorDriver
416
+ classes produce such generators.
417
+ """
418
+
139
419
  def __init__(
140
420
  self,
141
- mg: ta.Callable[..., _MaysyncGen[T]],
421
+ mg: ta.Callable[..., T],
142
422
  ) -> None:
143
423
  self._mg = mg
144
424
 
@@ -151,141 +431,261 @@ class _MgMaysync(Maysync_, ta.Generic[T]):
151
431
  return None
152
432
 
153
433
  def __get__(self, instance, owner=None):
154
- return _MgMaysync(
434
+ return self.__class__(
155
435
  self._mg.__get__(instance, owner), # noqa
156
436
  )
157
437
 
438
+ @abc.abstractmethod # noqa
158
439
  def __call__(self, *args, **kwargs):
159
- return _MgMaywaitable(self, *args, **kwargs)
440
+ raise NotImplementedError
160
441
 
161
442
 
162
443
  @ta.final
163
- class _MgMaywaitable(_Maywaitable[_MgMaysync[T], T]):
164
- def s(self) -> T:
165
- g = self._x._mg(*self._args, **self._kwargs) # noqa
166
-
167
- i: ta.Any = None
168
- e: ta.Any = None
444
+ class _MgMaysyncFn(
445
+ _MgMaysyncFnLike[ta.Awaitable[T]],
446
+ MaysyncFn_[T],
447
+ ta.Generic[T],
448
+ ):
449
+ def __call__(self, *args, **kwargs):
450
+ return _MgMaywaitable(self, args, kwargs)
169
451
 
170
- while True:
171
- try:
172
- if e is not None:
173
- o = g.throw(e)
174
- else:
175
- o = g.send(i)
176
- except StopIteration as ex:
177
- return ex.value
178
452
 
179
- i = None
180
- e = None
453
+ @ta.final
454
+ class _MgMaysyncDriver:
455
+ def __init__(self, ctx, mg):
456
+ self.ctx = ctx
457
+ self.mg = mg
181
458
 
182
- if not isinstance(o, _MaysyncOp):
183
- raise TypeError(o)
459
+ value: ta.Any
184
460
 
461
+ def __iter__(self):
462
+ try:
463
+ a = self.mg.__await__()
185
464
  try:
186
- i = o.x(*o.args, **o.kwargs).s()
187
- except BaseException as ex: # noqa
188
- e = ex
465
+ g = iter(a)
466
+ try:
467
+ while True:
468
+ try:
469
+ f = self.ctx.run(g.send, None)
470
+ except StopIteration as ex:
471
+ self.value = ex.value
472
+ return
189
473
 
190
- del o
474
+ if not isinstance(f, _MaysyncFuture):
475
+ raise TypeError(f)
191
476
 
192
- async def a(self) -> T:
193
- g = self._x._mg(*self._args, **self._kwargs) # noqa
477
+ yield f
478
+ del f
194
479
 
195
- i: ta.Any = None
196
- e: ta.Any = None
480
+ finally:
481
+ self.ctx.run(g.close)
197
482
 
198
- while True:
199
- try:
200
- if e is not None:
201
- o = g.throw(e)
202
- else:
203
- o = g.send(i)
204
- except StopIteration as ex:
205
- return ex.value
483
+ finally:
484
+ self.ctx.run(a.close)
485
+
486
+ finally:
487
+ self.ctx.run(self.mg.close)
206
488
 
207
- i = None
208
- e = None
209
489
 
210
- if not isinstance(o, _MaysyncOp):
211
- raise TypeError(o)
490
+ @ta.final
491
+ class _MgMaywaitable(
492
+ _Maywaitable[_MgMaysyncFn[T], T],
493
+ ):
494
+ def _driver(self, ctx: _MaysyncContext) -> _MgMaysyncDriver:
495
+ return _MgMaysyncDriver(ctx, self._x._mg(*self._args, **self._kwargs)) # noqa
212
496
 
213
- try:
214
- i = await o.x(*o.args, **o.kwargs).a()
215
- except BaseException as ex: # noqa
216
- e = ex
497
+ def s(self) -> T:
498
+ for f in (drv := self._driver(_SyncMaysyncContext())):
499
+ f.s()
500
+ del f
217
501
 
218
- del o
502
+ return drv.value
219
503
 
504
+ def a(self) -> ta.Awaitable[T]:
505
+ if (ctx := _MaysyncContext.current()) is None or ctx.mode == 'a':
506
+ return self._x._mg(*self._args, **self._kwargs) # noqa
220
507
 
221
- @ta.final
222
- class _MgMaysyncFn:
223
- def __init__(self, m):
224
- self._m = m
508
+ async def inner():
509
+ for f in (drv := self._driver(_AsyncMaysyncContext())):
510
+ await f.a()
511
+ del f
225
512
 
226
- functools.update_wrapper(self, m, updated=())
513
+ return drv.value
227
514
 
228
- def __repr__(self) -> str:
229
- return f'{self.__class__.__name__}@{id(self):x}({self._m!r})'
515
+ return inner()
230
516
 
231
- def __get__(self, instance, owner=None):
232
- return _MgMaysyncFn(
233
- self._m.__get__(instance, owner),
234
- )
235
517
 
518
+ @ta.final
519
+ class _MgMaysyncGeneratorFn(
520
+ _MgMaysyncFnLike[ta.AsyncGenerator[O, I]],
521
+ MaysyncGeneratorFn_[O, I],
522
+ ta.Generic[O, I],
523
+ ):
236
524
  def __call__(self, *args, **kwargs):
237
- a = self._m(*args, **kwargs).__await__()
525
+ return _MgMaysyncGenerator(self, args, kwargs)
526
+
527
+
528
+ @ta.final
529
+ class _MgMaysyncGeneratorDriver:
530
+ def __init__(self, ctx, ag):
531
+ self.ctx = ctx
532
+ self.ag = ag
533
+
534
+ def __iter__(self):
238
535
  try:
239
- g = iter(a)
536
+ ai = self.ag.__aiter__()
240
537
  try:
538
+ i: ta.Any = None
539
+ e: ta.Any = None
540
+
241
541
  while True:
242
- try:
243
- o = g.send(None)
244
- except StopIteration as e:
245
- return e.value
542
+ if e is not None:
543
+ coro = ai.athrow(e)
544
+ else:
545
+ coro = ai.asend(i)
246
546
 
247
- if not isinstance(o, _MaysyncFuture):
248
- raise TypeError(o)
547
+ i = None
548
+ e = None
249
549
 
250
- if not o.done:
251
- try:
252
- o.result = yield o.op
253
- except BaseException as e: # noqa
254
- o.error = e
255
- o.done = True
550
+ for f in (drv := _MgMaysyncDriver(self.ctx, coro)):
551
+ if (x := (yield ('f', f))) is not None:
552
+ raise RuntimeError(x)
256
553
 
257
- del o
554
+ del f
555
+
556
+ i, e = yield ('o', drv.value)
258
557
 
259
558
  finally:
260
- g.close()
559
+ for f in _MgMaysyncDriver(self.ctx, ai.aclose()):
560
+ yield ('f', f)
261
561
 
262
562
  finally:
263
- a.close()
563
+ for f in _MgMaysyncDriver(self.ctx, self.ag.aclose()):
564
+ yield ('f', f)
264
565
 
265
566
 
266
- def maysync(m: ta.Callable[..., ta.Awaitable[T]]) -> Maysync[T]:
267
- return _MgMaysync(_MgMaysyncFn(m))
567
+ @ta.final
568
+ class _MgMaysyncGenerator(
569
+ _MaysyncGenerator[_MgMaysyncGeneratorFn[O, I], O, I],
570
+ ):
571
+ def _driver(self, ctx: _MaysyncContext) -> _MgMaysyncGeneratorDriver:
572
+ return _MgMaysyncGeneratorDriver(ctx, self._x._mg(*self._args, **self._kwargs)) # noqa
268
573
 
574
+ def s(self) -> ta.Generator[O, I, None]:
575
+ drv = self._driver(_SyncMaysyncContext())
576
+ di = iter(drv)
269
577
 
270
- ##
578
+ ie: ta.Any = None
271
579
 
580
+ while True:
581
+ try:
582
+ t, x = di.send(ie)
583
+ except StopAsyncIteration:
584
+ return
585
+ except StopIteration:
586
+ raise RuntimeError from None
272
587
 
273
- @ta.final
274
- class _MaysyncOp:
275
- def __init__(
276
- self,
277
- x: Maysync[T],
278
- args: ta.Tuple[ta.Any, ...],
279
- kwargs: ta.Mapping[str, ta.Any],
280
- ) -> None:
281
- self.x = x
282
- self.args = args
283
- self.kwargs = kwargs
588
+ ie = None
589
+
590
+ if t == 'f':
591
+ x.s()
592
+
593
+ elif t == 'o':
594
+ try:
595
+ y = yield x
596
+ ie = (y, None)
597
+ except BaseException as ex: # noqa
598
+ ie = (None, ex)
599
+
600
+ else:
601
+ raise RuntimeError((t, x))
602
+
603
+ del x
604
+
605
+ def a(self) -> ta.AsyncGenerator[O, I]:
606
+ if _MaysyncContext.current() is not None:
607
+ return self._x._mg(*self._args, **self._kwargs) # noqa
608
+
609
+ async def inner():
610
+ drv = self._driver(_AsyncMaysyncContext())
611
+ di = iter(drv)
612
+
613
+ ie: ta.Any = None
614
+
615
+ while True:
616
+ try:
617
+ t, x = di.send(ie)
618
+ except StopAsyncIteration:
619
+ return
620
+ except StopIteration:
621
+ raise RuntimeError from None
622
+
623
+ ie = None
624
+
625
+ if t == 'f':
626
+ await x.a()
627
+
628
+ elif t == 'o':
629
+ try:
630
+ y = yield x
631
+ ie = (y, None)
632
+ except BaseException as ex: # noqa
633
+ ie = (None, ex)
634
+
635
+ else:
636
+ raise RuntimeError((t, x))
637
+
638
+ del x
639
+
640
+ return inner()
641
+
642
+
643
+ def maysync_fn(
644
+ m: ta.Callable[..., ta.Awaitable[T]],
645
+ ) -> MaysyncFn[T]:
646
+ """Constructs a MaysyncFn from a 'maysync flavored' async function."""
647
+
648
+ return _MgMaysyncFn(m)
284
649
 
285
- def __repr__(self) -> str:
286
- return f'{self.__class__.__name__}@{id(self):x}({self.x!r})'
287
650
 
651
+ def maysync_generator_fn(
652
+ m: ta.Callable[..., ta.AsyncGenerator[O, I]],
653
+ ) -> MaysyncGeneratorFn[O, I]:
654
+ """Constructs a MaysyncGeneratorFn from a 'maysync flavored' async generator function."""
288
655
 
656
+ return _MgMaysyncGeneratorFn(m)
657
+
658
+
659
+ @ta.overload
660
+ def maysync(
661
+ m: ta.Callable[..., ta.Awaitable[T]],
662
+ ) -> MaysyncFn[T]:
663
+ ...
664
+
665
+
666
+ @ta.overload
667
+ def maysync(
668
+ m: ta.Callable[..., ta.AsyncGenerator[O, I]],
669
+ ) -> MaysyncGeneratorFn[O, I]:
670
+ ...
671
+
672
+
673
+ def maysync(m):
674
+ """
675
+ Constructs a MaysyncFn or MaysyncGeneratorFn from 'maysync flavored' async function or async generator function,
676
+ using `inspect.isasyncgenfunction` to determine the type. Usable as a decorator.
677
+ """
678
+
679
+ if inspect.isasyncgenfunction(m):
680
+ return maysync_generator_fn(m)
681
+ else:
682
+ return maysync_fn(m)
683
+
684
+
685
+ ##
686
+
687
+
688
+ @ta.final
289
689
  class _MaysyncFutureNotAwaitedError(RuntimeError):
290
690
  pass
291
691
 
@@ -294,12 +694,16 @@ class _MaysyncFutureNotAwaitedError(RuntimeError):
294
694
  class _MaysyncFuture(ta.Generic[T]):
295
695
  def __init__(
296
696
  self,
297
- op: _MaysyncOp,
697
+ x: ta.Any,
698
+ args: ta.Tuple[ta.Any, ...],
699
+ kwargs: ta.Mapping[str, ta.Any],
298
700
  ) -> None:
299
- self.op = op
701
+ self.x = x
702
+ self.args = args
703
+ self.kwargs = kwargs
300
704
 
301
705
  def __repr__(self) -> str:
302
- return f'{self.__class__.__name__}@{id(self):x}({self.op!r}, done={self.done!r})'
706
+ return f'{self.__class__.__name__}@{id(self):x}({self.x!r}, done={self.done!r})'
303
707
 
304
708
  done: bool = False
305
709
  result: T
@@ -314,3 +718,23 @@ class _MaysyncFuture(ta.Generic[T]):
314
718
  raise self.error
315
719
  else:
316
720
  return self.result
721
+
722
+ def s(self) -> None:
723
+ if self.done:
724
+ return
725
+
726
+ try:
727
+ self.result = self.x(*self.args, **self.kwargs).s()
728
+ except BaseException as ex: # noqa
729
+ self.error = ex
730
+ self.done = True
731
+
732
+ async def a(self) -> None:
733
+ if self.done:
734
+ return
735
+
736
+ try:
737
+ self.result = await self.x(*self.args, **self.kwargs).a()
738
+ except BaseException as ex: # noqa
739
+ self.error = ex
740
+ self.done = True