@@ -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 ):
0 commit comments