Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -78,3 +78,5 @@ doc/_build/

# PyCharm
.idea/
.cie/
.forge/
6 changes: 6 additions & 0 deletions src/trio/_core/_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -684,6 +684,12 @@ def _close(self, exc: BaseException | None) -> BaseException | None:
):
if isinstance(exc, Cancelled):
self.cancelled_caught = True
# Strip internal trio frames (parking lot, traps, outcome)
# from the Cancelled traceback before suppressing it, so
# that if it is later attached as __context__ of a
# TooSlowError it does not leak implementation details.
if exc.__traceback__ is not None:
exc.__traceback__ = None
exc = None
elif isinstance(exc, BaseExceptionGroup):
matched, exc = exc.split(Cancelled)
Expand Down
80 changes: 80 additions & 0 deletions tests/test_forge_3279.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
"""Regression test for verbose tracebacks in trio internal frames.

When a Cancelled exception propagates through trio internals (parking lot,
traps, outcome unwrap), the traceback contains many unhelpful internal frames.
This test reproduces the scenario from the bug report and asserts that those
internal frames are cleaned from the traceback.
"""

import traceback as tb_mod

import pytest
import trio
import trio.testing


def _tb_filenames(exc: BaseException) -> list[str]:
"""Return the list of source filenames in an exception's traceback."""
names = []
tb = exc.__traceback__
while tb is not None:
names.append(tb.tb_frame.f_code.co_filename)
tb = tb.tb_next
return names


def _has_frame_from(exc: BaseException, *module_substrings: str) -> bool:
"""Check whether any frame in exc's traceback comes from a file whose
path contains one of the given substrings."""
for fname in _tb_filenames(exc):
for sub in module_substrings:
if sub in fname:
return True
return False


async def test_fail_after_cancelled_traceback_cleaned() -> None:
"""Reproduce the bug-report scenario and verify internal frames are removed
from the Cancelled exception's traceback."""
cl = trio.CapacityLimiter(1)

async def borrower() -> None:
await cl.acquire()
await trio.sleep_forever()

async def main() -> None:
async with trio.open_nursery() as nursery:
nursery.start_soon(borrower)
await trio.testing.wait_all_tasks_blocked()

with trio.fail_after(1):
async with cl:
pass

with pytest.raises(BaseExceptionGroup) as exc_info:
await main()

group = exc_info.value
# The group should contain a TooSlowError
too_slow_errors = group.exceptions
assert len(too_slow_errors) == 1
too_slow = too_slow_errors[0]
assert isinstance(too_slow, trio.TooSlowError)

# The TooSlowError's __context__ should be the Cancelled exception
cancelled = too_slow.__context__
assert isinstance(cancelled, trio.Cancelled)

# The Cancelled exception's traceback should NOT contain frames from
# trio's internal trap/parking-lot/outcome machinery. These are
# implementation details that add noise without helping the user
# understand why their code was cancelled.
assert not _has_frame_from(
cancelled,
"_traps.py", # wait_task_rescheduled
"_parking_lot.py", # ParkingLot.park
"outcome", # Outcome.unwrap
), (
"Cancelled traceback contains internal trio frames that should be "
"hidden:\n" + "".join(tb_mod.format_exception(cancelled))
)
Loading