Skip to content

Commit e7556b4

Browse files
committed
unwrap exceptiongroup, add test
1 parent 72ac535 commit e7556b4

File tree

2 files changed

+55
-31
lines changed

2 files changed

+55
-31
lines changed

src/trio/_channel.py

Lines changed: 32 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,15 @@
1818

1919
from ._abc import ReceiveChannel, ReceiveType, SendChannel, SendType, T
2020
from ._core import Abort, RaiseCancelT, Task, enable_ki_protection
21-
from ._util import NoPublicConstructor, final, generic_function
21+
from ._util import (
22+
NoPublicConstructor,
23+
final,
24+
generic_function,
25+
raise_single_exception_from_group,
26+
)
27+
28+
if sys.version_info < (3, 11):
29+
from exceptiongroup import BaseExceptionGroup
2230

2331
if TYPE_CHECKING:
2432
from types import TracebackType
@@ -534,19 +542,29 @@ async def context_manager(
534542
*args: P.args, **kwargs: P.kwargs
535543
) -> AsyncGenerator[trio._channel.RecvChanWrapper[T], None]:
536544
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()
545+
try:
546+
async with trio.open_nursery(strict_exception_groups=True) as nursery:
547+
agen = fn(*args, **kwargs)
548+
send_semaphore = trio.Semaphore(0)
549+
# `nursery.start` to make sure that we will clean up send_chan & agen
550+
# If this errors we don't close `recv_chan`, but the caller
551+
# never gets access to it, so that's not a problem.
552+
await nursery.start(
553+
_move_elems_to_channel, agen, send_chan, send_semaphore
554+
)
555+
# `async with recv_chan` could eat exceptions, so use sync cm
556+
with RecvChanWrapper(recv_chan, send_semaphore) as wrapped_recv_chan:
557+
yield wrapped_recv_chan
558+
# User has exited context manager, cancel to immediately close the
559+
# abandoned generator if it's still alive.
560+
nursery.cancel_scope.cancel()
561+
except BaseExceptionGroup as eg:
562+
try:
563+
raise_single_exception_from_group(eg)
564+
except AssertionError:
565+
raise RuntimeError(
566+
"Encountered exception during cleanup of generator object, as well as exception in the contextmanager body"
567+
) from eg
550568

551569
async def _move_elems_to_channel(
552570
agen: AsyncGenerator[T, None],

src/trio/_tests/test_channel.py

Lines changed: 23 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -416,18 +416,6 @@ async def do_send(s: trio.MemorySendChannel[int], v: int) -> None:
416416
r.receive_nowait()
417417

418418

419-
async def test_background_with_channel() -> None:
420-
@background_with_channel
421-
async def agen() -> AsyncGenerator[int]:
422-
yield 1
423-
await trio.sleep_forever() # simulate deadlock
424-
425-
async with agen() as recv_chan:
426-
async for x in recv_chan:
427-
assert x == 1
428-
break # exit, cleanup should be quick
429-
430-
431419
async def test_background_with_channel_exhaust() -> None:
432420
@background_with_channel
433421
async def agen() -> AsyncGenerator[int]:
@@ -496,7 +484,7 @@ async def agen() -> AsyncGenerator[int]:
496484
yield 1
497485
raise ValueError("oae")
498486

499-
with RaisesGroup(ValueError):
487+
with pytest.raises(ValueError, match=r"^oae$"):
500488
async with agen() as recv_chan:
501489
async for x in recv_chan:
502490
assert x == 1
@@ -544,16 +532,20 @@ async def agen(stuff: list[str]) -> AsyncGenerator[int]:
544532
stuff.append("finally")
545533
raise ValueError("agen")
546534

547-
with RaisesGroup(
548-
Matcher(ValueError, match="^agen$"),
549-
Matcher(TypeError, match="^iterator$"),
550-
):
535+
with pytest.raises(
536+
RuntimeError,
537+
match=r"^Encountered exception during cleanup of generator object, as well as exception in the contextmanager body.$",
538+
) as excinfo:
551539
async with agen(events) as recv_chan:
552540
async for i in recv_chan: # pragma: no branch
553541
assert i == 1
554542
raise TypeError("iterator")
555543

556544
assert events == ["GeneratorExit()", "finally"]
545+
RaisesGroup(
546+
Matcher(ValueError, match="^agen$"),
547+
Matcher(TypeError, match="^iterator$"),
548+
).matches(excinfo.value.__cause__)
557549

558550

559551
async def test_background_with_channel_nested_loop() -> None:
@@ -571,3 +563,17 @@ async def agen() -> AsyncGenerator[int]:
571563
assert (i, j) == (ii, jj)
572564
jj += 1
573565
ii += 1
566+
567+
568+
async def test_doesnt_leak_cancellation() -> None:
569+
@background_with_channel
570+
async def agenfn() -> AsyncGenerator[None]:
571+
with trio.CancelScope() as cscope:
572+
cscope.cancel()
573+
yield
574+
575+
with pytest.raises(AssertionError):
576+
async with agenfn() as recv_chan:
577+
async for _ in recv_chan:
578+
pass
579+
raise AssertionError("should be reachable")

0 commit comments

Comments
 (0)