mirror of
https://github.com/python/cpython.git
synced 2025-11-07 09:02:02 +00:00
[3.14] gh-132775: Fix Interpreter.call() __main__ Visibility (gh-135638)
As noted in the new tests, there are a few situations we must carefully accommodate
for functions that get pickled during interp.call(). We do so by running the script
from the main interpreter's __main__ module in a hidden module in the other
interpreter. That hidden module is used as the function __globals__.
(cherry picked from commit 269e19e0a7, AKA gh-135595)
Co-authored-by: Eric Snow <ericsnowcurrently@gmail.com>
This commit is contained in:
parent
8ec4186b25
commit
2c29ee835a
4 changed files with 419 additions and 246 deletions
|
|
@ -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):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue