@@ -458,16 +458,16 @@ async def aclose(self) -> None:
458
458
459
459
class RecvChanWrapper (ReceiveChannel [T ]):
460
460
def __init__ (
461
- self , recv_chan : MemoryReceiveChannel [T ], send_semaphore : trio .Semaphore | None
461
+ self , recv_chan : MemoryReceiveChannel [T ], send_semaphore : trio .Semaphore
462
462
) -> None :
463
463
self .recv_chan = recv_chan
464
464
self .send_semaphore = send_semaphore
465
465
466
- # TODO: should this allow clones?
466
+ # TODO: should this allow clones? We'd signal that by inheriting from
467
+ # MemoryReceiveChannel.
467
468
468
469
async def receive (self ) -> T :
469
- if self .send_semaphore is not None :
470
- self .send_semaphore .release ()
470
+ self .send_semaphore .release ()
471
471
return await self .recv_chan .receive ()
472
472
473
473
async def aclose (self ) -> None :
@@ -485,12 +485,9 @@ def __exit__(
485
485
self .recv_chan .close ()
486
486
487
487
488
- def background_with_channel (max_buffer_size : int | None = 0 ) -> Callable [
489
- [
490
- Callable [P , AsyncGenerator [T , None ]],
491
- ],
492
- Callable [P , AbstractAsyncContextManager [trio .abc .ReceiveChannel [T ]]],
493
- ]:
488
+ def background_with_channel (
489
+ fn : Callable [P , AsyncGenerator [T , None ]],
490
+ ) -> Callable [P , AbstractAsyncContextManager [ReceiveChannel [T ]]]:
494
491
"""Decorate an async generator function to make it cancellation-safe.
495
492
496
493
This is mostly a drop-in replacement, except for the fact that it will
@@ -511,7 +508,7 @@ def background_with_channel(max_buffer_size: int | None = 0) -> Callable[
511
508
offering only the safe interface, and you can still write your iterables
512
509
with the convenience of ``yield``. For example::
513
510
514
- @background_with_channel()
511
+ @background_with_channel
515
512
async def my_async_iterable(arg, *, kwarg=True):
516
513
while ...:
517
514
item = await ...
@@ -531,46 +528,30 @@ async def my_async_iterable(arg, *, kwarg=True):
531
528
# Perhaps a future PEP will adopt `async with for` syntax, like
532
529
# https://coconut.readthedocs.io/en/master/DOCS.html#async-with-for
533
530
534
- if not isinstance (max_buffer_size , int ) and max_buffer_size is not None :
535
- raise TypeError (
536
- "`max_buffer_size` must be int or None, not {type(max_buffer_size)}. "
537
- "Did you forget the parentheses in `@background_with_channel()`?"
538
- )
539
-
540
- def decorator (
541
- fn : Callable [P , AsyncGenerator [T , None ]],
542
- ) -> Callable [P , AbstractAsyncContextManager [trio ._channel .RecvChanWrapper [T ]]]:
543
- @asynccontextmanager
544
- @wraps (fn )
545
- async def context_manager (
546
- * args : P .args , ** kwargs : P .kwargs
547
- ) -> AsyncGenerator [trio ._channel .RecvChanWrapper [T ], None ]:
548
- max_buf_size_float = inf if max_buffer_size is None else max_buffer_size
549
- send_chan , recv_chan = trio .open_memory_channel [T ](max_buf_size_float )
550
- async with trio .open_nursery (strict_exception_groups = True ) as nursery :
551
- agen = fn (* args , ** kwargs )
552
- send_semaphore = (
553
- None if max_buffer_size is None else trio .Semaphore (max_buffer_size )
554
- )
555
- # `nursery.start` to make sure that we will clean up send_chan & agen
556
- # If this errors we don't close `recv_chan`, but the caller
557
- # never gets access to it, so that's not a problem.
558
- await nursery .start (
559
- _move_elems_to_channel , agen , send_chan , send_semaphore
560
- )
561
- # `async with recv_chan` could eat exceptions, so use sync cm
562
- with RecvChanWrapper (recv_chan , send_semaphore ) as wrapped_recv_chan :
563
- yield wrapped_recv_chan
564
- # User has exited context manager, cancel to immediately close the
565
- # abandoned generator if it's still alive.
566
- nursery .cancel_scope .cancel ()
567
-
568
- return context_manager
531
+ @asynccontextmanager
532
+ @wraps (fn )
533
+ async def context_manager (
534
+ * args : P .args , ** kwargs : P .kwargs
535
+ ) -> AsyncGenerator [trio ._channel .RecvChanWrapper [T ], None ]:
536
+ send_chan , recv_chan = trio .open_memory_channel [T ](0 )
537
+ async with trio .open_nursery (strict_exception_groups = True ) as nursery :
538
+ agen = fn (* args , ** kwargs )
539
+ send_semaphore = trio .Semaphore (0 )
540
+ # `nursery.start` to make sure that we will clean up send_chan & agen
541
+ # If this errors we don't close `recv_chan`, but the caller
542
+ # never gets access to it, so that's not a problem.
543
+ await nursery .start (_move_elems_to_channel , agen , send_chan , send_semaphore )
544
+ # `async with recv_chan` could eat exceptions, so use sync cm
545
+ with RecvChanWrapper (recv_chan , send_semaphore ) as wrapped_recv_chan :
546
+ yield wrapped_recv_chan
547
+ # User has exited context manager, cancel to immediately close the
548
+ # abandoned generator if it's still alive.
549
+ nursery .cancel_scope .cancel ()
569
550
570
551
async def _move_elems_to_channel (
571
552
agen : AsyncGenerator [T , None ],
572
553
send_chan : trio .MemorySendChannel [T ],
573
- send_semaphore : trio .Semaphore | None ,
554
+ send_semaphore : trio .Semaphore ,
574
555
task_status : trio .TaskStatus ,
575
556
) -> None :
576
557
# `async with send_chan` will eat exceptions,
@@ -579,22 +560,16 @@ async def _move_elems_to_channel(
579
560
try :
580
561
task_status .started ()
581
562
while True :
582
- # wait for send_chan to be unblocked
583
- if send_semaphore is not None :
584
- await send_semaphore .acquire ()
563
+ # wait for receiver to call next on the aiter
564
+ await send_semaphore .acquire ()
585
565
try :
586
566
value = await agen .__anext__ ()
587
567
except StopAsyncIteration :
588
568
return
589
- try :
590
- # Send the value to the channel
591
- await send_chan .send (value )
592
- except trio .BrokenResourceError :
593
- # Closing the corresponding receive channel should cause
594
- # a clean shutdown of the generator.
595
- return
569
+ # Send the value to the channel
570
+ await send_chan .send (value )
596
571
finally :
597
572
# replace try-finally with contextlib.aclosing once python39 is dropped
598
573
await agen .aclose ()
599
574
600
- return decorator
575
+ return context_manager
0 commit comments