8000 Simplify Interpreter.call() for now. · python/cpython@ca1dadf · GitHub
[go: up one dir, main page]

Skip to content

Commit ca1dadf

Browse files
Simplify Interpreter.call() for now.
1 parent 3bd01f2 commit ca1dadf

File tree

4 files changed

+151
-112
lines changed

4 files changed

+151
-112
lines changed

Lib/test/support/interpreters/__init__.py

Lines changed: 28 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@
1414
__all__ = [
1515
'get_current', 'get_main', 'create', 'list_all', 'is_shareable',
1616
'Interpreter',
17-
'InterpreterError', 'InterpreterNotFoundError', 'ExecFailure',
17+
'InterpreterError', 'InterpreterNotFoundError',
18+
'ExecFailure', 'CallFailure',
1819
'NotShareableError',
1920
'create_queue', 'Queue', 'QueueEmpty', 'QueueFull',
2021
]
@@ -43,7 +44,7 @@ def __getattr__(name):
4344
{formatted}
4445
""".strip()
4546

46-
class ExecFailure(RuntimeError):
47+
class _ExecFailure(RuntimeError):
4748

4849
def __init__(self, excinfo):
4950
msg = excinfo.formatted
@@ -67,6 +68,14 @@ def __str__(self):
6768
)
6869

6970

71+
class ExecFailure(_ExecFailure):
72+
"""Raised from Interpreter.exec() for unhandled exceptions."""
73+
74+
75+
class CallFailure(_ExecFailure):
76+
"""Raised from Interpreter.call() for unhandled exceptions."""
77+
78+
7079
def create():
7180
"""Return a new (idle) Python interpreter."""
7281
id = _interpreters.create(isolated=True)
@@ -180,60 +189,33 @@ def exec(self, code, /):
180189
if excinfo is not None:
181190
raise ExecFailure(excinfo)
182191

183-
def call(self, callable, /, args=None, kwargs=None):
192+
def call(self, callable, /):
184193
"""Call the object in the interpreter with given args/kwargs.
185194
186-
Return the function's return value. If it raises an exception,
187-
raise it in the calling interpreter. This contrasts with
188-
Interpreter.exec(), which discards the return value and only
189-
propagates the exception as ExecFailure.
195+
Only functions that take no arguments and have no closure
196+
are supported.
190197
191-
Unlike Interpreter.exec() and prepare_main(), all objects are
192-
supported, at the expense of some performance.
198+
The return value is discarded.
199+
200+
If the callable raises an exception then the error display
201+
(including full traceback) is send back between the interpreters
202+
and a CallFailedError is raised, much like what happens with
203+
Interpreter.exec().
193204
"""
194-
pickled_callable = pickle.dumps(callable)
195-
pickled_args = pickle.dumps(args)
196-
pickled_kwargs = pickle.dumps(kwargs)
197-
198-
results = create_queue(sharedonly=False)
199-
self.prepare_main(_call_results=results)
200-
self.exec(f"""
201-
def _call_impl():
202-
try:
203-
import pickle
204-
callable = pickle.loads({pickled_callable!r})
205-
if {pickled_args!r} is None:
206-
args = ()
207-
else:
208-
args = pickle.loads({pickled_args!r})
209-
if {pickled_kwargs!r} is None:
210-
kwargs = {}
211-
else:
212-
kwargs = pickle.loads({pickled_kwargs!r})
213-
214-
res = callable(*args, **kwargs)
215-
except Exception as exc:
216-
res = pickle.dumps((None, exc))
217-
else:
218-
res = pickle.dumps((res, None))
219-
_call_results.put(res)
220-
_call_impl()
221-
del _call_impl
222-
del _call_results
223-
""")
224-
res, exc = results.get()
225-
if exc is None:
226-
raise exc
227-
else:
228-
return res
205+
# XXX Support args and kwargs.
206+
# XXX Support arbitrary callables.
207+
# XXX Support returning the return value (e.g. via pickle).
208+
excinfo = _interpreters.call(self._id, callable)
209+
if excinfo is not None:
210+
raise CallFailure(excinfo)
229211

230-
def call_in_thread(self, callable, /, args=None, kwargs=None):
212+
def call_in_thread(self, callable, /):
231213
"""Return a new thread that calls the object in the interpreter.
232214
233215
The return value and any raised exception are discarded.
234216
"""
235217
def task():
236-
self.call(callable, args, kwargs)
218+
self.call(callable)
237219
t = threading.Thread(target=task)
238220
t.start()
239221
return t

Lib/test/test_interpreters/test_api.py

Lines changed: 57 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -746,7 +746,7 @@ def call_func_complex(op, /, value=None, *args, exc=None, **kwargs):
746746
class Eggs(Spam):
747747
pass
748748
return Eggs(value)
749-
else if not isinstance(op, str):
749+
elif not isinstance(op, str):
750750
raise TypeError(op)
751751
else:
752752
raise NotImplementedError(op)
@@ -787,61 +787,43 @@ class TestInterpreterCall(TestBase):
787787
def test_call(self):
788788
interp = interpreters.create()
789789

790-
for i, ((callable, args, kwargs), expected) in enumerate([
791-
((call_func_noop, (), {}),
792-
None),
793-
((call_func_return_shareable, (), {}),
794-
(1, None)),
795-
((call_func_return_not_shareable, (), {}),
796-
[1, 2, 3]),
797-
((call_func_ident, ('spamspamspam',), {}),
798-
'spamspamspam'),
799-
((get_call_func_closure, (42,), {}),
800-
...),
801-
((get_call_func_closure(42), (), {}),
802-
42),
803-
((Spam.noop, (), {}),
804-
None),
805-
((Spam.from_values, (), {}),
806-
None),
807-
((Spam.from_values, (1, 2, 3), {}),
808-
Spam((1, 2, 3)),
809-
((Spam, ('???'), {}),
810-
Spam('???')),
811-
((Spam(101), (), {}),
812-
101),
813-
((Spam(10101).run, (), {}),
814-
10101),
815-
((call_func_complex, ('ident', 'spam'), {}),
816-
'spam'),
817-
((call_func_complex, ('full-ident', 'spam'), {}),
818-
('spam', (), {})),
819-
((call_func_complex, ('full-ident', 'spam', 'ham'), {'eggs': '!!!'}),
820-
('spam', ('ham',), {'eggs': '!!!'})),
821-
((call_func_complex, ('globals',), {}),
822-
'test.test_interpreters.test_api'),
823-
((call_func_complex, ('interpid',), {}),
824-
interp.id),
825-
((call_func_complex, ('closure',), {'value': '~~~'}),
826-
'~~~'),
827-
((call_func_complex, ('custom', 'spam!'), {}),
828-
Spam('spam!')),
829-
((call_func_complex, ('custom-inner', 'eggs!'), {}),
830-
...),
790+
for i, (callable, args, kwargs) in enumerate([
791+
(call_func_noop, (), {}),
792+
(call_func_return_shareable, (), {}),
793+
(call_func_return_not_shareable, (), {}),
794+
(Spam.noop, (), {}),
831795
]):
832796
with self.subTest(f'success case #{i+1}'):
833-
res = interp.call(callable, args, kwargs)
834-
self.assertEqual(res, expected)
835-
836-
for i, ((callable, args, kwargs), expected) in enumerate([
837-
((call_func_failure, (), {}),
838-
Exception),
839-
((call_func_complex, ('???',), {exc=ValueError('spam')}),
840-
ValueError),
797+
res = interp.call(callable)
798+
self.assertIs(res, None)
799+
800+
for i, (callable, args, kwargs) in enumerate([
801+
(call_func_ident, ('spamspamspam',), {}),
802+
(get_call_func_closure, (42,), {}),
803+
(get_call_func_closure(42), (), {}),
804+
(Spam.from_values, (), {}),
805+
(Spam.from_values, (1, 2, 3), {}),
806+
(Spam, ('???'), {}),
807+
(Spam(101), (), {}),
808+
(Spam(10101).run, (), {}),
809+
(call_func_complex, ('ident', 'spam'), {}),
810+
(call_func_complex, ('full-ident', 'spam'), {}),
811+
(call_func_complex, ('full-ident', 'spam', 'ham'), {'eggs': '!!!'}),
812+
(call_func_complex, ('globals',), {}),
813+
(call_func_complex, ('interpid',), {}),
814+
(call_func_complex, ('closure',), {'value': '~~~'}),
815+
(call_func_complex, ('custom', 'spam!'), {}),
816+
(call_func_complex, ('custom-inner', 'eggs!'), {}),
817+
(call_func_complex, ('???',), {'exc': ValueError('spam')}),
841818
]):
842-
with self.subTest(f'failure case #{i+1}'):
843-
with self.assertRaises(expected):
844-
interp.call(callable, args, kwargs)
819+
with self.subTest(f'invalid case #{i+1}'):
820+
with self.assertRaises(Exception):
821+
if args or kwargs:
822+
raise Exception((args, kwargs))
823+
interp.call(callable)
824+
825+
with self.assertRaises(interpreters.CallFailure):
826+
interp.call(call_func_failure)
845827

846828
def test_call_in_thread(self):
847829
interp = interpreters.create()
@@ -850,10 +832,18 @@ def test_call_in_thread(self):
850832
(call_func_noop, (), {}),
851833
(call_func_return_shareable, (), {}),
852834
(call_func_return_not_shareable, (), {}),
835+
(Spam.noop, (), {}),
836+
]):
837+
with self.subTest(f'success case #{i+1}'):
838+
with self.captured_thread_exception() as ctx:
839+
t = interp.call_in_thread(callable)
840+
t.join()
841+
self.assertIsNone(ctx.caught)
842+
843+
for i, (callable, args, kwargs) in enumerate([
853844
(call_func_ident, ('spamspamspam',), {}),
854845
(get_call_func_closure, (42,), {}),
855846
(get_call_func_closure(42), (), {}),
856-
(Spam.noop, (), {}),
857847
(Spam.from_values, (), {}),
858848
(Spam.from_values, (1, 2, 3), {}),
859849
(Spam, ('???'), {}),
@@ -867,18 +857,20 @@ def test_call_in_thread(self):
867857
(call_func_complex, ('closure',), {'value': '~~~'}),
868858
(call_func_complex, ('custom', 'spam!'), {}),
869859
(call_func_complex, ('custom-inner', 'eggs!'), {}),
860+
(call_func_complex, ('???',), {'exc': ValueError('spam')}),
870861
]):
871-
with self.subTest(f'success case #{i+1}'):
872-
t = interp.call_in_thread(callable, args, kwargs)
873-
t.join()
874-
875-
for i, (callable, args, kwargs) in enumerate([
876-
(call_func_failure, (), {}),
877-
(call_func_complex, ('???',), {exc=ValueError('spam')}),
878-
]):
879-
with self.subTest(f'failure case #{i+1}'):
880-
t = interp.call_in_thread(callable, args, kwargs)
881-
t.join()
862+
with self.subTest(f'invalid case #{i+1}'):
863+
if args or kwargs:
864+
continue
865+
with self.captured_thread_exception() as ctx:
866+
t = interp.call_in_thread(callable)
867+
t.join()
868+
self.assertIsNotNone(ctx.caught)
869+
870+
with self.captured_thread_exception() as ctx:
871+
t = interp.call_in_thread(call_func_failure)
872+
t.join()
873+
self.assertIsNotNone(ctx.caught)
882874

883875

884876
class TestIsShareable(TestBase):

Lib/test/test_interpreters/utils.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@
44
import subprocess
55
import sys
66
import tempfile
7-
import threading
87
from textwrap import dedent
8+
import threading
9+
import types
910
import unittest
1011

1112
from test import support
@@ -84,6 +85,18 @@ def temp_dir(self):
8485
self.addCleanup(lambda: os_helper.rmtree(tempdir))
8586
return tempdir
8687

88+
@contextlib.contextmanager
89+
def captured_thread_exception(self):
90+
ctx = types.SimpleNamespace(caught=None)
91+
def excepthook(args):
92+
ctx.caught = args
93+
orig_excepthook = threading.excepthook
94+
threading.excepthook = excepthook
95+
try:
96+
yield ctx
97+
finally:
98+
threading.excepthook = orig_excepthook
99+
87100
def make_script(self, filename, dirname=None, text=None):
88101
if text:
89102
text = dedent(text)

Modules/_xxsubinterpretersmodule.c

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -902,6 +902,56 @@ The code/function must not take any arguments or be a closure\n\
902902
If a function is provided, its code object is used and all its state\n\
903903
is ignored, including its __globals__ dict.");
904904

905+
static PyObject *
906+
interp_call(PyObject *self, PyObject *args, PyObject *kwds)
907+
{
908+
static char *kwlist[] = {"id", "callable", "args", "kwargs", NULL};
909+
PyObject *id, *callable;
910+
PyObject *args_obj = NULL;
911+
PyObject *kwargs_obj = NULL;
912+
if (!PyArg_ParseTupleAndKeywords(args, kwds,
913+
"OO|OO:" MODULE_NAME_STR ".call", kwlist,
914+
&id, &callable, &args_obj, &kwargs_obj)) {
915+
return NULL;
916+
}
917+
918+
if (args_obj != NULL) {
919+
PyErr_SetString(PyExc_ValueError, "got unexpected args");
920+
return NULL;
921+
}
922+
if (kwargs_obj != NULL) {
923+
PyErr_SetString(PyExc_ValueError, "got unexpected kwargs");
924+
return NULL;
925+
}
926+
927+
PyObject *code = (PyObject *)convert_code_arg(callable, MODULE_NAME_STR ".call",
928+
"argument 2", "a function");
929+
if (code == NULL) {
930+
return NULL;
931+
}
932+
933+
PyObject *excinfo = NULL;
934+
int res = _interp_exec(self, id, code, NULL, &excinfo);
935+
Py_DECREF(code);
936+
if (res < 0) {
937+
assert((excinfo == NULL) != (PyErr_Occurred() == NULL));
938+
return excinfo;
939+
}
940+
Py_RETURN_NONE;
941+
}
942+
943+
PyDoc_STRVAR(call_doc,
944+
"call(id, callable, args=None, kwargs=None)\n\
945+
\n\
946+
Call the provided object in the identified interpreter.\n\
947+
Pass the given args and kwargs, if possible.\n\
948+
\n\
949+
\"callable\" may be a plain function with no free vars that takes\n\
950+
no arguments.\n\
951+
\n\
952+
The function's code object is used and all its state\n\
953+
is ignored, including its __globals__ dict.");
954+
905955
static PyObject *
906956
interp_run_string(PyObject *self, PyObject *args, PyObject *kwds)
907957
{
@@ -1085,6 +1135,8 @@ static PyMethodDef module_functions[] = {
10851135
METH_VARARGS | METH_KEYWORDS, is_running_doc},
10861136
{"exec", _PyCFunction_CAST(interp_exec),
10871137
METH_VARARGS | METH_KEYWORDS, exec_doc},
1138+
{"call", _PyCFunction_CAST(interp_call),
1139+
METH_VARARGS | METH_KEYWORDS, call_doc},
10881140
{"run_string", _PyCFunction_CAST(interp_run_string),
10891141
METH_VARARGS | METH_KEYWORDS, run_string_doc},
10901142
{"run_func", _PyCFunction_CAST(interp_run_func),

0 commit comments

Comments
 (0)
0