Skip to content

Commit

Permalink
pythongh-76785: Return an "excinfo" Object From Interpreter.run() (py…
Browse files Browse the repository at this point in the history
  • Loading branch information
ericsnowcurrently authored and Glyphack committed Jan 27, 2024
1 parent 40e2af3 commit 85f99b2
Show file tree
Hide file tree
Showing 9 changed files with 418 additions and 243 deletions.
33 changes: 18 additions & 15 deletions Include/internal/pycore_crossinterp.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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;
Expand Down Expand Up @@ -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(
Expand All @@ -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);


Expand Down
18 changes: 17 additions & 1 deletion Lib/test/support/interpreters.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,27 @@

__all__ = [
'Interpreter', 'get_current', 'get_main', 'create', 'list_all',
'RunFailedError',
'SendChannel', 'RecvChannel',
'create_channel', 'list_all_channels', 'is_shareable',
'ChannelError', 'ChannelNotFoundError',
'ChannelEmptyError',
]


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)
Expand Down Expand Up @@ -110,7 +124,9 @@ def run(self, src_str, /, channels=None):
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():
Expand Down
12 changes: 6 additions & 6 deletions Lib/test/test__xxinterpchannels.py
Original file line number Diff line number Diff line change
Expand Up @@ -1017,16 +1017,16 @@ def test_close_multiple_users(self):
_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()
Expand Down
19 changes: 10 additions & 9 deletions Lib/test/test__xxsubinterpreters.py
Original file line number Diff line number Diff line change
Expand Up @@ -940,7 +940,6 @@ def add_module(self, modname, text):
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"""
Expand All @@ -956,11 +955,12 @@ class NeverError(Exception): pass
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
Expand All @@ -979,17 +979,18 @@ def _assert_run_failed(self, exctype, msg, script):
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)
Expand Down
10 changes: 6 additions & 4 deletions Lib/test/test_import/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1968,10 +1968,12 @@ def test_disallowed_reimport(self):
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):
Expand Down
22 changes: 8 additions & 14 deletions Lib/test/test_importlib/test_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -655,25 +655,19 @@ def test_magic_number(self):
@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):
Expand Down
5 changes: 5 additions & 0 deletions Lib/test/test_interpreters.py
Original file line number Diff line number Diff line change
Expand Up @@ -478,6 +478,11 @@ def test_success(self):

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="")')
Expand Down
Loading

0 comments on commit 85f99b2

Please sign in to comment.