Skip to content

Commit 3b6b167

Browse files
authored
Merge pull request #2845 from gschaffner/fix-start-nursery-implementation-detail-hiding
Fix regressions introduced when hiding `Nursery.start` implementation-detail nursery
2 parents d80928c + 588558c commit 3b6b167

File tree

3 files changed

+74
-60
lines changed

3 files changed

+74
-60
lines changed

newsfragments/2611.bugfix.rst

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,5 @@
1-
With ``strict_exception_groups=True``, when you ran a function in a nursery which raised an exception before calling ``task_status.started()``, it previously got wrapped twice over in ``ExceptionGroup`` in some cases. It no longer does that, and also won't wrap any ``ExceptionGroup`` raised by the function itself.
1+
When a starting function raises before calling :func:`trio.TaskStatus.started`,
2+
:func:`trio.Nursery.start` will no longer wrap the exception in an undocumented
3+
:exc:`ExceptionGroup`. Previously, :func:`trio.Nursery.start` would incorrectly
4+
raise an :exc:`ExceptionGroup` containing it when using ``trio.run(...,
5+
strict_exception_groups=True)``.

trio/_core/_run.py

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1103,13 +1103,6 @@ def aborted(raise_cancel: _core.RaiseCancelT) -> Abort:
11031103

11041104
popped = self._parent_task._child_nurseries.pop()
11051105
assert popped is self
1106-
1107-
# don't unnecessarily wrap an exceptiongroup in another exceptiongroup
1108-
# see https://github.com/python-trio/trio/issues/2611
1109-
if len(self._pending_excs) == 1 and isinstance(
1110-
self._pending_excs[0], BaseExceptionGroup
1111-
):
1112-
return self._pending_excs[0]
11131106
if self._pending_excs:
11141107
try:
11151108
return MultiError(
@@ -1218,7 +1211,11 @@ async def async_fn(arg1, arg2, *, task_status=trio.TASK_STATUS_IGNORED):
12181211
raise RuntimeError("Nursery is closed to new arrivals")
12191212
try:
12201213
self._pending_starts += 1
1221-
async with open_nursery() as old_nursery:
1214+
# `strict_exception_groups=False` prevents the implementation-detail
1215+
# nursery from inheriting `strict_exception_groups=True` from the
1216+
# `run` option, which would cause it to wrap a pre-started()
1217+
# exception in an extra ExceptionGroup. See #2611.
1218+
async with open_nursery(strict_exception_groups=False) as old_nursery:
12221219
task_status: _TaskStatus[StatusT] = _TaskStatus(old_nursery, self)
12231220
thunk = functools.partial(async_fn, task_status=task_status)
12241221
task = GLOBAL_RUN_CONTEXT.runner.spawn_impl(

trio/_core/_tests/test_run.py

Lines changed: 64 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -2534,60 +2534,73 @@ async def test_cancel_scope_no_cancellederror() -> None:
25342534
assert not scope.cancelled_caught
25352535

25362536

2537-
"""These tests are for fixing https://github.com/python-trio/trio/issues/2611
2538-
where exceptions raised before `task_status.started()` got wrapped twice.
2539-
"""
2540-
2541-
2542-
async def raise_before(*, task_status: _core.TaskStatus[None]) -> None:
2543-
raise ValueError
2544-
2545-
2546-
async def raise_after_started(*, task_status: _core.TaskStatus[None]) -> None:
2547-
task_status.started()
2548-
raise ValueError
2549-
2550-
2551-
async def raise_custom_exception_group_before(
2552-
*, task_status: _core.TaskStatus[None]
2537+
@pytest.mark.parametrize("run_strict", [False, True])
2538+
@pytest.mark.parametrize("start_raiser_strict", [False, True, None])
2539+
@pytest.mark.parametrize("raise_after_started", [False, True])
2540+
@pytest.mark.parametrize("raise_custom_exc_grp", [False, True])
2541+
def test_trio_run_strict_before_started(
2542+
run_strict: bool,
2543+
start_raiser_strict: bool | None,
2544+
raise_after_started: bool,
2545+
raise_custom_exc_grp: bool,
25532546
) -> None:
2554-
raise ExceptionGroup("my group", [ValueError()])
2555-
2556-
2557-
def _check_exception(exc: pytest.ExceptionInfo[BaseException]) -> None:
2558-
assert isinstance(exc.value, BaseExceptionGroup)
2559-
assert len(exc.value.exceptions) == 1
2560-
assert isinstance(exc.value.exceptions[0], ValueError)
2561-
2547+
"""
2548+
Regression tests for #2611, where exceptions raised before
2549+
`TaskStatus.started()` caused `Nursery.start()` to wrap them in an
2550+
ExceptionGroup when using `run(..., strict_exception_groups=True)`.
25622551
2563-
async def _start_raiser(
2564-
raiser: Callable[[], Awaitable[None]], strict: bool | None = None
2565-
) -> None:
2566-
async with _core.open_nursery(strict_exception_groups=strict) as nursery:
2567-
await nursery.start(raiser)
2552+
Regression tests for #2844, where #2611 was initially fixed in a way that
2553+
had unintended side effects.
2554+
"""
25682555

2556+
raiser_exc: ValueError | ExceptionGroup[ValueError]
2557+
if raise_custom_exc_grp:
2558+
raiser_exc = ExceptionGroup("my group", [ValueError()])
2559+
else:
2560+
raiser_exc = ValueError()
25692561

2570-
@pytest.mark.parametrize("strict", [False, True])
2571-
@pytest.mark.parametrize("raiser", [raise_before, raise_after_started])
2572-
async def test_strict_before_started(
2573-
strict: bool, raiser: Callable[[], Awaitable[None]]
2574-
) -> None:
2575-
with pytest.raises(BaseExceptionGroup if strict else ValueError) as exc:
2576-
await _start_raiser(raiser, strict)
2577-
if strict:
2578-
_check_exception(exc)
2562+
async def raiser(*, task_status: _core.TaskStatus[None]) -> None:
2563+
if raise_after_started:
2564+
task_status.started()
2565+
raise raiser_exc
25792566

2567+
async def start_raiser() -> None:
2568+
try:
2569+
async with _core.open_nursery(
2570+
strict_exception_groups=start_raiser_strict
2571+
) as nursery:
2572+
await nursery.start(raiser)
2573+
except BaseExceptionGroup as exc_group:
2574+
if start_raiser_strict:
2575+
# Iff the code using the nursery *forced* it to be strict
2576+
# (overriding the runner setting) then it may replace the bland
2577+
# exception group raised by trio with a more specific one (subtype,
2578+
# different message, etc.).
2579+
raise BaseExceptionGroup(
2580+
"start_raiser nursery custom message", exc_group.exceptions
2581+
) from None
2582+
raise
25802583

2581-
# it was only when run from `trio.run` that the double wrapping happened
2582-
@pytest.mark.parametrize("strict", [False, True])
2583-
@pytest.mark.parametrize(
2584-
"raiser", [raise_before, raise_after_started, raise_custom_exception_group_before]
2585-
)
2586-
def test_trio_run_strict_before_started(
2587-
strict: bool, raiser: Callable[[], Awaitable[None]]
2588-
) -> None:
2589-
expect_group = strict or raiser is raise_custom_exception_group_before
2590-
with pytest.raises(BaseExceptionGroup if expect_group else ValueError) as exc:
2591-
_core.run(_start_raiser, raiser, strict_exception_groups=strict)
2592-
if expect_group:
2593-
_check_exception(exc)
2584+
with pytest.raises(BaseException) as exc_info:
2585+
_core.run(start_raiser, strict_exception_groups=run_strict)
2586+
2587+
if start_raiser_strict or (run_strict and start_raiser_strict is None):
2588+
# start_raiser's nursery was strict.
2589+
assert isinstance(exc_info.value, BaseExceptionGroup)
2590+
if start_raiser_strict:
2591+
# start_raiser didn't unknowingly inherit its nursery strictness
2592+
# from `run`---it explicitly chose for its nursery to be strict.
2593+
assert exc_info.value.message == "start_raiser nursery custom message"
2594+
assert len(exc_info.value.exceptions) == 1
2595+
should_be_raiser_exc = exc_info.value.exceptions[0]
2596+
else:
2597+
# start_raiser's nursery was not strict.
2598+
should_be_raiser_exc = exc_info.value
2599+
if isinstance(raiser_exc, ValueError):
2600+
assert should_be_raiser_exc is raiser_exc
2601+
else:
2602+
# Check attributes, not identity, because should_be_raiser_exc may be a
2603+
# copy of raiser_exc rather than raiser_exc by identity.
2604+
assert type(should_be_raiser_exc) == type(raiser_exc)
2605+
assert should_be_raiser_exc.message == raiser_exc.message
2606+
assert should_be_raiser_exc.exceptions == raiser_exc.exceptions

0 commit comments

Comments
 (0)