From cef5c8c40310131740fc04dfd48a64e49fa5ce06 Mon Sep 17 00:00:00 2001 From: Zac Hatfield-Dodds Date: Mon, 11 Apr 2022 20:04:50 -0700 Subject: [PATCH 1/2] PEP 678: new API for add_note() --- CHANGES.rst | 6 +- src/exceptiongroup/_exceptions.py | 14 +++- src/exceptiongroup/_formatting.py | 18 +++-- tests/test_exceptions.py | 15 ++-- tests/test_formatting.py | 112 +++++++++++++++--------------- 5 files changed, 95 insertions(+), 70 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 5236177..2a8a4fe 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -3,13 +3,17 @@ Version history This library adheres to `Semantic Versioning 2.0 `_. +**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/ diff --git a/src/exceptiongroup/_exceptions.py b/src/exceptiongroup/_exceptions.py index 947caee..cad259d 100644 --- a/src/exceptiongroup/_exceptions.py +++ b/src/exceptiongroup/_exceptions.py @@ -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: @@ -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: diff --git a/src/exceptiongroup/_formatting.py b/src/exceptiongroup/_formatting.py index b19f1bf..47f000c 100644 --- a/src/exceptiongroup/_formatting.py +++ b/src/exceptiongroup/_formatting.py @@ -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 @@ -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: @@ -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 = "" + 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") @@ -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 = "" + yield from _ctx.emit(msg + "\n") num_excs = len(exc.exceptions) if num_excs <= max_group_width: n = num_excs diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index 2a19971..e338a37 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -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): @@ -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): @@ -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 = [ diff --git a/tests/test_formatting.py b/tests/test_formatting.py index 37f0479..f131f72 100644 --- a/tests/test_formatting.py +++ b/tests/test_formatting.py @@ -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 + +------------------------------------ +""" + ) From dfa75b8db64b1af49e97bcdb8739190e70f61d94 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 12 Apr 2022 03:15:41 +0000 Subject: [PATCH 2/2] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- tests/test_formatting.py | 112 +++++++++++++++++++-------------------- 1 file changed, 56 insertions(+), 56 deletions(-) diff --git a/tests/test_formatting.py b/tests/test_formatting.py index f131f72..37cbf45 100644 --- a/tests/test_formatting.py +++ b/tests/test_formatting.py @@ -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.__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 - +------------------------------------ -""" - ) +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 + +------------------------------------ +""" + )