Skip to content

Commit 49c5e60

Browse files
committed
Fixed AttributeError when formatting unpickled TBEs from an unpatched process
Fixes #144.
1 parent 1be517f commit 49c5e60

4 files changed

Lines changed: 44 additions & 5 deletions

File tree

CHANGES.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,13 @@ Version history
33

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

6+
**UNRELEASED**
7+
8+
- Fixed ``AttributeError: 'TracebackException' object has no attribute 'exceptions'``
9+
when formatting unpickled TBEs from another Python process which did not apply the
10+
``exceptiongroup`` patches
11+
(`#144 <https://github.com/agronholm/exceptiongroup/issues/144>`_)
12+
613
**1.3.0**
714

815
- Added ``**kwargs`` to function and method signatures as appropriate to match the

src/exceptiongroup/_formatting.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,7 @@ def format(self, *, chain=True, _ctx=None, **kwargs):
241241
for msg, exc in reversed(output):
242242
if msg is not None:
243243
yield from _ctx.emit(msg)
244-
if exc.exceptions is None:
244+
if getattr(exc, "exceptions", None) is None:
245245
if exc.stack:
246246
yield from _ctx.emit("Traceback (most recent call last):\n")
247247
yield from _ctx.emit(exc.stack.format())
@@ -332,12 +332,13 @@ def format_exception_only(self, **kwargs):
332332
else:
333333
yield from traceback_exception_original_format_exception_only(self)
334334

335-
if isinstance(self.__notes__, collections.abc.Sequence):
336-
for note in self.__notes__:
335+
notes = getattr(self, "__notes__", None)
336+
if isinstance(notes, collections.abc.Sequence):
337+
for note in notes:
337338
note = _safe_string(note, "note")
338339
yield from [line + "\n" for line in note.split("\n")]
339-
elif self.__notes__ is not None:
340-
yield _safe_string(self.__notes__, "__notes__", func=repr)
340+
elif notes is not None:
341+
yield _safe_string(notes, "__notes__", func=repr)
341342

342343

343344
traceback_exception_original_format = traceback.TracebackException.format

tests/dummyscript.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# This script exists solely for test_unpatched_tracebackexception_format()
2+
import pickle
3+
import sys
4+
import traceback
5+
6+
assert "exceptiongroup" not in sys.modules, "exceptiongroup was already imported"
7+
8+
try:
9+
raise ValueError("hello")
10+
except ValueError as exc:
11+
tbe = traceback.TracebackException(type(exc), exc, exc.__traceback__)
12+
sys.stdout.buffer.write(pickle.dumps(tbe, protocol=pickle.HIGHEST_PROTOCOL))

tests/test_formatting.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1+
import pickle
2+
import subprocess
13
import sys
24
import traceback
5+
from pathlib import Path
36
from typing import NoReturn
47
from urllib.error import HTTPError
58

@@ -55,6 +58,22 @@ def old_argstyle(request: SubRequest) -> bool:
5558
return request.param
5659

5760

61+
@pytest.mark.skipif(
62+
sys.version_info >= (3, 11),
63+
reason="The failure only occurs on Python 3.10 and earlier",
64+
)
65+
def test_unpatched_tracebackexception_format():
66+
dummy_script_path = Path(__file__).parent / "dummyscript.py"
67+
process = subprocess.run(
68+
[sys.executable, str(dummy_script_path)], capture_output=True
69+
)
70+
tbe = pickle.loads(process.stdout)
71+
assert not hasattr(tbe, "exceptions")
72+
assert not hasattr(tbe, "__notes__")
73+
formatted = tbe.format()
74+
"".join(formatted)
75+
76+
5877
def test_exceptionhook(capsys: CaptureFixture) -> None:
5978
try:
6079
raise_excgroup()

0 commit comments

Comments
 (0)