diff --git a/Doc/library/exceptions.rst b/Doc/library/exceptions.rst index 137566e079d204..2eccbd17c482c0 100644 --- a/Doc/library/exceptions.rst +++ b/Doc/library/exceptions.rst @@ -126,13 +126,20 @@ The following exceptions are used mostly as base classes for other exceptions. tb = sys.exc_info()[2] raise OtherException(...).with_traceback(tb) - .. attribute:: __note__ + .. method:: add_note(note) - A mutable field which is :const:`None` by default and can be set to a string. - If it is not :const:`None`, it is included in the traceback. This field can - be used to enrich exceptions after they have been caught. + Add the string ``note`` to the exception's notes which appear in the standard + traceback after the exception string. A :exc:`TypeError` is raised if ``note`` + is not a string. - .. versionadded:: 3.11 + .. versionadded:: 3.11 + + .. attribute:: __notes__ + + A list of the notes of this exception, which were added with :meth:`add_note`. + This attribute is created when :meth:`add_note` is called. + + .. versionadded:: 3.11 .. exception:: Exception @@ -907,7 +914,7 @@ their subgroups based on the types of the contained exceptions. The nesting structure of the current exception is preserved in the result, as are the values of its :attr:`message`, :attr:`__traceback__`, - :attr:`__cause__`, :attr:`__context__` and :attr:`__note__` fields. + :attr:`__cause__`, :attr:`__context__` and :attr:`__notes__` fields. Empty nested groups are omitted from the result. The condition is checked for all exceptions in the nested exception group, @@ -924,7 +931,7 @@ their subgroups based on the types of the contained exceptions. Returns an exception group with the same :attr:`message`, :attr:`__traceback__`, :attr:`__cause__`, :attr:`__context__` - and :attr:`__note__` but which wraps the exceptions in ``excs``. + and :attr:`__notes__` but which wraps the exceptions in ``excs``. This method is used by :meth:`subgroup` and :meth:`split`. A subclass needs to override it in order to make :meth:`subgroup` diff --git a/Doc/whatsnew/3.11.rst b/Doc/whatsnew/3.11.rst index 894ec8a9d0d928..56fc75102b48b1 100644 --- a/Doc/whatsnew/3.11.rst +++ b/Doc/whatsnew/3.11.rst @@ -157,12 +157,15 @@ The :option:`-X` ``no_debug_ranges`` option and the environment variable See :pep:`657` for more details. (Contributed by Pablo Galindo, Batuhan Taskaya and Ammar Askar in :issue:`43950`.) -Exceptions can be enriched with a string ``__note__`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Exceptions can be enriched with notes (PEP 678) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The :meth:`add_note` method was added to :exc:`BaseException`. It can be +used to enrich exceptions with context information which is not available +at the time when the exception is raised. The notes added appear in the +default traceback. See :pep:`678` for more details. (Contributed by +Irit Katriel in :issue:`45607`.) -The ``__note__`` field was added to :exc:`BaseException`. It is ``None`` -by default but can be set to a string which is added to the exception's -traceback. (Contributed by Irit Katriel in :issue:`45607`.) Other Language Changes ====================== diff --git a/Include/cpython/pyerrors.h b/Include/cpython/pyerrors.h index 5281fde1f1a54c..b8aa041687ee42 100644 --- a/Include/cpython/pyerrors.h +++ b/Include/cpython/pyerrors.h @@ -6,7 +6,7 @@ /* PyException_HEAD defines the initial segment of every exception class. */ #define PyException_HEAD PyObject_HEAD PyObject *dict;\ - PyObject *args; PyObject *note; PyObject *traceback;\ + PyObject *args; PyObject *notes; PyObject *traceback;\ PyObject *context; PyObject *cause;\ char suppress_context; diff --git a/Include/internal/pycore_global_strings.h b/Include/internal/pycore_global_strings.h index 833ff2710a787c..77f96cd5ed2f0b 100644 --- a/Include/internal/pycore_global_strings.h +++ b/Include/internal/pycore_global_strings.h @@ -155,7 +155,7 @@ struct _Py_global_strings { STRUCT_FOR_ID(__newobj__) STRUCT_FOR_ID(__newobj_ex__) STRUCT_FOR_ID(__next__) - STRUCT_FOR_ID(__note__) + STRUCT_FOR_ID(__notes__) STRUCT_FOR_ID(__or__) STRUCT_FOR_ID(__orig_class__) STRUCT_FOR_ID(__origin__) diff --git a/Include/internal/pycore_runtime_init.h b/Include/internal/pycore_runtime_init.h index fd925b3e060dfe..371f2d23ad8ec5 100644 --- a/Include/internal/pycore_runtime_init.h +++ b/Include/internal/pycore_runtime_init.h @@ -778,7 +778,7 @@ extern "C" { INIT_ID(__newobj__), \ INIT_ID(__newobj_ex__), \ INIT_ID(__next__), \ - INIT_ID(__note__), \ + INIT_ID(__notes__), \ INIT_ID(__or__), \ INIT_ID(__orig_class__), \ INIT_ID(__origin__), \ diff --git a/Lib/test/test_exception_group.py b/Lib/test/test_exception_group.py index 793e8d20de7e33..2cfd8738304d1c 100644 --- a/Lib/test/test_exception_group.py +++ b/Lib/test/test_exception_group.py @@ -567,7 +567,9 @@ 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): @@ -632,7 +634,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 = [ @@ -728,6 +730,35 @@ def exc(ex): self.assertMatchesTemplate( rest, ExceptionGroup, [ValueError(1)]) + def test_split_copies_notes(self): + # make sure each exception group after a split has its own __notes__ list + eg = ExceptionGroup("eg", [ValueError(1), TypeError(2)]) + eg.add_note("note1") + eg.add_note("note2") + orig_notes = list(eg.__notes__) + match, rest = eg.split(TypeError) + self.assertEqual(eg.__notes__, orig_notes) + self.assertEqual(match.__notes__, orig_notes) + self.assertEqual(rest.__notes__, orig_notes) + self.assertIsNot(eg.__notes__, match.__notes__) + self.assertIsNot(eg.__notes__, rest.__notes__) + self.assertIsNot(match.__notes__, rest.__notes__) + eg.add_note("eg") + match.add_note("match") + rest.add_note("rest") + self.assertEqual(eg.__notes__, orig_notes + ["eg"]) + self.assertEqual(match.__notes__, orig_notes + ["match"]) + self.assertEqual(rest.__notes__, orig_notes + ["rest"]) + + def test_split_does_not_copy_non_sequence_notes(self): + # __notes__ should be a sequence, which is shallow copied. + # If it is not a sequence, the split parts don't get any notes. + eg = ExceptionGroup("eg", [ValueError(1), TypeError(2)]) + eg.__notes__ = 123 + match, rest = eg.split(TypeError) + self.assertFalse(hasattr(match, '__notes__')) + self.assertFalse(hasattr(rest, '__notes__')) + class NestedExceptionGroupSubclassSplitTest(ExceptionGroupSplitTestBase): diff --git a/Lib/test/test_exceptions.py b/Lib/test/test_exceptions.py index 6dca79efef1802..2b5b51934562a1 100644 --- a/Lib/test/test_exceptions.py +++ b/Lib/test/test_exceptions.py @@ -547,26 +547,32 @@ def testAttributes(self): 'pickled "%r", attribute "%s' % (e, checkArgName)) - def test_note(self): + def test_notes(self): for e in [BaseException(1), Exception(2), ValueError(3)]: with self.subTest(e=e): - self.assertIsNone(e.__note__) - e.__note__ = "My Note" - self.assertEqual(e.__note__, "My Note") + self.assertFalse(hasattr(e, '__notes__')) + e.add_note("My Note") + self.assertEqual(e.__notes__, ["My Note"]) with self.assertRaises(TypeError): - e.__note__ = 42 - self.assertEqual(e.__note__, "My Note") + e.add_note(42) + self.assertEqual(e.__notes__, ["My Note"]) - e.__note__ = "Your Note" - self.assertEqual(e.__note__, "Your Note") + e.add_note("Your Note") + self.assertEqual(e.__notes__, ["My Note", "Your Note"]) - with self.assertRaises(TypeError): - del e.__note__ - self.assertEqual(e.__note__, "Your Note") + del e.__notes__ + self.assertFalse(hasattr(e, '__notes__')) + + e.add_note("Our Note") + self.assertEqual(e.__notes__, ["Our Note"]) - e.__note__ = None - self.assertIsNone(e.__note__) + e.__notes__ = 42 + self.assertEqual(e.__notes__, 42) + + with self.assertRaises(TypeError): + e.add_note("will not work") + self.assertEqual(e.__notes__, 42) def testWithTraceback(self): try: diff --git a/Lib/test/test_traceback.py b/Lib/test/test_traceback.py index 75d668df64d4c8..962322c89ff66c 100644 --- a/Lib/test/test_traceback.py +++ b/Lib/test/test_traceback.py @@ -1323,21 +1323,80 @@ def test_syntax_error_various_offsets(self): self.assertEqual(exp, err) def test_exception_with_note(self): - e = ValueError(42) + e = ValueError(123) vanilla = self.get_report(e) - e.__note__ = 'My Note' + e.add_note('My Note') self.assertEqual(self.get_report(e), vanilla + 'My Note\n') - e.__note__ = '' + del e.__notes__ + e.add_note('') self.assertEqual(self.get_report(e), vanilla + '\n') - e.__note__ = 'Your Note' + del e.__notes__ + e.add_note('Your Note') self.assertEqual(self.get_report(e), vanilla + 'Your Note\n') - e.__note__ = None + del e.__notes__ self.assertEqual(self.get_report(e), vanilla) + def test_exception_with_invalid_notes(self): + e = ValueError(123) + vanilla = self.get_report(e) + + # non-sequence __notes__ + class BadThing: + def __str__(self): + return 'bad str' + + def __repr__(self): + return 'bad repr' + + # unprintable, non-sequence __notes__ + class Unprintable: + def __repr__(self): + raise ValueError('bad value') + + e.__notes__ = BadThing() + notes_repr = 'bad repr' + self.assertEqual(self.get_report(e), vanilla + notes_repr) + + e.__notes__ = Unprintable() + err_msg = '<__notes__ repr() failed>' + self.assertEqual(self.get_report(e), vanilla + err_msg) + + # non-string item in the __notes__ sequence + e.__notes__ = [BadThing(), 'Final Note'] + bad_note = 'bad str' + self.assertEqual(self.get_report(e), vanilla + bad_note + '\nFinal Note\n') + + # unprintable, non-string item in the __notes__ sequence + e.__notes__ = [Unprintable(), 'Final Note'] + err_msg = '' + self.assertEqual(self.get_report(e), vanilla + err_msg + '\nFinal Note\n') + + def test_exception_with_note_with_multiple_notes(self): + e = ValueError(42) + vanilla = self.get_report(e) + + e.add_note('Note 1') + e.add_note('Note 2') + e.add_note('Note 3') + + self.assertEqual( + self.get_report(e), + vanilla + 'Note 1\n' + 'Note 2\n' + 'Note 3\n') + + del e.__notes__ + e.add_note('Note 4') + del e.__notes__ + e.add_note('Note 5') + e.add_note('Note 6') + + self.assertEqual( + self.get_report(e), + vanilla + 'Note 5\n' + 'Note 6\n') + def test_exception_qualname(self): class A: class B: @@ -1688,16 +1747,16 @@ def exc(): try: raise ValueError(msg) except ValueError as e: - e.__note__ = f'the {msg}' + e.add_note(f'the {msg}') excs.append(e) raise ExceptionGroup("nested", excs) except ExceptionGroup as e: - e.__note__ = ('>> Multi line note\n' - '>> Because I am such\n' - '>> an important exception.\n' - '>> empty lines work too\n' - '\n' - '(that was an empty line)') + e.add_note(('>> Multi line note\n' + '>> Because I am such\n' + '>> an important exception.\n' + '>> empty lines work too\n' + '\n' + '(that was an empty line)')) raise expected = (f' + Exception Group Traceback (most recent call last):\n' @@ -1733,6 +1792,64 @@ def exc(): report = self.get_report(exc) self.assertEqual(report, expected) + def test_exception_group_with_multiple_notes(self): + def exc(): + try: + excs = [] + for msg in ['bad value', 'terrible value']: + try: + raise ValueError(msg) + except ValueError as e: + e.add_note(f'the {msg}') + e.add_note(f'Goodbye {msg}') + excs.append(e) + raise ExceptionGroup("nested", excs) + except ExceptionGroup as e: + e.add_note(('>> Multi line note\n' + '>> Because I am such\n' + '>> an important exception.\n' + '>> empty lines work too\n' + '\n' + '(that was an empty line)')) + e.add_note('Goodbye!') + raise + + expected = (f' + Exception Group Traceback (most recent call last):\n' + f' | File "{__file__}", line {self.callable_line}, in get_exception\n' + f' | exception_or_callable()\n' + f' | ^^^^^^^^^^^^^^^^^^^^^^^\n' + f' | File "{__file__}", line {exc.__code__.co_firstlineno + 10}, in exc\n' + f' | raise ExceptionGroup("nested", excs)\n' + f' | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n' + f' | ExceptionGroup: nested (2 sub-exceptions)\n' + f' | >> Multi line note\n' + f' | >> Because I am such\n' + f' | >> an important exception.\n' + f' | >> empty lines work too\n' + f' | \n' + f' | (that was an empty line)\n' + f' | Goodbye!\n' + f' +-+---------------- 1 ----------------\n' + f' | Traceback (most recent call last):\n' + f' | File "{__file__}", line {exc.__code__.co_firstlineno + 5}, in exc\n' + f' | raise ValueError(msg)\n' + f' | ^^^^^^^^^^^^^^^^^^^^^\n' + f' | ValueError: bad value\n' + f' | the bad value\n' + f' | Goodbye bad value\n' + f' +---------------- 2 ----------------\n' + f' | Traceback (most recent call last):\n' + f' | File "{__file__}", line {exc.__code__.co_firstlineno + 5}, in exc\n' + f' | raise ValueError(msg)\n' + f' | ^^^^^^^^^^^^^^^^^^^^^\n' + f' | ValueError: terrible value\n' + f' | the terrible value\n' + f' | Goodbye terrible value\n' + f' +------------------------------------\n') + + report = self.get_report(exc) + self.assertEqual(report, expected) + class PyExcReportingTests(BaseExceptionReportingTests, unittest.TestCase): # @@ -2077,32 +2194,32 @@ def some_inner(): [f'{__file__}:{some_inner.__code__.co_firstlineno + 1}']) def test_dropping_frames(self): - def f(): - 1/0 - - def g(): - try: - f() - except: - return sys.exc_info() - - exc_info = g() - - class Skip_G(traceback.StackSummary): - def format_frame_summary(self, frame_summary): - if frame_summary.name == 'g': - return None - return super().format_frame_summary(frame_summary) - - stack = Skip_G.extract( - traceback.walk_tb(exc_info[2])).format() - - self.assertEqual(len(stack), 1) - lno = f.__code__.co_firstlineno + 1 - self.assertEqual( - stack[0], - f' File "{__file__}", line {lno}, in f\n 1/0\n' - ) + def f(): + 1/0 + + def g(): + try: + f() + except: + return sys.exc_info() + + exc_info = g() + + class Skip_G(traceback.StackSummary): + def format_frame_summary(self, frame_summary): + if frame_summary.name == 'g': + return None + return super().format_frame_summary(frame_summary) + + stack = Skip_G.extract( + traceback.walk_tb(exc_info[2])).format() + + self.assertEqual(len(stack), 1) + lno = f.__code__.co_firstlineno + 1 + self.assertEqual( + stack[0], + f' File "{__file__}", line {lno}, in f\n 1/0\n' + ) class TestTracebackException(unittest.TestCase): diff --git a/Lib/traceback.py b/Lib/traceback.py index 05f1fffef0d3b0..3afe49d1d8a0e6 100644 --- a/Lib/traceback.py +++ b/Lib/traceback.py @@ -1,6 +1,6 @@ """Extract, format and print information about Python stack traces.""" -import collections +import collections.abc import itertools import linecache import sys @@ -163,18 +163,18 @@ def format_exception_only(exc, /, value=_sentinel): # -- not official API but folk probably use these two functions. def _format_final_exc_line(etype, value): - valuestr = _some_str(value) + valuestr = _safe_string(value, 'exception') if value is None or not valuestr: line = "%s\n" % etype else: line = "%s: %s\n" % (etype, valuestr) return line -def _some_str(value): +def _safe_string(value, what, func=str): try: - return str(value) + return func(value) except: - return '' + return f'<{what} {func.__name__}() failed>' # -- @@ -688,8 +688,8 @@ def __init__(self, exc_type, exc_value, exc_traceback, *, limit=None, self.exc_type = exc_type # Capture now to permit freeing resources: only complication is in the # unofficial API _format_final_exc_line - self._str = _some_str(exc_value) - self.__note__ = exc_value.__note__ if exc_value else None + self._str = _safe_string(exc_value, 'exception') + self.__notes__ = getattr(exc_value, '__notes__', None) if exc_type and issubclass(exc_type, SyntaxError): # Handle SyntaxError's specially @@ -822,8 +822,12 @@ def format_exception_only(self): yield _format_final_exc_line(stype, self._str) else: yield from self._format_syntax_error(stype) - if self.__note__ is not None: - yield from [l + '\n' for l in self.__note__.split('\n')] + if isinstance(self.__notes__, collections.abc.Sequence): + for note in self.__notes__: + note = _safe_string(note, 'note') + yield from [l + '\n' for l in note.split('\n')] + elif self.__notes__ is not None: + yield _safe_string(self.__notes__, '__notes__', func=repr) def _format_syntax_error(self, stype): """Format SyntaxError exceptions (internal helper).""" @@ -913,7 +917,7 @@ def format(self, *, chain=True, _ctx=None): # format exception group is_toplevel = (_ctx.exception_group_depth == 0) if is_toplevel: - _ctx.exception_group_depth += 1 + _ctx.exception_group_depth += 1 if exc.stack: yield from _ctx.emit( diff --git a/Misc/NEWS.d/next/Core and Builtins/2022-04-12-11-56-23.gh-issue-91479.-dyGJX.rst b/Misc/NEWS.d/next/Core and Builtins/2022-04-12-11-56-23.gh-issue-91479.-dyGJX.rst new file mode 100644 index 00000000000000..e131e91e753126 --- /dev/null +++ b/Misc/NEWS.d/next/Core and Builtins/2022-04-12-11-56-23.gh-issue-91479.-dyGJX.rst @@ -0,0 +1 @@ +Replaced the ``__note__`` field of :exc:`BaseException` (added in an earlier version of 3.11) with the final design of :pep:`678`. Namely, :exc:`BaseException` gets an :meth:`add_note` method, and its ``__notes__`` field is created when necessary. diff --git a/Objects/exceptions.c b/Objects/exceptions.c index df10a3c2416e3d..b26a0e93af484d 100644 --- a/Objects/exceptions.c +++ b/Objects/exceptions.c @@ -47,7 +47,7 @@ BaseException_new(PyTypeObject *type, PyObject *args, PyObject *kwds) return NULL; /* the dict is created on the fly in PyObject_GenericSetAttr */ self->dict = NULL; - self->note = NULL; + self->notes = NULL; self->traceback = self->cause = self->context = NULL; self->suppress_context = 0; @@ -83,7 +83,7 @@ BaseException_clear(PyBaseExceptionObject *self) { Py_CLEAR(self->dict); Py_CLEAR(self->args); - Py_CLEAR(self->note); + Py_CLEAR(self->notes); Py_CLEAR(self->traceback); Py_CLEAR(self->cause); Py_CLEAR(self->context); @@ -108,7 +108,7 @@ BaseException_traverse(PyBaseExceptionObject *self, visitproc visit, void *arg) { Py_VISIT(self->dict); Py_VISIT(self->args); - Py_VISIT(self->note); + Py_VISIT(self->notes); Py_VISIT(self->traceback); Py_VISIT(self->cause); Py_VISIT(self->context); @@ -186,12 +186,62 @@ PyDoc_STRVAR(with_traceback_doc, "Exception.with_traceback(tb) --\n\ set self.__traceback__ to tb and return self."); +static inline PyBaseExceptionObject* +_PyBaseExceptionObject_cast(PyObject *exc) +{ + assert(PyExceptionInstance_Check(exc)); + return (PyBaseExceptionObject *)exc; +} + +static PyObject * +BaseException_add_note(PyObject *self, PyObject *note) +{ + if (!PyUnicode_Check(note)) { + PyErr_Format(PyExc_TypeError, + "note must be a str, not '%s'", + Py_TYPE(note)->tp_name); + return NULL; + } + + if (!PyObject_HasAttr(self, &_Py_ID(__notes__))) { + PyObject *new_notes = PyList_New(0); + if (new_notes == NULL) { + return NULL; + } + if (PyObject_SetAttr(self, &_Py_ID(__notes__), new_notes) < 0) { + Py_DECREF(new_notes); + return NULL; + } + Py_DECREF(new_notes); + } + PyObject *notes = PyObject_GetAttr(self, &_Py_ID(__notes__)); + if (notes == NULL) { + return NULL; + } + if (!PyList_Check(notes)) { + Py_DECREF(notes); + PyErr_SetString(PyExc_TypeError, "Cannot add note: __notes__ is not a list"); + return NULL; + } + if (PyList_Append(notes, note) < 0) { + Py_DECREF(notes); + return NULL; + } + Py_DECREF(notes); + Py_RETURN_NONE; +} + +PyDoc_STRVAR(add_note_doc, +"Exception.add_note(note) --\n\ + add a note to the exception"); static PyMethodDef BaseException_methods[] = { {"__reduce__", (PyCFunction)BaseException_reduce, METH_NOARGS }, {"__setstate__", (PyCFunction)BaseException_setstate, METH_O }, {"with_traceback", (PyCFunction)BaseException_with_traceback, METH_O, with_traceback_doc}, + {"add_note", (PyCFunction)BaseException_add_note, METH_O, + add_note_doc}, {NULL, NULL, 0, NULL}, }; @@ -220,33 +270,6 @@ BaseException_set_args(PyBaseExceptionObject *self, PyObject *val, void *Py_UNUS return 0; } -static PyObject * -BaseException_get_note(PyBaseExceptionObject *self, void *Py_UNUSED(ignored)) -{ - if (self->note == NULL) { - Py_RETURN_NONE; - } - return Py_NewRef(self->note); -} - -static int -BaseException_set_note(PyBaseExceptionObject *self, PyObject *note, - void *Py_UNUSED(ignored)) -{ - if (note == NULL) { - PyErr_SetString(PyExc_TypeError, "__note__ may not be deleted"); - return -1; - } - else if (note != Py_None && !PyUnicode_CheckExact(note)) { - PyErr_SetString(PyExc_TypeError, "__note__ must be a string or None"); - return -1; - } - - Py_INCREF(note); - Py_XSETREF(self->note, note); - return 0; -} - static PyObject * BaseException_get_tb(PyBaseExceptionObject *self, void *Py_UNUSED(ignored)) { @@ -337,7 +360,6 @@ BaseException_set_cause(PyObject *self, PyObject *arg, void *Py_UNUSED(ignored)) static PyGetSetDef BaseException_getset[] = { {"__dict__", PyObject_GenericGetDict, PyObject_GenericSetDict}, {"args", (getter)BaseException_get_args, (setter)BaseException_set_args}, - {"__note__", (getter)BaseException_get_note, (setter)BaseException_set_note}, {"__traceback__", (getter)BaseException_get_tb, (setter)BaseException_set_tb}, {"__context__", BaseException_get_context, BaseException_set_context, PyDoc_STR("exception context")}, @@ -347,14 +369,6 @@ static PyGetSetDef BaseException_getset[] = { }; -static inline PyBaseExceptionObject* -_PyBaseExceptionObject_cast(PyObject *exc) -{ - assert(PyExceptionInstance_Check(exc)); - return (PyBaseExceptionObject *)exc; -} - - PyObject * PyException_GetTraceback(PyObject *self) { @@ -910,9 +924,32 @@ exceptiongroup_subset( PyException_SetContext(eg, PyException_GetContext(orig)); PyException_SetCause(eg, PyException_GetCause(orig)); - PyObject *note = _PyBaseExceptionObject_cast(orig)->note; - Py_XINCREF(note); - _PyBaseExceptionObject_cast(eg)->note = note; + if (PyObject_HasAttr(orig, &_Py_ID(__notes__))) { + PyObject *notes = PyObject_GetAttr(orig, &_Py_ID(__notes__)); + if (notes == NULL) { + goto error; + } + if (PySequence_Check(notes)) { + /* Make a copy so the parts have independent notes lists. */ + PyObject *notes_copy = PySequence_List(notes); + Py_DECREF(notes); + if (notes_copy == NULL) { + goto error; + } + int res = PyObject_SetAttr(eg, &_Py_ID(__notes__), notes_copy); + Py_DECREF(notes_copy); + if (res < 0) { + goto error; + } + } + else { + /* __notes__ is supposed to be a list, and split() is not a + * good place to report earlier user errors, so we just ignore + * notes of non-sequence type. + */ + Py_DECREF(notes); + } + } *result = eg; return 0; @@ -1262,7 +1299,7 @@ is_same_exception_metadata(PyObject *exc1, PyObject *exc2) PyBaseExceptionObject *e1 = (PyBaseExceptionObject *)exc1; PyBaseExceptionObject *e2 = (PyBaseExceptionObject *)exc2; - return (e1->note == e2->note && + return (e1->notes == e2->notes && e1->traceback == e2->traceback && e1->cause == e2->cause && e1->context == e2->context); diff --git a/Python/pythonrun.c b/Python/pythonrun.c index e086f0f345c222..769c34ea161e06 100644 --- a/Python/pythonrun.c +++ b/Python/pythonrun.c @@ -1129,7 +1129,7 @@ print_exception_suggestions(struct exception_print_context *ctx, } static int -print_exception_note(struct exception_print_context *ctx, PyObject *value) +print_exception_notes(struct exception_print_context *ctx, PyObject *value) { PyObject *f = ctx->file; @@ -1137,41 +1137,74 @@ print_exception_note(struct exception_print_context *ctx, PyObject *value) return 0; } - PyObject *note = PyObject_GetAttr(value, &_Py_ID(__note__)); - if (note == NULL) { + if (!PyObject_HasAttr(value, &_Py_ID(__notes__))) { + return 0; + } + PyObject *notes = PyObject_GetAttr(value, &_Py_ID(__notes__)); + if (notes == NULL) { return -1; } - if (!PyUnicode_Check(note)) { - Py_DECREF(note); - return 0; + if (!PySequence_Check(notes)) { + int res = 0; + if (write_indented_margin(ctx, f) < 0) { + res = -1; + } + PyObject *s = PyObject_Repr(notes); + if (s == NULL) { + PyErr_Clear(); + res = PyFile_WriteString("<__notes__ repr() failed>", f); + } + else { + res = PyFile_WriteObject(s, f, Py_PRINT_RAW); + Py_DECREF(s); + } + Py_DECREF(notes); + return res; } + Py_ssize_t num_notes = PySequence_Length(notes); + PyObject *lines = NULL; + for (Py_ssize_t ni = 0; ni < num_notes; ni++) { + PyObject *note = PySequence_GetItem(notes, ni); + PyObject *note_str = PyObject_Str(note); + Py_DECREF(note); - PyObject *lines = PyUnicode_Splitlines(note, 1); - Py_DECREF(note); + if (note_str == NULL) { + PyErr_Clear(); + if (PyFile_WriteString("", f) < 0) { + goto error; + } + } + else { + lines = PyUnicode_Splitlines(note_str, 1); + Py_DECREF(note_str); - if (lines == NULL) { - return -1; - } + if (lines == NULL) { + goto error; + } - Py_ssize_t n = PyList_GET_SIZE(lines); - for (Py_ssize_t i = 0; i < n; i++) { - PyObject *line = PyList_GET_ITEM(lines, i); - assert(PyUnicode_Check(line)); - if (write_indented_margin(ctx, f) < 0) { - goto error; + Py_ssize_t n = PyList_GET_SIZE(lines); + for (Py_ssize_t i = 0; i < n; i++) { + PyObject *line = PyList_GET_ITEM(lines, i); + assert(PyUnicode_Check(line)); + if (write_indented_margin(ctx, f) < 0) { + goto error; + } + if (PyFile_WriteObject(line, f, Py_PRINT_RAW) < 0) { + goto error; + } + } + Py_CLEAR(lines); } - if (PyFile_WriteObject(line, f, Py_PRINT_RAW) < 0) { + if (PyFile_WriteString("\n", f) < 0) { goto error; } } - if (PyFile_WriteString("\n", f) < 0) { - goto error; - } - Py_DECREF(lines); + Py_DECREF(notes); return 0; error: - Py_DECREF(lines); + Py_XDECREF(lines); + Py_DECREF(notes); return -1; } @@ -1206,7 +1239,7 @@ print_exception(struct exception_print_context *ctx, PyObject *value) if (PyFile_WriteString("\n", f) < 0) { goto error; } - if (print_exception_note(ctx, value) < 0) { + if (print_exception_notes(ctx, value) < 0) { goto error; }