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.
Files changed (41) hide show
  1. {asynkit-0.19.2/src/asynkit.egg-info → asynkit-0.20.0}/PKG-INFO +82 -15
  2. {asynkit-0.19.2 → asynkit-0.20.0}/README.md +81 -14
  3. {asynkit-0.19.2 → asynkit-0.20.0}/pyproject.toml +1 -1
  4. {asynkit-0.19.2 → asynkit-0.20.0}/src/asynkit/coroutine.py +190 -21
  5. {asynkit-0.19.2 → asynkit-0.20.0/src/asynkit.egg-info}/PKG-INFO +82 -15
  6. {asynkit-0.19.2 → asynkit-0.20.0}/tests/test_coro.py +308 -17
  7. {asynkit-0.19.2 → asynkit-0.20.0}/tests/test_namespace.py +10 -2
  8. {asynkit-0.19.2 → asynkit-0.20.0}/LICENSE +0 -0
  9. {asynkit-0.19.2 → asynkit-0.20.0}/setup.cfg +0 -0
  10. {asynkit-0.19.2 → asynkit-0.20.0}/setup.py +0 -0
  11. {asynkit-0.19.2 → asynkit-0.20.0}/src/asynkit/__init__.py +0 -0
  12. {asynkit-0.19.2 → asynkit-0.20.0}/src/asynkit/_cext/corostart.c +0 -0
  13. {asynkit-0.19.2 → asynkit-0.20.0}/src/asynkit/compat.py +0 -0
  14. {asynkit-0.19.2 → asynkit-0.20.0}/src/asynkit/experimental/__init__.py +0 -0
  15. {asynkit-0.19.2 → asynkit-0.20.0}/src/asynkit/experimental/anyio.py +0 -0
  16. {asynkit-0.19.2 → asynkit-0.20.0}/src/asynkit/experimental/interrupt.py +0 -0
  17. {asynkit-0.19.2 → asynkit-0.20.0}/src/asynkit/experimental/priority.py +0 -0
  18. {asynkit-0.19.2 → asynkit-0.20.0}/src/asynkit/loop/__init__.py +0 -0
  19. {asynkit-0.19.2 → asynkit-0.20.0}/src/asynkit/loop/default.py +0 -0
  20. {asynkit-0.19.2 → asynkit-0.20.0}/src/asynkit/loop/eventloop.py +0 -0
  21. {asynkit-0.19.2 → asynkit-0.20.0}/src/asynkit/loop/extensions.py +0 -0
  22. {asynkit-0.19.2 → asynkit-0.20.0}/src/asynkit/loop/schedulingloop.py +0 -0
  23. {asynkit-0.19.2 → asynkit-0.20.0}/src/asynkit/loop/types.py +0 -0
  24. {asynkit-0.19.2 → asynkit-0.20.0}/src/asynkit/monitor.py +0 -0
  25. {asynkit-0.19.2 → asynkit-0.20.0}/src/asynkit/py.typed +0 -0
  26. {asynkit-0.19.2 → asynkit-0.20.0}/src/asynkit/scheduling.py +0 -0
  27. {asynkit-0.19.2 → asynkit-0.20.0}/src/asynkit/tools.py +0 -0
  28. {asynkit-0.19.2 → asynkit-0.20.0}/src/asynkit.egg-info/SOURCES.txt +0 -0
  29. {asynkit-0.19.2 → asynkit-0.20.0}/src/asynkit.egg-info/dependency_links.txt +0 -0
  30. {asynkit-0.19.2 → asynkit-0.20.0}/src/asynkit.egg-info/not-zip-safe +0 -0
  31. {asynkit-0.19.2 → asynkit-0.20.0}/src/asynkit.egg-info/requires.txt +0 -0
  32. {asynkit-0.19.2 → asynkit-0.20.0}/src/asynkit.egg-info/top_level.txt +0 -0
  33. {asynkit-0.19.2 → asynkit-0.20.0}/tests/test_anyio.py +0 -0
  34. {asynkit-0.19.2 → asynkit-0.20.0}/tests/test_compat_eager.py +0 -0
  35. {asynkit-0.19.2 → asynkit-0.20.0}/tests/test_df.py +0 -0
  36. {asynkit-0.19.2 → asynkit-0.20.0}/tests/test_eager_task_factory.py +0 -0
  37. {asynkit-0.19.2 → asynkit-0.20.0}/tests/test_eager_task_factory_context.py +0 -0
  38. {asynkit-0.19.2 → asynkit-0.20.0}/tests/test_implementation_params.py +0 -0
  39. {asynkit-0.19.2 → asynkit-0.20.0}/tests/test_loop.py +0 -0
  40. {asynkit-0.19.2 → asynkit-0.20.0}/tests/test_monitor.py +0 -0
  41. {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.19.2
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
- The `syncfunction()` decorator can be used to automatically wrap an async function
449
- so that it is executed using `await_sync()`:
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.syncfunction
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 `syncmethod()` when creating a class-body alias. It behaves
464
- like `syncfunction()` at runtime, but exposes descriptor typing so type checkers
465
- can distinguish bound and unbound method access:
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.syncmethod(arun)
486
+ run = asynkit.enterasyncmethod(arun)
472
487
  ```
473
488
 
474
- the `asyncfunction()` utility can be used when passing synchronous callbacks to async
475
- code, to make them async. This, along with `syncfunction()` and `await_sync()`,
476
- can be used to integrate synchronous code with async middleware:
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.syncfunction
502
+ @asynkit.enterasync
480
503
  async def sync_client(sync_callback):
481
- middleware = AsyncMiddleware(asynkit.asyncfunction(sync_callback))
504
+ middleware = AsyncMiddleware(asynkit.leavesync(sync_callback))
482
505
  return await middleware.run()
483
506
  ```
484
507
 
485
- Using this pattern, one can write the middleware completely async, make it also work
486
- for synchronous code, while avoiding the hybrid function _antipattern._
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
- The `syncfunction()` decorator can be used to automatically wrap an async function
421
- so that it is executed using `await_sync()`:
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.syncfunction
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 `syncmethod()` when creating a class-body alias. It behaves
436
- like `syncfunction()` at runtime, but exposes descriptor typing so type checkers
437
- can distinguish bound and unbound method access:
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.syncmethod(arun)
458
+ run = asynkit.enterasyncmethod(arun)
444
459
  ```
445
460
 
446
- the `asyncfunction()` utility can be used when passing synchronous callbacks to async
447
- code, to make them async. This, along with `syncfunction()` and `await_sync()`,
448
- can be used to integrate synchronous code with async middleware:
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.syncfunction
474
+ @asynkit.enterasync
452
475
  async def sync_client(sync_callback):
453
- middleware = AsyncMiddleware(asynkit.asyncfunction(sync_callback))
476
+ middleware = AsyncMiddleware(asynkit.leavesync(sync_callback))
454
477
  return await middleware.run()
455
478
  ```
456
479
 
457
- Using this pattern, one can write the middleware completely async, make it also work
458
- for synchronous code, while avoiding the hybrid function _antipattern._
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
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "asynkit"
3
- version = "0.19.2"
3
+ version = "0.20.0"
4
4
  description = "A toolkit for Python coroutines"
5
5
  authors = [
6
6
  {name = "Kristján Valur Jónsson", email = "sweskman@gmail.com"}
@@ -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
- "asyncfunction",
136
- "syncfunction",
137
- "syncmethod",
138
- "SyncMethod",
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 coro_drive(coro, callback)
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 syncfunction(func: Callable[P, Coroutine[Any, Any, T]]) -> Callable[P, T]:
1164
- """Make an async function synchronous, by invoking
1165
- `await_sync()` on its coroutine. Useful as a decorator.
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 SyncMethod(Generic[SelfT, P, T]):
1176
- """Descriptor which makes an async method synchronous.
1277
+ class EnterAsyncMethod(Generic[SelfT, P, T]):
1278
+ """Descriptor for entering non-blocking async methods from synchronous callers.
1177
1279
 
1178
- Unlike `syncfunction()`, this preserves method binding semantics when used
1179
- for class-body aliases such as `run = syncmethod(arun)`.
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 syncmethod(
1323
+ def enterasyncmethod(
1222
1324
  func: Callable[Concatenate[SelfT, P], Coroutine[Any, Any, T]],
1223
- ) -> SyncMethod[SelfT, P, T]:
1224
- """Make an async method synchronous while preserving descriptor typing."""
1225
- return SyncMethod(func)
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 asyncfunction(func: Callable[P, T]) -> Callable[P, Coroutine[Any, Any, T]]:
1229
- """Make a regular function async, so that its result needs to be awaited.
1230
- Useful when providing synchronous callbacks to async code.
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.19.2
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
- The `syncfunction()` decorator can be used to automatically wrap an async function
449
- so that it is executed using `await_sync()`:
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.syncfunction
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 `syncmethod()` when creating a class-body alias. It behaves
464
- like `syncfunction()` at runtime, but exposes descriptor typing so type checkers
465
- can distinguish bound and unbound method access:
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.syncmethod(arun)
486
+ run = asynkit.enterasyncmethod(arun)
472
487
  ```
473
488
 
474
- the `asyncfunction()` utility can be used when passing synchronous callbacks to async
475
- code, to make them async. This, along with `syncfunction()` and `await_sync()`,
476
- can be used to integrate synchronous code with async middleware:
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.syncfunction
502
+ @asynkit.enterasync
480
503
  async def sync_client(sync_callback):
481
- middleware = AsyncMiddleware(asynkit.asyncfunction(sync_callback))
504
+ middleware = AsyncMiddleware(asynkit.leavesync(sync_callback))
482
505
  return await middleware.run()
483
506
  ```
484
507
 
485
- Using this pattern, one can write the middleware completely async, make it also work
486
- for synchronous code, while avoiding the hybrid function _antipattern._
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.syncfunction
832
+ @asynkit.enterasync
833
833
  async def sync_simple(self):
834
834
  return await self.simple()
835
835
 
836
- @asynkit.syncfunction
836
+ @asynkit.enterasync
837
837
  async def sync_cleanup(self):
838
838
  return await self.cleanupper()
839
839
 
840
- @asynkit.syncfunction
840
+ @asynkit.enterasync
841
841
  async def sync_genexit(self):
842
842
  return await self.genexit()
843
843
 
844
- method_simple = asynkit.syncmethod(simple)
845
- method_blocks = asynkit.syncmethod(blocks)
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 test_syncmethod_bound(self):
861
+ def test_enterasyncmethod_bound(self):
862
862
  assert self.method_simple() == "simple"
863
863
 
864
- def test_syncmethod_unbound(self):
864
+ def test_enterasyncmethod_unbound(self):
865
865
  assert type(self).method_simple(self) == "simple"
866
866
 
867
- def test_syncmethod_descriptor(self):
867
+ def test_enterasyncmethod_descriptor(self):
868
868
  descriptor = type(self).__dict__["method_simple"]
869
- assert isinstance(descriptor, asynkit.SyncMethod)
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 test_syncmethod_blocks(self):
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
- async def test_async_function():
1329
+ def test_leavesync_inside_await_sync():
1330
1330
  def sync_method():
1331
1331
  return "foo"
1332
1332
 
1333
- @asynkit.asyncfunction
1333
+ @asynkit.leavesync
1334
1334
  def sync_method2():
1335
1335
  return "bar"
1336
1336
 
1337
- assert await asynkit.asyncfunction(sync_method)() == "foo"
1338
- assert await sync_method2() == "bar"
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
- async def test_sync_function():
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.syncfunction
1636
+ @asynkit.enterasync
1346
1637
  async def async_method2():
1347
1638
  return "bar"
1348
1639
 
1349
- assert asynkit.syncfunction(async_method)() == "foo"
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 "syncmethod" in exports
24
- assert "SyncMethod" in exports
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