Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Add Interpreter.call().
  • Loading branch information
ericsnowcurrently committed Feb 27, 2024
commit 3bd01f2cf75636d6d0e19796b76c24458d06f679
55 changes: 53 additions & 2 deletions Lib/test/support/interpreters/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,9 +180,60 @@ def exec(self, code, /):
if excinfo is not None:
raise ExecFailure(excinfo)

def run(self, code, /):
def call(self, callable, /, args=None, kwargs=None):
"""Call the object in the interpreter with given args/kwargs.

Return the function's return value. If it raises an exception,
raise it in the calling interpreter. This contrasts with
Interpreter.exec(), which discards the return value and only
propagates the exception as ExecFailure.

Unlike Interpreter.exec() and prepare_main(), all objects are
supported, at the expense of some performance.
"""
pickled_callable = pickle.dumps(callable)
pickled_args = pickle.dumps(args)
pickled_kwargs = pickle.dumps(kwargs)

results = create_queue(sharedonly=False)
self.prepare_main(_call_results=results)
self.exec(f"""
def _call_impl():
try:
import pickle
callable = pickle.loads({pickled_callable!r})
if {pickled_args!r} is None:
args = ()
else:
args = pickle.loads({pickled_args!r})
if {pickled_kwargs!r} is None:
kwargs = {}
else:
kwargs = pickle.loads({pickled_kwargs!r})

res = callable(*args, **kwargs)
except Exception as exc:
res = pickle.dumps((None, exc))
else:
res = pickle.dumps((res, None))
_call_results.put(res)
_call_impl()
del _call_impl
del _call_results
""")
res, exc = results.get()
if exc is None:
raise exc
else:
return res

def call_in_thread(self, callable, /, args=None, kwargs=None):
"""Return a new thread that calls the object in the interpreter.

The return value and any raised exception are discarded.
"""
def task():
self.exec(code)
self.call(callable, args, kwargs)
t = threading.Thread(target=task)
t.start()
return t
243 changes: 217 additions & 26 deletions Lib/test/test_interpreters/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -509,7 +509,7 @@ def test_not_shareable(self):
interp.exec('print(spam)')


class TestInterpreterExecSync(TestBase):
class TestInterpreterExec(TestBase):

def test_success(self):
interp = interpreters.create()
Expand Down Expand Up @@ -662,32 +662,223 @@ def task():
# Interpreter.exec() behavior.


class TestInterpreterRun(TestBase):

def test_success(self):
interp = interpreters.create()
script, file = _captured_script('print("it worked!", end="")')
with file:
t = interp.run(script)
t.join()
out = file.read()

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

def test_failure(self):
caught = False
def excepthook(args):
nonlocal caught
caught = True
threading.excepthook = excepthook
try:
interp = interpreters.create()
t = interp.run('raise Exception')
t.join()
def call_func_noop():
pass


def call_func_return_shareable():
return (1, None)


def call_func_return_not_shareable():
return [1, 2, 3]


def call_func_failure():
raise Exception('spam!')


def call_func_ident(value):
return value


def get_call_func_closure(value):
def call_func_closure():
return value
return call_func_closure


class Spam:

@staticmethod
def noop():
pass

@classmethod
def from_values(cls, *values):
return cls(values)

def __init__(self, value):
self.value = value

def __call__(self, *args, **kwargs):
return (self.value, args, kwargs)

def __eq__(self, other):
if not isinstance(other, Spam):
return NotImplemented
return self.value == other.value

def run(self, *args, **kwargs):
return (self.value, args, kwargs)


def call_func_complex(op, /, value=None, *args, exc=None, **kwargs):
if exc is not None:
raise exc
if op == '':
raise ValueError('missing op')
elif op == 'ident':
if args or kwargs:
raise Exception((args, kwargs))
return value
elif op == 'full-ident':
return (value, args, kwargs)
elif op == 'globals':
if value is not None or args or kwargs:
raise Exception((value, args, kwargs))
return __name__
elif op == 'interpid':
if value is not None or args or kwargs:
raise Exception((value, args, kwargs))
return interpreters.get_current().id
elif op == 'closure':
if args or kwargs:
raise Exception((args, kwargs))
return get_call_func_closure(value)
elif op == 'custom':
if args or kwargs:
raise Exception((args, kwargs))
return Spam(value)
elif op == 'custom-inner':
if args or kwargs:
raise Exception((args, kwargs))
class Eggs(Spam):
pass
return Eggs(value)
else if not isinstance(op, str):
raise TypeError(op)
else:
raise NotImplementedError(op)


class TestInterpreterCall(TestBase):

# signature
# - blank
# - args
# - kwargs
# - args, kwargs
# return
# - nothing (None)
# - simple
# - closure
# - custom
# ops:
# - do nothing
# - fail
# - echo
# - do complex, relative to interpreter
# scope
# - global func
# - local closure
# - returned closure
# - callable type instance
# - type
# - classmethod
# - staticmethod
# - instance method
# exception
# - builtin
# - custom
# - preserves info (e.g. SyntaxError)
# - matching error display

def test_call(self):
interp = interpreters.create()

for i, ((callable, args, kwargs), expected) in enumerate([
((call_func_noop, (), {}),
None),
((call_func_return_shareable, (), {}),
(1, None)),
((call_func_return_not_shareable, (), {}),
[1, 2, 3]),
((call_func_ident, ('spamspamspam',), {}),
'spamspamspam'),
((get_call_func_closure, (42,), {}),
...),
((get_call_func_closure(42), (), {}),
42),
((Spam.noop, (), {}),
None),
((Spam.from_values, (), {}),
None),
((Spam.from_values, (1, 2, 3), {}),
Spam((1, 2, 3)),
((Spam, ('???'), {}),
Spam('???')),
((Spam(101), (), {}),
101),
((Spam(10101).run, (), {}),
10101),
((call_func_complex, ('ident', 'spam'), {}),
'spam'),
((call_func_complex, ('full-ident', 'spam'), {}),
('spam', (), {})),
((call_func_complex, ('full-ident', 'spam', 'ham'), {'eggs': '!!!'}),
('spam', ('ham',), {'eggs': '!!!'})),
((call_func_complex, ('globals',), {}),
'test.test_interpreters.test_api'),
((call_func_complex, ('interpid',), {}),
interp.id),
((call_func_complex, ('closure',), {'value': '~~~'}),
'~~~'),
((call_func_complex, ('custom', 'spam!'), {}),
Spam('spam!')),
((call_func_complex, ('custom-inner', 'eggs!'), {}),
...),
]):
with self.subTest(f'success case #{i+1}'):
res = interp.call(callable, args, kwargs)
self.assertEqual(res, expected)

for i, ((callable, args, kwargs), expected) in enumerate([
((call_func_failure, (), {}),
Exception),
((call_func_complex, ('???',), {exc=ValueError('spam')}),
ValueError),
]):
with self.subTest(f'failure case #{i+1}'):
with self.assertRaises(expected):
interp.call(callable, args, kwargs)

def test_call_in_thread(self):
interp = interpreters.create()

for i, (callable, args, kwargs) in enumerate([
(call_func_noop, (), {}),
(call_func_return_shareable, (), {}),
(call_func_return_not_shareable, (), {}),
(call_func_ident, ('spamspamspam',), {}),
(get_call_func_closure, (42,), {}),
(get_call_func_closure(42), (), {}),
(Spam.noop, (), {}),
(Spam.from_values, (), {}),
(Spam.from_values, (1, 2, 3), {}),
(Spam, ('???'), {}),
(Spam(101), (), {}),
(Spam(10101).run, (), {}),
(call_func_complex, ('ident', 'spam'), {}),
(call_func_complex, ('full-ident', 'spam'), {}),
(call_func_complex, ('full-ident', 'spam', 'ham'), {'eggs': '!!!'}),
(call_func_complex, ('globals',), {}),
(call_func_complex, ('interpid',), {}),
(call_func_complex, ('closure',), {'value': '~~~'}),
(call_func_complex, ('custom', 'spam!'), {}),
(call_func_complex, ('custom-inner', 'eggs!'), {}),
]):
with self.subTest(f'success case #{i+1}'):
t = interp.call_in_thread(callable, args, kwargs)
t.join()

self.assertTrue(caught)
except BaseException:
threading.excepthook = threading.__excepthook__
for i, (callable, args, kwargs) in enumerate([
(call_func_failure, (), {}),
(call_func_complex, ('???',), {exc=ValueError('spam')}),
]):
with self.subTest(f'failure case #{i+1}'):
t = interp.call_in_thread(callable, args, kwargs)
t.join()


class TestIsShareable(TestBase):
Expand Down