@@ -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