Skip to content

Commit

Permalink
PEP 678: new API for add_note()
Browse files Browse the repository at this point in the history
  • Loading branch information
Zac-HD committed Apr 12, 2022
1 parent c9c4b34 commit cef5c8c
Show file tree
Hide file tree
Showing 5 changed files with 95 additions and 70 deletions.
6 changes: 5 additions & 1 deletion CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,17 @@ Version history

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

**1.0.0rc4**

- Update `PEP 678`_ support to use ``.add_note()`` and ``__notes__`` (PR by Zac Hatfield-Dodds)

**1.0.0rc3**

- Added message about the number of sub-exceptions

**1.0.0rc2**

- Display and copy ``__note__`` (`PEP 678`_) if available (PR by Zac Hatfield-Dodds)
- Display and copy ``__note__`` (draft `PEP 678`_) if available (PR by Zac Hatfield-Dodds)

.. _PEP 678: https://www.python.org/dev/peps/pep-0678/

Expand Down
14 changes: 12 additions & 2 deletions src/exceptiongroup/_exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,15 @@ def __init__(self, __message: str, __exceptions: Sequence[EBase], *args: Any):
super().__init__(__message, __exceptions, *args)
self._message = __message
self._exceptions = __exceptions
self.__note__ = None

def add_note(self, note: str):
if not isinstance(note, str):
raise TypeError(
f"Expected a string, got note={note!r} (type {type(note).__name__})"
)
if not hasattr(self, "__notes__"):
self.__notes__ = []
self.__notes__.append(note)

@property
def message(self) -> str:
Expand Down Expand Up @@ -151,7 +159,9 @@ def split(

def derive(self: T, __excs: Sequence[EBase]) -> T:
eg = BaseExceptionGroup(self.message, __excs)
eg.__note__ = self.__note__
if hasattr(self, "__notes__"):
# Create a new list so that add_note() only affects one exceptiongroup
eg.__notes__ = list(self.__notes__)
return eg

def __str__(self) -> str:
Expand Down
18 changes: 13 additions & 5 deletions src/exceptiongroup/_formatting.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,6 @@ def traceback_exception_init(
_seen=_seen,
**kwargs,
)
self.__note__ = getattr(exc_value, "__note__", None) if exc_value else None

seen_was_none = _seen is None

Expand Down Expand Up @@ -79,6 +78,7 @@ def traceback_exception_init(
self.msg = exc_value.message
else:
self.exceptions = None
self.__notes__ = getattr(exc_value, "__notes__", ())


class _ExceptionPrintContext:
Expand Down Expand Up @@ -135,8 +135,12 @@ def traceback_exception_format(self, *, chain=True, _ctx=None):
yield from _ctx.emit("Traceback (most recent call last):\n")
yield from _ctx.emit(exc.stack.format())
yield from _ctx.emit(exc.format_exception_only())
if isinstance(exc.__note__, str):
yield from _ctx.emit(line + "\n" for line in exc.__note__.split("\n"))
for note in exc.__notes__:
try:
msg = str(note)
except BaseException:
msg = "<note str() failed>"
yield from _ctx.emit(msg + "\n")
elif _ctx.exception_group_depth > max_group_depth:
# exception group, but depth exceeds limit
yield from _ctx.emit(f"... (max_group_depth is {max_group_depth})\n")
Expand All @@ -154,8 +158,12 @@ def traceback_exception_format(self, *, chain=True, _ctx=None):
yield from _ctx.emit(exc.stack.format())

yield from _ctx.emit(exc.format_exception_only())
if isinstance(exc.__note__, str):
yield from _ctx.emit(line + "\n" for line in exc.__note__.split("\n"))
for note in exc.__notes__:
try:
msg = str(note)
except BaseException:
msg = "<note str() failed>"
yield from _ctx.emit(msg + "\n")
num_excs = len(exc.exceptions)
if num_excs <= max_group_width:
n = num_excs
Expand Down
15 changes: 9 additions & 6 deletions tests/test_exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,13 +179,13 @@ def test_fields_are_readonly(self):
with self.assertRaises(AttributeError):
eg.exceptions = [OSError("xyz")]

def test_note_exists_and_is_string_or_none(self):
def test_notes_is_list_of_strings_if_it_exists(self):
eg = create_simple_eg()

note = "This is a happy note for the exception group"
self.assertIs(eg.__note__, None)
eg.__note__ = note
self.assertIs(eg.__note__, note)
self.assertFalse(hasattr(eg, "__notes__"))
eg.add_note(note)
self.assertEqual(eg.__notes__, [note])


class ExceptionGroupTestBase(unittest.TestCase):
Expand Down Expand Up @@ -497,7 +497,10 @@ def leaves(exc):
self.assertIs(eg.__cause__, part.__cause__)
self.assertIs(eg.__context__, part.__context__)
self.assertIs(eg.__traceback__, part.__traceback__)
self.assertIs(eg.__note__, part.__note__)
self.assertEqual(
getattr(eg, "__notes__", None),
getattr(part, "__notes__", None),
)

def tbs_for_leaf(leaf, eg):
for e, tbs in leaf_generator(eg):
Expand Down Expand Up @@ -561,7 +564,7 @@ def level3(i):
try:
nested_group()
except ExceptionGroup as e:
e.__note__ = f"the note: {id(e)}"
e.add_note(f"the note: {id(e)}")
eg = e

eg_template = [
Expand Down
112 changes: 56 additions & 56 deletions tests/test_formatting.py
Original file line number Diff line number Diff line change
@@ -1,56 +1,56 @@
import sys

from exceptiongroup import ExceptionGroup


def test_formatting(capsys):
exceptions = []
try:
raise ValueError("foo")
except ValueError as exc:
exceptions.append(exc)

try:
raise RuntimeError("bar")
except RuntimeError as exc:
exc.__note__ = "Note from bar handler"
exceptions.append(exc)

try:
raise ExceptionGroup("test message", exceptions)
except ExceptionGroup as exc:
exc.__note__ = "Displays notes attached to the group too"
sys.excepthook(type(exc), exc, exc.__traceback__)

lineno = test_formatting.__code__.co_firstlineno
if sys.version_info >= (3, 11):
module_prefix = ""
underline1 = "\n | " + "^" * 48
underline2 = "\n | " + "^" * 23
underline3 = "\n | " + "^" * 25
else:
module_prefix = "exceptiongroup."
underline1 = underline2 = underline3 = ""

output = capsys.readouterr().err
assert output == (
f"""\
+ Exception Group Traceback (most recent call last):
| File "{__file__}", line {lineno + 14}, in test_formatting
| raise ExceptionGroup("test message", exceptions){underline1}
| {module_prefix}ExceptionGroup: test message (2 sub-exceptions)
| Displays notes attached to the group too
+-+---------------- 1 ----------------
| Traceback (most recent call last):
| File "{__file__}", line {lineno + 3}, in test_formatting
| raise ValueError("foo"){underline2}
| ValueError: foo
+---------------- 2 ----------------
| Traceback (most recent call last):
| File "{__file__}", line {lineno + 8}, in test_formatting
| raise RuntimeError("bar"){underline3}
| RuntimeError: bar
| Note from bar handler
+------------------------------------
"""
)
import sys

from exceptiongroup import ExceptionGroup


def test_formatting(capsys):
exceptions = []
try:
raise ValueError("foo")
except ValueError as exc:
exceptions.append(exc)

try:
raise RuntimeError("bar")
except RuntimeError as exc:
exc.__notes__ = ["Note from bar handler"]
exceptions.append(exc)

try:
raise ExceptionGroup("test message", exceptions)
except ExceptionGroup as exc:
exc.add_note("Displays notes attached to the group too")
sys.excepthook(type(exc), exc, exc.__traceback__)

lineno = test_formatting.__code__.co_firstlineno
if sys.version_info >= (3, 11):
module_prefix = ""
underline1 = "\n | " + "^" * 48
underline2 = "\n | " + "^" * 23
underline3 = "\n | " + "^" * 25
else:
module_prefix = "exceptiongroup."
underline1 = underline2 = underline3 = ""

output = capsys.readouterr().err
assert output == (
f"""\
+ Exception Group Traceback (most recent call last):
| File "{__file__}", line {lineno + 14}, in test_formatting
| raise ExceptionGroup("test message", exceptions){underline1}
| {module_prefix}ExceptionGroup: test message (2 sub-exceptions)
| Displays notes attached to the group too
+-+---------------- 1 ----------------
| Traceback (most recent call last):
| File "{__file__}", line {lineno + 3}, in test_formatting
| raise ValueError("foo"){underline2}
| ValueError: foo
+---------------- 2 ----------------
| Traceback (most recent call last):
| File "{__file__}", line {lineno + 8}, in test_formatting
| raise RuntimeError("bar"){underline3}
| RuntimeError: bar
| Note from bar handler
+------------------------------------
"""
)

0 comments on commit cef5c8c

Please sign in to comment.