]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-76785: Return an "excinfo" Object From Interpreter.run() (gh-111573)
authorEric Snow <ericsnowcurrently@gmail.com>
Thu, 23 Nov 2023 00:55:00 +0000 (17:55 -0700)
committerGitHub <noreply@github.com>
Thu, 23 Nov 2023 00:55:00 +0000 (00:55 +0000)
Include/internal/pycore_crossinterp.h
Lib/test/support/interpreters.py
Lib/test/test__xxinterpchannels.py
Lib/test/test__xxsubinterpreters.py
Lib/test/test_import/__init__.py
Lib/test/test_importlib/test_util.py
Lib/test/test_interpreters.py
Modules/_xxsubinterpretersmodule.c
Python/crossinterp.c

index ee9ff0090c2484e32ff588047ca4baca09dd07cd..ec9dac96292f35388f7fed1060365b82c40145ce 100644 (file)
@@ -170,9 +170,14 @@ extern void _PyXI_Fini(PyInterpreterState *interp);
 // of the exception in the calling interpreter.
 
 typedef struct _excinfo {
-    const char *type;
+    struct _excinfo_type {
+        PyTypeObject *builtin;
+        const char *name;
+        const char *qualname;
+        const char *module;
+    } type;
     const char *msg;
-} _Py_excinfo;
+} _PyXI_excinfo;
 
 
 typedef enum error_code {
@@ -193,13 +198,13 @@ typedef struct _sharedexception {
     // The kind of error to propagate.
     _PyXI_errcode code;
     // The exception information to propagate, if applicable.
-    // This is populated only for _PyXI_ERR_UNCAUGHT_EXCEPTION.
-    _Py_excinfo uncaught;
-} _PyXI_exception_info;
+    // This is populated only for some error codes,
+    // but always for _PyXI_ERR_UNCAUGHT_EXCEPTION.
+    _PyXI_excinfo uncaught;
+} _PyXI_error;
+
+PyAPI_FUNC(PyObject *) _PyXI_ApplyError(_PyXI_error *err);
 
-PyAPI_FUNC(void) _PyXI_ApplyExceptionInfo(
-    _PyXI_exception_info *info,
-    PyObject *exctype);
 
 typedef struct xi_session _PyXI_session;
 typedef struct _sharedns _PyXI_namespace;
@@ -251,13 +256,13 @@ struct xi_session {
 
     // This is set if the interpreter is entered and raised an exception
     // that needs to be handled in some special way during exit.
-    _PyXI_errcode *exc_override;
+    _PyXI_errcode *error_override;
     // This is set if exit captured an exception to propagate.
-    _PyXI_exception_info *exc;
+    _PyXI_error *error;
 
     // -- pre-allocated memory --
-    _PyXI_exception_info _exc;
-    _PyXI_errcode _exc_override;
+    _PyXI_error _error;
+    _PyXI_errcode _error_override;
 };
 
 PyAPI_FUNC(int) _PyXI_Enter(
@@ -266,9 +271,7 @@ PyAPI_FUNC(int) _PyXI_Enter(
     PyObject *nsupdates);
 PyAPI_FUNC(void) _PyXI_Exit(_PyXI_session *session);
 
-PyAPI_FUNC(void) _PyXI_ApplyCapturedException(
-    _PyXI_session *session,
-    PyObject *excwrapper);
+PyAPI_FUNC(PyObject *) _PyXI_ApplyCapturedException(_PyXI_session *session);
 PyAPI_FUNC(int) _PyXI_HasCapturedException(_PyXI_session *session);
 
 
index ab9342b767dfaef4ab16eb2dd5b4bbe0fef9e326..089fe7ef56df78cd57810cb7472d767ffdda0d2f 100644 (file)
@@ -14,6 +14,7 @@ from _xxinterpchannels import (
 
 __all__ = [
     'Interpreter', 'get_current', 'get_main', 'create', 'list_all',
+    'RunFailedError',
     'SendChannel', 'RecvChannel',
     'create_channel', 'list_all_channels', 'is_shareable',
     'ChannelError', 'ChannelNotFoundError',
@@ -21,6 +22,19 @@ __all__ = [
     ]
 
 
+class RunFailedError(RuntimeError):
+
+    def __init__(self, excinfo):
+        msg = excinfo.formatted
+        if not msg:
+            if excinfo.type and snapshot.msg:
+                msg = f'{snapshot.type.__name__}: {snapshot.msg}'
+            else:
+                msg = snapshot.type.__name__ or snapshot.msg
+        super().__init__(msg)
+        self.snapshot = excinfo
+
+
 def create(*, isolated=True):
     """Return a new (idle) Python interpreter."""
     id = _interpreters.create(isolated=isolated)
@@ -110,7 +124,9 @@ class Interpreter:
         that time, the previous interpreter is allowed to run
         in other threads.
         """
-        _interpreters.exec(self._id, src_str, channels)
+        excinfo = _interpreters.exec(self._id, src_str, channels)
+        if excinfo is not None:
+            raise RunFailedError(excinfo)
 
 
 def create_channel():
index 1c1ef3fac9d65f5cad7c60d7327efe658406a55d..2b75e2f1916c820cca34219f497fd3a53a2f4c89 100644 (file)
@@ -1017,16 +1017,16 @@ class ChannelTests(TestBase):
             _channels.recv({cid})
             """))
         channels.close(cid)
-        with self.assertRaises(interpreters.RunFailedError) as cm:
-            interpreters.run_string(id1, dedent(f"""
+
+        excsnap = interpreters.run_string(id1, dedent(f"""
                 _channels.send({cid}, b'spam')
                 """))
-        self.assertIn('ChannelClosedError', str(cm.exception))
-        with self.assertRaises(interpreters.RunFailedError) as cm:
-            interpreters.run_string(id2, dedent(f"""
+        self.assertEqual(excsnap.type.__name__, 'ChannelClosedError')
+
+        excsnap = interpreters.run_string(id2, dedent(f"""
                 _channels.send({cid}, b'spam')
                 """))
-        self.assertIn('ChannelClosedError', str(cm.exception))
+        self.assertEqual(excsnap.type.__name__, 'ChannelClosedError')
 
     def test_close_multiple_times(self):
         cid = channels.create()
index d2056c9f52287b43715339e849655035262fce45..64a9db95e5eaf51ab4738c9accde33637c895d30 100644 (file)
@@ -940,7 +940,6 @@ class RunFailedTests(TestBase):
         return script_helper.make_script(tempdir, modname, text)
 
     def run_script(self, text, *, fails=False):
-        excwrapper = interpreters.RunFailedError
         r, w = os.pipe()
         try:
             script = dedent(f"""
@@ -956,11 +955,12 @@ class RunFailedTests(TestBase):
                 raise NeverError  # never raised
                 """).format(dedent(text))
             if fails:
-                with self.assertRaises(excwrapper) as caught:
-                    interpreters.run_string(self.id, script)
-                return caught.exception
+                err = interpreters.run_string(self.id, script)
+                self.assertIsNot(err, None)
+                return err
             else:
-                interpreters.run_string(self.id, script)
+                err = interpreters.run_string(self.id, script)
+                self.assertIs(err, None)
                 return None
         except:
             raise  # re-raise
@@ -979,17 +979,18 @@ class RunFailedTests(TestBase):
             exctype_name = exctype.__name__
 
         # Run the script.
-        exc = self.run_script(script, fails=True)
+        excinfo = self.run_script(script, fails=True)
 
         # Check the wrapper exception.
+        self.assertEqual(excinfo.type.__name__, exctype_name)
         if msg is None:
-            self.assertEqual(str(exc).split(':')[0],
+            self.assertEqual(excinfo.formatted.split(':')[0],
                              exctype_name)
         else:
-            self.assertEqual(str(exc),
+            self.assertEqual(excinfo.formatted,
                              '{}: {}'.format(exctype_name, msg))
 
-        return exc
+        return excinfo
 
     def assert_run_failed(self, exctype, script):
         self._assert_run_failed(exctype, None, script)
index aa465c70dfbcd0a50e780f5bc9add823c30a9072..1ecac4f37fe1c1cdc5e62b67af5d28c73364b8d5 100644 (file)
@@ -1968,10 +1968,12 @@ class SubinterpImportTests(unittest.TestCase):
             print(_testsinglephase)
             ''')
         interpid = _interpreters.create()
-        with self.assertRaises(_interpreters.RunFailedError):
-            _interpreters.run_string(interpid, script)
-        with self.assertRaises(_interpreters.RunFailedError):
-            _interpreters.run_string(interpid, script)
+
+        excsnap = _interpreters.run_string(interpid, script)
+        self.assertIsNot(excsnap, None)
+
+        excsnap = _interpreters.run_string(interpid, script)
+        self.assertIsNot(excsnap, None)
 
 
 class TestSinglePhaseSnapshot(ModuleSnapshot):
index 5da72a21f586ee7ac4b531ecadd0e2043dde74f3..914176559806f444765f881ce7eccb5029e71e06 100644 (file)
@@ -655,25 +655,19 @@ class MagicNumberTests(unittest.TestCase):
 @unittest.skipIf(_interpreters is None, 'subinterpreters required')
 class IncompatibleExtensionModuleRestrictionsTests(unittest.TestCase):
 
-    ERROR = re.compile("^ImportError: module (.*) does not support loading in subinterpreters")
-
     def run_with_own_gil(self, script):
         interpid = _interpreters.create(isolated=True)
-        try:
-            _interpreters.run_string(interpid, script)
-        except _interpreters.RunFailedError as exc:
-            if m := self.ERROR.match(str(exc)):
-                modname, = m.groups()
-                raise ImportError(modname)
+        excsnap = _interpreters.run_string(interpid, script)
+        if excsnap is not None:
+            if excsnap.type.__name__ == 'ImportError':
+                raise ImportError(excsnap.msg)
 
     def run_with_shared_gil(self, script):
         interpid = _interpreters.create(isolated=False)
-        try:
-            _interpreters.run_string(interpid, script)
-        except _interpreters.RunFailedError as exc:
-            if m := self.ERROR.match(str(exc)):
-                modname, = m.groups()
-                raise ImportError(modname)
+        excsnap = _interpreters.run_string(interpid, script)
+        if excsnap is not None:
+            if excsnap.type.__name__ == 'ImportError':
+                raise ImportError(excsnap.msg)
 
     @unittest.skipIf(_testsinglephase is None, "test requires _testsinglephase module")
     def test_single_phase_init_module(self):
index 7c030bcf0321cde0bc124cd13615cce2f5f60a65..5663706c0ccfb7ebb49748e75416cc8ad239afda 100644 (file)
@@ -478,6 +478,11 @@ class TestInterpreterRun(TestBase):
 
         self.assertEqual(out, 'it worked!')
 
+    def test_failure(self):
+        interp = interpreters.create()
+        with self.assertRaises(interpreters.RunFailedError):
+            interp.run('raise Exception')
+
     def test_in_thread(self):
         interp = interpreters.create()
         script, file = _captured_script('print("it worked!", end="")')
index 001fa887847cbd6c83c5ec546b0fc76d74fd0f05..02c2abed27ddfac8b4e22f2de5a6dad54b676c7b 100644 (file)
@@ -28,31 +28,11 @@ _get_current_interp(void)
     return PyInterpreterState_Get();
 }
 
-static PyObject *
-add_new_exception(PyObject *mod, const char *name, PyObject *base)
-{
-    assert(!PyObject_HasAttrStringWithError(mod, name));
-    PyObject *exctype = PyErr_NewException(name, base, NULL);
-    if (exctype == NULL) {
-        return NULL;
-    }
-    int res = PyModule_AddType(mod, (PyTypeObject *)exctype);
-    if (res < 0) {
-        Py_DECREF(exctype);
-        return NULL;
-    }
-    return exctype;
-}
-
-#define ADD_NEW_EXCEPTION(MOD, NAME, BASE) \
-    add_new_exception(MOD, MODULE_NAME "." Py_STRINGIFY(NAME), BASE)
-
 
 /* module state *************************************************************/
 
 typedef struct {
-    /* exceptions */
-    PyObject *RunFailedError;
+    int _notused;
 } module_state;
 
 static inline module_state *
@@ -67,18 +47,12 @@ get_module_state(PyObject *mod)
 static int
 traverse_module_state(module_state *state, visitproc visit, void *arg)
 {
-    /* exceptions */
-    Py_VISIT(state->RunFailedError);
-
     return 0;
 }
 
 static int
 clear_module_state(module_state *state)
 {
-    /* exceptions */
-    Py_CLEAR(state->RunFailedError);
-
     return 0;
 }
 
@@ -177,30 +151,6 @@ get_code_str(PyObject *arg, Py_ssize_t *len_p, PyObject **bytes_p, int *flags_p)
 
 /* interpreter-specific code ************************************************/
 
-static int
-exceptions_init(PyObject *mod)
-{
-    module_state *state = get_module_state(mod);
-    if (state == NULL) {
-        return -1;
-    }
-
-#define ADD(NAME, BASE) \
-    do { \
-        assert(state->NAME == NULL); \
-        state->NAME = ADD_NEW_EXCEPTION(mod, NAME, BASE); \
-        if (state->NAME == NULL) { \
-            return -1; \
-        } \
-    } while (0)
-
-    // An uncaught exception came out of interp_run_string().
-    ADD(RunFailedError, PyExc_RuntimeError);
-#undef ADD
-
-    return 0;
-}
-
 static int
 _run_script(PyObject *ns, const char *codestr, Py_ssize_t codestrlen, int flags)
 {
@@ -229,7 +179,7 @@ static int
 _run_in_interpreter(PyInterpreterState *interp,
                     const char *codestr, Py_ssize_t codestrlen,
                     PyObject *shareables, int flags,
-                    PyObject *excwrapper)
+                    PyObject **p_excinfo)
 {
     assert(!PyErr_Occurred());
     _PyXI_session session = {0};
@@ -237,7 +187,10 @@ _run_in_interpreter(PyInterpreterState *interp,
     // Prep and switch interpreters.
     if (_PyXI_Enter(&session, interp, shareables) < 0) {
         assert(!PyErr_Occurred());
-        _PyXI_ApplyExceptionInfo(session.exc, excwrapper);
+        PyObject *excinfo = _PyXI_ApplyError(session.error);
+        if (excinfo != NULL) {
+            *p_excinfo = excinfo;
+        }
         assert(PyErr_Occurred());
         return -1;
     }
@@ -251,7 +204,10 @@ _run_in_interpreter(PyInterpreterState *interp,
     // Propagate any exception out to the caller.
     assert(!PyErr_Occurred());
     if (res < 0) {
-        _PyXI_ApplyCapturedException(&session, excwrapper);
+        PyObject *excinfo = _PyXI_ApplyCapturedException(&session);
+        if (excinfo != NULL) {
+            *p_excinfo = excinfo;
+        }
     }
     else {
         assert(!_PyXI_HasCapturedException(&session));
@@ -521,7 +477,8 @@ convert_code_arg(PyObject *arg, const char *fname, const char *displayname,
 
 static int
 _interp_exec(PyObject *self,
-             PyObject *id_arg, PyObject *code_arg, PyObject *shared_arg)
+             PyObject *id_arg, PyObject *code_arg, PyObject *shared_arg,
+             PyObject **p_excinfo)
 {
     // Look up the interpreter.
     PyInterpreterState *interp = PyInterpreterID_LookUp(id_arg);
@@ -540,10 +497,8 @@ _interp_exec(PyObject *self,
     }
 
     // Run the code in the interpreter.
-    module_state *state = get_module_state(self);
-    assert(state != NULL);
     int res = _run_in_interpreter(interp, codestr, codestrlen,
-                                  shared_arg, flags, state->RunFailedError);
+                                  shared_arg, flags, p_excinfo);
     Py_XDECREF(bytes_obj);
     if (res < 0) {
         return -1;
@@ -577,10 +532,12 @@ interp_exec(PyObject *self, PyObject *args, PyObject *kwds)
         return NULL;
     }
 
-    int res = _interp_exec(self, id, code, shared);
+    PyObject *excinfo = NULL;
+    int res = _interp_exec(self, id, code, shared, &excinfo);
     Py_DECREF(code);
     if (res < 0) {
-        return NULL;
+        assert((excinfo == NULL) != (PyErr_Occurred() == NULL));
+        return excinfo;
     }
     Py_RETURN_NONE;
 }
@@ -620,10 +577,12 @@ interp_run_string(PyObject *self, PyObject *args, PyObject *kwds)
         return NULL;
     }
 
-    int res = _interp_exec(self, id, script, shared);
+    PyObject *excinfo = NULL;
+    int res = _interp_exec(self, id, script, shared, &excinfo);
     Py_DECREF(script);
     if (res < 0) {
-        return NULL;
+        assert((excinfo == NULL) != (PyErr_Occurred() == NULL));
+        return excinfo;
     }
     Py_RETURN_NONE;
 }
@@ -654,10 +613,12 @@ interp_run_func(PyObject *self, PyObject *args, PyObject *kwds)
         return NULL;
     }
 
-    int res = _interp_exec(self, id, (PyObject *)code, shared);
+    PyObject *excinfo = NULL;
+    int res = _interp_exec(self, id, (PyObject *)code, shared, &excinfo);
     Py_DECREF(code);
     if (res < 0) {
-        return NULL;
+        assert((excinfo == NULL) != (PyErr_Occurred() == NULL));
+        return excinfo;
     }
     Py_RETURN_NONE;
 }
@@ -759,11 +720,6 @@ The 'interpreters' module provides a more convenient interface.");
 static int
 module_exec(PyObject *mod)
 {
-    /* Add exception types */
-    if (exceptions_init(mod) != 0) {
-        goto error;
-    }
-
     // PyInterpreterID
     if (PyModule_AddType(mod, &PyInterpreterID_Type) < 0) {
         goto error;
index a908f9ae340ee93a187df36299746381df926966..21b96ef05ed79968d1eb9e6c2fd7a7afd6b2a4a3 100644 (file)
@@ -5,8 +5,10 @@
 #include "pycore_ceval.h"         // _Py_simple_func
 #include "pycore_crossinterp.h"   // struct _xid
 #include "pycore_initconfig.h"    // _PyStatus_OK()
+#include "pycore_namespace.h"     //_PyNamespace_New()
 #include "pycore_pyerrors.h"      // _PyErr_Clear()
 #include "pycore_pystate.h"       // _PyInterpreterState_GET()
+#include "pycore_typeobject.h"    // _PyType_GetModuleName()
 #include "pycore_weakref.h"       // _PyWeakref_GET_REF()
 
 
@@ -564,6 +566,8 @@ _lookup_getdata_from_registry(PyInterpreterState *interp, PyObject *obj)
 
 /* cross-interpreter data for builtin types */
 
+// bytes
+
 struct _shared_bytes_data {
     char *bytes;
     Py_ssize_t len;
@@ -595,6 +599,8 @@ _bytes_shared(PyThreadState *tstate, PyObject *obj,
     return 0;
 }
 
+// str
+
 struct _shared_str_data {
     int kind;
     const void *buffer;
@@ -626,6 +632,8 @@ _str_shared(PyThreadState *tstate, PyObject *obj,
     return 0;
 }
 
+// int
+
 static PyObject *
 _new_long_object(_PyCrossInterpreterData *data)
 {
@@ -653,6 +661,8 @@ _long_shared(PyThreadState *tstate, PyObject *obj,
     return 0;
 }
 
+// float
+
 static PyObject *
 _new_float_object(_PyCrossInterpreterData *data)
 {
@@ -676,6 +686,8 @@ _float_shared(PyThreadState *tstate, PyObject *obj,
     return 0;
 }
 
+// None
+
 static PyObject *
 _new_none_object(_PyCrossInterpreterData *data)
 {
@@ -693,6 +705,8 @@ _none_shared(PyThreadState *tstate, PyObject *obj,
     return 0;
 }
 
+// bool
+
 static PyObject *
 _new_bool_object(_PyCrossInterpreterData *data)
 {
@@ -713,6 +727,8 @@ _bool_shared(PyThreadState *tstate, PyObject *obj,
     return 0;
 }
 
+// tuple
+
 struct _shared_tuple_data {
     Py_ssize_t len;
     _PyCrossInterpreterData **data;
@@ -806,6 +822,8 @@ error:
     return -1;
 }
 
+// registration
+
 static void
 _register_builtins_for_crossinterpreter_data(struct _xidregistry *xidregistry)
 {
@@ -898,17 +916,6 @@ _xidregistry_fini(struct _xidregistry *registry)
 /* convenience utilities */
 /*************************/
 
-static const char *
-_copy_raw_string(const char *str)
-{
-    char *copied = PyMem_RawMalloc(strlen(str)+1);
-    if (copied == NULL) {
-        return NULL;
-    }
-    strcpy(copied, str);
-    return copied;
-}
-
 static const char *
 _copy_string_obj_raw(PyObject *strobj)
 {
@@ -944,115 +951,309 @@ _release_xid_data(_PyCrossInterpreterData *data, int rawfree)
 }
 
 
+/***********************/
 /* exception snapshots */
+/***********************/
 
 static int
-_exc_type_name_as_utf8(PyObject *exc, const char **p_typename)
+_excinfo_init_type(struct _excinfo_type *info, PyObject *exc)
 {
-    // XXX Use PyObject_GetAttrString(Py_TYPE(exc), '__name__')?
-    PyObject *nameobj = PyUnicode_FromString(Py_TYPE(exc)->tp_name);
-    if (nameobj == NULL) {
-        assert(PyErr_Occurred());
-        *p_typename = "unable to format exception type name";
-        return -1;
+    /* Note that this copies directly rather than into an intermediate
+       struct and does not clear on error.  If we need that then we
+       should have a separate function to wrap this one
+       and do all that there. */
+    PyObject *strobj = NULL;
+
+    PyTypeObject *type = Py_TYPE(exc);
+    if (type->tp_flags & _Py_TPFLAGS_STATIC_BUILTIN) {
+        assert(_Py_IsImmortal((PyObject *)type));
+        info->builtin = type;
     }
-    const char *name = PyUnicode_AsUTF8(nameobj);
-    if (name == NULL) {
-        assert(PyErr_Occurred());
-        Py_DECREF(nameobj);
-        *p_typename = "unable to encode exception type name";
+    else {
+        // Only builtin types are preserved.
+        info->builtin = NULL;
+    }
+
+    // __name__
+    strobj = PyType_GetName(type);
+    if (strobj == NULL) {
         return -1;
     }
-    name = _copy_raw_string(name);
-    Py_DECREF(nameobj);
-    if (name == NULL) {
-        *p_typename = "out of memory copying exception type name";
+    info->name = _copy_string_obj_raw(strobj);
+    Py_DECREF(strobj);
+    if (info->name == NULL) {
         return -1;
     }
-    *p_typename = name;
-    return 0;
-}
 
-static int
-_exc_msg_as_utf8(PyObject *exc, const char **p_msg)
-{
-    PyObject *msgobj = PyObject_Str(exc);
-    if (msgobj == NULL) {
-        assert(PyErr_Occurred());
-        *p_msg = "unable to format exception message";
+    // __qualname__
+    strobj = PyType_GetQualName(type);
+    if (strobj == NULL) {
         return -1;
     }
-    const char *msg = PyUnicode_AsUTF8(msgobj);
-    if (msg == NULL) {
-        assert(PyErr_Occurred());
-        Py_DECREF(msgobj);
-        *p_msg = "unable to encode exception message";
+    info->qualname = _copy_string_obj_raw(strobj);
+    Py_DECREF(strobj);
+    if (info->name == NULL) {
         return -1;
     }
-    msg = _copy_raw_string(msg);
-    Py_DECREF(msgobj);
-    if (msg == NULL) {
-        assert(PyErr_ExceptionMatches(PyExc_MemoryError));
-        *p_msg = "out of memory copying exception message";
+
+    // __module__
+    strobj = _PyType_GetModuleName(type);
+    if (strobj == NULL) {
+        return -1;
+    }
+    info->module = _copy_string_obj_raw(strobj);
+    Py_DECREF(strobj);
+    if (info->name == NULL) {
         return -1;
     }
-    *p_msg = msg;
+
     return 0;
 }
 
 static void
-_Py_excinfo_Clear(_Py_excinfo *info)
+_excinfo_clear_type(struct _excinfo_type *info)
 {
-    if (info->type != NULL) {
-        PyMem_RawFree((void *)info->type);
+    if (info->builtin != NULL) {
+        assert(info->builtin->tp_flags & _Py_TPFLAGS_STATIC_BUILTIN);
+        assert(_Py_IsImmortal((PyObject *)info->builtin));
+    }
+    if (info->name != NULL) {
+        PyMem_RawFree((void *)info->name);
+    }
+    if (info->qualname != NULL) {
+        PyMem_RawFree((void *)info->qualname);
+    }
+    if (info->module != NULL) {
+        PyMem_RawFree((void *)info->module);
+    }
+    *info = (struct _excinfo_type){NULL};
+}
+
+static void
+_excinfo_normalize_type(struct _excinfo_type *info,
+                        const char **p_module, const char **p_qualname)
+{
+    if (info->name == NULL) {
+        assert(info->builtin == NULL);
+        assert(info->qualname == NULL);
+        assert(info->module == NULL);
+        // This is inspired by TracebackException.format_exception_only().
+        *p_module = NULL;
+        *p_qualname = NULL;
+        return;
+    }
+
+    const char *module = info->module;
+    const char *qualname = info->qualname;
+    if (qualname == NULL) {
+        qualname = info->name;
     }
+    assert(module != NULL);
+    if (strcmp(module, "builtins") == 0) {
+        module = NULL;
+    }
+    else if (strcmp(module, "__main__") == 0) {
+        module = NULL;
+    }
+    *p_qualname = qualname;
+    *p_module = module;
+}
+
+static void
+_PyXI_excinfo_Clear(_PyXI_excinfo *info)
+{
+    _excinfo_clear_type(&info->type);
     if (info->msg != NULL) {
         PyMem_RawFree((void *)info->msg);
     }
-    *info = (_Py_excinfo){ NULL };
+    *info = (_PyXI_excinfo){{NULL}};
+}
+
+static PyObject *
+_PyXI_excinfo_format(_PyXI_excinfo *info)
+{
+    const char *module, *qualname;
+    _excinfo_normalize_type(&info->type, &module, &qualname);
+    if (qualname != NULL) {
+        if (module != NULL) {
+            if (info->msg != NULL) {
+                return PyUnicode_FromFormat("%s.%s: %s",
+                                            module, qualname, info->msg);
+            }
+            else {
+                return PyUnicode_FromFormat("%s.%s", module, qualname);
+            }
+        }
+        else {
+            if (info->msg != NULL) {
+                return PyUnicode_FromFormat("%s: %s", qualname, info->msg);
+            }
+            else {
+                return PyUnicode_FromString(qualname);
+            }
+        }
+    }
+    else if (info->msg != NULL) {
+        return PyUnicode_FromString(info->msg);
+    }
+    else {
+        Py_RETURN_NONE;
+    }
 }
 
 static const char *
-_Py_excinfo_InitFromException(_Py_excinfo *info, PyObject *exc)
+_PyXI_excinfo_InitFromException(_PyXI_excinfo *info, PyObject *exc)
 {
     assert(exc != NULL);
 
-    // Extract the exception type name.
-    const char *typename = NULL;
-    if (_exc_type_name_as_utf8(exc, &typename) < 0) {
-        assert(typename != NULL);
-        return typename;
+    if (PyErr_GivenExceptionMatches(exc, PyExc_MemoryError)) {
+        _PyXI_excinfo_Clear(info);
+        return NULL;
+    }
+    const char *failure = NULL;
+
+    if (_excinfo_init_type(&info->type, exc) < 0) {
+        failure = "error while initializing exception type snapshot";
+        goto error;
     }
 
     // Extract the exception message.
-    const char *msg = NULL;
-    if (_exc_msg_as_utf8(exc, &msg) < 0) {
-        assert(msg != NULL);
-        return msg;
+    PyObject *msgobj = PyObject_Str(exc);
+    if (msgobj == NULL) {
+        failure = "error while formatting exception";
+        goto error;
+    }
+    info->msg = _copy_string_obj_raw(msgobj);
+    Py_DECREF(msgobj);
+    if (info->msg == NULL) {
+        failure = "error while copying exception message";
+        goto error;
     }
 
-    info->type = typename;
-    info->msg = msg;
     return NULL;
+
+error:
+    assert(failure != NULL);
+    _PyXI_excinfo_Clear(info);
+    return failure;
 }
 
 static void
-_Py_excinfo_Apply(_Py_excinfo *info, PyObject *exctype)
+_PyXI_excinfo_Apply(_PyXI_excinfo *info, PyObject *exctype)
+{
+    PyObject *formatted = _PyXI_excinfo_format(info);
+    PyErr_SetObject(exctype, formatted);
+    Py_DECREF(formatted);
+}
+
+static PyObject *
+_PyXI_excinfo_TypeAsObject(_PyXI_excinfo *info)
 {
-    if (info->type != NULL) {
-        if (info->msg != NULL) {
-            PyErr_Format(exctype, "%s: %s",  info->type, info->msg);
+    PyObject *ns = _PyNamespace_New(NULL);
+    if (ns == NULL) {
+        return NULL;
+    }
+    int empty = 1;
+
+    if (info->type.name != NULL) {
+        PyObject *name = PyUnicode_FromString(info->type.name);
+        if (name == NULL) {
+            goto error;
         }
-        else {
-            PyErr_SetString(exctype, info->type);
+        int res = PyObject_SetAttrString(ns, "__name__", name);
+        Py_DECREF(name);
+        if (res < 0) {
+            goto error;
         }
+        empty = 0;
     }
-    else if (info->msg != NULL) {
-        PyErr_SetString(exctype, info->msg);
+
+    if (info->type.qualname != NULL) {
+        PyObject *qualname = PyUnicode_FromString(info->type.qualname);
+        if (qualname == NULL) {
+            goto error;
+        }
+        int res = PyObject_SetAttrString(ns, "__qualname__", qualname);
+        Py_DECREF(qualname);
+        if (res < 0) {
+            goto error;
+        }
+        empty = 0;
     }
-    else {
-        PyErr_SetNone(exctype);
+
+    if (info->type.module != NULL) {
+        PyObject *module = PyUnicode_FromString(info->type.module);
+        if (module == NULL) {
+            goto error;
+        }
+        int res = PyObject_SetAttrString(ns, "__module__", module);
+        Py_DECREF(module);
+        if (res < 0) {
+            goto error;
+        }
+        empty = 0;
+    }
+
+    if (empty) {
+        Py_CLEAR(ns);
+    }
+
+    return ns;
+
+error:
+    Py_DECREF(ns);
+    return NULL;
+}
+
+static PyObject *
+_PyXI_excinfo_AsObject(_PyXI_excinfo *info)
+{
+    PyObject *ns = _PyNamespace_New(NULL);
+    if (ns == NULL) {
+        return NULL;
+    }
+    int res;
+
+    PyObject *type = _PyXI_excinfo_TypeAsObject(info);
+    if (type == NULL) {
+        if (PyErr_Occurred()) {
+            goto error;
+        }
+        type = Py_NewRef(Py_None);
+    }
+    res = PyObject_SetAttrString(ns, "type", type);
+    Py_DECREF(type);
+    if (res < 0) {
+        goto error;
+    }
+
+    PyObject *msg = info->msg != NULL
+        ? PyUnicode_FromString(info->msg)
+        : Py_NewRef(Py_None);
+    if (msg == NULL) {
+        goto error;
+    }
+    res = PyObject_SetAttrString(ns, "msg", msg);
+    Py_DECREF(msg);
+    if (res < 0) {
+        goto error;
     }
+
+    PyObject *formatted = _PyXI_excinfo_format(info);
+    if (formatted == NULL) {
+        goto error;
+    }
+    res = PyObject_SetAttrString(ns, "formatted", formatted);
+    Py_DECREF(formatted);
+    if (res < 0) {
+        goto error;
+    }
+
+    return ns;
+
+error:
+    Py_DECREF(ns);
+    return NULL;
 }
 
 
@@ -1111,72 +1312,69 @@ _PyXI_ApplyErrorCode(_PyXI_errcode code, PyInterpreterState *interp)
 /* shared exceptions */
 
 static const char *
-_PyXI_InitExceptionInfo(_PyXI_exception_info *info,
-                        PyObject *excobj, _PyXI_errcode code)
+_PyXI_InitError(_PyXI_error *error, PyObject *excobj, _PyXI_errcode code)
 {
-    if (info->interp == NULL) {
-        info->interp = PyInterpreterState_Get();
+    if (error->interp == NULL) {
+        error->interp = PyInterpreterState_Get();
     }
 
     const char *failure = NULL;
     if (code == _PyXI_ERR_UNCAUGHT_EXCEPTION) {
         // There is an unhandled exception we need to propagate.
-        failure = _Py_excinfo_InitFromException(&info->uncaught, excobj);
+        failure = _PyXI_excinfo_InitFromException(&error->uncaught, excobj);
         if (failure != NULL) {
-            // We failed to initialize info->uncaught.
+            // We failed to initialize error->uncaught.
             // XXX Print the excobj/traceback?  Emit a warning?
             // XXX Print the current exception/traceback?
             if (PyErr_ExceptionMatches(PyExc_MemoryError)) {
-                info->code = _PyXI_ERR_NO_MEMORY;
+                error->code = _PyXI_ERR_NO_MEMORY;
             }
             else {
-                info->code = _PyXI_ERR_OTHER;
+                error->code = _PyXI_ERR_OTHER;
             }
             PyErr_Clear();
         }
         else {
-            info->code = code;
+            error->code = code;
         }
-        assert(info->code != _PyXI_ERR_NO_ERROR);
+        assert(error->code != _PyXI_ERR_NO_ERROR);
     }
     else {
         // There is an error code we need to propagate.
         assert(excobj == NULL);
         assert(code != _PyXI_ERR_NO_ERROR);
-        info->code = code;
-        _Py_excinfo_Clear(&info->uncaught);
+        error->code = code;
+        _PyXI_excinfo_Clear(&error->uncaught);
     }
     return failure;
 }
 
-void
-_PyXI_ApplyExceptionInfo(_PyXI_exception_info *info, PyObject *exctype)
+PyObject *
+_PyXI_ApplyError(_PyXI_error *error)
 {
-    if (exctype == NULL) {
-        exctype = PyExc_RuntimeError;
-    }
-    if (info->code == _PyXI_ERR_UNCAUGHT_EXCEPTION) {
+    if (error->code == _PyXI_ERR_UNCAUGHT_EXCEPTION) {
         // Raise an exception that proxies the propagated exception.
-        _Py_excinfo_Apply(&info->uncaught, exctype);
+       return _PyXI_excinfo_AsObject(&error->uncaught);
     }
-    else if (info->code == _PyXI_ERR_NOT_SHAREABLE) {
+    else if (error->code == _PyXI_ERR_NOT_SHAREABLE) {
         // Propagate the exception directly.
-        _set_xid_lookup_failure(info->interp, NULL, info->uncaught.msg);
+        _set_xid_lookup_failure(error->interp, NULL, error->uncaught.msg);
     }
     else {
         // Raise an exception corresponding to the code.
-        assert(info->code != _PyXI_ERR_NO_ERROR);
-        (void)_PyXI_ApplyErrorCode(info->code, info->interp);
-        if (info->uncaught.type != NULL || info->uncaught.msg != NULL) {
+        assert(error->code != _PyXI_ERR_NO_ERROR);
+        (void)_PyXI_ApplyErrorCode(error->code, error->interp);
+        if (error->uncaught.type.name != NULL || error->uncaught.msg != NULL) {
             // __context__ will be set to a proxy of the propagated exception.
             PyObject *exc = PyErr_GetRaisedException();
-            _Py_excinfo_Apply(&info->uncaught, exctype);
+            _PyXI_excinfo_Apply(&error->uncaught, PyExc_RuntimeError);
             PyObject *exc2 = PyErr_GetRaisedException();
             PyException_SetContext(exc, exc2);
             PyErr_SetRaisedException(exc);
         }
     }
     assert(PyErr_Occurred());
+    return NULL;
 }
 
 /* shared namespaces */
@@ -1603,7 +1801,7 @@ _PyXI_NamespaceFromDict(PyObject *nsobj, _PyXI_session *session)
 
 error:
     assert(PyErr_Occurred()
-           || (session != NULL && session->exc_override != NULL));
+           || (session != NULL && session->error_override != NULL));
     _sharedns_free(ns);
     return NULL;
 }
@@ -1637,9 +1835,9 @@ _enter_session(_PyXI_session *session, PyInterpreterState *interp)
     assert(!session->running);
     assert(session->main_ns == NULL);
     // Set elsewhere and cleared in _capture_current_exception().
-    assert(session->exc_override == NULL);
+    assert(session->error_override == NULL);
     // Set elsewhere and cleared in _PyXI_ApplyCapturedException().
-    assert(session->exc == NULL);
+    assert(session->error == NULL);
 
     // Switch to interpreter.
     PyThreadState *tstate = PyThreadState_Get();
@@ -1708,23 +1906,23 @@ _propagate_not_shareable_error(_PyXI_session *session)
     PyInterpreterState *interp = _PyInterpreterState_GET();
     if (PyErr_ExceptionMatches(_get_not_shareable_error_type(interp))) {
         // We want to propagate the exception directly.
-        session->_exc_override = _PyXI_ERR_NOT_SHAREABLE;
-        session->exc_override = &session->_exc_override;
+        session->_error_override = _PyXI_ERR_NOT_SHAREABLE;
+        session->error_override = &session->_error_override;
     }
 }
 
 static void
 _capture_current_exception(_PyXI_session *session)
 {
-    assert(session->exc == NULL);
+    assert(session->error == NULL);
     if (!PyErr_Occurred()) {
-        assert(session->exc_override == NULL);
+        assert(session->error_override == NULL);
         return;
     }
 
     // Handle the exception override.
-    _PyXI_errcode *override = session->exc_override;
-    session->exc_override = NULL;
+    _PyXI_errcode *override = session->error_override;
+    session->error_override = NULL;
     _PyXI_errcode errcode = override != NULL
         ? *override
         : _PyXI_ERR_UNCAUGHT_EXCEPTION;
@@ -1747,19 +1945,18 @@ _capture_current_exception(_PyXI_session *session)
     }
 
     // Capture the exception.
-    _PyXI_exception_info *exc = &session->_exc;
-    *exc = (_PyXI_exception_info){
+    _PyXI_error *err = &session->_error;
+    *err = (_PyXI_error){
         .interp = session->init_tstate->interp,
     };
     const char *failure;
     if (excval == NULL) {
-        failure = _PyXI_InitExceptionInfo(exc, NULL, errcode);
+        failure = _PyXI_InitError(err, NULL, errcode);
     }
     else {
-        failure = _PyXI_InitExceptionInfo(exc, excval,
-                                          _PyXI_ERR_UNCAUGHT_EXCEPTION);
+        failure = _PyXI_InitError(err, excval, _PyXI_ERR_UNCAUGHT_EXCEPTION);
         if (failure == NULL && override != NULL) {
-            exc->code = errcode;
+            err->code = errcode;
         }
     }
 
@@ -1769,7 +1966,7 @@ _capture_current_exception(_PyXI_session *session)
         fprintf(stderr,
                 "RunFailedError: script raised an uncaught exception (%s)",
                 failure);
-        exc = NULL;
+        err = NULL;
     }
 
     // a temporary hack  (famous last words)
@@ -1786,23 +1983,24 @@ _capture_current_exception(_PyXI_session *session)
 
     // Finished!
     assert(!PyErr_Occurred());
-    session->exc = exc;
+    session->error  = err;
 }
 
-void
-_PyXI_ApplyCapturedException(_PyXI_session *session, PyObject *excwrapper)
+PyObject *
+_PyXI_ApplyCapturedException(_PyXI_session *session)
 {
     assert(!PyErr_Occurred());
-    assert(session->exc != NULL);
-    _PyXI_ApplyExceptionInfo(session->exc, excwrapper);
-    assert(PyErr_Occurred());
-    session->exc = NULL;
+    assert(session->error != NULL);
+    PyObject *res = _PyXI_ApplyError(session->error);
+    assert((res == NULL) != (PyErr_Occurred() == NULL));
+    session->error = NULL;
+    return res;
 }
 
 int
 _PyXI_HasCapturedException(_PyXI_session *session)
 {
-    return session->exc != NULL;
+    return session->error != NULL;
 }
 
 int
@@ -1814,7 +2012,7 @@ _PyXI_Enter(_PyXI_session *session,
     if (nsupdates != NULL) {
         sharedns = _PyXI_NamespaceFromDict(nsupdates, NULL);
         if (sharedns == NULL && PyErr_Occurred()) {
-            assert(session->exc == NULL);
+            assert(session->error == NULL);
             return -1;
         }
     }
@@ -1864,7 +2062,7 @@ error:
     assert(PyErr_Occurred());
     // We want to propagate all exceptions here directly (best effort).
     assert(errcode != _PyXI_ERR_UNCAUGHT_EXCEPTION);
-    session->exc_override = &errcode;
+    session->error_override = &errcode;
     _capture_current_exception(session);
     _exit_session(session);
     if (sharedns != NULL) {