Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[3.9] bpo-41631: _ast module uses again a global state (GH-21961) #22258

Merged
merged 1 commit into from
Sep 15, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Include/internal/pycore_pylifecycle.h
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ extern void _PyFaulthandler_Fini(void);
extern void _PyHash_Fini(void);
extern void _PyTraceMalloc_Fini(void);
extern void _PyWarnings_Fini(PyInterpreterState *interp);
extern void _PyAST_Fini();

extern PyStatus _PyGILState_Init(PyThreadState *tstate);
extern void _PyGILState_Fini(PyThreadState *tstate);
Expand Down
37 changes: 22 additions & 15 deletions Lib/ast.py
Original file line number Diff line number Diff line change
Expand Up @@ -497,18 +497,20 @@ def generic_visit(self, node):
return node


# The following code is for backward compatibility.
# It will be removed in future.
# If the ast module is loaded more than once, only add deprecated methods once
if not hasattr(Constant, 'n'):
# The following code is for backward compatibility.
# It will be removed in future.

def _getter(self):
"""Deprecated. Use value instead."""
return self.value
def _getter(self):
"""Deprecated. Use value instead."""
return self.value

def _setter(self, value):
self.value = value
def _setter(self, value):
self.value = value

Constant.n = property(_getter, _setter)
Constant.s = property(_getter, _setter)
Constant.n = property(_getter, _setter)
Constant.s = property(_getter, _setter)

class _ABC(type):

Expand Down Expand Up @@ -600,14 +602,19 @@ class ExtSlice(slice):
def __new__(cls, dims=(), **kwargs):
return Tuple(list(dims), Load(), **kwargs)

def _dims_getter(self):
"""Deprecated. Use elts instead."""
return self.elts
# If the ast module is loaded more than once, only add deprecated methods once
if not hasattr(Tuple, 'dims'):
# The following code is for backward compatibility.
# It will be removed in future.

def _dims_setter(self, value):
self.elts = value
def _dims_getter(self):
"""Deprecated. Use elts instead."""
return self.elts

Tuple.dims = property(_dims_getter, _dims_setter)
def _dims_setter(self, value):
self.elts = value

Tuple.dims = property(_dims_getter, _dims_setter)

class Suite(mod):
"""Deprecated AST node class. Unused in Python 3."""
Expand Down
84 changes: 84 additions & 0 deletions Lib/test/test_ast.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import ast
import builtins
import dis
import os
import sys
import types
import unittest
import warnings
import weakref
Expand Down Expand Up @@ -1945,6 +1947,88 @@ def visit_Ellipsis(self, node):
])


@support.cpython_only
class ModuleStateTests(unittest.TestCase):
# bpo-41194, bpo-41261, bpo-41631: The _ast module uses a global state.

def check_ast_module(self):
# Check that the _ast module still works as expected
code = 'x + 1'
filename = '<string>'
mode = 'eval'

# Create _ast.AST subclasses instances
ast_tree = compile(code, filename, mode, flags=ast.PyCF_ONLY_AST)

# Call PyAST_Check()
code = compile(ast_tree, filename, mode)
self.assertIsInstance(code, types.CodeType)

def test_reload_module(self):
# bpo-41194: Importing the _ast module twice must not crash.
with support.swap_item(sys.modules, '_ast', None):
del sys.modules['_ast']
import _ast as ast1

del sys.modules['_ast']
import _ast as ast2

self.check_ast_module()

# Unloading the two _ast module instances must not crash.
del ast1
del ast2
support.gc_collect()

self.check_ast_module()

def test_sys_modules(self):
# bpo-41631: Test reproducing a Mercurial crash when PyAST_Check()
# imported the _ast module internally.
lazy_mod = object()

def my_import(name, *args, **kw):
sys.modules[name] = lazy_mod
return lazy_mod

with support.swap_item(sys.modules, '_ast', None):
del sys.modules['_ast']

with support.swap_attr(builtins, '__import__', my_import):
# Test that compile() does not import the _ast module
self.check_ast_module()
self.assertNotIn('_ast', sys.modules)

# Sanity check of the test itself
import _ast
self.assertIs(_ast, lazy_mod)

def test_subinterpreter(self):
# bpo-41631: Importing and using the _ast module in a subinterpreter
# must not crash.
code = dedent('''
import _ast
import ast
import gc
import sys
import types

# Create _ast.AST subclasses instances and call PyAST_Check()
ast_tree = compile('x+1', '<string>', 'eval',
flags=ast.PyCF_ONLY_AST)
code = compile(ast_tree, 'string', 'eval')
if not isinstance(code, types.CodeType):
raise AssertionError

# Unloading the _ast module must not crash.
del ast, _ast
del sys.modules['ast'], sys.modules['_ast']
gc.collect()
''')
res = support.run_in_subinterp(code)
self.assertEqual(res, 0)


def main():
if __name__ != '__main__':
return
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
The ``_ast`` module uses again a global state. Using a module state per module
instance is causing subtle practical problems. For example, the Mercurial
project replaces the ``__import__()`` function to implement lazy import,
whereas Python expected that ``import _ast`` always return a fully initialized
``_ast`` module.
61 changes: 20 additions & 41 deletions Parser/asdl_c.py
Original file line number Diff line number Diff line change
Expand Up @@ -1089,11 +1089,9 @@ def visitModule(self, mod):
static struct PyModuleDef _astmodule = {
PyModuleDef_HEAD_INIT,
.m_name = "_ast",
.m_size = sizeof(astmodulestate),
// The _ast module uses a global state (global_ast_state).
.m_size = 0,
.m_slots = astmodule_slots,
.m_traverse = astmodule_traverse,
.m_clear = astmodule_clear,
.m_free = astmodule_free,
};

PyMODINIT_FUNC
Expand Down Expand Up @@ -1374,59 +1372,40 @@ def generate_module_def(f, mod):
f.write(' PyObject *' + s + ';\n')
f.write('} astmodulestate;\n\n')
f.write("""
static astmodulestate*
get_ast_state(PyObject *module)
{
void *state = PyModule_GetState(module);
assert(state != NULL);
return (astmodulestate*)state;
}
// Forward declaration
static int init_types(astmodulestate *state);

// bpo-41194, bpo-41261, bpo-41631: The _ast module uses a global state.
static astmodulestate global_ast_state = {0};

static astmodulestate*
get_global_ast_state(void)
{
_Py_IDENTIFIER(_ast);
PyObject *name = _PyUnicode_FromId(&PyId__ast); // borrowed reference
if (name == NULL) {
astmodulestate* state = &global_ast_state;
if (!init_types(state)) {
return NULL;
}
PyObject *module = PyImport_GetModule(name);
if (module == NULL) {
if (PyErr_Occurred()) {
return NULL;
}
module = PyImport_Import(name);
if (module == NULL) {
return NULL;
}
}
astmodulestate *state = get_ast_state(module);
Py_DECREF(module);
return state;
}

static int astmodule_clear(PyObject *module)
static astmodulestate*
get_ast_state(PyObject* Py_UNUSED(module))
{
astmodulestate *state = get_ast_state(module);
""")
for s in module_state:
f.write(" Py_CLEAR(state->" + s + ');\n')
f.write("""
return 0;
astmodulestate* state = get_global_ast_state();
// get_ast_state() must only be called after _ast module is imported,
// and astmodule_exec() calls init_types()
assert(state != NULL);
return state;
}

static int astmodule_traverse(PyObject *module, visitproc visit, void* arg)
void _PyAST_Fini()
{
astmodulestate *state = get_ast_state(module);
astmodulestate* state = &global_ast_state;
""")
for s in module_state:
f.write(" Py_VISIT(state->" + s + ');\n')
f.write(" Py_CLEAR(state->" + s + ');\n')
f.write("""
return 0;
}

static void astmodule_free(void* module) {
astmodule_clear((PyObject*)module);
state->initialized = 0;
}

""")
Expand Down
Loading