Skip to content

Commit 85f99b2

Browse files
ericsnowcurrentlyGlyphack
authored andcommitted
pythongh-76785: Return an "excinfo" Object From Interpreter.run() (pythongh-111573)
1 parent 40e2af3 commit 85f99b2

File tree

9 files changed

+418
-243
lines changed

9 files changed

+418
-243
lines changed

Include/internal/pycore_crossinterp.h

+18-15
Original file line numberDiff line numberDiff line change
@@ -170,9 +170,14 @@ extern void _PyXI_Fini(PyInterpreterState *interp);
170170
// of the exception in the calling interpreter.
171171

172172
typedef struct _excinfo {
173-
const char *type;
173+
struct _excinfo_type {
174+
PyTypeObject *builtin;
175+
const char *name;
176+
const char *qualname;
177+
const char *module;
178+
} type;
174179
const char *msg;
175-
} _Py_excinfo;
180+
} _PyXI_excinfo;
176181

177182

178183
typedef enum error_code {
@@ -193,13 +198,13 @@ typedef struct _sharedexception {
193198
// The kind of error to propagate.
194199
_PyXI_errcode code;
195200
// The exception information to propagate, if applicable.
196-
// This is populated only for _PyXI_ERR_UNCAUGHT_EXCEPTION.
197-
_Py_excinfo uncaught;
198-
} _PyXI_exception_info;
201+
// This is populated only for some error codes,
202+
// but always for _PyXI_ERR_UNCAUGHT_EXCEPTION.
203+
_PyXI_excinfo uncaught;
204+
} _PyXI_error;
205+
206+
PyAPI_FUNC(PyObject *) _PyXI_ApplyError(_PyXI_error *err);
199207

200-
PyAPI_FUNC(void) _PyXI_ApplyExceptionInfo(
201-
_PyXI_exception_info *info,
202-
PyObject *exctype);
203208

204209
typedef struct xi_session _PyXI_session;
205210
typedef struct _sharedns _PyXI_namespace;
@@ -251,13 +256,13 @@ struct xi_session {
251256

252257
// This is set if the interpreter is entered and raised an exception
253258
// that needs to be handled in some special way during exit.
254-
_PyXI_errcode *exc_override;
259+
_PyXI_errcode *error_override;
255260
// This is set if exit captured an exception to propagate.
256-
_PyXI_exception_info *exc;
261+
_PyXI_error *error;
257262

258263
// -- pre-allocated memory --
259-
_PyXI_exception_info _exc;
260-
_PyXI_errcode _exc_override;
264+
_PyXI_error _error;
265+
_PyXI_errcode _error_override;
261266
};
262267

263268
PyAPI_FUNC(int) _PyXI_Enter(
@@ -266,9 +271,7 @@ PyAPI_FUNC(int) _PyXI_Enter(
266271
PyObject *nsupdates);
267272
PyAPI_FUNC(void) _PyXI_Exit(_PyXI_session *session);
268273

269-
PyAPI_FUNC(void) _PyXI_ApplyCapturedException(
270-
_PyXI_session *session,
271-
PyObject *excwrapper);
274+
PyAPI_FUNC(PyObject *) _PyXI_ApplyCapturedException(_PyXI_session *session);
272275
PyAPI_FUNC(int) _PyXI_HasCapturedException(_PyXI_session *session);
273276

274277

Lib/test/support/interpreters.py

+17-1
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,27 @@
1414

1515
__all__ = [
1616
'Interpreter', 'get_current', 'get_main', 'create', 'list_all',
17+
'RunFailedError',
1718
'SendChannel', 'RecvChannel',
1819
'create_channel', 'list_all_channels', 'is_shareable',
1920
'ChannelError', 'ChannelNotFoundError',
2021
'ChannelEmptyError',
2122
]
2223

2324

25+
class RunFailedError(RuntimeError):
26+
27+
def __init__(self, excinfo):
28+
msg = excinfo.formatted
29+
if not msg:
30+
if excinfo.type and snapshot.msg:
31+
msg = f'{snapshot.type.__name__}: {snapshot.msg}'
32+
else:
33+
msg = snapshot.type.__name__ or snapshot.msg
34+
super().__init__(msg)
35+
self.snapshot = excinfo
36+
37+
2438
def create(*, isolated=True):
2539
"""Return a new (idle) Python interpreter."""
2640
id = _interpreters.create(isolated=isolated)
@@ -110,7 +124,9 @@ def run(self, src_str, /, channels=None):
110124
that time, the previous interpreter is allowed to run
111125
in other threads.
112126
"""
113-
_interpreters.exec(self._id, src_str, channels)
127+
excinfo = _interpreters.exec(self._id, src_str, channels)
128+
if excinfo is not None:
129+
raise RunFailedError(excinfo)
114130

115131

116132
def create_channel():

Lib/test/test__xxinterpchannels.py

+6-6
Original file line numberDiff line numberDiff line change
@@ -1017,16 +1017,16 @@ def test_close_multiple_users(self):
10171017
_channels.recv({cid})
10181018
"""))
10191019
channels.close(cid)
1020-
with self.assertRaises(interpreters.RunFailedError) as cm:
1021-
interpreters.run_string(id1, dedent(f"""
1020+
1021+
excsnap = interpreters.run_string(id1, dedent(f"""
10221022
_channels.send({cid}, b'spam')
10231023
"""))
1024-
self.assertIn('ChannelClosedError', str(cm.exception))
1025-
with self.assertRaises(interpreters.RunFailedError) as cm:
1026-
interpreters.run_string(id2, dedent(f"""
1024+
self.assertEqual(excsnap.type.__name__, 'ChannelClosedError')
1025+
1026+
excsnap = interpreters.run_string(id2, dedent(f"""
10271027
_channels.send({cid}, b'spam')
10281028
"""))
1029-
self.assertIn('ChannelClosedError', str(cm.exception))
1029+
self.assertEqual(excsnap.type.__name__, 'ChannelClosedError')
10301030

10311031
def test_close_multiple_times(self):
10321032
cid = channels.create()

Lib/test/test__xxsubinterpreters.py

+10-9
Original file line numberDiff line numberDiff line change
@@ -940,7 +940,6 @@ def add_module(self, modname, text):
940940
return script_helper.make_script(tempdir, modname, text)
941941

942942
def run_script(self, text, *, fails=False):
943-
excwrapper = interpreters.RunFailedError
944943
r, w = os.pipe()
945944
try:
946945
script = dedent(f"""
@@ -956,11 +955,12 @@ class NeverError(Exception): pass
956955
raise NeverError # never raised
957956
""").format(dedent(text))
958957
if fails:
959-
with self.assertRaises(excwrapper) as caught:
960-
interpreters.run_string(self.id, script)
961-
return caught.exception
958+
err = interpreters.run_string(self.id, script)
959+
self.assertIsNot(err, None)
960+
return err
962961
else:
963-
interpreters.run_string(self.id, script)
962+
err = interpreters.run_string(self.id, script)
963+
self.assertIs(err, None)
964964
return None
965965
except:
966966
raise # re-raise
@@ -979,17 +979,18 @@ def _assert_run_failed(self, exctype, msg, script):
979979
exctype_name = exctype.__name__
980980

981981
# Run the script.
982-
exc = self.run_script(script, fails=True)
982+
excinfo = self.run_script(script, fails=True)
983983

984984
# Check the wrapper exception.
985+
self.assertEqual(excinfo.type.__name__, exctype_name)
985986
if msg is None:
986-
self.assertEqual(str(exc).split(':')[0],
987+
self.assertEqual(excinfo.formatted.split(':')[0],
987988
exctype_name)
988989
else:
989-
self.assertEqual(str(exc),
990+
self.assertEqual(excinfo.formatted,
990991
'{}: {}'.format(exctype_name, msg))
991992

992-
return exc
993+
return excinfo
993994

994995
def assert_run_failed(self, exctype, script):
995996
self._assert_run_failed(exctype, None, script)

Lib/test/test_import/__init__.py

+6-4
Original file line numberDiff line numberDiff line change
@@ -1968,10 +1968,12 @@ def test_disallowed_reimport(self):
19681968
print(_testsinglephase)
19691969
''')
19701970
interpid = _interpreters.create()
1971-
with self.assertRaises(_interpreters.RunFailedError):
1972-
_interpreters.run_string(interpid, script)
1973-
with self.assertRaises(_interpreters.RunFailedError):
1974-
_interpreters.run_string(interpid, script)
1971+
1972+
excsnap = _interpreters.run_string(interpid, script)
1973+
self.assertIsNot(excsnap, None)
1974+
1975+
excsnap = _interpreters.run_string(interpid, script)
1976+
self.assertIsNot(excsnap, None)
19751977

19761978

19771979
class TestSinglePhaseSnapshot(ModuleSnapshot):

Lib/test/test_importlib/test_util.py

+8-14
Original file line numberDiff line numberDiff line change
@@ -655,25 +655,19 @@ def test_magic_number(self):
655655
@unittest.skipIf(_interpreters is None, 'subinterpreters required')
656656
class IncompatibleExtensionModuleRestrictionsTests(unittest.TestCase):
657657

658-
ERROR = re.compile("^ImportError: module (.*) does not support loading in subinterpreters")
659-
660658
def run_with_own_gil(self, script):
661659
interpid = _interpreters.create(isolated=True)
662-
try:
663-
_interpreters.run_string(interpid, script)
664-
except _interpreters.RunFailedError as exc:
665-
if m := self.ERROR.match(str(exc)):
666-
modname, = m.groups()
667-
raise ImportError(modname)
660+
excsnap = _interpreters.run_string(interpid, script)
661+
if excsnap is not None:
662+
if excsnap.type.__name__ == 'ImportError':
663+
raise ImportError(excsnap.msg)
668664

669665
def run_with_shared_gil(self, script):
670666
interpid = _interpreters.create(isolated=False)
671-
try:
672-
_interpreters.run_string(interpid, script)
673-
except _interpreters.RunFailedError as exc:
674-
if m := self.ERROR.match(str(exc)):
675-
modname, = m.groups()
676-
raise ImportError(modname)
667+
excsnap = _interpreters.run_string(interpid, script)
668+
if excsnap is not None:
669+
if excsnap.type.__name__ == 'ImportError':
670+
raise ImportError(excsnap.msg)
677671

678672
@unittest.skipIf(_testsinglephase is None, "test requires _testsinglephase module")
679673
def test_single_phase_init_module(self):

Lib/test/test_interpreters.py

+5
Original file line numberDiff line numberDiff line change
@@ -478,6 +478,11 @@ def test_success(self):
478478

479479
self.assertEqual(out, 'it worked!')
480480

481+
def test_failure(self):
482+
interp = interpreters.create()
483+
with self.assertRaises(interpreters.RunFailedError):
484+
interp.run('raise Exception')
485+
481486
def test_in_thread(self):
482487
interp = interpreters.create()
483488
script, file = _captured_script('print("it worked!", end="")')

0 commit comments

Comments
 (0)