Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 2c29ee8

Browse files
[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 269e19e, AKA gh-135595) Co-authored-by: Eric Snow <[email protected]>
1 parent 8ec4186 commit 2c29ee8

File tree

4 files changed

+400
-227
lines changed

4 files changed

+400
-227
lines changed

Lib/test/test_interpreters/test_api.py

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1356,6 +1356,187 @@ def {funcname}():
13561356
with self.assertRaises(interpreters.NotShareableError):
13571357
interp.call(defs.spam_returns_arg, arg)
13581358

1359+
def test_func_in___main___hidden(self):
1360+
# When a top-level function that uses global variables is called
1361+
# through Interpreter.call(), it will be pickled, sent over,
1362+
# and unpickled. That requires that it be found in the other
1363+
# interpreter's __main__ module. However, the original script
1364+
# that defined the function is only run in the main interpreter,
1365+
# so pickle.loads() would normally fail.
1366+
#
1367+
# We work around this by running the script in the other
1368+
# interpreter. However, this is a one-off solution for the sake
1369+
# of unpickling, so we avoid modifying that interpreter's
1370+
# __main__ module by running the script in a hidden module.
1371+
#
1372+
# In this test we verify that the function runs with the hidden
1373+
# module as its __globals__ when called in the other interpreter,
1374+
# and that the interpreter's __main__ module is unaffected.
1375+
text = dedent("""
1376+
eggs = True
1377+
1378+
def spam(*, explicit=False):
1379+
if explicit:
1380+
import __main__
1381+
ns = __main__.__dict__
1382+
else:
1383+
# For now we have to have a LOAD_GLOBAL in the
1384+
# function in order for globals() to actually return
1385+
# spam.__globals__. Maybe it doesn't go through pickle?
1386+
# XXX We will fix this later.
1387+
spam
1388+
ns = globals()
1389+
1390+
func = ns.get('spam')
1391+
return [
1392+
id(ns),
1393+
ns.get('__name__'),
1394+
ns.get('__file__'),
1395+
id(func),
1396+
None if func is None else repr(func),
1397+
ns.get('eggs'),
1398+
ns.get('ham'),
1399+
]
1400+
1401+
if __name__ == "__main__":
1402+
from concurrent import interpreters
1403+
interp = interpreters.create()
1404+
1405+
ham = True
1406+
print([
1407+
[
1408+
spam(explicit=True),
1409+
spam(),
1410+
],
1411+
[
1412+
interp.call(spam, explicit=True),
1413+
interp.call(spam),
1414+
],
1415+
])
1416+
""")
1417+
with os_helper.temp_dir() as tempdir:
1418+
filename = script_helper.make_script(tempdir, 'my-script', text)
1419+
res = script_helper.assert_python_ok(filename)
1420+
stdout = res.out.decode('utf-8').strip()
1421+
local, remote = eval(stdout)
1422+
1423+
# In the main interpreter.
1424+
main, unpickled = local
1425+
nsid, _, _, funcid, func, _, _ = main
1426+
self.assertEqual(main, [
1427+
nsid,
1428+
'__main__',
1429+
filename,
1430+
funcid,
1431+
func,
1432+
True,
1433+
True,
1434+
])
1435+
self.assertIsNot(func, None)
1436+
self.assertRegex(func, '^<function spam at 0x.*>$')
1437+
self.assertEqual(unpickled, main)
1438+
1439+
# In the subinterpreter.
1440+
main, unpickled = remote
1441+
nsid1, _, _, funcid1, _, _, _ = main
1442+
self.assertEqual(main, [
1443+
nsid1,
1444+
'__main__',
1445+
None,
1446+
funcid1,
1447+
None,
1448+
None,
1449+
None,
1450+
])
1451+
nsid2, _, _, funcid2, func, _, _ = unpickled
1452+
self.assertEqual(unpickled, [
1453+
nsid2,
1454+
'<fake __main__>',
1455+
filename,
1456+
funcid2,
1457+
func,
1458+
True,
1459+
None,
1460+
])
1461+
self.assertIsNot(func, None)
1462+
self.assertRegex(func, '^<function spam at 0x.*>$')
1463+
self.assertNotEqual(nsid2, nsid1)
1464+
self.assertNotEqual(funcid2, funcid1)
1465+
1466+
def test_func_in___main___uses_globals(self):
1467+
# See the note in test_func_in___main___hidden about pickle
1468+
# and the __main__ module.
1469+
#
1470+
# Additionally, the solution to that problem must provide
1471+
# for global variables on which a pickled function might rely.
1472+
#
1473+
# To check that, we run a script that has two global functions
1474+
# and a global variable in the __main__ module. One of the
1475+
# functions sets the global variable and the other returns
1476+
# the value.
1477+
#
1478+
# The script calls those functions multiple times in another
1479+
# interpreter, to verify the following:
1480+
#
1481+
# * the global variable is properly initialized
1482+
# * the global variable retains state between calls
1483+
# * the setter modifies that persistent variable
1484+
# * the getter uses the variable
1485+
# * the calls in the other interpreter do not modify
1486+
# the main interpreter
1487+
# * those calls don't modify the interpreter's __main__ module
1488+
# * the functions and variable do not actually show up in the
1489+
# other interpreter's __main__ module
1490+
text = dedent("""
1491+
count = 0
1492+
1493+
def inc(x=1):
1494+
global count
1495+
count += x
1496+
1497+
def get_count():
1498+
return count
1499+
1500+
if __name__ == "__main__":
1501+
counts = []
1502+
results = [count, counts]
1503+
1504+
from concurrent import interpreters
1505+
interp = interpreters.create()
1506+
1507+
val = interp.call(get_count)
1508+
counts.append(val)
1509+
1510+
interp.call(inc)
1511+
val = interp.call(get_count)
1512+
counts.append(val)
1513+
1514+
interp.call(inc, 3)
1515+
val = interp.call(get_count)
1516+
counts.append(val)
1517+
1518+
results.append(count)
1519+
1520+
modified = {name: interp.call(eval, f'{name!r} in vars()')
1521+
for name in ('count', 'inc', 'get_count')}
1522+
results.append(modified)
1523+
1524+
print(results)
1525+
""")
1526+
with os_helper.temp_dir() as tempdir:
1527+
filename = script_helper.make_script(tempdir, 'my-script', text)
1528+
res = script_helper.assert_python_ok(filename)
1529+
stdout = res.out.decode('utf-8').strip()
1530+
before, counts, after, modified = eval(stdout)
1531+
self.assertEqual(modified, {
1532+
'count': False,
1533+
'inc': False,
1534+
'get_count': False,
1535+
})
1536+
self.assertEqual(before, 0)
1537+
self.assertEqual(after, 0)
1538+
self.assertEqual(counts, [0, 1, 4])
1539+
13591540
def test_raises(self):
13601541
interp = interpreters.create()
13611542
with self.assertRaises(ExecutionFailed):

Modules/_interpretersmodule.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -601,6 +601,7 @@ _make_call(struct interp_call *call,
601601
unwrap_not_shareable(tstate, failure);
602602
return -1;
603603
}
604+
assert(!_PyErr_Occurred(tstate));
604605

605606
// Make the call.
606607
PyObject *resobj = PyObject_Call(func, args, kwargs);

0 commit comments

Comments
 (0)