From 51e3166b595dfe851d3f9d10a3ee5acb16916348 Mon Sep 17 00:00:00 2001 From: Friday Date: Tue, 17 Feb 2026 15:35:11 +0000 Subject: [PATCH 1/3] Fix exceptions raised before task_status.started() losing cause and context MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a task raised an exception before calling task_status.started(), Nursery.start() unwrapped it from the internal ExceptionGroup using `raise exc.exceptions[0] from None`, which explicitly set __cause__ to None and __suppress_context__ to True — destroying any cause or context the original exception carried. Use raise_saving_context() instead, which preserves both __cause__ and __context__ on the re-raised exception, consistent with how raise_single_exception_from_group() handles the same pattern. Fixes #3261. Co-Authored-By: Claude Opus 4.6 --- src/trio/_core/_run.py | 4 ++-- src/trio/_core/_tests/test_run.py | 20 ++++++++++++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/src/trio/_core/_run.py b/src/trio/_core/_run.py index 9ecdada1e..24071d8ec 100644 --- a/src/trio/_core/_run.py +++ b/src/trio/_core/_run.py @@ -33,7 +33,7 @@ from .. import _core from .._abc import Clock, Instrument from .._deprecate import warn_deprecated -from .._util import NoPublicConstructor, coroutine_or_error, final +from .._util import NoPublicConstructor, coroutine_or_error, final, raise_saving_context from ._asyncgens import AsyncGenerators from ._concat_tb import concat_tb from ._entry_queue import EntryQueue, TrioToken @@ -1466,7 +1466,7 @@ async def async_fn(arg1, arg2, *, task_status=trio.TASK_STATUS_IGNORED): # cancel this nursery: except BaseExceptionGroup as exc: if len(exc.exceptions) == 1: - raise exc.exceptions[0] from None + raise_saving_context(exc.exceptions[0]) raise TrioInternalError( "Internal nursery should not have multiple tasks. This can be " 'caused by the user managing to access the "old" nursery in ' diff --git a/src/trio/_core/_tests/test_run.py b/src/trio/_core/_tests/test_run.py index 94e448448..86683a1fb 100644 --- a/src/trio/_core/_tests/test_run.py +++ b/src/trio/_core/_tests/test_run.py @@ -2920,6 +2920,26 @@ async def start_raiser() -> None: assert should_be_raiser_exc.exceptions == raiser_exc.exceptions +async def test_start_exception_preserves_cause_and_context() -> None: + """Regression test for #3261: exceptions raised before task_status.started() + should preserve __cause__ and __context__.""" + + async def task(*, task_status: _core.TaskStatus[None]) -> None: + e = ValueError("foo") + e.__cause__ = SyntaxError("bar") + e.__context__ = TypeError("baz") + raise e + + with pytest.raises(BaseExceptionGroup) as exc_info: + async with _core.open_nursery() as nursery: + await nursery.start(task) + assert len(exc_info.value.exceptions) == 1 + exc = exc_info.value.exceptions[0] + assert isinstance(exc, ValueError) + assert isinstance(exc.__cause__, SyntaxError) + assert isinstance(exc.__context__, TypeError) + + async def test_internal_error_old_nursery_multiple_tasks() -> None: async def error_func() -> None: raise ValueError From 8204de25f57f914633a65f4239b6516049747124 Mon Sep 17 00:00:00 2001 From: Friday Date: Tue, 17 Feb 2026 20:29:38 +0000 Subject: [PATCH 2/3] Add newsfragment for #3261 Co-Authored-By: Claude Opus 4.6 --- newsfragments/3261.bugfix.rst | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 newsfragments/3261.bugfix.rst diff --git a/newsfragments/3261.bugfix.rst b/newsfragments/3261.bugfix.rst new file mode 100644 index 000000000..a5eb14117 --- /dev/null +++ b/newsfragments/3261.bugfix.rst @@ -0,0 +1,2 @@ +``Nursery.start()`` now preserves the ``__cause__`` and ``__context__`` of +exceptions raised before ``task_status.started()`` is called. From 48aadaf9c7f9eb1a958476357cc4e4fbbc71675d Mon Sep 17 00:00:00 2001 From: Friday Date: Wed, 18 Feb 2026 13:07:36 +0000 Subject: [PATCH 3/3] Use RaisesGroup instead of pytest.raises(BaseExceptionGroup) Address review feedback: use trio's pytest.RaisesGroup helper for cleaner exception group assertions. Co-Authored-By: Claude Opus 4.6 --- src/trio/_core/_tests/test_run.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/trio/_core/_tests/test_run.py b/src/trio/_core/_tests/test_run.py index 86683a1fb..b8edc85fa 100644 --- a/src/trio/_core/_tests/test_run.py +++ b/src/trio/_core/_tests/test_run.py @@ -2930,14 +2930,15 @@ async def task(*, task_status: _core.TaskStatus[None]) -> None: e.__context__ = TypeError("baz") raise e - with pytest.raises(BaseExceptionGroup) as exc_info: + with pytest.RaisesGroup( + pytest.RaisesExc( + ValueError, + check=lambda exc: isinstance(exc.__cause__, SyntaxError) + and isinstance(exc.__context__, TypeError), + ), + ): async with _core.open_nursery() as nursery: await nursery.start(task) - assert len(exc_info.value.exceptions) == 1 - exc = exc_info.value.exceptions[0] - assert isinstance(exc, ValueError) - assert isinstance(exc.__cause__, SyntaxError) - assert isinstance(exc.__context__, TypeError) async def test_internal_error_old_nursery_multiple_tasks() -> None: