Skip to content

Commit 2b1c0a7

Browse files
[3.14] gh-135443: Sometimes Fall Back to __main__.__dict__ For Globals (gh-135593)
For several builtin functions, we now fall back to __main__.__dict__ for the globals when there is no current frame and _PyInterpreterState_IsRunningMain() returns true. This allows those functions to be run with Interpreter.call(). The affected builtins: * exec() * eval() * globals() * locals() * vars() * dir() We take a similar approach with "stateless" functions, which don't use any global variables. (cherry picked from commit a450a0d, AKA gh-135491) Co-authored-by: Eric Snow <[email protected]>
1 parent 7aeddea commit 2b1c0a7

File tree

7 files changed

+392
-66
lines changed

7 files changed

+392
-66
lines changed

Include/internal/pycore_ceval.h

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,16 @@ static inline void _Py_LeaveRecursiveCall(void) {
239239

240240
extern _PyInterpreterFrame* _PyEval_GetFrame(void);
241241

242+
extern PyObject * _PyEval_GetGlobalsFromRunningMain(PyThreadState *);
243+
extern int _PyEval_EnsureBuiltins(
244+
PyThreadState *,
245+
PyObject *,
246+
PyObject **p_builtins);
247+
extern int _PyEval_EnsureBuiltinsWithModule(
248+
PyThreadState *,
249+
PyObject *,
250+
PyObject **p_builtins);
251+
242252
PyAPI_FUNC(PyObject *)_Py_MakeCoro(PyFunctionObject *func);
243253

244254
/* Handle signals, pending calls, GIL drop request

Lib/test/test_interpreters/test_api.py

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1414,6 +1414,113 @@ def test_call_invalid(self):
14141414
with self.assertRaises(interpreters.NotShareableError):
14151415
interp.call(func, op, 'eggs!')
14161416

1417+
def test_callable_requires_frame(self):
1418+
# There are various functions that require a current frame.
1419+
interp = interpreters.create()
1420+
for call, expected in [
1421+
((eval, '[1, 2, 3]'),
1422+
[1, 2, 3]),
1423+
((eval, 'sum([1, 2, 3])'),
1424+
6),
1425+
((exec, '...'),
1426+
None),
1427+
]:
1428+
with self.subTest(str(call)):
1429+
res = interp.call(*call)
1430+
self.assertEqual(res, expected)
1431+
1432+
result_not_pickleable = [
1433+
globals,
1434+
locals,
1435+
vars,
1436+
]
1437+
for func, expectedtype in {
1438+
globals: dict,
1439+
locals: dict,
1440+
vars: dict,
1441+
dir: list,
1442+
}.items():
1443+
with self.subTest(str(func)):
1444+
if func in result_not_pickleable:
1445+
with self.assertRaises(interpreters.NotShareableError):
1446+
interp.call(func)
1447+
else:
1448+
res = interp.call(func)
1449+
self.assertIsInstance(res, expectedtype)
1450+
self.assertIn('__builtins__', res)
1451+
1452+
def test_globals_from_builtins(self):
1453+
# The builtins exec(), eval(), globals(), locals(), vars(),
1454+
# and dir() each runs relative to the target interpreter's
1455+
# __main__ module, when called directly. However,
1456+
# globals(), locals(), and vars() don't work when called
1457+
# directly so we don't check them.
1458+
from _frozen_importlib import BuiltinImporter
1459+
interp = interpreters.create()
1460+
1461+
names = interp.call(dir)
1462+
self.assertEqual(names, [
1463+
'__builtins__',
1464+
'__doc__',
1465+
'__loader__',
1466+
'__name__',
1467+
'__package__',
1468+
'__spec__',
1469+
])
1470+
1471+
values = {name: interp.call(eval, name)
1472+
for name in names if name != '__builtins__'}
1473+
self.assertEqual(values, {
1474+
'__name__': '__main__',
1475+
'__doc__': None,
1476+
'__spec__': None, # It wasn't imported, so no module spec?
1477+
'__package__': None,
1478+
'__loader__': BuiltinImporter,
1479+
})
1480+
with self.assertRaises(ExecutionFailed):
1481+
interp.call(eval, 'spam'),
1482+
1483+
interp.call(exec, f'assert dir() == {names}')
1484+
1485+
# Update the interpreter's __main__.
1486+
interp.prepare_main(spam=42)
1487+
expected = names + ['spam']
1488+
1489+
names = interp.call(dir)
1490+
self.assertEqual(names, expected)
1491+
1492+
value = interp.call(eval, 'spam')
1493+
self.assertEqual(value, 42)
1494+
1495+
interp.call(exec, f'assert dir() == {expected}, dir()')
1496+
1497+
def test_globals_from_stateless_func(self):
1498+
# A stateless func, which doesn't depend on any globals,
1499+
# doesn't go through pickle, so it runs in __main__.
1500+
def set_global(name, value):
1501+
globals()[name] = value
1502+
1503+
def get_global(name):
1504+
return globals().get(name)
1505+
1506+
interp = interpreters.create()
1507+
1508+
modname = interp.call(get_global, '__name__')
1509+
self.assertEqual(modname, '__main__')
1510+
1511+
res = interp.call(get_global, 'spam')
1512+
self.assertIsNone(res)
1513+
1514+
interp.exec('spam = True')
1515+
res = interp.call(get_global, 'spam')
1516+
self.assertTrue(res)
1517+
1518+
interp.call(set_global, 'spam', 42)
1519+
res = interp.call(get_global, 'spam')
1520+
self.assertEqual(res, 42)
1521+
1522+
interp.exec('assert spam == 42, repr(spam)')
1523+
14171524
def test_call_in_thread(self):
14181525
interp = interpreters.create()
14191526

Objects/object.c

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1973,9 +1973,25 @@ _dir_locals(void)
19731973
PyObject *names;
19741974
PyObject *locals;
19751975

1976-
locals = _PyEval_GetFrameLocals();
1977-
if (locals == NULL)
1976+
if (_PyEval_GetFrame() != NULL) {
1977+
locals = _PyEval_GetFrameLocals();
1978+
}
1979+
else {
1980+
PyThreadState *tstate = _PyThreadState_GET();
1981+
locals = _PyEval_GetGlobalsFromRunningMain(tstate);
1982+
if (locals == NULL) {
1983+
if (!_PyErr_Occurred(tstate)) {
1984+
locals = _PyEval_GetFrameLocals();
1985+
assert(_PyErr_Occurred(tstate));
1986+
}
1987+
}
1988+
else {
1989+
Py_INCREF(locals);
1990+
}
1991+
}
1992+
if (locals == NULL) {
19781993
return NULL;
1994+
}
19791995

19801996
names = PyMapping_Keys(locals);
19811997
Py_DECREF(locals);

0 commit comments

Comments
 (0)