Skip to content

Commit 1b767fc

Browse files
committed
Deprecate support for instantiating abstract AST nodes
1 parent 3663b2a commit 1b767fc

File tree

7 files changed

+143
-9
lines changed

7 files changed

+143
-9
lines changed

Doc/library/ast.rst

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ Node classes
4343

4444
.. class:: AST
4545

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

164+
.. deprecated-removed:: next 3.20
165+
166+
In the :ref:`grammar above <abstract-grammar>`, the AST node classes that
167+
correspond to production rules with variants (aka "sums") are abstract
168+
classes. Previous versions of Python allowed for the creation of direct
169+
instances of these abstract node classes. This behavior is deprecated and
170+
will be removed in Python 3.20.
171+
172+
164173
.. note::
165174
The descriptions of the specific node classes displayed here
166175
were initially adapted from the fantastic `Green Tree

Doc/whatsnew/3.15.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -408,6 +408,14 @@ module_name
408408
Deprecated
409409
==========
410410

411+
ast
412+
---
413+
414+
* Creating instances of abstract AST nodes (such as :class:`ast.AST`
415+
or :class:`!ast.expr`) is deprecated and will raise an error in Python 3.20.
416+
(Contributed by Brian Schubert in :gh:`116021`.)
417+
418+
411419
hashlib
412420
-------
413421

Include/internal/pycore_ast_state.h

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Lib/test/test_ast/test_ast.py

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,9 @@ def _assertTrueorder(self, ast_node, parent_pos):
8484
self.assertEqual(ast_node._fields, ast_node.__match_args__)
8585

8686
def test_AST_objects(self):
87-
x = ast.AST()
87+
# Directly instantiating abstract node class AST is allowed (but deprecated)
88+
with self.assertWarns(DeprecationWarning):
89+
x = ast.AST()
8890
self.assertEqual(x._fields, ())
8991
x.foobar = 42
9092
self.assertEqual(x.foobar, 42)
@@ -93,7 +95,7 @@ def test_AST_objects(self):
9395
with self.assertRaises(AttributeError):
9496
x.vararg
9597

96-
with self.assertRaises(TypeError):
98+
with self.assertRaises(TypeError), self.assertWarns(DeprecationWarning):
9799
# "ast.AST constructor takes 0 positional arguments"
98100
ast.AST(2)
99101

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

110112
msg = "type object 'ast.AST' has no attribute '_fields'"
111113
# Both examples used to crash:
112-
with self.assertRaisesRegex(AttributeError, msg):
114+
with self.assertRaisesRegex(AttributeError, msg), self.assertWarns(DeprecationWarning):
113115
ast.AST(arg1=123)
114-
with self.assertRaisesRegex(AttributeError, msg):
116+
with self.assertRaisesRegex(AttributeError, msg), self.assertWarns(DeprecationWarning):
115117
ast.AST()
116118

117-
def test_AST_garbage_collection(self):
119+
def test_node_garbage_collection(self):
118120
class X:
119121
pass
120-
a = ast.AST()
122+
a = ast.Module()
121123
a.x = X()
122124
a.x.a = a
123125
ref = weakref.ref(a.x)
@@ -427,7 +429,12 @@ def _construct_ast_class(self, cls):
427429
elif typ is object:
428430
kwargs[name] = b'capybara'
429431
elif isinstance(typ, type) and issubclass(typ, ast.AST):
430-
kwargs[name] = self._construct_ast_class(typ)
432+
if typ._is_abstract():
433+
# Use an arbitrary concrete subclass
434+
concrete = next(sub for sub in typ.__subclasses__() if not sub._is_abstract())
435+
kwargs[name] = self._construct_ast_class(concrete)
436+
else:
437+
kwargs[name] = self._construct_ast_class(typ)
431438
return cls(**kwargs)
432439

433440
def test_arguments(self):
@@ -573,14 +580,20 @@ def test_nodeclasses(self):
573580
x = ast.BinOp(1, 2, 3, foobarbaz=42)
574581
self.assertEqual(x.foobarbaz, 42)
575582

583+
# Directly instantiating abstract node types is allowed (but deprecated)
584+
with self.assertWarns(DeprecationWarning):
585+
ast.stmt()
586+
576587
def test_no_fields(self):
577588
# this used to fail because Sub._fields was None
578589
x = ast.Sub()
579590
self.assertEqual(x._fields, ())
580591

581592
def test_invalid_sum(self):
582593
pos = dict(lineno=2, col_offset=3)
583-
m = ast.Module([ast.Expr(ast.expr(**pos), **pos)], [])
594+
with self.assertWarns(DeprecationWarning):
595+
# Creating instances of ast.expr is deprecated
596+
m = ast.Module([ast.Expr(ast.expr(**pos), **pos)], [])
584597
with self.assertRaises(TypeError) as cm:
585598
compile(m, "<test>", "exec")
586599
self.assertIn("but got expr()", str(cm.exception))
@@ -1140,6 +1153,9 @@ def do(cls):
11401153
return
11411154
if cls is ast.Index:
11421155
return
1156+
# Don't attempt to create instances of abstract AST nodes
1157+
if cls._is_abstract():
1158+
return
11431159

11441160
yield cls
11451161
for sub in cls.__subclasses__():
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Support for creating instances of abstract AST nodes from the :mod:`ast` module
2+
is deprecated and scheduled for removal in Python 3.20. Patch by Brian Schubert.

Parser/asdl_c.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -881,6 +881,21 @@ def visitModule(self, mod):
881881
return -1;
882882
}
883883
884+
int contains = PySet_Contains(state->abstract_types, (PyObject *)Py_TYPE(self));
885+
if (contains == -1) {
886+
return -1;
887+
}
888+
else if (contains == 1) {
889+
if (PyErr_WarnFormat(
890+
PyExc_DeprecationWarning, 1,
891+
"Instantiating abstract AST node class %T is deprecated. "
892+
"This will become an error in Python 3.20",
893+
self
894+
) < 0) {
895+
return -1;
896+
}
897+
}
898+
884899
Py_ssize_t i, numfields = 0;
885900
int res = -1;
886901
PyObject *key, *value, *fields, *attributes = NULL, *remaining_fields = NULL;
@@ -1443,6 +1458,23 @@ def visitModule(self, mod):
14431458
return result;
14441459
}
14451460
1461+
/* Helper for checking if a node class is abstract in the tests. */
1462+
PyObject *
1463+
ast_is_abstract(PyObject *cls, void *Py_UNUSED(ignored)) {
1464+
struct ast_state *state = get_ast_state();
1465+
if (state == NULL) {
1466+
return NULL;
1467+
}
1468+
int contains = PySet_Contains(state->abstract_types, cls);
1469+
if (contains == -1) {
1470+
return NULL;
1471+
}
1472+
else if (contains == 1) {
1473+
Py_RETURN_TRUE;
1474+
}
1475+
Py_RETURN_FALSE;
1476+
}
1477+
14461478
static PyMemberDef ast_type_members[] = {
14471479
{"__dictoffset__", Py_T_PYSSIZET, offsetof(AST_object, dict), Py_READONLY},
14481480
{NULL} /* Sentinel */
@@ -1454,6 +1486,7 @@ def visitModule(self, mod):
14541486
PyDoc_STR("__replace__($self, /, **fields)\\n--\\n\\n"
14551487
"Return a copy of the AST node with new values "
14561488
"for the specified fields.")},
1489+
{"_is_abstract", _PyCFunction_CAST(ast_is_abstract), METH_CLASS | METH_NOARGS, NULL},
14571490
{NULL}
14581491
};
14591492
@@ -1887,6 +1920,13 @@ def visitModule(self, mod):
18871920
if (!state->AST_type) {
18881921
return -1;
18891922
}
1923+
state->abstract_types = PySet_New(NULL);
1924+
if (!state->abstract_types) {
1925+
return -1;
1926+
}
1927+
if (PySet_Add(state->abstract_types, state->AST_type) < 0) {
1928+
return -1;
1929+
}
18901930
if (add_ast_fields(state) < 0) {
18911931
return -1;
18921932
}
@@ -1928,6 +1968,7 @@ def visitSum(self, sum, name):
19281968
(name, name, len(sum.attributes)), 1)
19291969
else:
19301970
self.emit("if (add_attributes(state, state->%s_type, NULL, 0) < 0) return -1;" % name, 1)
1971+
self.emit("if (PySet_Add(state->abstract_types, state->%s_type) < 0) return -1;" % name, 1)
19311972
self.emit_defaults(name, sum.attributes, 1)
19321973
simple = is_simple(sum)
19331974
for t in sum.types:
@@ -2289,6 +2330,7 @@ def generate_module_def(mod, metadata, f, internal_h):
22892330
"%s_type" % type
22902331
for type in metadata.types
22912332
)
2333+
module_state.add("abstract_types")
22922334

22932335
state_strings = sorted(state_strings)
22942336
module_state = sorted(module_state)

Python/Python-ast.c

Lines changed: 56 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)