Skip to content

gh-116021: Deprecate support for instantiating abstract AST nodes #137865

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

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
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
11 changes: 10 additions & 1 deletion Doc/library/ast.rst
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ Node classes

.. class:: AST

This is the base of all AST node classes. The actual node classes are
This is the abstract base of all AST node classes. The actual node classes are
derived from the :file:`Parser/Python.asdl` file, which is reproduced
:ref:`above <abstract-grammar>`. They are defined in the :mod:`!_ast` C
module and re-exported in :mod:`ast`.
Expand Down Expand Up @@ -161,6 +161,15 @@ Node classes
match any of the fields of the AST node. This behavior is deprecated and will
be removed in Python 3.15.

.. deprecated-removed:: next 3.20

In the :ref:`grammar above <abstract-grammar>`, the AST node classes that
correspond to production rules with variants (aka "sums") are abstract
classes. Previous versions of Python allowed for the creation of direct
instances of these abstract node classes. This behavior is deprecated and
will be removed in Python 3.20.


.. note::
The descriptions of the specific node classes displayed here
were initially adapted from the fantastic `Green Tree
Expand Down
8 changes: 8 additions & 0 deletions Doc/whatsnew/3.15.rst
Original file line number Diff line number Diff line change
Expand Up @@ -408,6 +408,14 @@ module_name
Deprecated
==========

ast
---

* Creating instances of abstract AST nodes (such as :class:`ast.AST`
or :class:`!ast.expr`) is deprecated and will raise an error in Python 3.20.
(Contributed by Brian Schubert in :gh:`116021`.)


hashlib
-------

Expand Down
1 change: 1 addition & 0 deletions Include/internal/pycore_ast_state.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

32 changes: 24 additions & 8 deletions Lib/test/test_ast/test_ast.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,9 @@ def _assertTrueorder(self, ast_node, parent_pos):
self.assertEqual(ast_node._fields, ast_node.__match_args__)

def test_AST_objects(self):
x = ast.AST()
# Directly instantiating abstract node class AST is allowed (but deprecated)
with self.assertWarns(DeprecationWarning):
x = ast.AST()
self.assertEqual(x._fields, ())
x.foobar = 42
self.assertEqual(x.foobar, 42)
Expand All @@ -93,7 +95,7 @@ def test_AST_objects(self):
with self.assertRaises(AttributeError):
x.vararg

with self.assertRaises(TypeError):
with self.assertRaises(TypeError), self.assertWarns(DeprecationWarning):
# "ast.AST constructor takes 0 positional arguments"
ast.AST(2)

Expand All @@ -109,15 +111,15 @@ def cleanup():

msg = "type object 'ast.AST' has no attribute '_fields'"
# Both examples used to crash:
with self.assertRaisesRegex(AttributeError, msg):
with self.assertRaisesRegex(AttributeError, msg), self.assertWarns(DeprecationWarning):
ast.AST(arg1=123)
with self.assertRaisesRegex(AttributeError, msg):
with self.assertRaisesRegex(AttributeError, msg), self.assertWarns(DeprecationWarning):
ast.AST()

def test_AST_garbage_collection(self):
def test_node_garbage_collection(self):
class X:
pass
a = ast.AST()
a = ast.Module()
a.x = X()
a.x.a = a
ref = weakref.ref(a.x)
Expand Down Expand Up @@ -427,7 +429,12 @@ def _construct_ast_class(self, cls):
elif typ is object:
kwargs[name] = b'capybara'
elif isinstance(typ, type) and issubclass(typ, ast.AST):
kwargs[name] = self._construct_ast_class(typ)
if typ._is_abstract():
# Use an arbitrary concrete subclass
concrete = next(sub for sub in typ.__subclasses__() if not sub._is_abstract())
kwargs[name] = self._construct_ast_class(concrete)
else:
kwargs[name] = self._construct_ast_class(typ)
return cls(**kwargs)

def test_arguments(self):
Expand Down Expand Up @@ -573,14 +580,20 @@ def test_nodeclasses(self):
x = ast.BinOp(1, 2, 3, foobarbaz=42)
self.assertEqual(x.foobarbaz, 42)

# Directly instantiating abstract node types is allowed (but deprecated)
with self.assertWarns(DeprecationWarning):
ast.stmt()

def test_no_fields(self):
# this used to fail because Sub._fields was None
x = ast.Sub()
self.assertEqual(x._fields, ())

def test_invalid_sum(self):
pos = dict(lineno=2, col_offset=3)
m = ast.Module([ast.Expr(ast.expr(**pos), **pos)], [])
with self.assertWarns(DeprecationWarning):
# Creating instances of ast.expr is deprecated
m = ast.Module([ast.Expr(ast.expr(**pos), **pos)], [])
with self.assertRaises(TypeError) as cm:
compile(m, "<test>", "exec")
self.assertIn("but got expr()", str(cm.exception))
Expand Down Expand Up @@ -1140,6 +1153,9 @@ def do(cls):
return
if cls is ast.Index:
return
# Don't attempt to create instances of abstract AST nodes
if cls._is_abstract():
return

yield cls
for sub in cls.__subclasses__():
Expand Down
3 changes: 2 additions & 1 deletion Lib/test/test_sys.py
Original file line number Diff line number Diff line change
Expand Up @@ -1877,7 +1877,8 @@ def test_pythontypes(self):
check = self.check_sizeof
# _ast.AST
import _ast
check(_ast.AST(), size('P'))
with self.assertWarns(DeprecationWarning):
check(_ast.AST(), size('P'))
Comment on lines -1880 to +1881
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should probably be rewritten, but I'm not sure I understand the goal of this test well enough to do that

try:
raise TypeError
except TypeError as e:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Support for creating instances of abstract AST nodes from the :mod:`ast` module
is deprecated and scheduled for removal in Python 3.20. Patch by Brian Schubert.
42 changes: 42 additions & 0 deletions Parser/asdl_c.py
Original file line number Diff line number Diff line change
Expand Up @@ -881,6 +881,21 @@ def visitModule(self, mod):
return -1;
}

int contains = PySet_Contains(state->abstract_types, (PyObject *)Py_TYPE(self));
if (contains == -1) {
return -1;
}
else if (contains == 1) {
if (PyErr_WarnFormat(
PyExc_DeprecationWarning, 1,
"Instantiating abstract AST node class %T is deprecated. "
"This will become an error in Python 3.20",
self
) < 0) {
return -1;
}
}

Py_ssize_t i, numfields = 0;
int res = -1;
PyObject *key, *value, *fields, *attributes = NULL, *remaining_fields = NULL;
Expand Down Expand Up @@ -1443,6 +1458,23 @@ def visitModule(self, mod):
return result;
}

/* Helper for checking if a node class is abstract in the tests. */
static PyObject *
ast_is_abstract(PyObject *cls, void *Py_UNUSED(ignored)) {
struct ast_state *state = get_ast_state();
if (state == NULL) {
return NULL;
}
int contains = PySet_Contains(state->abstract_types, cls);
if (contains == -1) {
return NULL;
}
else if (contains == 1) {
Py_RETURN_TRUE;
}
Py_RETURN_FALSE;
}

static PyMemberDef ast_type_members[] = {
{"__dictoffset__", Py_T_PYSSIZET, offsetof(AST_object, dict), Py_READONLY},
{NULL} /* Sentinel */
Expand All @@ -1454,6 +1486,7 @@ def visitModule(self, mod):
PyDoc_STR("__replace__($self, /, **fields)\\n--\\n\\n"
"Return a copy of the AST node with new values "
"for the specified fields.")},
{"_is_abstract", _PyCFunction_CAST(ast_is_abstract), METH_CLASS | METH_NOARGS, NULL},
{NULL}
};

Expand Down Expand Up @@ -1887,6 +1920,13 @@ def visitModule(self, mod):
if (!state->AST_type) {
return -1;
}
state->abstract_types = PySet_New(NULL);
if (!state->abstract_types) {
return -1;
}
if (PySet_Add(state->abstract_types, state->AST_type) < 0) {
return -1;
}
if (add_ast_fields(state) < 0) {
return -1;
}
Expand Down Expand Up @@ -1928,6 +1968,7 @@ def visitSum(self, sum, name):
(name, name, len(sum.attributes)), 1)
else:
self.emit("if (add_attributes(state, state->%s_type, NULL, 0) < 0) return -1;" % name, 1)
self.emit("if (PySet_Add(state->abstract_types, state->%s_type) < 0) return -1;" % name, 1)
self.emit_defaults(name, sum.attributes, 1)
simple = is_simple(sum)
for t in sum.types:
Expand Down Expand Up @@ -2289,6 +2330,7 @@ def generate_module_def(mod, metadata, f, internal_h):
"%s_type" % type
for type in metadata.types
)
module_state.add("abstract_types")

state_strings = sorted(state_strings)
module_state = sorted(module_state)
Expand Down
Loading
Loading