From 3181282b05ff7723630264aa3cad96c9e61db362 Mon Sep 17 00:00:00 2001 From: Irit Katriel <1055913+iritkatriel@users.noreply.github.com> Date: Fri, 3 Sep 2021 08:30:17 +0100 Subject: [PATCH 1/2] bpo-45083: Include the exception class qualname when formatting an exception (GH-28119) Co-authored-by: Erlend Egeberg Aasland (cherry picked from commit b4b6342848ec0459182a992151099252434cc619) Co-authored-by: Irit Katriel <1055913+iritkatriel@users.noreply.github.com> --- Lib/test/test_sys.py | 14 +++++++ Lib/test/test_traceback.py | 13 +++++++ .../2021-09-01-23-55-49.bpo-45083.cLi9G3.rst | 3 ++ Python/errors.c | 33 ++++++++--------- Python/pythonrun.c | 37 ++++++++++--------- 5 files changed, 65 insertions(+), 35 deletions(-) create mode 100644 Misc/NEWS.d/next/Core and Builtins/2021-09-01-23-55-49.bpo-45083.cLi9G3.rst diff --git a/Lib/test/test_sys.py b/Lib/test/test_sys.py index ed9b1770ab55f42..f277c92c408d5ab 100644 --- a/Lib/test/test_sys.py +++ b/Lib/test/test_sys.py @@ -1005,6 +1005,20 @@ def __del__(self): self.assertIn("del is broken", report) self.assertTrue(report.endswith("\n")) + def test_original_unraisablehook_exception_qualname(self): + class A: + class B: + class X(Exception): + pass + + with test.support.captured_stderr() as stderr, \ + test.support.swap_attr(sys, 'unraisablehook', + sys.__unraisablehook__): + expected = self.write_unraisable_exc( + A.B.X(), "msg", "obj"); + report = stderr.getvalue() + testName = 'test_original_unraisablehook_exception_qualname' + self.assertIn(f"{testName}..A.B.X", report) def test_original_unraisablehook_wrong_type(self): exc = ValueError(42) diff --git a/Lib/test/test_traceback.py b/Lib/test/test_traceback.py index 987fcb955087e48..8d9d514d8deaaaf 100644 --- a/Lib/test/test_traceback.py +++ b/Lib/test/test_traceback.py @@ -759,6 +759,19 @@ def test_syntax_error_various_offsets(self): exp = "\n".join(expected) self.assertEqual(exp, err) + def test_format_exception_only_qualname(self): + class A: + class B: + class X(Exception): + def __str__(self): + return "I am X" + pass + err = self.get_report(A.B.X()) + str_value = 'I am X' + str_name = '.'.join([A.B.X.__module__, A.B.X.__qualname__]) + exp = "%s: %s\n" % (str_name, str_value) + self.assertEqual(exp, err) + class PyExcReportingTests(BaseExceptionReportingTests, unittest.TestCase): # diff --git a/Misc/NEWS.d/next/Core and Builtins/2021-09-01-23-55-49.bpo-45083.cLi9G3.rst b/Misc/NEWS.d/next/Core and Builtins/2021-09-01-23-55-49.bpo-45083.cLi9G3.rst new file mode 100644 index 000000000000000..7bfd87b94205930 --- /dev/null +++ b/Misc/NEWS.d/next/Core and Builtins/2021-09-01-23-55-49.bpo-45083.cLi9G3.rst @@ -0,0 +1,3 @@ +When the interpreter renders an exception, its name now has a complete qualname. Previously only the class name was concatenated to the module name, which sometimes resulted in an incorrect full name being displayed. + +(This issue impacted only the C code exception rendering, the :mod:`traceback` module was using qualname already). \ No newline at end of file diff --git a/Python/errors.c b/Python/errors.c index 8a2ba8f9d716319..d588a310639bc8a 100644 --- a/Python/errors.c +++ b/Python/errors.c @@ -1321,46 +1321,45 @@ write_unraisable_exc_file(PyThreadState *tstate, PyObject *exc_type, } assert(PyExceptionClass_Check(exc_type)); - const char *className = PyExceptionClass_Name(exc_type); - if (className != NULL) { - const char *dot = strrchr(className, '.'); - if (dot != NULL) { - className = dot+1; - } - } - PyObject *moduleName = _PyObject_GetAttrId(exc_type, &PyId___module__); - if (moduleName == NULL || !PyUnicode_Check(moduleName)) { - Py_XDECREF(moduleName); + PyObject *modulename = _PyObject_GetAttrId(exc_type, &PyId___module__); + if (modulename == NULL || !PyUnicode_Check(modulename)) { + Py_XDECREF(modulename); _PyErr_Clear(tstate); if (PyFile_WriteString("", file) < 0) { return -1; } } else { - if (!_PyUnicode_EqualToASCIIId(moduleName, &PyId_builtins)) { - if (PyFile_WriteObject(moduleName, file, Py_PRINT_RAW) < 0) { - Py_DECREF(moduleName); + if (!_PyUnicode_EqualToASCIIId(modulename, &PyId_builtins)) { + if (PyFile_WriteObject(modulename, file, Py_PRINT_RAW) < 0) { + Py_DECREF(modulename); return -1; } - Py_DECREF(moduleName); + Py_DECREF(modulename); if (PyFile_WriteString(".", file) < 0) { return -1; } } else { - Py_DECREF(moduleName); + Py_DECREF(modulename); } } - if (className == NULL) { + + PyObject *qualname = PyType_GetQualName((PyTypeObject *)exc_type); + if (qualname == NULL || !PyUnicode_Check(qualname)) { + Py_XDECREF(qualname); + _PyErr_Clear(tstate); if (PyFile_WriteString("", file) < 0) { return -1; } } else { - if (PyFile_WriteString(className, file) < 0) { + if (PyFile_WriteObject(qualname, file, Py_PRINT_RAW) < 0) { + Py_DECREF(qualname); return -1; } + Py_DECREF(qualname); } if (exc_value && exc_value != Py_None) { diff --git a/Python/pythonrun.c b/Python/pythonrun.c index 34cfb7f4b4fd783..fc2ea4d1afacdd2 100644 --- a/Python/pythonrun.c +++ b/Python/pythonrun.c @@ -892,36 +892,37 @@ print_exception(PyObject *f, PyObject *value) /* Don't do anything else */ } else { - PyObject* moduleName; - const char *className; + PyObject* modulename; + _Py_IDENTIFIER(__module__); assert(PyExceptionClass_Check(type)); - className = PyExceptionClass_Name(type); - if (className != NULL) { - const char *dot = strrchr(className, '.'); - if (dot != NULL) - className = dot+1; - } - moduleName = _PyObject_GetAttrId(type, &PyId___module__); - if (moduleName == NULL || !PyUnicode_Check(moduleName)) + modulename = _PyObject_GetAttrId(type, &PyId___module__); + if (modulename == NULL || !PyUnicode_Check(modulename)) { - Py_XDECREF(moduleName); + Py_XDECREF(modulename); + PyErr_Clear(); err = PyFile_WriteString("", f); } else { - if (!_PyUnicode_EqualToASCIIId(moduleName, &PyId_builtins)) + if (!_PyUnicode_EqualToASCIIId(modulename, &PyId_builtins)) { - err = PyFile_WriteObject(moduleName, f, Py_PRINT_RAW); + err = PyFile_WriteObject(modulename, f, Py_PRINT_RAW); err += PyFile_WriteString(".", f); } - Py_DECREF(moduleName); + Py_DECREF(modulename); } if (err == 0) { - if (className == NULL) - err = PyFile_WriteString("", f); - else - err = PyFile_WriteString(className, f); + PyObject* qualname = PyType_GetQualName((PyTypeObject *)type); + if (qualname == NULL || !PyUnicode_Check(qualname)) { + Py_XDECREF(qualname); + PyErr_Clear(); + err = PyFile_WriteString("", f); + } + else { + err = PyFile_WriteObject(qualname, f, Py_PRINT_RAW); + Py_DECREF(qualname); + } } } if (err == 0 && (value != Py_None)) { From 477ffceba5f9ae820da161abdb7b0a4ef3f89fe1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Langa?= Date: Fri, 3 Sep 2021 18:22:01 +0200 Subject: [PATCH 2/2] Use a private version of _PyType_GetQualName --- Include/internal/pycore_object.h | 3 +++ Objects/typeobject.c | 8 ++++++++ Python/errors.c | 3 ++- Python/pythonrun.c | 5 +++-- 4 files changed, 16 insertions(+), 3 deletions(-) diff --git a/Include/internal/pycore_object.h b/Include/internal/pycore_object.h index 32e86d06db5b432..c66ff12d7ec5a79 100644 --- a/Include/internal/pycore_object.h +++ b/Include/internal/pycore_object.h @@ -15,6 +15,9 @@ extern "C" { PyAPI_FUNC(int) _PyType_CheckConsistency(PyTypeObject *type); PyAPI_FUNC(int) _PyDict_CheckConsistency(PyObject *mp, int check_content); +/* Only private in Python 3.10 and 3.9.8+; public in 3.11 */ +extern PyObject *_PyType_GetQualName(PyTypeObject *type); + /* Tell the GC to track this object. * * NB: While the object is tracked by the collector, it must be safe to call the diff --git a/Objects/typeobject.c b/Objects/typeobject.c index f201515d7db3301..feae25f2eb367a8 100644 --- a/Objects/typeobject.c +++ b/Objects/typeobject.c @@ -3119,6 +3119,14 @@ PyType_FromSpec(PyType_Spec *spec) return PyType_FromSpecWithBases(spec, NULL); } +/* private in 3.10 and 3.9.8+; public in 3.11 */ +PyObject * +_PyType_GetQualName(PyTypeObject *type) +{ + return type_qualname(type, NULL); +} + + void * PyType_GetSlot(PyTypeObject *type, int slot) { diff --git a/Python/errors.c b/Python/errors.c index d588a310639bc8a..40d8e68bde7839c 100644 --- a/Python/errors.c +++ b/Python/errors.c @@ -3,6 +3,7 @@ #include "Python.h" #include "pycore_initconfig.h" +#include "pycore_object.h" // _PyType_GetQualName #include "pycore_pyerrors.h" #include "pycore_pystate.h" // _PyThreadState_GET() #include "pycore_sysmodule.h" @@ -1346,7 +1347,7 @@ write_unraisable_exc_file(PyThreadState *tstate, PyObject *exc_type, } } - PyObject *qualname = PyType_GetQualName((PyTypeObject *)exc_type); + PyObject *qualname = _PyType_GetQualName((PyTypeObject *)exc_type); if (qualname == NULL || !PyUnicode_Check(qualname)) { Py_XDECREF(qualname); _PyErr_Clear(tstate); diff --git a/Python/pythonrun.c b/Python/pythonrun.c index fc2ea4d1afacdd2..8cdbb3790fbcfa4 100644 --- a/Python/pythonrun.c +++ b/Python/pythonrun.c @@ -14,7 +14,8 @@ #undef Yield /* undefine macro conflicting with */ #include "pycore_interp.h" // PyInterpreterState.importlib -#include "pycore_object.h" // _PyDebug_PrintTotalRefs() +#include "pycore_object.h" // _PyDebug_PrintTotalRefs(), + // _PyType_GetQualName() #include "pycore_pyerrors.h" // _PyErr_Fetch #include "pycore_pylifecycle.h" // _Py_UnhandledKeyboardInterrupt #include "pycore_pystate.h" // _PyInterpreterState_GET() @@ -913,7 +914,7 @@ print_exception(PyObject *f, PyObject *value) Py_DECREF(modulename); } if (err == 0) { - PyObject* qualname = PyType_GetQualName((PyTypeObject *)type); + PyObject* qualname = _PyType_GetQualName((PyTypeObject *)type); if (qualname == NULL || !PyUnicode_Check(qualname)) { Py_XDECREF(qualname); PyErr_Clear();