asynkit 0.19.2__tar.gz → 0.20.0__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.
- {asynkit-0.19.2/src/asynkit.egg-info → asynkit-0.20.0}/PKG-INFO +82 -15
- {asynkit-0.19.2 → asynkit-0.20.0}/README.md +81 -14
- {asynkit-0.19.2 → asynkit-0.20.0}/pyproject.toml +1 -1
- {asynkit-0.19.2 → asynkit-0.20.0}/src/asynkit/coroutine.py +190 -21
- {asynkit-0.19.2 → asynkit-0.20.0/src/asynkit.egg-info}/PKG-INFO +82 -15
- {asynkit-0.19.2 → asynkit-0.20.0}/tests/test_coro.py +308 -17
- {asynkit-0.19.2 → asynkit-0.20.0}/tests/test_namespace.py +10 -2
- {asynkit-0.19.2 → asynkit-0.20.0}/LICENSE +0 -0
- {asynkit-0.19.2 → asynkit-0.20.0}/setup.cfg +0 -0
- {asynkit-0.19.2 → asynkit-0.20.0}/setup.py +0 -0
- {asynkit-0.19.2 → asynkit-0.20.0}/src/asynkit/__init__.py +0 -0
- {asynkit-0.19.2 → asynkit-0.20.0}/src/asynkit/_cext/corostart.c +0 -0
- {asynkit-0.19.2 → asynkit-0.20.0}/src/asynkit/compat.py +0 -0
- {asynkit-0.19.2 → asynkit-0.20.0}/src/asynkit/experimental/__init__.py +0 -0
- {asynkit-0.19.2 → asynkit-0.20.0}/src/asynkit/experimental/anyio.py +0 -0
- {asynkit-0.19.2 → asynkit-0.20.0}/src/asynkit/experimental/interrupt.py +0 -0
- {asynkit-0.19.2 → asynkit-0.20.0}/src/asynkit/experimental/priority.py +0 -0
- {asynkit-0.19.2 → asynkit-0.20.0}/src/asynkit/loop/__init__.py +0 -0
- {asynkit-0.19.2 → asynkit-0.20.0}/src/asynkit/loop/default.py +0 -0
- {asynkit-0.19.2 → asynkit-0.20.0}/src/asynkit/loop/eventloop.py +0 -0
- {asynkit-0.19.2 → asynkit-0.20.0}/src/asynkit/loop/extensions.py +0 -0
- {asynkit-0.19.2 → asynkit-0.20.0}/src/asynkit/loop/schedulingloop.py +0 -0
- {asynkit-0.19.2 → asynkit-0.20.0}/src/asynkit/loop/types.py +0 -0
- {asynkit-0.19.2 → asynkit-0.20.0}/src/asynkit/monitor.py +0 -0
- {asynkit-0.19.2 → asynkit-0.20.0}/src/asynkit/py.typed +0 -0
- {asynkit-0.19.2 → asynkit-0.20.0}/src/asynkit/scheduling.py +0 -0
- {asynkit-0.19.2 → asynkit-0.20.0}/src/asynkit/tools.py +0 -0
- {asynkit-0.19.2 → asynkit-0.20.0}/src/asynkit.egg-info/SOURCES.txt +0 -0
- {asynkit-0.19.2 → asynkit-0.20.0}/src/asynkit.egg-info/dependency_links.txt +0 -0
- {asynkit-0.19.2 → asynkit-0.20.0}/src/asynkit.egg-info/not-zip-safe +0 -0
- {asynkit-0.19.2 → asynkit-0.20.0}/src/asynkit.egg-info/requires.txt +0 -0
- {asynkit-0.19.2 → asynkit-0.20.0}/src/asynkit.egg-info/top_level.txt +0 -0
- {asynkit-0.19.2 → asynkit-0.20.0}/tests/test_anyio.py +0 -0
- {asynkit-0.19.2 → asynkit-0.20.0}/tests/test_compat_eager.py +0 -0
- {asynkit-0.19.2 → asynkit-0.20.0}/tests/test_df.py +0 -0
- {asynkit-0.19.2 → asynkit-0.20.0}/tests/test_eager_task_factory.py +0 -0
- {asynkit-0.19.2 → asynkit-0.20.0}/tests/test_eager_task_factory_context.py +0 -0
- {asynkit-0.19.2 → asynkit-0.20.0}/tests/test_implementation_params.py +0 -0
- {asynkit-0.19.2 → asynkit-0.20.0}/tests/test_loop.py +0 -0
- {asynkit-0.19.2 → asynkit-0.20.0}/tests/test_monitor.py +0 -0
- {asynkit-0.19.2 → asynkit-0.20.0}/tests/test_tools.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: asynkit
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.20.0
|
|
4
4
|
Summary: A toolkit for Python coroutines
|
|
5
5
|
Author-email: Kristján Valur Jónsson <sweskman@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -445,11 +445,26 @@ stop signal and return instead; in that case it has intentionally decided not to
|
|
|
445
445
|
block after all, and `await_sync()` returns its result. If the code tries to
|
|
446
446
|
access the event loop, e.g. by creating a `Task`, a `RuntimeError` will be raised.
|
|
447
447
|
|
|
448
|
-
|
|
449
|
-
|
|
448
|
+
#### Entering and leaving async code from synchronous code
|
|
449
|
+
|
|
450
|
+
Sometimes you want to write logic once as `async def` but call it from synchronous
|
|
451
|
+
code — and along the way, have that coroutine step back out into blocking synchronous
|
|
452
|
+
implementations. asynkit supports this round trip with two complementary decorator
|
|
453
|
+
pairs:
|
|
454
|
+
|
|
455
|
+
| Step | Function | Method |
|
|
456
|
+
| --- | --- | --- |
|
|
457
|
+
| **Enter** async from sync | `enterasync()` | `enterasyncmethod()` |
|
|
458
|
+
| **Leave** back to blocking sync | `leavesync()` | `leavesyncmethod()` |
|
|
459
|
+
|
|
460
|
+
**Entering async from sync** — `enterasync()` / `enterasyncmethod()` let a synchronous
|
|
461
|
+
caller enter non-blocking async code. Write the logic once as `async def`; the
|
|
462
|
+
decorator runs it via `await_sync()`. The coroutine must complete without suspending
|
|
463
|
+
on real I/O — if it blocks, `SynchronousError` is raised. This replaces hybrid
|
|
464
|
+
functions that sometimes return awaitables.
|
|
450
465
|
|
|
451
466
|
```pycon
|
|
452
|
-
>>> @asynkit.
|
|
467
|
+
>>> @asynkit.enterasync
|
|
453
468
|
... async def sync_function():
|
|
454
469
|
... async def async_function():
|
|
455
470
|
... return "look, no async!"
|
|
@@ -460,30 +475,82 @@ so that it is executed using `await_sync()`:
|
|
|
460
475
|
>>>
|
|
461
476
|
```
|
|
462
477
|
|
|
463
|
-
For methods, use `
|
|
464
|
-
|
|
465
|
-
|
|
478
|
+
For methods, use `enterasyncmethod()` when creating a class-body alias. It behaves like
|
|
479
|
+
`enterasync()` at runtime, but exposes descriptor typing so type checkers can
|
|
480
|
+
distinguish bound and unbound method access:
|
|
466
481
|
|
|
467
482
|
```python
|
|
468
483
|
class Runner:
|
|
469
484
|
async def arun(self, *, yield_every: int | None = None) -> None: ...
|
|
470
485
|
|
|
471
|
-
run = asynkit.
|
|
486
|
+
run = asynkit.enterasyncmethod(arun)
|
|
472
487
|
```
|
|
473
488
|
|
|
474
|
-
|
|
475
|
-
code
|
|
476
|
-
|
|
489
|
+
**Leaving async back to sync** — `leavesync()` / `leavesyncmethod()` let sync-driven
|
|
490
|
+
async code call back into a **blocking sync** implementation before returning to the
|
|
491
|
+
synchronous caller. A common case is monkeypatching an async API with a blocking
|
|
492
|
+
variant — for example greenlet-backed I/O or a thread-blocking socket `recv()`
|
|
493
|
+
standing in for an async read. The wrapper presents that sync code as `async def` so
|
|
494
|
+
it fits the async call chain, but raises `SyncDriveRequiredError` if `await`ed on a
|
|
495
|
+
normal event loop. That guard assures the patched function is only reachable while
|
|
496
|
+
the coroutine is being sync-driven — not from regular asyncio scheduling.
|
|
497
|
+
|
|
498
|
+
The middleware pattern below shows the full round trip: the synchronous client
|
|
499
|
+
**enters** async middleware; the middleware **leaves** back into a sync callback:
|
|
477
500
|
|
|
478
501
|
```python
|
|
479
|
-
@asynkit.
|
|
502
|
+
@asynkit.enterasync
|
|
480
503
|
async def sync_client(sync_callback):
|
|
481
|
-
middleware = AsyncMiddleware(asynkit.
|
|
504
|
+
middleware = AsyncMiddleware(asynkit.leavesync(sync_callback))
|
|
482
505
|
return await middleware.run()
|
|
483
506
|
```
|
|
484
507
|
|
|
485
|
-
|
|
486
|
-
|
|
508
|
+
For methods, use `leavesyncmethod()` when creating a class-body alias. It behaves like
|
|
509
|
+
`leavesync()` at runtime, but exposes descriptor typing so type checkers can
|
|
510
|
+
distinguish bound and unbound method access:
|
|
511
|
+
|
|
512
|
+
```python
|
|
513
|
+
class Client:
|
|
514
|
+
def blocking_read(self, sock) -> bytes:
|
|
515
|
+
return sock.recv(4096) # blocking stand-in for an async read
|
|
516
|
+
|
|
517
|
+
ablocking_read = asynkit.leavesyncmethod(blocking_read)
|
|
518
|
+
|
|
519
|
+
async def afetch(self, sock) -> bytes:
|
|
520
|
+
return await self.ablocking_read(sock)
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
# OK: driven synchronously via await_sync / enterasync / enterasyncmethod
|
|
524
|
+
asynkit.await_sync(Client().afetch(sock))
|
|
525
|
+
|
|
526
|
+
# Raises SyncDriveRequiredError on a real event loop
|
|
527
|
+
await Client().afetch(sock)
|
|
528
|
+
```
|
|
529
|
+
|
|
530
|
+
`enterasync()` and `enterasyncmethod()` establish the sync-drive context indirectly,
|
|
531
|
+
because they call `await_sync()`, which uses `drive_async()`.
|
|
532
|
+
|
|
533
|
+
#### `drive_async()` and sync-drive context
|
|
534
|
+
|
|
535
|
+
`coro_drive()` is the low-level coroutine pump and may be implemented in C for
|
|
536
|
+
performance. `drive_async()` is the Python entry point that wraps `coro_drive()` and
|
|
537
|
+
establishes sync-drive context for the duration of the pump. Use `drive_async()`
|
|
538
|
+
rather than `coro_drive()` alone when `leavesync()` or `require_sync_drive()`
|
|
539
|
+
must work.
|
|
540
|
+
|
|
541
|
+
Helpers such as `in_sync_drive()` and `require_sync_drive()`
|
|
542
|
+
let custom wrappers participate in the same contract. All coroutine code that runs
|
|
543
|
+
between yields executes inside this context, so `leavesync()` checks work
|
|
544
|
+
whether the pump underneath is pure Python or the C extension.
|
|
545
|
+
|
|
546
|
+
```python
|
|
547
|
+
def my_callback(yielded):
|
|
548
|
+
asynkit.require_sync_drive() # OK while drive_async() is active
|
|
549
|
+
...
|
|
550
|
+
|
|
551
|
+
|
|
552
|
+
asynkit.drive_async(coro, my_callback)
|
|
553
|
+
```
|
|
487
554
|
|
|
488
555
|
#### `aiter_sync()`
|
|
489
556
|
|
|
@@ -417,11 +417,26 @@ stop signal and return instead; in that case it has intentionally decided not to
|
|
|
417
417
|
block after all, and `await_sync()` returns its result. If the code tries to
|
|
418
418
|
access the event loop, e.g. by creating a `Task`, a `RuntimeError` will be raised.
|
|
419
419
|
|
|
420
|
-
|
|
421
|
-
|
|
420
|
+
#### Entering and leaving async code from synchronous code
|
|
421
|
+
|
|
422
|
+
Sometimes you want to write logic once as `async def` but call it from synchronous
|
|
423
|
+
code — and along the way, have that coroutine step back out into blocking synchronous
|
|
424
|
+
implementations. asynkit supports this round trip with two complementary decorator
|
|
425
|
+
pairs:
|
|
426
|
+
|
|
427
|
+
| Step | Function | Method |
|
|
428
|
+
| --- | --- | --- |
|
|
429
|
+
| **Enter** async from sync | `enterasync()` | `enterasyncmethod()` |
|
|
430
|
+
| **Leave** back to blocking sync | `leavesync()` | `leavesyncmethod()` |
|
|
431
|
+
|
|
432
|
+
**Entering async from sync** — `enterasync()` / `enterasyncmethod()` let a synchronous
|
|
433
|
+
caller enter non-blocking async code. Write the logic once as `async def`; the
|
|
434
|
+
decorator runs it via `await_sync()`. The coroutine must complete without suspending
|
|
435
|
+
on real I/O — if it blocks, `SynchronousError` is raised. This replaces hybrid
|
|
436
|
+
functions that sometimes return awaitables.
|
|
422
437
|
|
|
423
438
|
```pycon
|
|
424
|
-
>>> @asynkit.
|
|
439
|
+
>>> @asynkit.enterasync
|
|
425
440
|
... async def sync_function():
|
|
426
441
|
... async def async_function():
|
|
427
442
|
... return "look, no async!"
|
|
@@ -432,30 +447,82 @@ so that it is executed using `await_sync()`:
|
|
|
432
447
|
>>>
|
|
433
448
|
```
|
|
434
449
|
|
|
435
|
-
For methods, use `
|
|
436
|
-
|
|
437
|
-
|
|
450
|
+
For methods, use `enterasyncmethod()` when creating a class-body alias. It behaves like
|
|
451
|
+
`enterasync()` at runtime, but exposes descriptor typing so type checkers can
|
|
452
|
+
distinguish bound and unbound method access:
|
|
438
453
|
|
|
439
454
|
```python
|
|
440
455
|
class Runner:
|
|
441
456
|
async def arun(self, *, yield_every: int | None = None) -> None: ...
|
|
442
457
|
|
|
443
|
-
run = asynkit.
|
|
458
|
+
run = asynkit.enterasyncmethod(arun)
|
|
444
459
|
```
|
|
445
460
|
|
|
446
|
-
|
|
447
|
-
code
|
|
448
|
-
|
|
461
|
+
**Leaving async back to sync** — `leavesync()` / `leavesyncmethod()` let sync-driven
|
|
462
|
+
async code call back into a **blocking sync** implementation before returning to the
|
|
463
|
+
synchronous caller. A common case is monkeypatching an async API with a blocking
|
|
464
|
+
variant — for example greenlet-backed I/O or a thread-blocking socket `recv()`
|
|
465
|
+
standing in for an async read. The wrapper presents that sync code as `async def` so
|
|
466
|
+
it fits the async call chain, but raises `SyncDriveRequiredError` if `await`ed on a
|
|
467
|
+
normal event loop. That guard assures the patched function is only reachable while
|
|
468
|
+
the coroutine is being sync-driven — not from regular asyncio scheduling.
|
|
469
|
+
|
|
470
|
+
The middleware pattern below shows the full round trip: the synchronous client
|
|
471
|
+
**enters** async middleware; the middleware **leaves** back into a sync callback:
|
|
449
472
|
|
|
450
473
|
```python
|
|
451
|
-
@asynkit.
|
|
474
|
+
@asynkit.enterasync
|
|
452
475
|
async def sync_client(sync_callback):
|
|
453
|
-
middleware = AsyncMiddleware(asynkit.
|
|
476
|
+
middleware = AsyncMiddleware(asynkit.leavesync(sync_callback))
|
|
454
477
|
return await middleware.run()
|
|
455
478
|
```
|
|
456
479
|
|
|
457
|
-
|
|
458
|
-
|
|
480
|
+
For methods, use `leavesyncmethod()` when creating a class-body alias. It behaves like
|
|
481
|
+
`leavesync()` at runtime, but exposes descriptor typing so type checkers can
|
|
482
|
+
distinguish bound and unbound method access:
|
|
483
|
+
|
|
484
|
+
```python
|
|
485
|
+
class Client:
|
|
486
|
+
def blocking_read(self, sock) -> bytes:
|
|
487
|
+
return sock.recv(4096) # blocking stand-in for an async read
|
|
488
|
+
|
|
489
|
+
ablocking_read = asynkit.leavesyncmethod(blocking_read)
|
|
490
|
+
|
|
491
|
+
async def afetch(self, sock) -> bytes:
|
|
492
|
+
return await self.ablocking_read(sock)
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
# OK: driven synchronously via await_sync / enterasync / enterasyncmethod
|
|
496
|
+
asynkit.await_sync(Client().afetch(sock))
|
|
497
|
+
|
|
498
|
+
# Raises SyncDriveRequiredError on a real event loop
|
|
499
|
+
await Client().afetch(sock)
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
`enterasync()` and `enterasyncmethod()` establish the sync-drive context indirectly,
|
|
503
|
+
because they call `await_sync()`, which uses `drive_async()`.
|
|
504
|
+
|
|
505
|
+
#### `drive_async()` and sync-drive context
|
|
506
|
+
|
|
507
|
+
`coro_drive()` is the low-level coroutine pump and may be implemented in C for
|
|
508
|
+
performance. `drive_async()` is the Python entry point that wraps `coro_drive()` and
|
|
509
|
+
establishes sync-drive context for the duration of the pump. Use `drive_async()`
|
|
510
|
+
rather than `coro_drive()` alone when `leavesync()` or `require_sync_drive()`
|
|
511
|
+
must work.
|
|
512
|
+
|
|
513
|
+
Helpers such as `in_sync_drive()` and `require_sync_drive()`
|
|
514
|
+
let custom wrappers participate in the same contract. All coroutine code that runs
|
|
515
|
+
between yields executes inside this context, so `leavesync()` checks work
|
|
516
|
+
whether the pump underneath is pure Python or the C extension.
|
|
517
|
+
|
|
518
|
+
```python
|
|
519
|
+
def my_callback(yielded):
|
|
520
|
+
asynkit.require_sync_drive() # OK while drive_async() is active
|
|
521
|
+
...
|
|
522
|
+
|
|
523
|
+
|
|
524
|
+
asynkit.drive_async(coro, my_callback)
|
|
525
|
+
```
|
|
459
526
|
|
|
460
527
|
#### `aiter_sync()`
|
|
461
528
|
|
|
@@ -6,6 +6,7 @@ import functools
|
|
|
6
6
|
import importlib
|
|
7
7
|
import inspect
|
|
8
8
|
import sys
|
|
9
|
+
import threading
|
|
9
10
|
import types
|
|
10
11
|
from asyncio import Future, Task
|
|
11
12
|
from collections.abc import (
|
|
@@ -17,7 +18,7 @@ from collections.abc import (
|
|
|
17
18
|
Generator,
|
|
18
19
|
Iterator,
|
|
19
20
|
)
|
|
20
|
-
from contextvars import Context, copy_context
|
|
21
|
+
from contextvars import Context, ContextVar, copy_context
|
|
21
22
|
from types import FrameType
|
|
22
23
|
from typing import (
|
|
23
24
|
TYPE_CHECKING,
|
|
@@ -130,12 +131,18 @@ __all__ = [
|
|
|
130
131
|
"coro_iter",
|
|
131
132
|
"aiter_sync",
|
|
132
133
|
"await_sync",
|
|
134
|
+
"drive_async",
|
|
135
|
+
"in_sync_drive",
|
|
136
|
+
"require_sync_drive",
|
|
133
137
|
"SynchronousError",
|
|
134
138
|
"SynchronousAbort",
|
|
135
|
-
"
|
|
136
|
-
"
|
|
137
|
-
"
|
|
138
|
-
"
|
|
139
|
+
"SyncDriveRequiredError",
|
|
140
|
+
"enterasync",
|
|
141
|
+
"enterasyncmethod",
|
|
142
|
+
"EnterAsyncMethod",
|
|
143
|
+
"leavesync",
|
|
144
|
+
"leavesyncmethod",
|
|
145
|
+
"LeaveSyncMethod",
|
|
139
146
|
]
|
|
140
147
|
|
|
141
148
|
|
|
@@ -187,6 +194,84 @@ class SynchronousAbort(BaseException):
|
|
|
187
194
|
"""
|
|
188
195
|
|
|
189
196
|
|
|
197
|
+
class SyncDriveRequiredError(RuntimeError):
|
|
198
|
+
"""Raised when a sync-drive-only async wrapper is awaited outside drive_async."""
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
_sync_drive_session: ContextVar[int] = ContextVar("_sync_drive_session", default=0)
|
|
202
|
+
_sync_drive_thread_state = threading.local()
|
|
203
|
+
_session_id_lock = threading.Lock()
|
|
204
|
+
_next_session_id = 0
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
class _ThreadSyncDriveState:
|
|
208
|
+
__slots__ = ("lock", "active_sessions")
|
|
209
|
+
|
|
210
|
+
lock: threading.Lock
|
|
211
|
+
active_sessions: list[int]
|
|
212
|
+
|
|
213
|
+
def __init__(self) -> None:
|
|
214
|
+
self.lock = threading.Lock()
|
|
215
|
+
self.active_sessions = []
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _allocate_sync_drive_session() -> int:
|
|
219
|
+
global _next_session_id
|
|
220
|
+
|
|
221
|
+
with _session_id_lock:
|
|
222
|
+
_next_session_id += 1
|
|
223
|
+
return _next_session_id
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def _thread_sync_drive_state() -> _ThreadSyncDriveState:
|
|
227
|
+
state = getattr(_sync_drive_thread_state, "state", None)
|
|
228
|
+
if state is None:
|
|
229
|
+
state = _ThreadSyncDriveState()
|
|
230
|
+
_sync_drive_thread_state.state = state
|
|
231
|
+
return state
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _sync_drive_session_active() -> bool:
|
|
235
|
+
session = _sync_drive_session.get()
|
|
236
|
+
if session == 0:
|
|
237
|
+
return False
|
|
238
|
+
state = _thread_sync_drive_state()
|
|
239
|
+
with state.lock:
|
|
240
|
+
return session in state.active_sessions
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def in_sync_drive() -> bool:
|
|
244
|
+
"""Return True when the current context is inside an active drive_async()."""
|
|
245
|
+
return _sync_drive_session_active()
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def require_sync_drive() -> None:
|
|
249
|
+
"""Require that the current context is inside an active drive_async()."""
|
|
250
|
+
if not _sync_drive_session_active():
|
|
251
|
+
raise SyncDriveRequiredError(
|
|
252
|
+
"operation requires a coroutine being synchronously driven "
|
|
253
|
+
"(e.g. via await_sync())"
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
@contextlib.contextmanager
|
|
258
|
+
def _sync_drive_context() -> Generator[None, None, None]:
|
|
259
|
+
session = _allocate_sync_drive_session()
|
|
260
|
+
session_token = _sync_drive_session.set(session)
|
|
261
|
+
state = _thread_sync_drive_state()
|
|
262
|
+
with state.lock:
|
|
263
|
+
state.active_sessions.append(session)
|
|
264
|
+
try:
|
|
265
|
+
yield
|
|
266
|
+
finally:
|
|
267
|
+
try:
|
|
268
|
+
with state.lock:
|
|
269
|
+
if session in state.active_sessions:
|
|
270
|
+
state.active_sessions.remove(session)
|
|
271
|
+
finally:
|
|
272
|
+
_sync_drive_session.reset(session_token)
|
|
273
|
+
|
|
274
|
+
|
|
190
275
|
# Helpers to find if a coroutine (or a generator as created by types.coroutine)
|
|
191
276
|
# has started or finished
|
|
192
277
|
|
|
@@ -1071,7 +1156,11 @@ def coro_iter(coro: Coroutine[Any, Any, T]) -> Generator[Any, Any, T]:
|
|
|
1071
1156
|
|
|
1072
1157
|
|
|
1073
1158
|
def coro_drive(coro: Coroutine[Any, Any, T], callback: _CoroYieldCallback) -> T:
|
|
1074
|
-
"""Drive a coroutine by passing each yielded value to a callback.
|
|
1159
|
+
"""Drive a coroutine by passing each yielded value to a callback.
|
|
1160
|
+
|
|
1161
|
+
This is the low-level pump. Use `drive_async()` instead when
|
|
1162
|
+
`leavesync()` or `require_sync_drive()` must work.
|
|
1163
|
+
"""
|
|
1075
1164
|
send_value = None
|
|
1076
1165
|
pending_error: BaseException | None = None
|
|
1077
1166
|
|
|
@@ -1103,6 +1192,17 @@ if not TYPE_CHECKING and _HAVE_C_EXTENSION and _c_coro_drive is not None:
|
|
|
1103
1192
|
coro_drive = _c_coro_drive
|
|
1104
1193
|
|
|
1105
1194
|
|
|
1195
|
+
def drive_async(coro: Coroutine[Any, Any, T], callback: _CoroYieldCallback) -> T:
|
|
1196
|
+
"""Drive a coroutine synchronously and mark the context as sync-driven.
|
|
1197
|
+
|
|
1198
|
+
Custom synchronous pumps must use this rather than `coro_drive()` alone
|
|
1199
|
+
when participating in the sync-drive contract used by `leavesync()`
|
|
1200
|
+
and `require_sync_drive()`. Session tracking is per-thread.
|
|
1201
|
+
"""
|
|
1202
|
+
with _sync_drive_context():
|
|
1203
|
+
return coro_drive(coro, callback)
|
|
1204
|
+
|
|
1205
|
+
|
|
1106
1206
|
def awaitmethod(func: Callable[P, Coroutine[Any, Any, T]]) -> Callable[P, Iterator[T]]:
|
|
1107
1207
|
"""
|
|
1108
1208
|
Decorator to make a function return an awaitable.
|
|
@@ -1155,14 +1255,16 @@ def await_sync(coro: Coroutine[Any, Any, T], *, ignore_nullsleep: bool = True) -
|
|
|
1155
1255
|
raise GeneratorExit()
|
|
1156
1256
|
|
|
1157
1257
|
try:
|
|
1158
|
-
return
|
|
1258
|
+
return drive_async(coro, callback)
|
|
1159
1259
|
except (GeneratorExit, SynchronousAbort) as err:
|
|
1160
1260
|
raise SynchronousError("coroutine failed to complete synchronously") from err
|
|
1161
1261
|
|
|
1162
1262
|
|
|
1163
|
-
def
|
|
1164
|
-
"""
|
|
1165
|
-
|
|
1263
|
+
def enterasync(func: Callable[P, Coroutine[Any, Any, T]]) -> Callable[P, T]:
|
|
1264
|
+
"""Let synchronous callers enter non-blocking async code.
|
|
1265
|
+
|
|
1266
|
+
Runs the coroutine via `await_sync()`. Pair with `leavesync()` when
|
|
1267
|
+
sync-driven async code must leave back into blocking sync implementations.
|
|
1166
1268
|
"""
|
|
1167
1269
|
|
|
1168
1270
|
@functools.wraps(func)
|
|
@@ -1172,11 +1274,11 @@ def syncfunction(func: Callable[P, Coroutine[Any, Any, T]]) -> Callable[P, T]:
|
|
|
1172
1274
|
return helper
|
|
1173
1275
|
|
|
1174
1276
|
|
|
1175
|
-
class
|
|
1176
|
-
"""Descriptor
|
|
1277
|
+
class EnterAsyncMethod(Generic[SelfT, P, T]):
|
|
1278
|
+
"""Descriptor for entering non-blocking async methods from synchronous callers.
|
|
1177
1279
|
|
|
1178
|
-
Unlike `
|
|
1179
|
-
for class-body aliases such as `run =
|
|
1280
|
+
Unlike `enterasync()`, this preserves method binding semantics when used
|
|
1281
|
+
for class-body aliases such as `run = enterasyncmethod(arun)`.
|
|
1180
1282
|
"""
|
|
1181
1283
|
|
|
1182
1284
|
def __init__(
|
|
@@ -1218,25 +1320,92 @@ class SyncMethod(Generic[SelfT, P, T]):
|
|
|
1218
1320
|
return await_sync(self._func(instance, *args, **kwargs))
|
|
1219
1321
|
|
|
1220
1322
|
|
|
1221
|
-
def
|
|
1323
|
+
def enterasyncmethod(
|
|
1222
1324
|
func: Callable[Concatenate[SelfT, P], Coroutine[Any, Any, T]],
|
|
1223
|
-
) ->
|
|
1224
|
-
"""
|
|
1225
|
-
|
|
1325
|
+
) -> EnterAsyncMethod[SelfT, P, T]:
|
|
1326
|
+
"""Enter non-blocking async methods from synchronous callers.
|
|
1327
|
+
|
|
1328
|
+
Preserves descriptor typing for class-body aliases such as
|
|
1329
|
+
`run = enterasyncmethod(arun)`.
|
|
1330
|
+
"""
|
|
1331
|
+
return EnterAsyncMethod(func)
|
|
1226
1332
|
|
|
1227
1333
|
|
|
1228
|
-
def
|
|
1229
|
-
"""
|
|
1230
|
-
|
|
1334
|
+
def leavesync(func: Callable[P, T]) -> Callable[P, Coroutine[Any, Any, T]]:
|
|
1335
|
+
"""Let sync-driven async code leave back into blocking sync implementations.
|
|
1336
|
+
|
|
1337
|
+
Pair with `enterasync()`: after entering async from sync, a coroutine may
|
|
1338
|
+
step out into blocking sync callbacks such as patched stand-ins for formerly-
|
|
1339
|
+
async APIs. Raises `SyncDriveRequiredError` when awaited outside `drive_async()`.
|
|
1231
1340
|
"""
|
|
1232
1341
|
|
|
1233
1342
|
@functools.wraps(func)
|
|
1234
1343
|
async def helper(*args: P.args, **kwargs: P.kwargs) -> T:
|
|
1344
|
+
require_sync_drive()
|
|
1235
1345
|
return func(*args, **kwargs)
|
|
1236
1346
|
|
|
1237
1347
|
return helper
|
|
1238
1348
|
|
|
1239
1349
|
|
|
1350
|
+
class LeaveSyncMethod(Generic[SelfT, P, T]):
|
|
1351
|
+
"""Descriptor for leaving sync-driven async code back into blocking sync methods.
|
|
1352
|
+
|
|
1353
|
+
Unlike `leavesync()`, this preserves method binding semantics when used
|
|
1354
|
+
for class-body aliases such as `ablocking_read = leavesyncmethod(blocking_read)`.
|
|
1355
|
+
"""
|
|
1356
|
+
|
|
1357
|
+
def __init__(
|
|
1358
|
+
self,
|
|
1359
|
+
func: Callable[Concatenate[SelfT, P], T],
|
|
1360
|
+
) -> None:
|
|
1361
|
+
self._func = func
|
|
1362
|
+
functools.update_wrapper(self, func)
|
|
1363
|
+
|
|
1364
|
+
@overload
|
|
1365
|
+
def __get__(
|
|
1366
|
+
self,
|
|
1367
|
+
instance: None,
|
|
1368
|
+
owner: type[SelfT],
|
|
1369
|
+
) -> Callable[Concatenate[SelfT, P], Coroutine[Any, Any, T]]: ...
|
|
1370
|
+
|
|
1371
|
+
@overload
|
|
1372
|
+
def __get__(
|
|
1373
|
+
self,
|
|
1374
|
+
instance: SelfT,
|
|
1375
|
+
owner: type[SelfT] | None = None,
|
|
1376
|
+
) -> Callable[P, Coroutine[Any, Any, T]]: ...
|
|
1377
|
+
|
|
1378
|
+
def __get__(
|
|
1379
|
+
self,
|
|
1380
|
+
instance: SelfT | None,
|
|
1381
|
+
owner: type[SelfT] | None = None,
|
|
1382
|
+
) -> Callable[..., Coroutine[Any, Any, T]]:
|
|
1383
|
+
if instance is None:
|
|
1384
|
+
return self
|
|
1385
|
+
|
|
1386
|
+
@functools.wraps(self._func)
|
|
1387
|
+
async def helper(*args: P.args, **kwargs: P.kwargs) -> T:
|
|
1388
|
+
require_sync_drive()
|
|
1389
|
+
return self._func(instance, *args, **kwargs)
|
|
1390
|
+
|
|
1391
|
+
return helper
|
|
1392
|
+
|
|
1393
|
+
async def __call__(self, instance: SelfT, *args: P.args, **kwargs: P.kwargs) -> T:
|
|
1394
|
+
require_sync_drive()
|
|
1395
|
+
return self._func(instance, *args, **kwargs)
|
|
1396
|
+
|
|
1397
|
+
|
|
1398
|
+
def leavesyncmethod(
|
|
1399
|
+
func: Callable[Concatenate[SelfT, P], T],
|
|
1400
|
+
) -> LeaveSyncMethod[SelfT, P, T]:
|
|
1401
|
+
"""Leave sync-driven async code back into blocking sync methods.
|
|
1402
|
+
|
|
1403
|
+
Preserves descriptor typing for class-body aliases such as
|
|
1404
|
+
`ablocking_read = leavesyncmethod(blocking_read)`.
|
|
1405
|
+
"""
|
|
1406
|
+
return LeaveSyncMethod(func)
|
|
1407
|
+
|
|
1408
|
+
|
|
1240
1409
|
def aiter_sync(async_iterable: AsyncIterable[T]) -> Generator[T, None, None]:
|
|
1241
1410
|
"""Iterate synchronously over an async iterable"""
|
|
1242
1411
|
ai = async_iterable.__aiter__()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: asynkit
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.20.0
|
|
4
4
|
Summary: A toolkit for Python coroutines
|
|
5
5
|
Author-email: Kristján Valur Jónsson <sweskman@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -445,11 +445,26 @@ stop signal and return instead; in that case it has intentionally decided not to
|
|
|
445
445
|
block after all, and `await_sync()` returns its result. If the code tries to
|
|
446
446
|
access the event loop, e.g. by creating a `Task`, a `RuntimeError` will be raised.
|
|
447
447
|
|
|
448
|
-
|
|
449
|
-
|
|
448
|
+
#### Entering and leaving async code from synchronous code
|
|
449
|
+
|
|
450
|
+
Sometimes you want to write logic once as `async def` but call it from synchronous
|
|
451
|
+
code — and along the way, have that coroutine step back out into blocking synchronous
|
|
452
|
+
implementations. asynkit supports this round trip with two complementary decorator
|
|
453
|
+
pairs:
|
|
454
|
+
|
|
455
|
+
| Step | Function | Method |
|
|
456
|
+
| --- | --- | --- |
|
|
457
|
+
| **Enter** async from sync | `enterasync()` | `enterasyncmethod()` |
|
|
458
|
+
| **Leave** back to blocking sync | `leavesync()` | `leavesyncmethod()` |
|
|
459
|
+
|
|
460
|
+
**Entering async from sync** — `enterasync()` / `enterasyncmethod()` let a synchronous
|
|
461
|
+
caller enter non-blocking async code. Write the logic once as `async def`; the
|
|
462
|
+
decorator runs it via `await_sync()`. The coroutine must complete without suspending
|
|
463
|
+
on real I/O — if it blocks, `SynchronousError` is raised. This replaces hybrid
|
|
464
|
+
functions that sometimes return awaitables.
|
|
450
465
|
|
|
451
466
|
```pycon
|
|
452
|
-
>>> @asynkit.
|
|
467
|
+
>>> @asynkit.enterasync
|
|
453
468
|
... async def sync_function():
|
|
454
469
|
... async def async_function():
|
|
455
470
|
... return "look, no async!"
|
|
@@ -460,30 +475,82 @@ so that it is executed using `await_sync()`:
|
|
|
460
475
|
>>>
|
|
461
476
|
```
|
|
462
477
|
|
|
463
|
-
For methods, use `
|
|
464
|
-
|
|
465
|
-
|
|
478
|
+
For methods, use `enterasyncmethod()` when creating a class-body alias. It behaves like
|
|
479
|
+
`enterasync()` at runtime, but exposes descriptor typing so type checkers can
|
|
480
|
+
distinguish bound and unbound method access:
|
|
466
481
|
|
|
467
482
|
```python
|
|
468
483
|
class Runner:
|
|
469
484
|
async def arun(self, *, yield_every: int | None = None) -> None: ...
|
|
470
485
|
|
|
471
|
-
run = asynkit.
|
|
486
|
+
run = asynkit.enterasyncmethod(arun)
|
|
472
487
|
```
|
|
473
488
|
|
|
474
|
-
|
|
475
|
-
code
|
|
476
|
-
|
|
489
|
+
**Leaving async back to sync** — `leavesync()` / `leavesyncmethod()` let sync-driven
|
|
490
|
+
async code call back into a **blocking sync** implementation before returning to the
|
|
491
|
+
synchronous caller. A common case is monkeypatching an async API with a blocking
|
|
492
|
+
variant — for example greenlet-backed I/O or a thread-blocking socket `recv()`
|
|
493
|
+
standing in for an async read. The wrapper presents that sync code as `async def` so
|
|
494
|
+
it fits the async call chain, but raises `SyncDriveRequiredError` if `await`ed on a
|
|
495
|
+
normal event loop. That guard assures the patched function is only reachable while
|
|
496
|
+
the coroutine is being sync-driven — not from regular asyncio scheduling.
|
|
497
|
+
|
|
498
|
+
The middleware pattern below shows the full round trip: the synchronous client
|
|
499
|
+
**enters** async middleware; the middleware **leaves** back into a sync callback:
|
|
477
500
|
|
|
478
501
|
```python
|
|
479
|
-
@asynkit.
|
|
502
|
+
@asynkit.enterasync
|
|
480
503
|
async def sync_client(sync_callback):
|
|
481
|
-
middleware = AsyncMiddleware(asynkit.
|
|
504
|
+
middleware = AsyncMiddleware(asynkit.leavesync(sync_callback))
|
|
482
505
|
return await middleware.run()
|
|
483
506
|
```
|
|
484
507
|
|
|
485
|
-
|
|
486
|
-
|
|
508
|
+
For methods, use `leavesyncmethod()` when creating a class-body alias. It behaves like
|
|
509
|
+
`leavesync()` at runtime, but exposes descriptor typing so type checkers can
|
|
510
|
+
distinguish bound and unbound method access:
|
|
511
|
+
|
|
512
|
+
```python
|
|
513
|
+
class Client:
|
|
514
|
+
def blocking_read(self, sock) -> bytes:
|
|
515
|
+
return sock.recv(4096) # blocking stand-in for an async read
|
|
516
|
+
|
|
517
|
+
ablocking_read = asynkit.leavesyncmethod(blocking_read)
|
|
518
|
+
|
|
519
|
+
async def afetch(self, sock) -> bytes:
|
|
520
|
+
return await self.ablocking_read(sock)
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
# OK: driven synchronously via await_sync / enterasync / enterasyncmethod
|
|
524
|
+
asynkit.await_sync(Client().afetch(sock))
|
|
525
|
+
|
|
526
|
+
# Raises SyncDriveRequiredError on a real event loop
|
|
527
|
+
await Client().afetch(sock)
|
|
528
|
+
```
|
|
529
|
+
|
|
530
|
+
`enterasync()` and `enterasyncmethod()` establish the sync-drive context indirectly,
|
|
531
|
+
because they call `await_sync()`, which uses `drive_async()`.
|
|
532
|
+
|
|
533
|
+
#### `drive_async()` and sync-drive context
|
|
534
|
+
|
|
535
|
+
`coro_drive()` is the low-level coroutine pump and may be implemented in C for
|
|
536
|
+
performance. `drive_async()` is the Python entry point that wraps `coro_drive()` and
|
|
537
|
+
establishes sync-drive context for the duration of the pump. Use `drive_async()`
|
|
538
|
+
rather than `coro_drive()` alone when `leavesync()` or `require_sync_drive()`
|
|
539
|
+
must work.
|
|
540
|
+
|
|
541
|
+
Helpers such as `in_sync_drive()` and `require_sync_drive()`
|
|
542
|
+
let custom wrappers participate in the same contract. All coroutine code that runs
|
|
543
|
+
between yields executes inside this context, so `leavesync()` checks work
|
|
544
|
+
whether the pump underneath is pure Python or the C extension.
|
|
545
|
+
|
|
546
|
+
```python
|
|
547
|
+
def my_callback(yielded):
|
|
548
|
+
asynkit.require_sync_drive() # OK while drive_async() is active
|
|
549
|
+
...
|
|
550
|
+
|
|
551
|
+
|
|
552
|
+
asynkit.drive_async(coro, my_callback)
|
|
553
|
+
```
|
|
487
554
|
|
|
488
555
|
#### `aiter_sync()`
|
|
489
556
|
|
|
@@ -829,20 +829,20 @@ class TestCoroRun:
|
|
|
829
829
|
async def blocks(self):
|
|
830
830
|
await drive_yield("blocked")
|
|
831
831
|
|
|
832
|
-
@asynkit.
|
|
832
|
+
@asynkit.enterasync
|
|
833
833
|
async def sync_simple(self):
|
|
834
834
|
return await self.simple()
|
|
835
835
|
|
|
836
|
-
@asynkit.
|
|
836
|
+
@asynkit.enterasync
|
|
837
837
|
async def sync_cleanup(self):
|
|
838
838
|
return await self.cleanupper()
|
|
839
839
|
|
|
840
|
-
@asynkit.
|
|
840
|
+
@asynkit.enterasync
|
|
841
841
|
async def sync_genexit(self):
|
|
842
842
|
return await self.genexit()
|
|
843
843
|
|
|
844
|
-
method_simple = asynkit.
|
|
845
|
-
method_blocks = asynkit.
|
|
844
|
+
method_simple = asynkit.enterasyncmethod(simple)
|
|
845
|
+
method_blocks = asynkit.enterasyncmethod(blocks)
|
|
846
846
|
|
|
847
847
|
def test_simple(self):
|
|
848
848
|
assert asynkit.await_sync(self.simple()) == "simple"
|
|
@@ -858,19 +858,19 @@ class TestCoroRun:
|
|
|
858
858
|
def test_sync_simple(self):
|
|
859
859
|
assert self.sync_simple() == "simple"
|
|
860
860
|
|
|
861
|
-
def
|
|
861
|
+
def test_enterasyncmethod_bound(self):
|
|
862
862
|
assert self.method_simple() == "simple"
|
|
863
863
|
|
|
864
|
-
def
|
|
864
|
+
def test_enterasyncmethod_unbound(self):
|
|
865
865
|
assert type(self).method_simple(self) == "simple"
|
|
866
866
|
|
|
867
|
-
def
|
|
867
|
+
def test_enterasyncmethod_descriptor(self):
|
|
868
868
|
descriptor = type(self).__dict__["method_simple"]
|
|
869
|
-
assert isinstance(descriptor, asynkit.
|
|
869
|
+
assert isinstance(descriptor, asynkit.EnterAsyncMethod)
|
|
870
870
|
assert type(self).method_simple.__name__ == "simple"
|
|
871
871
|
assert self.method_simple.__name__ == "simple"
|
|
872
872
|
|
|
873
|
-
def
|
|
873
|
+
def test_enterasyncmethod_blocks(self):
|
|
874
874
|
with pytest.raises(asynkit.SynchronousError) as err:
|
|
875
875
|
self.method_blocks()
|
|
876
876
|
assert err.match("failed to complete synchronously")
|
|
@@ -1326,27 +1326,318 @@ class TestCoroIter:
|
|
|
1326
1326
|
assert await a == "Awaiter5"
|
|
1327
1327
|
|
|
1328
1328
|
|
|
1329
|
-
|
|
1329
|
+
def test_leavesync_inside_await_sync():
|
|
1330
1330
|
def sync_method():
|
|
1331
1331
|
return "foo"
|
|
1332
1332
|
|
|
1333
|
-
@asynkit.
|
|
1333
|
+
@asynkit.leavesync
|
|
1334
1334
|
def sync_method2():
|
|
1335
1335
|
return "bar"
|
|
1336
1336
|
|
|
1337
|
-
|
|
1338
|
-
|
|
1337
|
+
async def wrapper() -> str:
|
|
1338
|
+
return await asynkit.leavesync(sync_method)()
|
|
1339
1339
|
|
|
1340
|
+
assert asynkit.await_sync(wrapper()) == "foo"
|
|
1341
|
+
assert asynkit.await_sync(sync_method2()) == "bar"
|
|
1340
1342
|
|
|
1341
|
-
|
|
1343
|
+
|
|
1344
|
+
async def test_leavesync_outside_sync_drive():
|
|
1345
|
+
def sync_method():
|
|
1346
|
+
return "foo"
|
|
1347
|
+
|
|
1348
|
+
@asynkit.leavesync
|
|
1349
|
+
def sync_method2():
|
|
1350
|
+
return "bar"
|
|
1351
|
+
|
|
1352
|
+
with pytest.raises(asynkit.SyncDriveRequiredError) as err:
|
|
1353
|
+
await asynkit.leavesync(sync_method)()
|
|
1354
|
+
assert err.match("synchronously driven")
|
|
1355
|
+
|
|
1356
|
+
with pytest.raises(asynkit.SyncDriveRequiredError) as err:
|
|
1357
|
+
await sync_method2()
|
|
1358
|
+
assert err.match("synchronously driven")
|
|
1359
|
+
|
|
1360
|
+
|
|
1361
|
+
async def test_leavesyncmethod_outside_sync_drive():
|
|
1362
|
+
class Client:
|
|
1363
|
+
def blocking_read(self) -> str:
|
|
1364
|
+
return "payload"
|
|
1365
|
+
|
|
1366
|
+
ablocking_read = asynkit.leavesyncmethod(blocking_read)
|
|
1367
|
+
|
|
1368
|
+
client = Client()
|
|
1369
|
+
|
|
1370
|
+
with pytest.raises(asynkit.SyncDriveRequiredError) as err:
|
|
1371
|
+
await client.ablocking_read()
|
|
1372
|
+
assert err.match("synchronously driven")
|
|
1373
|
+
|
|
1374
|
+
with pytest.raises(asynkit.SyncDriveRequiredError) as err:
|
|
1375
|
+
await Client.ablocking_read(client)
|
|
1376
|
+
assert err.match("synchronously driven")
|
|
1377
|
+
|
|
1378
|
+
|
|
1379
|
+
def test_leavesyncmethod_descriptor():
|
|
1380
|
+
class Client:
|
|
1381
|
+
def blocking_read(self) -> str:
|
|
1382
|
+
return "payload"
|
|
1383
|
+
|
|
1384
|
+
ablocking_read = asynkit.leavesyncmethod(blocking_read)
|
|
1385
|
+
|
|
1386
|
+
descriptor = Client.__dict__["ablocking_read"]
|
|
1387
|
+
assert isinstance(descriptor, asynkit.LeaveSyncMethod)
|
|
1388
|
+
assert Client.ablocking_read.__name__ == "blocking_read"
|
|
1389
|
+
assert Client().ablocking_read.__name__ == "blocking_read"
|
|
1390
|
+
|
|
1391
|
+
|
|
1392
|
+
def test_leavesyncmethod_bound_inside_await_sync():
|
|
1393
|
+
class Client:
|
|
1394
|
+
def blocking_read(self) -> str:
|
|
1395
|
+
return "payload"
|
|
1396
|
+
|
|
1397
|
+
ablocking_read = asynkit.leavesyncmethod(blocking_read)
|
|
1398
|
+
|
|
1399
|
+
async def fetch() -> str:
|
|
1400
|
+
return await Client().ablocking_read()
|
|
1401
|
+
|
|
1402
|
+
assert asynkit.await_sync(fetch()) == "payload"
|
|
1403
|
+
|
|
1404
|
+
|
|
1405
|
+
def test_leavesyncmethod_unbound_inside_await_sync():
|
|
1406
|
+
class Client:
|
|
1407
|
+
def blocking_read(self) -> str:
|
|
1408
|
+
return "payload"
|
|
1409
|
+
|
|
1410
|
+
ablocking_read = asynkit.leavesyncmethod(blocking_read)
|
|
1411
|
+
|
|
1412
|
+
async def fetch(client: Client) -> str:
|
|
1413
|
+
return await Client.ablocking_read(client)
|
|
1414
|
+
|
|
1415
|
+
assert asynkit.await_sync(fetch(Client())) == "payload"
|
|
1416
|
+
|
|
1417
|
+
|
|
1418
|
+
def test_sync_drive_context_cleared_after_await_sync_raises():
|
|
1419
|
+
async def fails() -> None:
|
|
1420
|
+
raise ValueError("boom")
|
|
1421
|
+
|
|
1422
|
+
with pytest.raises(ValueError, match="boom"):
|
|
1423
|
+
asynkit.await_sync(fails())
|
|
1424
|
+
|
|
1425
|
+
assert not asynkit.in_sync_drive()
|
|
1426
|
+
with pytest.raises(asynkit.SyncDriveRequiredError):
|
|
1427
|
+
asynkit.require_sync_drive()
|
|
1428
|
+
|
|
1429
|
+
|
|
1430
|
+
def test_sync_drive_context_cleared_after_drive_async_raises():
|
|
1431
|
+
async def coro() -> str:
|
|
1432
|
+
await drive_yield(None)
|
|
1433
|
+
return "done"
|
|
1434
|
+
|
|
1435
|
+
def callback(_yielded: object) -> None:
|
|
1436
|
+
raise ValueError("callback boom")
|
|
1437
|
+
|
|
1438
|
+
with pytest.raises(ValueError, match="callback boom"):
|
|
1439
|
+
asynkit.drive_async(coro(), callback)
|
|
1440
|
+
|
|
1441
|
+
assert not asynkit.in_sync_drive()
|
|
1442
|
+
with pytest.raises(asynkit.SyncDriveRequiredError):
|
|
1443
|
+
asynkit.require_sync_drive()
|
|
1444
|
+
|
|
1445
|
+
|
|
1446
|
+
def test_sync_drive_context_outside_drive():
|
|
1447
|
+
assert not asynkit.in_sync_drive()
|
|
1448
|
+
|
|
1449
|
+
|
|
1450
|
+
def test_require_sync_drive_outside_drive():
|
|
1451
|
+
with pytest.raises(asynkit.SyncDriveRequiredError) as err:
|
|
1452
|
+
asynkit.require_sync_drive()
|
|
1453
|
+
assert err.match("synchronously driven")
|
|
1454
|
+
|
|
1455
|
+
|
|
1456
|
+
@pytest.mark.parametrize("_name,drive", coro_drive_implementations())
|
|
1457
|
+
def test_drive_async_sets_sync_drive_context(_name, drive, monkeypatch):
|
|
1458
|
+
monkeypatch.setattr(asynkit.coroutine, "coro_drive", drive)
|
|
1459
|
+
observations: dict[str, bool | None] = {
|
|
1460
|
+
"callback": None,
|
|
1461
|
+
"coro": None,
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
async def coro() -> str:
|
|
1465
|
+
observations["coro"] = asynkit.in_sync_drive()
|
|
1466
|
+
await drive_yield(None)
|
|
1467
|
+
return "done"
|
|
1468
|
+
|
|
1469
|
+
def callback(yielded: object) -> None:
|
|
1470
|
+
assert yielded is None
|
|
1471
|
+
observations["callback"] = asynkit.in_sync_drive()
|
|
1472
|
+
asynkit.require_sync_drive()
|
|
1473
|
+
|
|
1474
|
+
assert asynkit.drive_async(coro(), callback) == "done"
|
|
1475
|
+
assert observations["callback"] is True
|
|
1476
|
+
assert observations["coro"] is True
|
|
1477
|
+
|
|
1478
|
+
|
|
1479
|
+
@pytest.mark.parametrize("_name,drive", coro_drive_implementations())
|
|
1480
|
+
def test_coro_drive_does_not_set_sync_drive_context(_name, drive):
|
|
1481
|
+
async def coro() -> str:
|
|
1482
|
+
asynkit.require_sync_drive()
|
|
1483
|
+
return "done"
|
|
1484
|
+
|
|
1485
|
+
with pytest.raises(asynkit.SyncDriveRequiredError) as err:
|
|
1486
|
+
drive(coro(), lambda _yielded: None)
|
|
1487
|
+
assert err.match("synchronously driven")
|
|
1488
|
+
|
|
1489
|
+
|
|
1490
|
+
def test_copied_sync_drive_context_rejected_after_drive_ends():
|
|
1491
|
+
captured: list[Any] = []
|
|
1492
|
+
|
|
1493
|
+
async def coro() -> str:
|
|
1494
|
+
captured.append(copy_context())
|
|
1495
|
+
return "done"
|
|
1496
|
+
|
|
1497
|
+
asynkit.drive_async(coro(), lambda _yielded: None)
|
|
1498
|
+
|
|
1499
|
+
with pytest.raises(asynkit.SyncDriveRequiredError):
|
|
1500
|
+
captured[0].run(asynkit.require_sync_drive)
|
|
1501
|
+
|
|
1502
|
+
|
|
1503
|
+
def test_sync_drive_context_inside_await_sync():
|
|
1504
|
+
async def record_context() -> None:
|
|
1505
|
+
assert asynkit.in_sync_drive()
|
|
1506
|
+
|
|
1507
|
+
asynkit.await_sync(record_context())
|
|
1508
|
+
assert not asynkit.in_sync_drive()
|
|
1509
|
+
|
|
1510
|
+
|
|
1511
|
+
def test_sync_drive_context_nested():
|
|
1512
|
+
flags: list[bool] = []
|
|
1513
|
+
|
|
1514
|
+
async def inner() -> None:
|
|
1515
|
+
flags.append(asynkit.in_sync_drive())
|
|
1516
|
+
|
|
1517
|
+
async def outer() -> None:
|
|
1518
|
+
flags.append(asynkit.in_sync_drive())
|
|
1519
|
+
asynkit.await_sync(inner())
|
|
1520
|
+
|
|
1521
|
+
asynkit.await_sync(outer())
|
|
1522
|
+
assert flags == [True, True]
|
|
1523
|
+
assert not asynkit.in_sync_drive()
|
|
1524
|
+
|
|
1525
|
+
|
|
1526
|
+
def test_enterasync_uses_drive_async_via_await_sync():
|
|
1527
|
+
@asynkit.leavesync
|
|
1528
|
+
def blocking_read() -> str:
|
|
1529
|
+
return "payload"
|
|
1530
|
+
|
|
1531
|
+
@asynkit.enterasync
|
|
1532
|
+
async def fetch() -> str:
|
|
1533
|
+
return await blocking_read()
|
|
1534
|
+
|
|
1535
|
+
assert fetch() == "payload"
|
|
1536
|
+
|
|
1537
|
+
|
|
1538
|
+
def test_enterasyncmethod_uses_drive_async_via_await_sync():
|
|
1539
|
+
class Client:
|
|
1540
|
+
def blocking_read(self) -> str:
|
|
1541
|
+
return "payload"
|
|
1542
|
+
|
|
1543
|
+
ablocking_read = asynkit.leavesyncmethod(blocking_read)
|
|
1544
|
+
|
|
1545
|
+
async def afetch(self) -> str:
|
|
1546
|
+
return await self.ablocking_read()
|
|
1547
|
+
|
|
1548
|
+
fetch = asynkit.enterasyncmethod(afetch)
|
|
1549
|
+
|
|
1550
|
+
assert Client().fetch() == "payload"
|
|
1551
|
+
|
|
1552
|
+
|
|
1553
|
+
def test_copied_sync_drive_context_rejected_on_other_thread_during_drive():
|
|
1554
|
+
import threading
|
|
1555
|
+
|
|
1556
|
+
a_context: list[Any] = []
|
|
1557
|
+
result: list[str] = []
|
|
1558
|
+
|
|
1559
|
+
async def on_b() -> None:
|
|
1560
|
+
async def on_a_capture() -> None:
|
|
1561
|
+
a_context.append(copy_context())
|
|
1562
|
+
|
|
1563
|
+
thread_a = threading.Thread(target=lambda: asynkit.await_sync(on_a_capture()))
|
|
1564
|
+
thread_a.start()
|
|
1565
|
+
thread_a.join()
|
|
1566
|
+
|
|
1567
|
+
def probe() -> None:
|
|
1568
|
+
try:
|
|
1569
|
+
a_context[0].run(asynkit.require_sync_drive)
|
|
1570
|
+
result.append("passed")
|
|
1571
|
+
except asynkit.SyncDriveRequiredError:
|
|
1572
|
+
result.append("rejected")
|
|
1573
|
+
|
|
1574
|
+
probe()
|
|
1575
|
+
|
|
1576
|
+
thread_b = threading.Thread(target=lambda: asynkit.await_sync(on_b()))
|
|
1577
|
+
thread_b.start()
|
|
1578
|
+
thread_b.join()
|
|
1579
|
+
assert result == ["rejected"]
|
|
1580
|
+
|
|
1581
|
+
|
|
1582
|
+
def test_concurrent_drive_async_isolated_per_thread():
|
|
1583
|
+
import threading
|
|
1584
|
+
|
|
1585
|
+
observations: list[tuple[str, bool]] = []
|
|
1586
|
+
lock = threading.Lock()
|
|
1587
|
+
start = threading.Barrier(2)
|
|
1588
|
+
|
|
1589
|
+
async def work(label: str) -> None:
|
|
1590
|
+
start.wait(timeout=5)
|
|
1591
|
+
with lock:
|
|
1592
|
+
observations.append((label, asynkit.in_sync_drive()))
|
|
1593
|
+
|
|
1594
|
+
def run(label: str) -> None:
|
|
1595
|
+
asynkit.await_sync(work(label))
|
|
1596
|
+
|
|
1597
|
+
threads = [threading.Thread(target=run, args=(label,)) for label in ("a", "b")]
|
|
1598
|
+
for thread in threads:
|
|
1599
|
+
thread.start()
|
|
1600
|
+
for thread in threads:
|
|
1601
|
+
thread.join()
|
|
1602
|
+
|
|
1603
|
+
assert sorted(observations) == [("a", True), ("b", True)]
|
|
1604
|
+
|
|
1605
|
+
|
|
1606
|
+
class TestSyncDriveStaleContext:
|
|
1607
|
+
@pytest.fixture
|
|
1608
|
+
def anyio_backend(self):
|
|
1609
|
+
return "asyncio"
|
|
1610
|
+
|
|
1611
|
+
async def test_leavesync_rejects_stale_copied_context(self):
|
|
1612
|
+
ran: list[bool] = []
|
|
1613
|
+
|
|
1614
|
+
@asynkit.leavesync
|
|
1615
|
+
def blocking_read() -> str:
|
|
1616
|
+
ran.append(True)
|
|
1617
|
+
return "payload"
|
|
1618
|
+
|
|
1619
|
+
async def deferred() -> str:
|
|
1620
|
+
return await blocking_read()
|
|
1621
|
+
|
|
1622
|
+
async def coro_with_task():
|
|
1623
|
+
return asyncio.create_task(deferred())
|
|
1624
|
+
|
|
1625
|
+
task = asynkit.await_sync(coro_with_task())
|
|
1626
|
+
with pytest.raises(asynkit.SyncDriveRequiredError) as err:
|
|
1627
|
+
await task
|
|
1628
|
+
assert err.match("synchronously driven")
|
|
1629
|
+
assert ran == []
|
|
1630
|
+
|
|
1631
|
+
|
|
1632
|
+
def test_enterasync_function():
|
|
1342
1633
|
async def async_method():
|
|
1343
1634
|
return "foo"
|
|
1344
1635
|
|
|
1345
|
-
@asynkit.
|
|
1636
|
+
@asynkit.enterasync
|
|
1346
1637
|
async def async_method2():
|
|
1347
1638
|
return "bar"
|
|
1348
1639
|
|
|
1349
|
-
assert asynkit.
|
|
1640
|
+
assert asynkit.enterasync(async_method)() == "foo"
|
|
1350
1641
|
assert async_method2() == "bar"
|
|
1351
1642
|
|
|
1352
1643
|
|
|
@@ -20,8 +20,16 @@ def test_package_star_exports_public_api_only():
|
|
|
20
20
|
assert exports == set(asynkit.__all__)
|
|
21
21
|
assert "coro_start" in exports
|
|
22
22
|
assert "coro_drive" in exports
|
|
23
|
-
assert "
|
|
24
|
-
assert "
|
|
23
|
+
assert "enterasync" in exports
|
|
24
|
+
assert "enterasyncmethod" in exports
|
|
25
|
+
assert "EnterAsyncMethod" in exports
|
|
26
|
+
assert "leavesync" in exports
|
|
27
|
+
assert "leavesyncmethod" in exports
|
|
28
|
+
assert "LeaveSyncMethod" in exports
|
|
29
|
+
assert "drive_async" in exports
|
|
30
|
+
assert "in_sync_drive" in exports
|
|
31
|
+
assert "require_sync_drive" in exports
|
|
32
|
+
assert "SyncDriveRequiredError" in exports
|
|
25
33
|
assert "cancelling" in exports
|
|
26
34
|
assert "coroutine" not in exports
|
|
27
35
|
assert "loop" not in exports
|
|
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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|