Skip to content

gh-132775: Fix Interpreter.call() __main__ Visibility #135595

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from all commits
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
181 changes: 181 additions & 0 deletions Lib/test/test_interpreters/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -1356,6 +1356,187 @@ def {funcname}():
with self.assertRaises(interpreters.NotShareableError):
interp.call(defs.spam_returns_arg, arg)

def test_func_in___main___hidden(self):
# When a top-level function that uses global variables is called
# through Interpreter.call(), it will be pickled, sent over,
# and unpickled. That requires that it be found in the other
# interpreter's __main__ module. However, the original script
# that defined the function is only run in the main interpreter,
# so pickle.loads() would normally fail.
#
# We work around this by running the script in the other
# interpreter. However, this is a one-off solution for the sake
# of unpickling, so we avoid modifying that interpreter's
# __main__ module by running the script in a hidden module.
#
# In this test we verify that the function runs with the hidden
# module as its __globals__ when called in the other interpreter,
# and that the interpreter's __main__ module is unaffected.
text = dedent("""
eggs = True

def spam(*, explicit=False):
if explicit:
import __main__
ns = __main__.__dict__
else:
# For now we have to have a LOAD_GLOBAL in the
# function in order for globals() to actually return
# spam.__globals__. Maybe it doesn't go through pickle?
# XXX We will fix this later.
spam
ns = globals()

func = ns.get('spam')
return [
id(ns),
ns.get('__name__'),
ns.get('__file__'),
id(func),
None if func is None else repr(func),
ns.get('eggs'),
ns.get('ham'),
]

if __name__ == "__main__":
from concurrent import interpreters
interp = interpreters.create()

ham = True
print([
[
spam(explicit=True),
spam(),
],
[
interp.call(spam, explicit=True),
interp.call(spam),
],
])
""")
with os_helper.temp_dir() as tempdir:
filename = script_helper.make_script(tempdir, 'my-script', text)
res = script_helper.assert_python_ok(filename)
stdout = res.out.decode('utf-8').strip()
local, remote = eval(stdout)

# In the main interpreter.
main, unpickled = local
nsid, _, _, funcid, func, _, _ = main
self.assertEqual(main, [
nsid,
'__main__',
filename,
funcid,
func,
True,
True,
])
self.assertIsNot(func, None)
self.assertRegex(func, '^<function spam at 0x.*>$')
self.assertEqual(unpickled, main)

# In the subinterpreter.
main, unpickled = remote
nsid1, _, _, funcid1, _, _, _ = main
self.assertEqual(main, [
nsid1,
'__main__',
None,
funcid1,
None,
None,
None,
])
nsid2, _, _, funcid2, func, _, _ = unpickled
self.assertEqual(unpickled, [
nsid2,
'<fake __main__>',
filename,
funcid2,
func,
True,
None,
])
self.assertIsNot(func, None)
self.assertRegex(func, '^<function spam at 0x.*>$')
self.assertNotEqual(nsid2, nsid1)
self.assertNotEqual(funcid2, funcid1)

def test_func_in___main___uses_globals(self):
# See the note in test_func_in___main___hidden about pickle
# and the __main__ module.
#
# Additionally, the solution to that problem must provide
# for global variables on which a pickled function might rely.
#
# To check that, we run a script that has two global functions
# and a global variable in the __main__ module. One of the
# functions sets the global variable and the other returns
# the value.
#
# The script calls those functions multiple times in another
# interpreter, to verify the following:
#
# * the global variable is properly initialized
# * the global variable retains state between calls
# * the setter modifies that persistent variable
# * the getter uses the variable
# * the calls in the other interpreter do not modify
# the main interpreter
# * those calls don't modify the interpreter's __main__ module
# * the functions and variable do not actually show up in the
# other interpreter's __main__ module
text = dedent("""
count = 0

def inc(x=1):
global count
count += x

def get_count():
return count

if __name__ == "__main__":
counts = []
results = [count, counts]

from concurrent import interpreters
interp = interpreters.create()

val = interp.call(get_count)
counts.append(val)

interp.call(inc)
val = interp.call(get_count)
counts.append(val)

interp.call(inc, 3)
val = interp.call(get_count)
counts.append(val)

results.append(count)

modified = {name: interp.call(eval, f'{name!r} in vars()')
for name in ('count', 'inc', 'get_count')}
results.append(modified)

print(results)
""")
with os_helper.temp_dir() as tempdir:
filename = script_helper.make_script(tempdir, 'my-script', text)
res = script_helper.assert_python_ok(filename)
stdout = res.out.decode('utf-8').strip()
before, counts, after, modified = eval(stdout)
self.assertEqual(modified, {
'count': False,
'inc': False,
'get_count': False,
})
self.assertEqual(before, 0)
self.assertEqual(after, 0)
self.assertEqual(counts, [0, 1, 4])

def test_raises(self):
interp = interpreters.create()
with self.assertRaises(ExecutionFailed):
Expand Down
1 change: 1 addition & 0 deletions Modules/_interpretersmodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -601,6 +601,7 @@ _make_call(struct interp_call *call,
unwrap_not_shareable(tstate, failure);
return -1;
}
assert(!_PyErr_Occurred(tstate));

// Make the call.
PyObject *resobj = PyObject_Call(func, args, kwargs);
Expand Down
Loading
Loading