bpo-45607: Make it possible to enrich exception displays via setting their __note__ field (GH-29880)
This commit is contained in:
parent
d9301703fb
commit
5bb7ef2768
@ -127,6 +127,14 @@ The following exceptions are used mostly as base classes for other exceptions.
|
|||||||
tb = sys.exc_info()[2]
|
tb = sys.exc_info()[2]
|
||||||
raise OtherException(...).with_traceback(tb)
|
raise OtherException(...).with_traceback(tb)
|
||||||
|
|
||||||
|
.. attribute:: __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.
|
||||||
|
|
||||||
|
.. versionadded:: 3.11
|
||||||
|
|
||||||
|
|
||||||
.. exception:: Exception
|
.. exception:: Exception
|
||||||
|
|
||||||
|
@ -146,6 +146,12 @@ The :option:`-X` ``no_debug_ranges`` option and the environment variable
|
|||||||
See :pep:`657` for more details. (Contributed by Pablo Galindo, Batuhan Taskaya
|
See :pep:`657` for more details. (Contributed by Pablo Galindo, Batuhan Taskaya
|
||||||
and Ammar Askar in :issue:`43950`.)
|
and Ammar Askar in :issue:`43950`.)
|
||||||
|
|
||||||
|
Exceptions can be enriched with a string ``__note__``
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
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
|
Other Language Changes
|
||||||
======================
|
======================
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
/* PyException_HEAD defines the initial segment of every exception class. */
|
/* PyException_HEAD defines the initial segment of every exception class. */
|
||||||
#define PyException_HEAD PyObject_HEAD PyObject *dict;\
|
#define PyException_HEAD PyObject_HEAD PyObject *dict;\
|
||||||
PyObject *args; PyObject *traceback;\
|
PyObject *args; PyObject *note; PyObject *traceback;\
|
||||||
PyObject *context; PyObject *cause;\
|
PyObject *context; PyObject *cause;\
|
||||||
char suppress_context;
|
char suppress_context;
|
||||||
|
|
||||||
|
@ -516,6 +516,27 @@ class ExceptionTests(unittest.TestCase):
|
|||||||
'pickled "%r", attribute "%s' %
|
'pickled "%r", attribute "%s' %
|
||||||
(e, checkArgName))
|
(e, checkArgName))
|
||||||
|
|
||||||
|
def test_note(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")
|
||||||
|
|
||||||
|
with self.assertRaises(TypeError):
|
||||||
|
e.__note__ = 42
|
||||||
|
self.assertEqual(e.__note__, "My Note")
|
||||||
|
|
||||||
|
e.__note__ = "Your Note"
|
||||||
|
self.assertEqual(e.__note__, "Your Note")
|
||||||
|
|
||||||
|
with self.assertRaises(TypeError):
|
||||||
|
del e.__note__
|
||||||
|
self.assertEqual(e.__note__, "Your Note")
|
||||||
|
|
||||||
|
e.__note__ = None
|
||||||
|
self.assertIsNone(e.__note__)
|
||||||
|
|
||||||
def testWithTraceback(self):
|
def testWithTraceback(self):
|
||||||
try:
|
try:
|
||||||
raise IndexError(4)
|
raise IndexError(4)
|
||||||
|
@ -1298,13 +1298,13 @@ class SizeofTest(unittest.TestCase):
|
|||||||
class C(object): pass
|
class C(object): pass
|
||||||
check(C.__dict__, size('P'))
|
check(C.__dict__, size('P'))
|
||||||
# BaseException
|
# BaseException
|
||||||
check(BaseException(), size('5Pb'))
|
check(BaseException(), size('6Pb'))
|
||||||
# UnicodeEncodeError
|
# UnicodeEncodeError
|
||||||
check(UnicodeEncodeError("", "", 0, 0, ""), size('5Pb 2P2nP'))
|
check(UnicodeEncodeError("", "", 0, 0, ""), size('6Pb 2P2nP'))
|
||||||
# UnicodeDecodeError
|
# UnicodeDecodeError
|
||||||
check(UnicodeDecodeError("", b"", 0, 0, ""), size('5Pb 2P2nP'))
|
check(UnicodeDecodeError("", b"", 0, 0, ""), size('6Pb 2P2nP'))
|
||||||
# UnicodeTranslateError
|
# UnicodeTranslateError
|
||||||
check(UnicodeTranslateError("", 0, 1, ""), size('5Pb 2P2nP'))
|
check(UnicodeTranslateError("", 0, 1, ""), size('6Pb 2P2nP'))
|
||||||
# ellipses
|
# ellipses
|
||||||
check(Ellipsis, size(''))
|
check(Ellipsis, size(''))
|
||||||
# EncodingMap
|
# EncodingMap
|
||||||
|
@ -1224,6 +1224,22 @@ class BaseExceptionReportingTests:
|
|||||||
exp = "\n".join(expected)
|
exp = "\n".join(expected)
|
||||||
self.assertEqual(exp, err)
|
self.assertEqual(exp, err)
|
||||||
|
|
||||||
|
def test_exception_with_note(self):
|
||||||
|
e = ValueError(42)
|
||||||
|
vanilla = self.get_report(e)
|
||||||
|
|
||||||
|
e.__note__ = 'My Note'
|
||||||
|
self.assertEqual(self.get_report(e), vanilla + 'My Note\n')
|
||||||
|
|
||||||
|
e.__note__ = ''
|
||||||
|
self.assertEqual(self.get_report(e), vanilla + '\n')
|
||||||
|
|
||||||
|
e.__note__ = 'Your Note'
|
||||||
|
self.assertEqual(self.get_report(e), vanilla + 'Your Note\n')
|
||||||
|
|
||||||
|
e.__note__ = None
|
||||||
|
self.assertEqual(self.get_report(e), vanilla)
|
||||||
|
|
||||||
def test_exception_qualname(self):
|
def test_exception_qualname(self):
|
||||||
class A:
|
class A:
|
||||||
class B:
|
class B:
|
||||||
@ -1566,6 +1582,59 @@ class BaseExceptionReportingTests:
|
|||||||
report = self.get_report(exc)
|
report = self.get_report(exc)
|
||||||
self.assertEqual(report, expected)
|
self.assertEqual(report, expected)
|
||||||
|
|
||||||
|
def test_exception_group_with_notes(self):
|
||||||
|
def exc():
|
||||||
|
try:
|
||||||
|
excs = []
|
||||||
|
for msg in ['bad value', 'terrible value']:
|
||||||
|
try:
|
||||||
|
raise ValueError(msg)
|
||||||
|
except ValueError as e:
|
||||||
|
e.__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)')
|
||||||
|
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 + 9}, 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' +-+---------------- 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' +---------------- 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' +------------------------------------\n')
|
||||||
|
|
||||||
|
report = self.get_report(exc)
|
||||||
|
self.assertEqual(report, expected)
|
||||||
|
|
||||||
|
|
||||||
class PyExcReportingTests(BaseExceptionReportingTests, unittest.TestCase):
|
class PyExcReportingTests(BaseExceptionReportingTests, unittest.TestCase):
|
||||||
#
|
#
|
||||||
|
@ -685,6 +685,8 @@ class TracebackException:
|
|||||||
# Capture now to permit freeing resources: only complication is in the
|
# Capture now to permit freeing resources: only complication is in the
|
||||||
# unofficial API _format_final_exc_line
|
# unofficial API _format_final_exc_line
|
||||||
self._str = _some_str(exc_value)
|
self._str = _some_str(exc_value)
|
||||||
|
self.__note__ = exc_value.__note__ if exc_value else None
|
||||||
|
|
||||||
if exc_type and issubclass(exc_type, SyntaxError):
|
if exc_type and issubclass(exc_type, SyntaxError):
|
||||||
# Handle SyntaxError's specially
|
# Handle SyntaxError's specially
|
||||||
self.filename = exc_value.filename
|
self.filename = exc_value.filename
|
||||||
@ -816,6 +818,8 @@ class TracebackException:
|
|||||||
yield _format_final_exc_line(stype, self._str)
|
yield _format_final_exc_line(stype, self._str)
|
||||||
else:
|
else:
|
||||||
yield from self._format_syntax_error(stype)
|
yield from self._format_syntax_error(stype)
|
||||||
|
if self.__note__ is not None:
|
||||||
|
yield from [l + '\n' for l in self.__note__.split('\n')]
|
||||||
|
|
||||||
def _format_syntax_error(self, stype):
|
def _format_syntax_error(self, stype):
|
||||||
"""Format SyntaxError exceptions (internal helper)."""
|
"""Format SyntaxError exceptions (internal helper)."""
|
||||||
|
@ -0,0 +1,4 @@
|
|||||||
|
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.
|
||||||
|
|
@ -46,6 +46,7 @@ BaseException_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
|
|||||||
return NULL;
|
return NULL;
|
||||||
/* the dict is created on the fly in PyObject_GenericSetAttr */
|
/* the dict is created on the fly in PyObject_GenericSetAttr */
|
||||||
self->dict = NULL;
|
self->dict = NULL;
|
||||||
|
self->note = NULL;
|
||||||
self->traceback = self->cause = self->context = NULL;
|
self->traceback = self->cause = self->context = NULL;
|
||||||
self->suppress_context = 0;
|
self->suppress_context = 0;
|
||||||
|
|
||||||
@ -81,6 +82,7 @@ BaseException_clear(PyBaseExceptionObject *self)
|
|||||||
{
|
{
|
||||||
Py_CLEAR(self->dict);
|
Py_CLEAR(self->dict);
|
||||||
Py_CLEAR(self->args);
|
Py_CLEAR(self->args);
|
||||||
|
Py_CLEAR(self->note);
|
||||||
Py_CLEAR(self->traceback);
|
Py_CLEAR(self->traceback);
|
||||||
Py_CLEAR(self->cause);
|
Py_CLEAR(self->cause);
|
||||||
Py_CLEAR(self->context);
|
Py_CLEAR(self->context);
|
||||||
@ -105,6 +107,7 @@ BaseException_traverse(PyBaseExceptionObject *self, visitproc visit, void *arg)
|
|||||||
{
|
{
|
||||||
Py_VISIT(self->dict);
|
Py_VISIT(self->dict);
|
||||||
Py_VISIT(self->args);
|
Py_VISIT(self->args);
|
||||||
|
Py_VISIT(self->note);
|
||||||
Py_VISIT(self->traceback);
|
Py_VISIT(self->traceback);
|
||||||
Py_VISIT(self->cause);
|
Py_VISIT(self->cause);
|
||||||
Py_VISIT(self->context);
|
Py_VISIT(self->context);
|
||||||
@ -216,6 +219,33 @@ BaseException_set_args(PyBaseExceptionObject *self, PyObject *val, void *Py_UNUS
|
|||||||
return 0;
|
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 *
|
static PyObject *
|
||||||
BaseException_get_tb(PyBaseExceptionObject *self, void *Py_UNUSED(ignored))
|
BaseException_get_tb(PyBaseExceptionObject *self, void *Py_UNUSED(ignored))
|
||||||
{
|
{
|
||||||
@ -306,6 +336,7 @@ BaseException_set_cause(PyObject *self, PyObject *arg, void *Py_UNUSED(ignored))
|
|||||||
static PyGetSetDef BaseException_getset[] = {
|
static PyGetSetDef BaseException_getset[] = {
|
||||||
{"__dict__", PyObject_GenericGetDict, PyObject_GenericSetDict},
|
{"__dict__", PyObject_GenericGetDict, PyObject_GenericSetDict},
|
||||||
{"args", (getter)BaseException_get_args, (setter)BaseException_set_args},
|
{"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},
|
{"__traceback__", (getter)BaseException_get_tb, (setter)BaseException_set_tb},
|
||||||
{"__context__", BaseException_get_context,
|
{"__context__", BaseException_get_context,
|
||||||
BaseException_set_context, PyDoc_STR("exception context")},
|
BaseException_set_context, PyDoc_STR("exception context")},
|
||||||
|
@ -1083,6 +1083,41 @@ print_exception(struct exception_print_context *ctx, PyObject *value)
|
|||||||
PyErr_Clear();
|
PyErr_Clear();
|
||||||
}
|
}
|
||||||
err += PyFile_WriteString("\n", f);
|
err += PyFile_WriteString("\n", f);
|
||||||
|
|
||||||
|
if (err == 0 && PyExceptionInstance_Check(value)) {
|
||||||
|
_Py_IDENTIFIER(__note__);
|
||||||
|
|
||||||
|
PyObject *note = _PyObject_GetAttrId(value, &PyId___note__);
|
||||||
|
if (note == NULL) {
|
||||||
|
err = -1;
|
||||||
|
}
|
||||||
|
if (err == 0 && PyUnicode_Check(note)) {
|
||||||
|
_Py_static_string(PyId_newline, "\n");
|
||||||
|
PyObject *lines = PyUnicode_Split(
|
||||||
|
note, _PyUnicode_FromId(&PyId_newline), -1);
|
||||||
|
if (lines == NULL) {
|
||||||
|
err = -1;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Py_ssize_t n = PyList_GET_SIZE(lines);
|
||||||
|
for (Py_ssize_t i = 0; i < n; i++) {
|
||||||
|
if (err == 0) {
|
||||||
|
PyObject *line = PyList_GET_ITEM(lines, i);
|
||||||
|
assert(PyUnicode_Check(line));
|
||||||
|
err = write_indented_margin(ctx, f);
|
||||||
|
if (err == 0) {
|
||||||
|
err = PyFile_WriteObject(line, f, Py_PRINT_RAW);
|
||||||
|
}
|
||||||
|
if (err == 0) {
|
||||||
|
err = PyFile_WriteString("\n", f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Py_DECREF(lines);
|
||||||
|
}
|
||||||
|
Py_XDECREF(note);
|
||||||
|
}
|
||||||
Py_XDECREF(tb);
|
Py_XDECREF(tb);
|
||||||
Py_DECREF(value);
|
Py_DECREF(value);
|
||||||
/* If an error happened here, don't show it.
|
/* If an error happened here, don't show it.
|
||||||
|
Loading…
x
Reference in New Issue
Block a user