8000 Add Interpreter.call(). · python/cpython@3bd01f2 · GitHub
[go: up one dir, main page]

Skip to content

Commit 3bd01f2

Browse files
Add Interpreter.call().
1 parent 52cfcfb commit 3bd01f2

File tree

2 files changed

+270
-28
lines changed

2 files changed

+270
-28
lines changed

Lib/test/support/interpreters/__init__.py

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -180,9 +180,60 @@ def exec(self, code, /):
180180
if excinfo is not None:
181181
raise ExecFailure(excinfo)
182182

183-
def run(self, code, /):
183+
def call(self, callable, /, args=None, kwargs=None):
184+
"""Call the object in the interpreter with given args/kwargs.
185+
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.
190+
191+
Unlike Interpreter.exec() and prepare_main(), all objects are
192+
supported, at the expense of some performance.
193+
"""
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
229+
230+
def call_in_thread(self, callable, /, args=None, kwargs=None):
231+
"""Return a new thread that calls the object in the interpreter.
232+
233+
The return value and any raised exception are discarded.
234+
"""
184235
def task():
185-
self.exec(code)
236+
self.call(callable, args, kwargs)
186237
t = threading.Thread(target=task)
187238
t.start()
188239
return t

Lib/test/test_interpreters/test_api.py

Lines changed: 217 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -509,7 +509,7 @@ def test_not_shareable(self):
509509
interp.exec('print(spam)')
510510

511511

512-
class TestInterpreterExecSync(TestBase):
512+
class TestInterpreterExec(TestBase):
513513

514514
def test_success(self):
515515
interp = interpreters.create()
@@ -662,32 +662,223 @@ def task():
662662
# Interpreter.exec() behavior.
663663

664664

665-
class TestInterpreterRun(TestBase):
666-
667-
def test_success(self):
668-
interp = interpreters.create()
669-
script, file = _captured_script('print("it worked!", end="")')
670-
with file:
671-
t = interp.run(script)
672-
t.join()
673-
out = file.read()
674-
675-
self.assertEqual(out, 'it worked!')
676-
677-
def test_failure(self):
678-
caught = False
679-
def excepthook(args):
680-
nonlocal caught
681-
caught = True
682-
threading.excepthook = excepthook
683-
try:
684-
interp = interpreters.create()
685-
t = interp.run('raise Exception')
686-
t.join()
665+
def call_func_noop():
666+
pass
667+
668+
669+
def call_func_return_shareable():
670+
return (1, None)
671+
672+
673+
def call_func_return_not_shareable():
674+
return [1, 2, 3]
675+
676+
677+
def call_func_failure():
678+
raise Exception('spam!')
679+
680+
681+
def call_func_ident(value):
682+
return value
683+
684+
685+
def get_call_func_closure(value):
686+
def call_func_closure():
687+
return value
688+
return call_func_closure
689+
690+
691+
class Spam:
692+
693+
@staticmethod
694+
def noop():
695+
pass
696+
697+
@classmethod
698+
def from_values(cls, *values):
699+
return cls(values)
700+
701+
def __init__(self, value):
702+
self.value = value
703+
704+
def __call__(self, *args, **kwargs):
705+
return (self.value, args, kwargs)
706+
707+
def __eq__(self, other):
708+
if not isinstance(other, Spam):
709+
return NotImplemented
710+
return self.value == other.value
711+
712+
def run(self, *args, **kwargs):
713+
return (self.value, args, kwargs)
714+
715+
716+
def call_func_complex(op, /, value=None, *args, exc=None, **kwargs):
717+
if exc is not None:
718+
raise exc
719+
if op == '':
720+
raise ValueError('missing op')
721+
elif op == 'ident':
722+
if args or kwargs:
723+
raise Exception((args, kwargs))
724+
return value
725+
elif op == 'full-ident':
726+
return (value, args, kwargs)
727+
elif op == 'globals':
728+
if value is not None or args or kwargs:
729+
raise Exception((value, args, kwargs))
730+
return __name__
731+
elif op == 'interpid':
732+
if value is not None or args or kwargs:
733+
raise Exception((value, args, kwargs))
734+
return interpreters.get_current().id
735+
elif op == 'closure':
736+
if args or kwargs:
737+
raise Exception((args, kwargs))
738+
return get_call_func_closure(value)
739+
elif op == 'custom':
740+
if args or kwargs:
741+
raise Exception((args, kwargs))
742+
return Spam(value)
743+
elif op == 'custom-inner':
744+
if args or kwargs:
745+
raise Exception((args, kwargs))
746+
class Eggs(Spam):
747+
pass
748+
return Eggs(value)
749+
else if not isinstance(op, str):
750+
raise TypeError(op)
751+
else:
752+
raise NotImplementedError(op)
753+
754+
755+
class TestInterpreterCall(TestBase):
756+
757+
# signature
758+
# - blank
759+
# - args
760+
# - kwargs
761+
# - args, kwargs
762+
# return
763+
# - nothing (None)
764+
# - simple
765+
# - closure
766+
# - custom
767+
# ops:
768+
# - do nothing
769+
# - fail
770+
# - echo
771+
# - do complex, relative to interpreter
772+
# scope
773+
# - global func
774+
# - local closure
775+
# - returned closure
776+
# - callable type instance
777+
# - type
778+
# - classmethod
779+
# - staticmethod
780+
# - instance method
781+
# exception
782+
# - builtin
783+
# - custom
784+
# - preserves info (e.g. SyntaxError)
785+
# - matching error display
786+
787+
def test_call(self):
788+
interp = interpreters.create()
789+
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+
...),
831+
]):
832+
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),
841+
]):
842+
with self.subTest(f'failure case #{i+1}'):
843+
with self.assertRaises(expected):
844+
interp.call(callable, args, kwargs)
845+
846+
def test_call_in_thread(self):
847+
interp = interpreters.create()
848+
849+
for i, (callable, args, kwargs) in enumerate([
850+
(call_func_noop, (), {}),
851+
(call_func_return_shareable, (), {}),
852+
(call_func_return_not_shareable, (), {}),
853+
(call_func_ident, ('spamspamspam',), {}),
854+
(get_call_func_closure, (42,), {}),
855+
(get_call_func_closure(42), (), {}),
856+
(Spam.noop, (), {}),
857+
(Spam.from_values, (), {}),
858+
(Spam.from_values, (1, 2, 3), {}),
859+
(Spam, ('???'), {}),
860+
(Spam(101), (), {}),
861+
(Spam(10101).run, (), {}),
862+
(call_func_complex, ('ident', 'spam'), {}),
863+
(call_func_complex, ('full-ident', 'spam'), {}),
864+
(call_func_complex, ('full-ident', 'spam', 'ham'), {'eggs': '!!!'}),
865+
(call_func_complex, ('globals',), {}),
866+
(call_func_complex, ('interpid',), {}),
867+
(call_func_complex, ('closure',), {'value': '~~~'}),
868+
(call_func_complex, ('custom', 'spam!'), {}),
869+
(call_func_complex, ('custom-inner', 'eggs!'), {}),
870+
]):
871+
with self.subTest(f'success case #{i+1}'):
872+
t = interp.call_in_thread(callable, args, kwargs)
873+
t.join()
687874

688-
self.assertTrue(caught)
689-
except BaseException:
690-
threading.excepthook = threading.__excepthook__
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()
691882

692883

693884
class TestIsShareable(TestBase):

0 commit comments

Comments
 (0)
0