@@ -1356,6 +1356,187 @@ def {funcname}():
1356
1356
with self .assertRaises (interpreters .NotShareableError ):
1357
1357
interp .call (defs .spam_returns_arg , arg )
1358
1358
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
+
1359
1540
def test_raises (self ):
1360
1541
interp = interpreters .create ()
1361
1542
with self .assertRaises (ExecutionFailed ):
0 commit comments