Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixed AttributeError when rendering an excgroup as a cause #34

Merged
merged 2 commits into from Oct 27, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGES.rst
Expand Up @@ -3,6 +3,16 @@ Version history

This library adheres to `Semantic Versioning 2.0 <http://semver.org/>`_.

**UNRELEASED**

- Fixed
``AttributeError: 'PatchedTracebackException' object has no attribute '__cause__'``
on Python 3.10 (only) when a traceback is printed from an exception where an exception
group is set as the cause (#33)
- Fixed a loop in exception groups being rendered incorrectly (#35)
- Fixed the patched formatting functions (``format_exception()``etc.) not passing the
``compact=True`` flag on Python 3.10 like the original functions do

**1.0.0rc9**

- Added custom versions of several ``traceback`` functions that work with exception
Expand Down
95 changes: 72 additions & 23 deletions src/exceptiongroup/_formatting.py
Expand Up @@ -76,7 +76,7 @@ def __init__(
self,
exc_type: type[BaseException],
exc_value: BaseException,
exc_traceback: TracebackType,
exc_traceback: TracebackType | None,
*,
limit: int | None = None,
lookup_lines: bool = True,
Expand All @@ -88,8 +88,6 @@ def __init__(
if sys.version_info >= (3, 10):
kwargs["compact"] = compact

# Capture the original exception and its cause and context as
# TracebackExceptions
traceback_exception_original_init(
self,
exc_type,
Expand All @@ -102,33 +100,82 @@ def __init__(
**kwargs,
)

seen_was_none = _seen is None

is_recursive_call = _seen is not None
if _seen is None:
_seen = set()
_seen.add(id(exc_value))

# Convert __cause__ and __context__ to `TracebackExceptions`s, use a
# queue to avoid recursion (only the top-level call gets _seen == None)
if not is_recursive_call:
queue = [(self, exc_value)]
while queue:
te, e = queue.pop()

if e and e.__cause__ is not None and id(e.__cause__) not in _seen:
cause = PatchedTracebackException(
type(e.__cause__),
e.__cause__,
e.__cause__.__traceback__,
limit=limit,
lookup_lines=lookup_lines,
capture_locals=capture_locals,
_seen=_seen,
)
else:
cause = None

# Capture each of the exceptions in the ExceptionGroup along with each of
# their causes and contexts
if isinstance(exc_value, BaseExceptionGroup):
embedded = []
for exc in exc_value.exceptions:
if id(exc) not in _seen:
embedded.append(
PatchedTracebackException(
if compact:
need_context = (
cause is None and e is not None and not e.__suppress_context__
)
else:
need_context = True
if (
e
and e.__context__ is not None
and need_context
and id(e.__context__) not in _seen
):
context = PatchedTracebackException(
type(e.__context__),
e.__context__,
e.__context__.__traceback__,
limit=limit,
lookup_lines=lookup_lines,
capture_locals=capture_locals,
_seen=_seen,
)
else:
context = None

# Capture each of the exceptions in the ExceptionGroup along with each
# of their causes and contexts
if e and isinstance(e, BaseExceptionGroup):
exceptions = []
for exc in e.exceptions:
texc = PatchedTracebackException(
type(exc),
exc,
exc.__traceback__,
lookup_lines=lookup_lines,
capture_locals=capture_locals,
# copy the set of _seen exceptions so that duplicates
# shared between sub-exceptions are not omitted
_seen=None if seen_was_none else set(_seen),
_seen=_seen,
)
)
self.exceptions = embedded
self.msg = exc_value.message
else:
self.exceptions = None
exceptions.append(texc)
else:
exceptions = None

te.__cause__ = cause
te.__context__ = context
te.exceptions = exceptions
if cause:
queue.append((te.__cause__, e.__cause__))
if context:
queue.append((te.__context__, e.__context__))
if exceptions:
queue.extend(zip(te.exceptions, e.exceptions))

self.__notes__ = getattr(exc_value, "__notes__", ())

def format(self, *, chain=True, _ctx=None):
Expand Down Expand Up @@ -280,7 +327,9 @@ def format_exception_only(self):
@singledispatch
def format_exception_only(__exc: BaseException) -> List[str]:
return list(
PatchedTracebackException(type(__exc), __exc, None).format_exception_only()
PatchedTracebackException(
type(__exc), __exc, None, compact=True
).format_exception_only()
)


Expand All @@ -297,7 +346,7 @@ def format_exception(
) -> List[str]:
return list(
PatchedTracebackException(
type(__exc), __exc, __exc.__traceback__, limit=limit
type(__exc), __exc, __exc.__traceback__, limit=limit, compact=True
).format(chain=chain)
)

Expand Down
64 changes: 64 additions & 0 deletions tests/test_formatting.py
Expand Up @@ -88,6 +88,70 @@ def test_exceptionhook(capsys: CaptureFixture) -> None:
)


def test_exceptiongroup_as_cause(capsys: CaptureFixture) -> None:
try:
raise Exception() from ExceptionGroup("", (Exception(),))
except Exception as exc:
sys.excepthook(type(exc), exc, exc.__traceback__)

lineno = test_exceptiongroup_as_cause.__code__.co_firstlineno
module_prefix = "" if sys.version_info >= (3, 11) else "exceptiongroup."
output = capsys.readouterr().err
assert output == (
f"""\
| {module_prefix}ExceptionGroup: (1 sub-exception)
+-+---------------- 1 ----------------
| Exception
+------------------------------------

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
File "{__file__}", line {lineno + 2}, in test_exceptiongroup_as_cause
raise Exception() from ExceptionGroup("", (Exception(),))
Exception
"""
)


def test_exceptiongroup_loop(capsys: CaptureFixture) -> None:
e0 = Exception("e0")
eg0 = ExceptionGroup("eg0", (e0,))
eg1 = ExceptionGroup("eg1", (eg0,))

try:
raise eg0 from eg1
except ExceptionGroup as exc:
sys.excepthook(type(exc), exc, exc.__traceback__)

lineno = test_exceptiongroup_loop.__code__.co_firstlineno + 6
module_prefix = "" if sys.version_info >= (3, 11) else "exceptiongroup."
output = capsys.readouterr().err
assert output == (
f"""\
| {module_prefix}ExceptionGroup: eg1 (1 sub-exception)
+-+---------------- 1 ----------------
| Exception Group Traceback (most recent call last):
| File "{__file__}", line {lineno}, in test_exceptiongroup_loop
| raise eg0 from eg1
| {module_prefix}ExceptionGroup: eg0 (1 sub-exception)
+-+---------------- 1 ----------------
| Exception: e0
+------------------------------------

The above exception was the direct cause of the following exception:

+ Exception Group Traceback (most recent call last):
| File "{__file__}", line {lineno}, in test_exceptiongroup_loop
| raise eg0 from eg1
| {module_prefix}ExceptionGroup: eg0 (1 sub-exception)
+-+---------------- 1 ----------------
| Exception: e0
+------------------------------------
"""
)


def test_exceptionhook_format_exception_only(capsys: CaptureFixture) -> None:
try:
raise_excgroup()
Expand Down