From 1555087621005673a4929f9ebe92fc2a7f7f9ecc Mon Sep 17 00:00:00 2001 From: Irit Katriel Date: Sun, 13 Feb 2022 21:44:06 +0000 Subject: [PATCH 01/20] PEP-678: exception notes are set by add_note(). __notes__ holds a tuple of the assigned notes --- Doc/library/exceptions.rst | 23 ++++-- Include/cpython/pyerrors.h | 2 +- Include/internal/pycore_global_strings.h | 2 +- Include/internal/pycore_runtime_init.h | 2 +- Lib/test/test_exception_group.py | 4 +- Lib/test/test_exceptions.py | 40 ++++++--- Lib/test/test_traceback.py | 100 ++++++++++++++++++++--- Lib/traceback.py | 7 +- Objects/exceptions.c | 95 ++++++++++++--------- Python/pythonrun.c | 56 +++++++------ Tools/scripts/generate_global_objects.py | 2 +- 11 files changed, 230 insertions(+), 103 deletions(-) diff --git a/Doc/library/exceptions.rst b/Doc/library/exceptions.rst index d0b2e62c34c967..9ff5d566c49367 100644 --- a/Doc/library/exceptions.rst +++ b/Doc/library/exceptions.rst @@ -126,13 +126,22 @@ 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, / replace=False) - 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. + If ``note`` is a string, it is added to the exception's notes which appear + in the standard traceback after the exception string. If ``replace`` is + true, all previously existing notes are removed before the new one is added. + To clear all notes, use ``add_note(None, replace=True)``. A :exc:`TypeError` + is raise if ``note`` is neither a string nor ``None``. - .. versionadded:: 3.11 + .. versionadded:: 3.11 + + .. attribute:: __notes__ + + A read-only field that contains a tuple of the notes of this exception. + See :meth:`add_note`. + + .. versionadded:: 3.11 .. exception:: Exception @@ -898,7 +907,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, @@ -915,7 +924,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/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 17241b3a3dd166..5f17cf0a9959ea 100644 --- a/Include/internal/pycore_global_strings.h +++ b/Include/internal/pycore_global_strings.h @@ -147,7 +147,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(__origin__) STRUCT_FOR_ID(__package__) diff --git a/Include/internal/pycore_runtime_init.h b/Include/internal/pycore_runtime_init.h index 045ae5d2835b1f..566b4b4f632f1b 100644 --- a/Include/internal/pycore_runtime_init.h +++ b/Include/internal/pycore_runtime_init.h @@ -761,7 +761,7 @@ extern "C" { INIT_ID(__newobj__), \ INIT_ID(__newobj_ex__), \ INIT_ID(__next__), \ - INIT_ID(__note__), \ + INIT_ID(__notes__), \ INIT_ID(__or__), \ INIT_ID(__origin__), \ INIT_ID(__package__), \ diff --git a/Lib/test/test_exception_group.py b/Lib/test/test_exception_group.py index b7b53bb2f0ecef..665ffdb8325383 100644 --- a/Lib/test/test_exception_group.py +++ b/Lib/test/test_exception_group.py @@ -502,7 +502,7 @@ 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(eg.__notes__, part.__notes__) def tbs_for_leaf(leaf, eg): for e, tbs in leaf_generator(eg): @@ -567,7 +567,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)}", replace=True) eg = e eg_template = [ diff --git a/Lib/test/test_exceptions.py b/Lib/test/test_exceptions.py index e3897a0d89c5ea..c2f38f94022bd5 100644 --- a/Lib/test/test_exceptions.py +++ b/Lib/test/test_exceptions.py @@ -538,26 +538,40 @@ 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.assertEqual(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") + with self.assertRaises(AttributeError): + e.__notes__ = ("NewNote",) + self.assertEqual(e.__notes__, ("My Note", "Your Note")) + + with self.assertRaises(AttributeError): + del e.__notes__ + self.assertEqual(e.__notes__, ("My Note", "Your Note")) + + e.add_note("Our Note", replace=True) + self.assertEqual(e.__notes__, ("Our Note",)) + + e.add_note("Our Final Note", replace=False) + self.assertEqual(e.__notes__, ("Our Note", "Our Final Note")) + + e.add_note(None, replace=False) + self.assertEqual(e.__notes__, ("Our Note", "Our Final Note")) + + e.add_note(None, replace=True) + self.assertEqual(e.__notes__, ()) - e.__note__ = None - self.assertIsNone(e.__note__) def testWithTraceback(self): try: diff --git a/Lib/test/test_traceback.py b/Lib/test/test_traceback.py index 0aa76aeb4ce093..2f822d299330e9 100644 --- a/Lib/test/test_traceback.py +++ b/Lib/test/test_traceback.py @@ -1326,18 +1326,38 @@ def test_exception_with_note(self): e = ValueError(42) vanilla = self.get_report(e) - e.__note__ = 'My Note' + e.add_note('My Note', replace=True) self.assertEqual(self.get_report(e), vanilla + 'My Note\n') - e.__note__ = '' + e.add_note('', replace=True) self.assertEqual(self.get_report(e), vanilla + '\n') - e.__note__ = 'Your Note' + e.add_note('Your Note', replace=True) self.assertEqual(self.get_report(e), vanilla + 'Your Note\n') - e.__note__ = None + e.add_note(None, replace=True) self.assertEqual(self.get_report(e), vanilla) + 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') + + e.add_note('Note 4', replace=True) + e.add_note('Note 5', replace=True) + 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 +1708,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 +1753,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\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): # diff --git a/Lib/traceback.py b/Lib/traceback.py index 05f1fffef0d3b0..63734b16606631 100644 --- a/Lib/traceback.py +++ b/Lib/traceback.py @@ -689,7 +689,7 @@ def __init__(self, exc_type, exc_value, exc_traceback, *, limit=None, # 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.__notes__ = exc_value.__notes__ if exc_value else None if exc_type and issubclass(exc_type, SyntaxError): # Handle SyntaxError's specially @@ -822,8 +822,9 @@ 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 self.__notes__ is not None: + for note in self.__notes__: + yield from [l + '\n' for l in note.split('\n')] def _format_syntax_error(self, stype): """Format SyntaxError exceptions (internal helper).""" diff --git a/Objects/exceptions.c b/Objects/exceptions.c index 9b1c9f1018077b..fe0cfad887676f 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,57 @@ 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 *args, PyObject *kwds) +{ + PyBaseExceptionObject *self = _PyBaseExceptionObject_cast(self_); + PyObject *note = NULL; + int replace = 0; + static char *kwlist[] = {"note", "replace", NULL}; + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|$p:add_note", + kwlist, ¬e, &replace)) { + return NULL; + } + + if (replace) { + Py_CLEAR(self->notes); + } + if (self->notes == NULL) { + self->notes = PyList_New(0); + if (self->notes == NULL) { + return NULL; + } + } + if (! Py_IsNone(note)) { + if (!PyUnicode_CheckExact(note)) { + PyErr_SetString(PyExc_TypeError, "a note must be a string or None"); + return NULL; + } + if (PyList_Append(self->notes, note) < 0) { + return NULL; + } + } + Py_RETURN_NONE; +} + +PyDoc_STRVAR(add_note_doc, +"Exception.add_note(note, replace=False) --\n\ + add note to the exception. If replace is true, clear previous notes."); 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_VARARGS | METH_KEYWORDS, + add_note_doc}, {NULL, NULL, 0, NULL}, }; @@ -221,30 +266,12 @@ BaseException_set_args(PyBaseExceptionObject *self, PyObject *val, void *Py_UNUS } static PyObject * -BaseException_get_note(PyBaseExceptionObject *self, void *Py_UNUSED(ignored)) +BaseException_get_notes(PyBaseExceptionObject *self, void *Py_UNUSED(ignored)) { - if (self->note == NULL) { - Py_RETURN_NONE; + if (self->notes == NULL) { + return PyTuple_New(0); } - 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; + return PySequence_Tuple(self->notes); } static PyObject * @@ -337,7 +364,7 @@ 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}, + {"__notes__", (getter)BaseException_get_notes, NULL}, {"__traceback__", (getter)BaseException_get_tb, (setter)BaseException_set_tb}, {"__context__", BaseException_get_context, BaseException_set_context, PyDoc_STR("exception context")}, @@ -347,14 +374,6 @@ static PyGetSetDef BaseException_getset[] = { }; -static inline PyBaseExceptionObject* -_PyBaseExceptionObject_cast(PyObject *exc) -{ - assert(PyExceptionInstance_Check(exc)); - return (PyBaseExceptionObject *)exc; -} - - PyObject * PyException_GetTraceback(PyObject *self) { @@ -905,9 +924,9 @@ 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; + PyObject *notes = _PyBaseExceptionObject_cast(orig)->notes; + Py_XINCREF(notes); + _PyBaseExceptionObject_cast(eg)->notes = notes; *result = eg; return 0; @@ -1257,7 +1276,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 b34a22391822b7..7af2b43326786f 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,47 @@ print_exception_note(struct exception_print_context *ctx, PyObject *value) return 0; } - PyObject *note = PyObject_GetAttr(value, &_Py_ID(__note__)); - if (note == NULL) { + PyObject *notes = PyObject_GetAttr(value, &_Py_ID(__notes__)); + if (notes == NULL) { return -1; } - if (!PyUnicode_Check(note)) { - Py_DECREF(note); + if (!PyTuple_Check(notes)) { + Py_DECREF(notes); return 0; } + Py_ssize_t num_notes = PyTuple_GET_SIZE(notes); + PyObject *lines = NULL; + for (Py_ssize_t ni = 0; ni < num_notes; ni++) { + PyObject *note = PyTuple_GET_ITEM(notes, ni); + assert(PyUnicode_Check(note)); + lines = PyUnicode_Splitlines(note, 1); + if (lines == NULL) { + Py_DECREF(notes); + return -1; + } - PyObject *lines = PyUnicode_Splitlines(note, 1); - Py_DECREF(note); - - if (lines == NULL) { - return -1; - } - - 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; + } } - 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_CLEAR(lines); } - Py_DECREF(lines); + Py_DECREF(notes); return 0; error: - Py_DECREF(lines); + Py_XDECREF(lines); + Py_DECREF(notes); return -1; } @@ -1206,7 +1212,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; } diff --git a/Tools/scripts/generate_global_objects.py b/Tools/scripts/generate_global_objects.py index b184e748989406..b88b7e6b654958 100644 --- a/Tools/scripts/generate_global_objects.py +++ b/Tools/scripts/generate_global_objects.py @@ -129,7 +129,7 @@ '__newobj__', '__newobj_ex__', '__next__', - '__note__', + '__notes__', '__or__', '__origin__', '__package__', From 6a386883def521badccdc7cce7936489cf98847a Mon Sep 17 00:00:00 2001 From: Irit Katriel Date: Wed, 23 Feb 2022 14:05:58 +0000 Subject: [PATCH 02/20] clear notes with del e.__notes__, remove replace arg and None note option --- Doc/library/exceptions.rst | 14 +++++------ Lib/test/test_exception_group.py | 2 +- Lib/test/test_exceptions.py | 16 +++---------- Lib/test/test_traceback.py | 16 ++++++++----- Objects/exceptions.c | 40 ++++++++++++++++++-------------- 5 files changed, 43 insertions(+), 45 deletions(-) diff --git a/Doc/library/exceptions.rst b/Doc/library/exceptions.rst index 9ff5d566c49367..0ce46bd0047fcd 100644 --- a/Doc/library/exceptions.rst +++ b/Doc/library/exceptions.rst @@ -126,20 +126,18 @@ The following exceptions are used mostly as base classes for other exceptions. tb = sys.exc_info()[2] raise OtherException(...).with_traceback(tb) - .. method:: add_note(note, / replace=False) + .. method:: add_note(note) - If ``note`` is a string, it is added to the exception's notes which appear - in the standard traceback after the exception string. If ``replace`` is - true, all previously existing notes are removed before the new one is added. - To clear all notes, use ``add_note(None, replace=True)``. A :exc:`TypeError` - is raise if ``note`` is neither a string nor ``None``. + Add the string ``note`` to the exception's notes which appear in the standard + traceback after the exception string. A :exc:`TypeError` is raise if ``note`` + is not a string. .. versionadded:: 3.11 .. attribute:: __notes__ - A read-only field that contains a tuple of the notes of this exception. - See :meth:`add_note`. + A tuple of the notes of this exception, which were added with :meth:`add_note`. + Its contents can be cleared with ``del e.__notes__``. .. versionadded:: 3.11 diff --git a/Lib/test/test_exception_group.py b/Lib/test/test_exception_group.py index 665ffdb8325383..507dc6abb88294 100644 --- a/Lib/test/test_exception_group.py +++ b/Lib/test/test_exception_group.py @@ -567,7 +567,7 @@ def level3(i): try: nested_group() except ExceptionGroup as e: - e.add_note(f"the note: {id(e)}", replace=True) + e.add_note(f"the note: {id(e)}") eg = e eg_template = [ diff --git a/Lib/test/test_exceptions.py b/Lib/test/test_exceptions.py index c2f38f94022bd5..03c655c44be932 100644 --- a/Lib/test/test_exceptions.py +++ b/Lib/test/test_exceptions.py @@ -556,22 +556,12 @@ def test_notes(self): e.__notes__ = ("NewNote",) self.assertEqual(e.__notes__, ("My Note", "Your Note")) - with self.assertRaises(AttributeError): - del e.__notes__ - self.assertEqual(e.__notes__, ("My Note", "Your Note")) + del e.__notes__ + self.assertEqual(e.__notes__, ()) - e.add_note("Our Note", replace=True) + e.add_note("Our Note") self.assertEqual(e.__notes__, ("Our Note",)) - e.add_note("Our Final Note", replace=False) - self.assertEqual(e.__notes__, ("Our Note", "Our Final Note")) - - e.add_note(None, replace=False) - self.assertEqual(e.__notes__, ("Our Note", "Our Final Note")) - - e.add_note(None, replace=True) - self.assertEqual(e.__notes__, ()) - def testWithTraceback(self): try: diff --git a/Lib/test/test_traceback.py b/Lib/test/test_traceback.py index 2f822d299330e9..ebd9c0b1c77567 100644 --- a/Lib/test/test_traceback.py +++ b/Lib/test/test_traceback.py @@ -1326,16 +1326,18 @@ def test_exception_with_note(self): e = ValueError(42) vanilla = self.get_report(e) - e.add_note('My Note', replace=True) + e.add_note('My Note') self.assertEqual(self.get_report(e), vanilla + 'My Note\n') - e.add_note('', replace=True) + del e.__notes__ + e.add_note('') self.assertEqual(self.get_report(e), vanilla + '\n') - e.add_note('Your Note', replace=True) + del e.__notes__ + e.add_note('Your Note') self.assertEqual(self.get_report(e), vanilla + 'Your Note\n') - e.add_note(None, replace=True) + del e.__notes__ self.assertEqual(self.get_report(e), vanilla) def test_exception_with_note_with_multiple_notes(self): @@ -1350,8 +1352,10 @@ def test_exception_with_note_with_multiple_notes(self): self.get_report(e), vanilla + 'Note 1\n' + 'Note 2\n' + 'Note 3\n') - e.add_note('Note 4', replace=True) - e.add_note('Note 5', replace=True) + del e.__notes__ + e.add_note('Note 4') + del e.__notes__ + e.add_note('Note 5') e.add_note('Note 6') self.assertEqual( diff --git a/Objects/exceptions.c b/Objects/exceptions.c index fe0cfad887676f..f25c12e5bb5b66 100644 --- a/Objects/exceptions.c +++ b/Objects/exceptions.c @@ -198,30 +198,20 @@ BaseException_add_note(PyObject *self_, PyObject *args, PyObject *kwds) { PyBaseExceptionObject *self = _PyBaseExceptionObject_cast(self_); PyObject *note = NULL; - int replace = 0; - static char *kwlist[] = {"note", "replace", NULL}; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|$p:add_note", - kwlist, ¬e, &replace)) { + + if (!PyArg_ParseTuple(args, "U:add_note", ¬e)) { return NULL; } - if (replace) { - Py_CLEAR(self->notes); - } if (self->notes == NULL) { self->notes = PyList_New(0); if (self->notes == NULL) { return NULL; } } - if (! Py_IsNone(note)) { - if (!PyUnicode_CheckExact(note)) { - PyErr_SetString(PyExc_TypeError, "a note must be a string or None"); - return NULL; - } - if (PyList_Append(self->notes, note) < 0) { - return NULL; - } + assert(PyUnicode_CheckExact(note)); + if (PyList_Append(self->notes, note) < 0) { + return NULL; } Py_RETURN_NONE; } @@ -235,7 +225,7 @@ static PyMethodDef BaseException_methods[] = { {"__setstate__", (PyCFunction)BaseException_setstate, METH_O }, {"with_traceback", (PyCFunction)BaseException_with_traceback, METH_O, with_traceback_doc}, - {"add_note", (PyCFunction)BaseException_add_note, METH_VARARGS | METH_KEYWORDS, + {"add_note", (PyCFunction)BaseException_add_note, METH_VARARGS, add_note_doc}, {NULL, NULL, 0, NULL}, }; @@ -274,6 +264,22 @@ BaseException_get_notes(PyBaseExceptionObject *self, void *Py_UNUSED(ignored)) return PySequence_Tuple(self->notes); } +static int +BaseException_set_notes(PyBaseExceptionObject *self, PyObject *note, + void *Py_UNUSED(ignored)) +{ + if (note == NULL) { + Py_CLEAR(self->notes); + } + else { + PyErr_SetString( + PyExc_AttributeError, + "Cannot assign a value to __notes__. Use add_note()."); + return -1; + } + return 0; +} + static PyObject * BaseException_get_tb(PyBaseExceptionObject *self, void *Py_UNUSED(ignored)) { @@ -364,7 +370,7 @@ 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}, - {"__notes__", (getter)BaseException_get_notes, NULL}, + {"__notes__", (getter)BaseException_get_notes, (setter)BaseException_set_notes}, {"__traceback__", (getter)BaseException_get_tb, (setter)BaseException_set_tb}, {"__context__", BaseException_get_context, BaseException_set_context, PyDoc_STR("exception context")}, From 7bab63ea4321de30e6891ed846018a56098a1836 Mon Sep 17 00:00:00 2001 From: Irit Katriel Date: Wed, 2 Mar 2022 19:50:16 +0000 Subject: [PATCH 03/20] do not create new tuple when __notes__ is accessed --- Lib/test/test_exception_group.py | 2 +- Lib/test/test_exceptions.py | 2 ++ Objects/exceptions.c | 22 +++++++++++++++++----- 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/Lib/test/test_exception_group.py b/Lib/test/test_exception_group.py index 507dc6abb88294..150dbdd3b2b55e 100644 --- a/Lib/test/test_exception_group.py +++ b/Lib/test/test_exception_group.py @@ -502,7 +502,7 @@ def leaves(exc): self.assertIs(eg.__cause__, part.__cause__) self.assertIs(eg.__context__, part.__context__) self.assertIs(eg.__traceback__, part.__traceback__) - self.assertEqual(eg.__notes__, part.__notes__) + self.assertIs(eg.__notes__, part.__notes__) def tbs_for_leaf(leaf, eg): for e, tbs in leaf_generator(eg): diff --git a/Lib/test/test_exceptions.py b/Lib/test/test_exceptions.py index 03c655c44be932..1d61d070d43de2 100644 --- a/Lib/test/test_exceptions.py +++ b/Lib/test/test_exceptions.py @@ -562,6 +562,8 @@ def test_notes(self): e.add_note("Our Note") self.assertEqual(e.__notes__, ("Our Note",)) + # calling notes twice returns the same tuple object + self.assertIs(e.__notes__, e.__notes__) def testWithTraceback(self): try: diff --git a/Objects/exceptions.c b/Objects/exceptions.c index f25c12e5bb5b66..6a4ae29894aed2 100644 --- a/Objects/exceptions.c +++ b/Objects/exceptions.c @@ -203,15 +203,26 @@ BaseException_add_note(PyObject *self_, PyObject *args, PyObject *kwds) return NULL; } + assert(PyUnicode_CheckExact(note)); if (self->notes == NULL) { - self->notes = PyList_New(0); + self->notes = PyTuple_Pack(1, note); if (self->notes == NULL) { return NULL; } } - assert(PyUnicode_CheckExact(note)); - if (PyList_Append(self->notes, note) < 0) { - return NULL; + else { + assert(PyTuple_CheckExact(self->notes)); + Py_ssize_t num_notes = PyTuple_Size(self->notes); + PyObject *new_notes = PyTuple_New(1 + num_notes); + if (new_notes == NULL) { + return NULL; + } + for(Py_ssize_t i = 0; i < num_notes; i++) { + PyTuple_SET_ITEM( + new_notes, i, Py_NewRef(PyTuple_GET_ITEM(self->notes, i))); + } + PyTuple_SET_ITEM(new_notes, num_notes, Py_NewRef(note)); + Py_SETREF(self->notes, new_notes); } Py_RETURN_NONE; } @@ -261,7 +272,8 @@ BaseException_get_notes(PyBaseExceptionObject *self, void *Py_UNUSED(ignored)) if (self->notes == NULL) { return PyTuple_New(0); } - return PySequence_Tuple(self->notes); + assert(PyTuple_CheckExact(self->notes)); + return Py_NewRef(self->notes); } static int From 396b5f1fc76a696323e8ece01a5bbb31953c6b0c Mon Sep 17 00:00:00 2001 From: Irit Katriel Date: Wed, 16 Mar 2022 11:08:16 +0000 Subject: [PATCH 04/20] Revert "do not create new tuple when __notes__ is accessed" This reverts commit 7bab63ea4321de30e6891ed846018a56098a1836. --- Lib/test/test_exception_group.py | 2 +- Lib/test/test_exceptions.py | 2 -- Objects/exceptions.c | 22 +++++----------------- 3 files changed, 6 insertions(+), 20 deletions(-) diff --git a/Lib/test/test_exception_group.py b/Lib/test/test_exception_group.py index 150dbdd3b2b55e..507dc6abb88294 100644 --- a/Lib/test/test_exception_group.py +++ b/Lib/test/test_exception_group.py @@ -502,7 +502,7 @@ def leaves(exc): self.assertIs(eg.__cause__, part.__cause__) self.assertIs(eg.__context__, part.__context__) self.assertIs(eg.__traceback__, part.__traceback__) - self.assertIs(eg.__notes__, part.__notes__) + self.assertEqual(eg.__notes__, part.__notes__) def tbs_for_leaf(leaf, eg): for e, tbs in leaf_generator(eg): diff --git a/Lib/test/test_exceptions.py b/Lib/test/test_exceptions.py index 1d61d070d43de2..03c655c44be932 100644 --- a/Lib/test/test_exceptions.py +++ b/Lib/test/test_exceptions.py @@ -562,8 +562,6 @@ def test_notes(self): e.add_note("Our Note") self.assertEqual(e.__notes__, ("Our Note",)) - # calling notes twice returns the same tuple object - self.assertIs(e.__notes__, e.__notes__) def testWithTraceback(self): try: diff --git a/Objects/exceptions.c b/Objects/exceptions.c index 6a4ae29894aed2..f25c12e5bb5b66 100644 --- a/Objects/exceptions.c +++ b/Objects/exceptions.c @@ -203,26 +203,15 @@ BaseException_add_note(PyObject *self_, PyObject *args, PyObject *kwds) return NULL; } - assert(PyUnicode_CheckExact(note)); if (self->notes == NULL) { - self->notes = PyTuple_Pack(1, note); + self->notes = PyList_New(0); if (self->notes == NULL) { return NULL; } } - else { - assert(PyTuple_CheckExact(self->notes)); - Py_ssize_t num_notes = PyTuple_Size(self->notes); - PyObject *new_notes = PyTuple_New(1 + num_notes); - if (new_notes == NULL) { - return NULL; - } - for(Py_ssize_t i = 0; i < num_notes; i++) { - PyTuple_SET_ITEM( - new_notes, i, Py_NewRef(PyTuple_GET_ITEM(self->notes, i))); - } - PyTuple_SET_ITEM(new_notes, num_notes, Py_NewRef(note)); - Py_SETREF(self->notes, new_notes); + assert(PyUnicode_CheckExact(note)); + if (PyList_Append(self->notes, note) < 0) { + return NULL; } Py_RETURN_NONE; } @@ -272,8 +261,7 @@ BaseException_get_notes(PyBaseExceptionObject *self, void *Py_UNUSED(ignored)) if (self->notes == NULL) { return PyTuple_New(0); } - assert(PyTuple_CheckExact(self->notes)); - return Py_NewRef(self->notes); + return PySequence_Tuple(self->notes); } static int From f518aa5f67e0ca6466868cf71175c40548cedb86 Mon Sep 17 00:00:00 2001 From: Irit Katriel Date: Wed, 16 Mar 2022 13:39:49 +0000 Subject: [PATCH 05/20] add __notes__ attribute in add_note. Traceback ignores it if it's not a sequence of strings --- Lib/test/test_exception_group.py | 4 +- Lib/test/test_exceptions.py | 18 ++++----- Lib/test/test_traceback.py | 8 ++++ Lib/traceback.py | 9 +++-- Objects/exceptions.c | 63 ++++++++++++++------------------ Python/pythonrun.c | 46 +++++++++++++---------- 6 files changed, 78 insertions(+), 70 deletions(-) diff --git a/Lib/test/test_exception_group.py b/Lib/test/test_exception_group.py index 507dc6abb88294..15a1659c056158 100644 --- a/Lib/test/test_exception_group.py +++ b/Lib/test/test_exception_group.py @@ -502,7 +502,9 @@ def leaves(exc): self.assertIs(eg.__cause__, part.__cause__) self.assertIs(eg.__context__, part.__context__) self.assertIs(eg.__traceback__, part.__traceback__) - self.assertEqual(eg.__notes__, part.__notes__) + self.assertEqual( + getattr(eg, '__notes__', None), + getattr(part, '__notes__', None)) def tbs_for_leaf(leaf, eg): for e, tbs in leaf_generator(eg): diff --git a/Lib/test/test_exceptions.py b/Lib/test/test_exceptions.py index 03c655c44be932..f0856dd80e56b4 100644 --- a/Lib/test/test_exceptions.py +++ b/Lib/test/test_exceptions.py @@ -541,27 +541,25 @@ def testAttributes(self): def test_notes(self): for e in [BaseException(1), Exception(2), ValueError(3)]: with self.subTest(e=e): - self.assertEqual(e.__notes__, ()) + self.assertFalse(hasattr(e, '__notes__')) e.add_note("My Note") - self.assertEqual(e.__notes__, ("My Note",)) + self.assertEqual(e.__notes__, ["My Note"]) with self.assertRaises(TypeError): e.add_note(42) - self.assertEqual(e.__notes__, ("My Note",)) + self.assertEqual(e.__notes__, ["My Note"]) e.add_note("Your Note") - self.assertEqual(e.__notes__, ("My Note", "Your Note")) - - with self.assertRaises(AttributeError): - e.__notes__ = ("NewNote",) - self.assertEqual(e.__notes__, ("My Note", "Your Note")) + self.assertEqual(e.__notes__, ["My Note", "Your Note"]) del e.__notes__ - self.assertEqual(e.__notes__, ()) + self.assertFalse(hasattr(e, '__notes__')) e.add_note("Our Note") - self.assertEqual(e.__notes__, ("Our Note",)) + self.assertEqual(e.__notes__, ["Our Note"]) + e.__notes__ = 42 + self.assertEqual(e.__notes__, 42) def testWithTraceback(self): try: diff --git a/Lib/test/test_traceback.py b/Lib/test/test_traceback.py index ebd9c0b1c77567..a356327fa408d5 100644 --- a/Lib/test/test_traceback.py +++ b/Lib/test/test_traceback.py @@ -1340,6 +1340,14 @@ def test_exception_with_note(self): del e.__notes__ self.assertEqual(self.get_report(e), vanilla) + # non-sequence __notes__ is ignored + e.__notes__ = 42 + self.assertEqual(self.get_report(e), vanilla) + + # non-string items in the __notes__ sequence are ignored + e.__notes__ = [42, 'Final Note'] + self.assertEqual(self.get_report(e), vanilla + 'Final Note\n') + def test_exception_with_note_with_multiple_notes(self): e = ValueError(42) vanilla = self.get_report(e) diff --git a/Lib/traceback.py b/Lib/traceback.py index 63734b16606631..69722550fba46c 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 @@ -689,7 +689,7 @@ def __init__(self, exc_type, exc_value, exc_traceback, *, limit=None, # Capture now to permit freeing resources: only complication is in the # unofficial API _format_final_exc_line self._str = _some_str(exc_value) - self.__notes__ = exc_value.__notes__ if exc_value else None + self.__notes__ = getattr(exc_value, '__notes__', None) if exc_type and issubclass(exc_type, SyntaxError): # Handle SyntaxError's specially @@ -822,9 +822,10 @@ def format_exception_only(self): yield _format_final_exc_line(stype, self._str) else: yield from self._format_syntax_error(stype) - if self.__notes__ is not None: + if isinstance(self.__notes__, collections.abc.Sequence): for note in self.__notes__: - yield from [l + '\n' for l in note.split('\n')] + if isinstance(note, str): + yield from [l + '\n' for l in note.split('\n')] def _format_syntax_error(self, stype): """Format SyntaxError exceptions (internal helper).""" diff --git a/Objects/exceptions.c b/Objects/exceptions.c index f25c12e5bb5b66..be1594b94d6db6 100644 --- a/Objects/exceptions.c +++ b/Objects/exceptions.c @@ -194,25 +194,34 @@ _PyBaseExceptionObject_cast(PyObject *exc) } static PyObject * -BaseException_add_note(PyObject *self_, PyObject *args, PyObject *kwds) +BaseException_add_note(PyObject *self, PyObject *args, PyObject *kwds) { - PyBaseExceptionObject *self = _PyBaseExceptionObject_cast(self_); PyObject *note = NULL; if (!PyArg_ParseTuple(args, "U:add_note", ¬e)) { return NULL; } - if (self->notes == NULL) { - self->notes = PyList_New(0); - if (self->notes == 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; } - assert(PyUnicode_CheckExact(note)); - if (PyList_Append(self->notes, note) < 0) { + if (PyList_Append(notes, note) < 0) { + Py_DECREF(notes); return NULL; } + Py_DECREF(notes); Py_RETURN_NONE; } @@ -255,31 +264,6 @@ BaseException_set_args(PyBaseExceptionObject *self, PyObject *val, void *Py_UNUS return 0; } -static PyObject * -BaseException_get_notes(PyBaseExceptionObject *self, void *Py_UNUSED(ignored)) -{ - if (self->notes == NULL) { - return PyTuple_New(0); - } - return PySequence_Tuple(self->notes); -} - -static int -BaseException_set_notes(PyBaseExceptionObject *self, PyObject *note, - void *Py_UNUSED(ignored)) -{ - if (note == NULL) { - Py_CLEAR(self->notes); - } - else { - PyErr_SetString( - PyExc_AttributeError, - "Cannot assign a value to __notes__. Use add_note()."); - return -1; - } - return 0; -} - static PyObject * BaseException_get_tb(PyBaseExceptionObject *self, void *Py_UNUSED(ignored)) { @@ -370,7 +354,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}, - {"__notes__", (getter)BaseException_get_notes, (setter)BaseException_set_notes}, {"__traceback__", (getter)BaseException_get_tb, (setter)BaseException_set_tb}, {"__context__", BaseException_get_context, BaseException_set_context, PyDoc_STR("exception context")}, @@ -930,9 +913,17 @@ exceptiongroup_subset( PyException_SetContext(eg, PyException_GetContext(orig)); PyException_SetCause(eg, PyException_GetCause(orig)); - PyObject *notes = _PyBaseExceptionObject_cast(orig)->notes; - Py_XINCREF(notes); - _PyBaseExceptionObject_cast(eg)->notes = notes; + if (PyObject_HasAttr(orig, &_Py_ID(__notes__))) { + PyObject *notes = PyObject_GetAttr(orig, &_Py_ID(__notes__)); + if (notes == NULL) { + goto error; + } + if (PyObject_SetAttr(eg, &_Py_ID(__notes__), notes) < 0) { + Py_DECREF(notes); + goto error; + } + Py_DECREF(notes); + } *result = eg; return 0; diff --git a/Python/pythonrun.c b/Python/pythonrun.c index 7af2b43326786f..152eefd3a31645 100644 --- a/Python/pythonrun.c +++ b/Python/pythonrun.c @@ -1137,40 +1137,48 @@ print_exception_notes(struct exception_print_context *ctx, PyObject *value) return 0; } + if (!PyObject_HasAttr(value, &_Py_ID(__notes__))) { + return 0; + } PyObject *notes = PyObject_GetAttr(value, &_Py_ID(__notes__)); if (notes == NULL) { return -1; } - if (!PyTuple_Check(notes)) { + if (!PySequence_Check(notes)) { Py_DECREF(notes); return 0; } - Py_ssize_t num_notes = PyTuple_GET_SIZE(notes); + Py_ssize_t num_notes = PySequence_Length(notes); PyObject *lines = NULL; for (Py_ssize_t ni = 0; ni < num_notes; ni++) { - PyObject *note = PyTuple_GET_ITEM(notes, ni); - assert(PyUnicode_Check(note)); - lines = PyUnicode_Splitlines(note, 1); - if (lines == NULL) { - Py_DECREF(notes); - return -1; - } - - 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) { + PyObject *note = PySequence_GetItem(notes, ni); + if (PyUnicode_Check(note)) { + lines = PyUnicode_Splitlines(note, 1); + Py_DECREF(note); + if (lines == NULL) { goto error; } - if (PyFile_WriteObject(line, f, Py_PRINT_RAW) < 0) { + + 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; + } + } + if (PyFile_WriteString("\n", f) < 0) { goto error; } + Py_CLEAR(lines); } - if (PyFile_WriteString("\n", f) < 0) { - goto error; + else { + /* Ignore notes which are not strings */ + Py_DECREF(note); } - Py_CLEAR(lines); } Py_DECREF(notes); From 04389331dbaef9506f781f0cc21b81eef41ccd66 Mon Sep 17 00:00:00 2001 From: Irit Katriel Date: Wed, 16 Mar 2022 23:18:51 +0000 Subject: [PATCH 06/20] add in add_note a check that __notes__ is a list. Add the test. tweak __notes__ doc. --- Doc/library/exceptions.rst | 4 ++-- Lib/test/test_exceptions.py | 3 +++ Objects/exceptions.c | 8 +++++--- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/Doc/library/exceptions.rst b/Doc/library/exceptions.rst index 0ce46bd0047fcd..f904733fbf3c4a 100644 --- a/Doc/library/exceptions.rst +++ b/Doc/library/exceptions.rst @@ -136,8 +136,8 @@ The following exceptions are used mostly as base classes for other exceptions. .. attribute:: __notes__ - A tuple of the notes of this exception, which were added with :meth:`add_note`. - Its contents can be cleared with ``del e.__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 diff --git a/Lib/test/test_exceptions.py b/Lib/test/test_exceptions.py index f0856dd80e56b4..787623e54577ff 100644 --- a/Lib/test/test_exceptions.py +++ b/Lib/test/test_exceptions.py @@ -561,6 +561,9 @@ def test_notes(self): e.__notes__ = 42 self.assertEqual(e.__notes__, 42) + e.add_note("will not work") + self.assertEqual(e.__notes__, 42) + def testWithTraceback(self): try: raise IndexError(4) diff --git a/Objects/exceptions.c b/Objects/exceptions.c index be1594b94d6db6..9e4b97650f3087 100644 --- a/Objects/exceptions.c +++ b/Objects/exceptions.c @@ -217,9 +217,11 @@ BaseException_add_note(PyObject *self, PyObject *args, PyObject *kwds) if (notes == NULL) { return NULL; } - if (PyList_Append(notes, note) < 0) { - Py_DECREF(notes); - return NULL; + if (PyList_Check(notes)) { + if (PyList_Append(notes, note) < 0) { + Py_DECREF(notes); + return NULL; + } } Py_DECREF(notes); Py_RETURN_NONE; From dcc93efcf0606a154eb25090d85357fead9c4b61 Mon Sep 17 00:00:00 2001 From: Irit Katriel Date: Wed, 16 Mar 2022 23:30:24 +0000 Subject: [PATCH 07/20] TypeError in add_note if __notes__ is not a list --- Lib/test/test_exceptions.py | 3 ++- Objects/exceptions.c | 13 ++++++++----- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/Lib/test/test_exceptions.py b/Lib/test/test_exceptions.py index 787623e54577ff..8333c96c54ceb1 100644 --- a/Lib/test/test_exceptions.py +++ b/Lib/test/test_exceptions.py @@ -561,7 +561,8 @@ def test_notes(self): e.__notes__ = 42 self.assertEqual(e.__notes__, 42) - e.add_note("will not work") + with self.assertRaises(TypeError): + e.add_note("will not work") self.assertEqual(e.__notes__, 42) def testWithTraceback(self): diff --git a/Objects/exceptions.c b/Objects/exceptions.c index 9e4b97650f3087..09eb1ed90a88ba 100644 --- a/Objects/exceptions.c +++ b/Objects/exceptions.c @@ -217,11 +217,14 @@ BaseException_add_note(PyObject *self, PyObject *args, PyObject *kwds) if (notes == NULL) { return NULL; } - if (PyList_Check(notes)) { - if (PyList_Append(notes, note) < 0) { - Py_DECREF(notes); - 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; From 614378e465087f5529eed26d3a63c73de3b399f6 Mon Sep 17 00:00:00 2001 From: Irit Katriel Date: Thu, 17 Mar 2022 01:11:22 +0000 Subject: [PATCH 08/20] if __notes__ is not sequence, print repr(__notes__). If note is not a string, print str(note) --- Lib/test/test_traceback.py | 41 +++++++++++++++++++++++++++++++------- Lib/traceback.py | 17 +++++++++------- Python/pythonrun.c | 36 +++++++++++++++++++++++++++++++-- 3 files changed, 78 insertions(+), 16 deletions(-) diff --git a/Lib/test/test_traceback.py b/Lib/test/test_traceback.py index a356327fa408d5..3e81eb456049c0 100644 --- a/Lib/test/test_traceback.py +++ b/Lib/test/test_traceback.py @@ -1323,7 +1323,7 @@ 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.add_note('My Note') @@ -1340,13 +1340,40 @@ def test_exception_with_note(self): del e.__notes__ self.assertEqual(self.get_report(e), vanilla) - # non-sequence __notes__ is ignored - e.__notes__ = 42 - 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') - # non-string items in the __notes__ sequence are ignored - e.__notes__ = [42, 'Final Note'] - self.assertEqual(self.get_report(e), vanilla + 'Final 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) diff --git a/Lib/traceback.py b/Lib/traceback.py index 69722550fba46c..838637722d7e89 100644 --- a/Lib/traceback.py +++ b/Lib/traceback.py @@ -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,7 +688,7 @@ 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._str = _safe_string(exc_value, 'exception') self.__notes__ = getattr(exc_value, '__notes__', None) if exc_type and issubclass(exc_type, SyntaxError): @@ -824,8 +824,11 @@ def format_exception_only(self): yield from self._format_syntax_error(stype) if isinstance(self.__notes__, collections.abc.Sequence): for note in self.__notes__: - if isinstance(note, str): - yield from [l + '\n' for l in note.split('\n')] + if not isinstance(note, str): + 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).""" diff --git a/Python/pythonrun.c b/Python/pythonrun.c index 152eefd3a31645..6c347705863780 100644 --- a/Python/pythonrun.c +++ b/Python/pythonrun.c @@ -1145,8 +1145,21 @@ print_exception_notes(struct exception_print_context *ctx, PyObject *value) return -1; } 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 0; + return res; } Py_ssize_t num_notes = PySequence_Length(notes); PyObject *lines = NULL; @@ -1176,8 +1189,27 @@ print_exception_notes(struct exception_print_context *ctx, PyObject *value) Py_CLEAR(lines); } else { - /* Ignore notes which are not strings */ + int res = 0; + if (write_indented_margin(ctx, f) < 0) { + res = -1; + } + PyObject *s = PyObject_Str(note); + if (s == NULL) { + PyErr_Clear(); + res = PyFile_WriteString("", f); + } + else { + res = PyFile_WriteObject(s, f, Py_PRINT_RAW); + Py_DECREF(s); + } Py_DECREF(note); + if (res < 0) { + goto error; + } + if (PyFile_WriteString("\n", f) < 0) { + goto error; + } + } } From f240e717ecdb55288a2b1331fd2cdb7e63742a0d Mon Sep 17 00:00:00 2001 From: Irit Katriel Date: Mon, 21 Mar 2022 21:42:15 +0000 Subject: [PATCH 09/20] simplify traceback code (no need to special case note which is a string) --- Lib/traceback.py | 3 +-- Python/pythonrun.c | 43 +++++++++++++++---------------------------- 2 files changed, 16 insertions(+), 30 deletions(-) diff --git a/Lib/traceback.py b/Lib/traceback.py index 838637722d7e89..c011c9f683c161 100644 --- a/Lib/traceback.py +++ b/Lib/traceback.py @@ -824,8 +824,7 @@ def format_exception_only(self): yield from self._format_syntax_error(stype) if isinstance(self.__notes__, collections.abc.Sequence): for note in self.__notes__: - if not isinstance(note, str): - note = _safe_string(note, 'note') + 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) diff --git a/Python/pythonrun.c b/Python/pythonrun.c index 6c347705863780..9a3e9efa2c7862 100644 --- a/Python/pythonrun.c +++ b/Python/pythonrun.c @@ -1165,9 +1165,19 @@ print_exception_notes(struct exception_print_context *ctx, PyObject *value) PyObject *lines = NULL; for (Py_ssize_t ni = 0; ni < num_notes; ni++) { PyObject *note = PySequence_GetItem(notes, ni); - if (PyUnicode_Check(note)) { - lines = PyUnicode_Splitlines(note, 1); - Py_DECREF(note); + PyObject *note_str = PyObject_Str(note); + 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) { goto error; } @@ -1183,33 +1193,10 @@ print_exception_notes(struct exception_print_context *ctx, PyObject *value) goto error; } } - if (PyFile_WriteString("\n", f) < 0) { - goto error; - } Py_CLEAR(lines); } - else { - int res = 0; - if (write_indented_margin(ctx, f) < 0) { - res = -1; - } - PyObject *s = PyObject_Str(note); - if (s == NULL) { - PyErr_Clear(); - res = PyFile_WriteString("", f); - } - else { - res = PyFile_WriteObject(s, f, Py_PRINT_RAW); - Py_DECREF(s); - } - Py_DECREF(note); - if (res < 0) { - goto error; - } - if (PyFile_WriteString("\n", f) < 0) { - goto error; - } - + if (PyFile_WriteString("\n", f) < 0) { + goto error; } } From 6786dbdc25e74c0dcc318e96b4a3b91860c8b71a Mon Sep 17 00:00:00 2001 From: Irit Katriel Date: Tue, 22 Mar 2022 09:28:35 +0000 Subject: [PATCH 10/20] shallow copy the notes in split(), if it's a sequence --- Lib/test/test_exception_group.py | 20 ++++++++++++++++++++ Objects/exceptions.c | 10 ++++++++++ 2 files changed, 30 insertions(+) diff --git a/Lib/test/test_exception_group.py b/Lib/test/test_exception_group.py index 15a1659c056158..158c2863b25ef5 100644 --- a/Lib/test/test_exception_group.py +++ b/Lib/test/test_exception_group.py @@ -665,6 +665,26 @@ 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"]) + class NestedExceptionGroupSubclassSplitTest(ExceptionGroupSplitTestBase): diff --git a/Objects/exceptions.c b/Objects/exceptions.c index 09eb1ed90a88ba..70cdc2d5829cf4 100644 --- a/Objects/exceptions.c +++ b/Objects/exceptions.c @@ -923,6 +923,16 @@ exceptiongroup_subset( if (notes == NULL) { goto error; } + if (PySequence_Check(notes)) { + /* make a copy so the parts have independent notes. + * If __notes__ is not a sequence, we don't know how to copy it */ + PyObject *notes_copy = PySequence_List(notes); + Py_DECREF(notes); + if (notes_copy == NULL) { + goto error; + } + notes = notes_copy; + } if (PyObject_SetAttr(eg, &_Py_ID(__notes__), notes) < 0) { Py_DECREF(notes); goto error; From bdd4e2a1edd51dee682761d7dc2fd12c5514e8e4 Mon Sep 17 00:00:00 2001 From: Irit Katriel Date: Wed, 23 Mar 2022 10:32:47 +0000 Subject: [PATCH 11/20] split() ignores notes if they are not a sequence --- Lib/test/test_exception_group.py | 9 +++++++++ Objects/exceptions.c | 17 +++++++++++------ 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/Lib/test/test_exception_group.py b/Lib/test/test_exception_group.py index 158c2863b25ef5..6a65cfdc861c41 100644 --- a/Lib/test/test_exception_group.py +++ b/Lib/test/test_exception_group.py @@ -685,6 +685,15 @@ def test_split_copies_notes(self): 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/Objects/exceptions.c b/Objects/exceptions.c index 70cdc2d5829cf4..3275157c388389 100644 --- a/Objects/exceptions.c +++ b/Objects/exceptions.c @@ -924,20 +924,25 @@ exceptiongroup_subset( goto error; } if (PySequence_Check(notes)) { - /* make a copy so the parts have independent notes. - * If __notes__ is not a sequence, we don't know how to copy it */ + /* 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; } - notes = notes_copy; + int res = PyObject_SetAttr(eg, &_Py_ID(__notes__), notes_copy); + Py_DECREF(notes_copy); + if (res < 0) { + goto error; + } } - if (PyObject_SetAttr(eg, &_Py_ID(__notes__), notes) < 0) { + 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); - goto error; } - Py_DECREF(notes); } *result = eg; From 711e8042ae98b15b65a57c0f0e249a1744b2a5bd Mon Sep 17 00:00:00 2001 From: Irit Katriel <1055913+iritkatriel@users.noreply.github.com> Date: Mon, 28 Mar 2022 16:16:50 +0100 Subject: [PATCH 12/20] typo in doc Co-authored-by: Petr Viktorin --- Doc/library/exceptions.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/library/exceptions.rst b/Doc/library/exceptions.rst index f904733fbf3c4a..46880bee2a822b 100644 --- a/Doc/library/exceptions.rst +++ b/Doc/library/exceptions.rst @@ -129,7 +129,7 @@ The following exceptions are used mostly as base classes for other exceptions. .. method:: add_note(note) Add the string ``note`` to the exception's notes which appear in the standard - traceback after the exception string. A :exc:`TypeError` is raise if ``note`` + traceback after the exception string. A :exc:`TypeError` is raised if ``note`` is not a string. .. versionadded:: 3.11 From e147f52f0c30b47047e8d74281c6ad513c3a9573 Mon Sep 17 00:00:00 2001 From: Irit Katriel <1055913+iritkatriel@users.noreply.github.com> Date: Tue, 12 Apr 2022 12:01:45 +0100 Subject: [PATCH 13/20] fix typo in test Co-authored-by: Zac Hatfield-Dodds --- Lib/test/test_exception_group.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_exception_group.py b/Lib/test/test_exception_group.py index 6a65cfdc861c41..3038ce3f68333a 100644 --- a/Lib/test/test_exception_group.py +++ b/Lib/test/test_exception_group.py @@ -689,7 +689,7 @@ 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 + eg.__notes__ = 123 match, rest = eg.split(TypeError) self.assertFalse(hasattr(match, '__notes__')) self.assertFalse(hasattr(rest, '__notes__')) From 404f80d4bb8414164d1611c2e36290e016d52d96 Mon Sep 17 00:00:00 2001 From: "blurb-it[bot]" <43283697+blurb-it[bot]@users.noreply.github.com> Date: Tue, 12 Apr 2022 11:56:25 +0000 Subject: [PATCH 14/20] =?UTF-8?q?=F0=9F=93=9C=F0=9F=A4=96=20Added=20by=20b?= =?UTF-8?q?lurb=5Fit.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../2022-04-12-11-56-23.gh-issue-91479.-dyGJX.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Core and Builtins/2022-04-12-11-56-23.gh-issue-91479.-dyGJX.rst 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. From c014ad8691aed59168b6a85f5fce5b9d99e609ad Mon Sep 17 00:00:00 2001 From: Irit Katriel Date: Tue, 12 Apr 2022 13:59:21 +0100 Subject: [PATCH 15/20] update test_traceback --- Lib/test/test_traceback.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_traceback.py b/Lib/test/test_traceback.py index 58f8f74549d171..e4bd82ec39eae3 100644 --- a/Lib/test/test_traceback.py +++ b/Lib/test/test_traceback.py @@ -1821,7 +1821,7 @@ def exc(): f' | File "{__file__}", line {exc.__code__.co_firstlineno + 10}, in exc\n' f' | raise ExceptionGroup("nested", excs)\n' f' | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n' - f' | ExceptionGroup: nested\n' + f' | ExceptionGroup: nested (2 sub-exceptions)\n' f' | >> Multi line note\n' f' | >> Because I am such\n' f' | >> an important exception.\n' From 2ae22e4d587d8716e1007fe3d28d8c52be3cbe17 Mon Sep 17 00:00:00 2001 From: Irit Katriel Date: Tue, 12 Apr 2022 16:36:25 +0100 Subject: [PATCH 16/20] update whatnew --- Doc/whatsnew/3.11.rst | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/Doc/whatsnew/3.11.rst b/Doc/whatsnew/3.11.rst index d803801f273c55..25b2c4cd25a0d8 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 ====================== From 95be67065161a7ec22b9bbc78faaccc5b48b560e Mon Sep 17 00:00:00 2001 From: Irit Katriel Date: Thu, 14 Apr 2022 18:54:25 +0100 Subject: [PATCH 17/20] METH_VARARGS --> METH_O --- Objects/exceptions.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Objects/exceptions.c b/Objects/exceptions.c index db9221cc7adbd6..6e3bb996ff43ff 100644 --- a/Objects/exceptions.c +++ b/Objects/exceptions.c @@ -239,7 +239,7 @@ static PyMethodDef BaseException_methods[] = { {"__setstate__", (PyCFunction)BaseException_setstate, METH_O }, {"with_traceback", (PyCFunction)BaseException_with_traceback, METH_O, with_traceback_doc}, - {"add_note", (PyCFunction)BaseException_add_note, METH_VARARGS, + {"add_note", (PyCFunction)BaseException_add_note, METH_O, add_note_doc}, {NULL, NULL, 0, NULL}, }; From a14e915dc7cf3dd9bfdea164642b3ea4ba3f37b4 Mon Sep 17 00:00:00 2001 From: Irit Katriel <1055913+iritkatriel@users.noreply.github.com> Date: Thu, 14 Apr 2022 19:53:55 +0100 Subject: [PATCH 18/20] add_note no longer has a replace kwarg --- Objects/exceptions.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Objects/exceptions.c b/Objects/exceptions.c index 6e3bb996ff43ff..9989c883ad412b 100644 --- a/Objects/exceptions.c +++ b/Objects/exceptions.c @@ -231,8 +231,8 @@ BaseException_add_note(PyObject *self, PyObject *args, PyObject *kwds) } PyDoc_STRVAR(add_note_doc, -"Exception.add_note(note, replace=False) --\n\ - add note to the exception. If replace is true, clear previous notes."); +"Exception.add_note(note) --\n\ + add a note to the exception"); static PyMethodDef BaseException_methods[] = { {"__reduce__", (PyCFunction)BaseException_reduce, METH_NOARGS }, From 991e982242e8f4e4f3837d0b45485fcd1273e91b Mon Sep 17 00:00:00 2001 From: Irit Katriel Date: Thu, 14 Apr 2022 21:17:51 +0100 Subject: [PATCH 19/20] finish converting add_note to METH_O --- Objects/exceptions.c | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Objects/exceptions.c b/Objects/exceptions.c index 9989c883ad412b..b26a0e93af484d 100644 --- a/Objects/exceptions.c +++ b/Objects/exceptions.c @@ -194,11 +194,12 @@ _PyBaseExceptionObject_cast(PyObject *exc) } static PyObject * -BaseException_add_note(PyObject *self, PyObject *args, PyObject *kwds) +BaseException_add_note(PyObject *self, PyObject *note) { - PyObject *note = NULL; - - if (!PyArg_ParseTuple(args, "U:add_note", ¬e)) { + if (!PyUnicode_Check(note)) { + PyErr_Format(PyExc_TypeError, + "note must be a str, not '%s'", + Py_TYPE(note)->tp_name); return NULL; } From 602b4c431385cae209158902b1b044dffd899c81 Mon Sep 17 00:00:00 2001 From: Irit Katriel Date: Thu, 14 Apr 2022 21:47:27 +0100 Subject: [PATCH 20/20] fix whitespace --- Lib/test/test_traceback.py | 52 +++++++++++++++++++------------------- Lib/traceback.py | 2 +- 2 files changed, 27 insertions(+), 27 deletions(-) diff --git a/Lib/test/test_traceback.py b/Lib/test/test_traceback.py index e4bd82ec39eae3..962322c89ff66c 100644 --- a/Lib/test/test_traceback.py +++ b/Lib/test/test_traceback.py @@ -2194,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 c011c9f683c161..3afe49d1d8a0e6 100644 --- a/Lib/traceback.py +++ b/Lib/traceback.py @@ -917,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(