Repository: python/cpython
Pull request: gh-102755: Add PyErr_DisplayException(exc)
Fixes #102755.

<!-- gh-issue-number: gh-102755 -->
* Issue: gh-102755
<!-- /gh-issue-number -->


Problem statement:
Add PyErr_DisplayException(exc) as replacement for PyErr_Display(typ, exc, tb)
Create a single-arg version of PyErr_Display. 

PyErr_Display is in the stable ABI but it is not documented. PyErr_DisplayException will therefore need to be in the stable ABI as well (and documented).



<!-- gh-linked-prs -->
### Linked PRs
* gh-102756
* gh-102826
* gh-102849
<!-- /gh-linked-prs -->

Hints from issue discussion:

Commit message: add to stable-abi

Patch to review:
diff --git a/Doc/c-api/exceptions.rst b/Doc/c-api/exceptions.rst
index 8836c973f1f579..506333bc912175 100644
--- a/Doc/c-api/exceptions.rst
+++ b/Doc/c-api/exceptions.rst
@@ -86,6 +86,11 @@ Printing and clearing
 
    An exception must be set when calling this function.
 
+.. c:function: void PyErr_DisplayException(PyObject *exc)
+
+   Print the standard traceback of ``exc`` to ``sys.stderr``.
+
+   .. versionadded:: 3.12
 
 Raising exceptions
 ==================
diff --git a/Doc/data/stable_abi.dat b/Doc/data/stable_abi.dat
index 68ab0b5061f434..4cc06d22baaa93 100644
--- a/Doc/data/stable_abi.dat
+++ b/Doc/data/stable_abi.dat
@@ -133,6 +133,7 @@ function,PyErr_BadInternalCall,3.2,,
 function,PyErr_CheckSignals,3.2,,
 function,PyErr_Clear,3.2,,
 function,PyErr_Display,3.2,,
+function,PyErr_DisplayException,3.12,,
 function,PyErr_ExceptionMatches,3.2,,
 function,PyErr_Fetch,3.2,,
 function,PyErr_Format,3.2,,
diff --git a/Doc/whatsnew/3.12.rst b/Doc/whatsnew/3.12.rst
index 217ffec1ee1007..4020c33249d37e 100644
--- a/Doc/whatsnew/3.12.rst
+++ b/Doc/whatsnew/3.12.rst
@@ -924,6 +924,10 @@ New Features
   the :attr:`~BaseException.args` passed to the exception's constructor.
   (Contributed by Mark Shannon in :gh:`101578`.)
 
+* Add :c:func:`PyErr_DisplayException`, which takes an exception instance,
+  to replace the legacy-api :c:func:`PyErr_Display`. (Contributed by
+  Irit Katriel in :gh:`102755`).
+
 Porting to Python 3.12
 ----------------------
 
@@ -1057,6 +1061,9 @@ Deprecated
   :c:func:`PyErr_SetRaisedException` instead.
   (Contributed by Mark Shannon in :gh:`101578`.)
 
+* :c:func:`PyErr_Display` is deprecated. Use :c:func:`PyErr_DisplayException`
+  instead. (Contributed by Irit Katriel in :gh:`102755`).
+
 
 Removed
 -------
diff --git a/Include/internal/pycore_pylifecycle.h b/Include/internal/pycore_pylifecycle.h
index e7a31807205254..a899e848bb8b3c 100644
--- a/Include/internal/pycore_pylifecycle.h
+++ b/Include/internal/pycore_pylifecycle.h
@@ -87,6 +87,7 @@ PyAPI_FUNC(PyObject*) _PyErr_WriteUnraisableDefaultHook(PyObject *unraisable);
 PyAPI_FUNC(void) _PyErr_Print(PyThreadState *tstate);
 PyAPI_FUNC(void) _PyErr_Display(PyObject *file, PyObject *exception,
                                 PyObject *value, PyObject *tb);
+PyAPI_FUNC(void) _PyErr_DisplayException(PyObject *file, PyObject *exc);
 
 PyAPI_FUNC(void) _PyThreadState_DeleteCurrent(PyThreadState *tstate);
 
diff --git a/Include/pythonrun.h b/Include/pythonrun.h
index 1b208b734ab1bf..41d82e89f84876 100644
--- a/Include/pythonrun.h
+++ b/Include/pythonrun.h
@@ -12,6 +12,7 @@ PyAPI_FUNC(PyObject *) Py_CompileString(const char *, const char *, int);
 PyAPI_FUNC(void) PyErr_Print(void);
 PyAPI_FUNC(void) PyErr_PrintEx(int);
 PyAPI_FUNC(void) PyErr_Display(PyObject *, PyObject *, PyObject *);
+PyAPI_FUNC(void) PyErr_DisplayException(PyObject *);
 
 
 /* Stuff with no proper home (yet) */
diff --git a/Lib/test/test_stable_abi_ctypes.py b/Lib/test/test_stable_abi_ctypes.py
index e77c1c8409880d..2feaaf8603b831 100644
--- a/Lib/test/test_stable_abi_ctypes.py
+++ b/Lib/test/test_stable_abi_ctypes.py
@@ -166,6 +166,7 @@ def test_windows_feature_macros(self):
     "PyErr_CheckSignals",
     "PyErr_Clear",
     "PyErr_Display",
+    "PyErr_DisplayException",
     "PyErr_ExceptionMatches",
     "PyErr_Fetch",
     "PyErr_Format",
diff --git a/Misc/NEWS.d/next/Core and Builtins/2023-03-16-14-44-29.gh-issue-102755.j1GxlV.rst b/Misc/NEWS.d/next/Core and Builtins/2023-03-16-14-44-29.gh-issue-102755.j1GxlV.rst
new file mode 100644
index 00000000000000..d09af8d060d405
--- /dev/null
+++ b/Misc/NEWS.d/next/Core and Builtins/2023-03-16-14-44-29.gh-issue-102755.j1GxlV.rst	
@@ -0,0 +1,3 @@
+Add :c:func:`PyErr_DisplayException` which takes just an exception instance,
+to replace the legacy :c:func:`PyErr_Display` which takes the ``(typ, exc,
+tb)`` triplet.
diff --git a/Misc/stable_abi.toml b/Misc/stable_abi.toml
index 21ff9616133445..23baeeeae79193 100644
--- a/Misc/stable_abi.toml
+++ b/Misc/stable_abi.toml
@@ -609,6 +609,8 @@
     added = '3.2'
 [function.PyErr_Display]
     added = '3.2'
+[function.PyErr_DisplayException]
+    added = '3.12'
 [function.PyErr_ExceptionMatches]
     added = '3.2'
 [function.PyErr_Fetch]
diff --git a/Modules/_testcapi/exceptions.c b/Modules/_testcapi/exceptions.c
index c64b823663c32f..1922ca3beb7916 100644
--- a/Modules/_testcapi/exceptions.c
+++ b/Modules/_testcapi/exceptions.c
@@ -39,20 +39,13 @@ err_restore(PyObject *self, PyObject *args) {
 static PyObject *
 exception_print(PyObject *self, PyObject *args)
 {
-    PyObject *value;
-    PyObject *tb = NULL;
+    PyObject *exc;
 
-    if (!PyArg_ParseTuple(args, "O:exception_print", &value)) {
+    if (!PyArg_ParseTuple(args, "O:exception_print", &exc)) {
         return NULL;
     }
 
-    if (PyExceptionInstance_Check(value)) {
-        tb = PyException_GetTraceback(value);
-    }
-
-    PyErr_Display((PyObject *) Py_TYPE(value), value, tb);
-    Py_XDECREF(tb);
-
+    PyErr_DisplayException(exc);
     Py_RETURN_NONE;
 }
 
diff --git a/PC/python3dll.c b/PC/python3dll.c
index e300819365756e..706affa18351b3 100755
--- a/PC/python3dll.c
+++ b/PC/python3dll.c
@@ -192,6 +192,7 @@ EXPORT_FUNC(PyErr_BadInternalCall)
 EXPORT_FUNC(PyErr_CheckSignals)
 EXPORT_FUNC(PyErr_Clear)
 EXPORT_FUNC(PyErr_Display)
+EXPORT_FUNC(PyErr_DisplayException)
 EXPORT_FUNC(PyErr_ExceptionMatches)
 EXPORT_FUNC(PyErr_Fetch)
 EXPORT_FUNC(PyErr_Format)
diff --git a/Python/pylifecycle.c b/Python/pylifecycle.c
index 82e94090a6027a..d0c65cc1f7fd44 100644
--- a/Python/pylifecycle.c
+++ b/Python/pylifecycle.c
@@ -2537,41 +2537,28 @@ _Py_FatalError_DumpTracebacks(int fd, PyInterpreterState *interp,
 static int
 _Py_FatalError_PrintExc(PyThreadState *tstate)
 {
-    PyObject *ferr, *res;
-    PyObject *exception, *v, *tb;
-    int has_tb;
-
-    _PyErr_Fetch(tstate, &exception, &v, &tb);
-    if (exception == NULL) {
+    PyObject *exc = _PyErr_GetRaisedException(tstate);
+    if (exc == NULL) {
         /* No current exception */
         return 0;
     }
 
-    ferr = _PySys_GetAttr(tstate, &_Py_ID(stderr));
+    PyObject *ferr = _PySys_GetAttr(tstate, &_Py_ID(stderr));
     if (ferr == NULL || ferr == Py_None) {
         /* sys.stderr is not set yet or set to None,
            no need to try to display the exception */
         return 0;
     }
 
-    _PyErr_NormalizeException(tstate, &exception, &v, &tb);
-    if (tb == NULL) {
-        tb = Py_NewRef(Py_None);
-    }
-    PyException_SetTraceback(v, tb);
-    if (exception == NULL) {
-        /* PyErr_NormalizeException() failed */
-        return 0;
-    }
+    PyErr_DisplayException(exc);
 
-    has_tb = (tb != Py_None);
-    PyErr_Display(exception, v, tb);
-    Py_XDECREF(exception);
-    Py_XDECREF(v);
+    PyObject *tb = PyException_GetTraceback(exc);
+    int has_tb = (tb != NULL) && (tb != Py_None);
     Py_XDECREF(tb);
+    Py_XDECREF(exc);
 
     /* sys.stderr may be buffered: call sys.stderr.flush() */
-    res = PyObject_CallMethodNoArgs(ferr, &_Py_ID(flush));
+    PyObject *res = PyObject_CallMethodNoArgs(ferr, &_Py_ID(flush));
     if (res == NULL) {
         _PyErr_Clear(tstate);
     }
diff --git a/Python/pythonrun.c b/Python/pythonrun.c
index 34a44dd92847ba..bb17cfd779a857 100644
--- a/Python/pythonrun.c
+++ b/Python/pythonrun.c
@@ -764,39 +764,34 @@ handle_system_exit(void)
 static void
 _PyErr_PrintEx(PyThreadState *tstate, int set_sys_last_vars)
 {
-    PyObject *exception, *v, *tb, *hook;
-
+    PyObject *typ = NULL, *tb = NULL;
     handle_system_exit();
 
-    _PyErr_Fetch(tstate, &exception, &v, &tb);
-    if (exception == NULL) {
+    PyObject *exc = _PyErr_GetRaisedException(tstate);
+    if (exc == NULL) {
         goto done;
     }
-
-    _PyErr_NormalizeException(tstate, &exception, &v, &tb);
+    assert(PyExceptionInstance_Check(exc));
+    typ = Py_NewRef(Py_TYPE(exc));
+    tb = PyException_GetTraceback(exc);
     if (tb == NULL) {
         tb = Py_NewRef(Py_None);
     }
-    PyException_SetTraceback(v, tb);
-    if (exception == NULL) {
-        goto done;
-    }
 
-    /* Now we know v != NULL too */
     if (set_sys_last_vars) {
-        if (_PySys_SetAttr(&_Py_ID(last_type), exception) < 0) {
+        if (_PySys_SetAttr(&_Py_ID(last_type), typ) < 0) {
             _PyErr_Clear(tstate);
         }
-        if (_PySys_SetAttr(&_Py_ID(last_value), v) < 0) {
+        if (_PySys_SetAttr(&_Py_ID(last_value), exc) < 0) {
             _PyErr_Clear(tstate);
         }
         if (_PySys_SetAttr(&_Py_ID(last_traceback), tb) < 0) {
             _PyErr_Clear(tstate);
         }
     }
-    hook = _PySys_GetAttr(tstate, &_Py_ID(excepthook));
+    PyObject *hook = _PySys_GetAttr(tstate, &_Py_ID(excepthook));
     if (_PySys_Audit(tstate, "sys.excepthook", "OOOO", hook ? hook : Py_None,
-                     exception, v, tb) < 0) {
+                     typ, exc, tb) < 0) {
         if (PyErr_ExceptionMatches(PyExc_RuntimeError)) {
             PyErr_Clear();
             goto done;
@@ -805,46 +800,34 @@ _PyErr_PrintEx(PyThreadState *tstate, int set_sys_last_vars)
     }
     if (hook) {
         PyObject* stack[3];
-        PyObject *result;
-
-        stack[0] = exception;
-        stack[1] = v;
+        stack[0] = typ;
+        stack[1] = exc;
         stack[2] = tb;
-        result = _PyObject_FastCall(hook, stack, 3);
+        PyObject *result = _PyObject_FastCall(hook, stack, 3);
         if (result == NULL) {
             handle_system_exit();
 
-            PyObject *exception2, *v2, *tb2;
-            _PyErr_Fetch(tstate, &exception2, &v2, &tb2);
-            _PyErr_NormalizeException(tstate, &exception2, &v2, &tb2);
-            /* It should not be possible for exception2 or v2
-               to be NULL. However PyErr_Display() can't
-               tolerate NULLs, so just be safe. */
-            if (exception2 == NULL) {
-                exception2 = Py_NewRef(Py_None);
-            }
-            if (v2 == NULL) {
-                v2 = Py_NewRef(Py_None);
-            }
+            PyObject *exc2 = _PyErr_GetRaisedException(tstate);
+            assert(exc2 && PyExceptionInstance_Check(exc2));
             fflush(stdout);
             PySys_WriteStderr("Error in sys.excepthook:\n");
-            PyErr_Display(exception2, v2, tb2);
+            PyErr_DisplayException(exc2);
             PySys_WriteStderr("\nOriginal exception was:\n");
-            PyErr_Display(exception, v, tb);
-            Py_DECREF(exception2);
-            Py_DECREF(v2);
-            Py_XDECREF(tb2);
+            PyErr_DisplayException(exc);
+            Py_DECREF(exc2);
+        }
+        else {
+            Py_DECREF(result);
         }
-        Py_XDECREF(result);
     }
     else {
         PySys_WriteStderr("sys.excepthook is missing\n");
-        PyErr_Display(exception, v, tb);
+        PyErr_DisplayException(exc);
     }
 
 done:
-    Py_XDECREF(exception);
-    Py_XDECREF(v);
+    Py_XDECREF(typ);
+    Py_XDECREF(exc);
     Py_XDECREF(tb);
 }
 
@@ -1508,7 +1491,7 @@ print_exception_recursive(struct exception_print_context *ctx, PyObject *value)
 #define PyErr_MAX_GROUP_DEPTH 10
 
 void
-_PyErr_Display(PyObject *file, PyObject *exception, PyObject *value, PyObject *tb)
+_PyErr_Display(PyObject *file, PyObject *unused, PyObject *value, PyObject *tb)
 {
     assert(file != NULL && file != Py_None);
     if (PyExceptionInstance_Check(value)
@@ -1516,10 +1499,12 @@ _PyErr_Display(PyObject *file, PyObject *exception, PyObject *value, PyObject *t
         /* Put the traceback on the exception, otherwise it won't get
            displayed.  See issue #18776. */
         PyObject *cur_tb = PyException_GetTraceback(value);
-        if (cur_tb == NULL)
+        if (cur_tb == NULL) {
             PyException_SetTraceback(value, tb);
-        else
+        }
+        else {
             Py_DECREF(cur_tb);
+        }
     }
 
     struct exception_print_context ctx;
@@ -1555,7 +1540,7 @@ _PyErr_Display(PyObject *file, PyObject *exception, PyObject *value, PyObject *t
 }
 
 void
-PyErr_Display(PyObject *exception, PyObject *value, PyObject *tb)
+PyErr_Display(PyObject *unused, PyObject *value, PyObject *tb)
 {
     PyThreadState *tstate = _PyThreadState_GET();
     PyObject *file = _PySys_GetAttr(tstate, &_Py_ID(stderr));
@@ -1568,10 +1553,20 @@ PyErr_Display(PyObject *exception, PyObject *value, PyObject *tb)
         return;
     }
     Py_INCREF(file);
-    _PyErr_Display(file, exception, value, tb);
+    _PyErr_Display(file, NULL, value, tb);
     Py_DECREF(file);
 }
 
+void _PyErr_DisplayException(PyObject *file, PyObject *exc)
+{
+    _PyErr_Display(file, NULL, exc, NULL);
+}
+
+void PyErr_DisplayException(PyObject *exc)
+{
+    PyErr_Display(NULL, exc, NULL);
+}
+
 PyObject *
 PyRun_StringFlags(const char *str, int start, PyObject *globals,
                   PyObject *locals, PyCompilerFlags *flags)
diff --git a/Python/sysmodule.c b/Python/sysmodule.c
index d282104dcad414..cc5b9a6d418bfa 100644
--- a/Python/sysmodule.c
+++ b/Python/sysmodule.c
@@ -742,7 +742,7 @@ sys_excepthook_impl(PyObject *module, PyObject *exctype, PyObject *value,
                     PyObject *traceback)
 /*[clinic end generated code: output=18d99fdda21b6b5e input=ecf606fa826f19d9]*/
 {
-    PyErr_Display(exctype, value, traceback);
+    PyErr_Display(NULL, value, traceback);
     Py_RETURN_NONE;
 }
 


Code review comments:
diff_hunk: @@ -86,6 +86,11 @@ Printing and clearing
 
    An exception must be set when calling this function.
 
+.. c:function: void PyErr_DisplayException(PyObject *exc)
+
+   Print the standard traceback of ``exc`` to ``sys.stderr``.
path: Doc/c-api/exceptions.rst
text: @user1:
This does include the context/cause chain and the final `ExceptionClass: message` part, right? The brief description makes me think that it might only display the traceback, without those other things.