Skip to content

Commit 32f6da7

Browse files
committed
gh-135797: Add _field_types to abstract AST classes
Classes without fields never got a _field_types dictionary, so the attribute was missing on ast.AST and on the abstract classes such as ast.mod, while typeshed declares it on ast.AST. make_type() now sets an empty dictionary on every class it creates, and ast.AST gets one too; add_ast_annotations() keeps filling in the concrete classes. The constructor relied on the missing attribute to detect user subclasses that declare _fields but no _field_types, and those must keep working as before 3.13. An empty dictionary is treated the same way now, otherwise they would start failing with a missing-field TypeError.
1 parent d3ada45 commit 32f6da7

5 files changed

Lines changed: 98 additions & 16 deletions

File tree

‎Doc/library/ast.rst‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,8 +75,9 @@ Node classes
7575

7676
.. attribute:: _field_types
7777

78-
The :attr:`!_field_types` attribute on each concrete class is a dictionary
79-
mapping field names (as also listed in :attr:`_fields`) to their types.
78+
The :attr:`!_field_types` attribute on each class is a dictionary mapping
79+
field names (as also listed in :attr:`_fields`) to their types. Classes
80+
without fields, such as the abstract classes, have an empty dictionary.
8081

8182
.. doctest::
8283

@@ -85,6 +86,10 @@ Node classes
8586

8687
.. versionadded:: 3.13
8788

89+
.. versionchanged:: 3.16
90+
The attribute is now also available on abstract classes and on other
91+
classes without fields.
92+
8893
.. attribute:: lineno
8994
col_offset
9095
end_lineno

‎Lib/test/test_ast/test_ast.py‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -438,6 +438,18 @@ def test_field_attr_existence(self):
438438
if isinstance(x, ast.AST):
439439
self.assertIs(type(x._fields), tuple)
440440

441+
def test_field_types_attr_existence(self):
442+
# gh-135797: every AST class has a _field_types dictionary whose keys
443+
# are the names listed in _fields. Abstract classes have no fields
444+
# and thus an empty dictionary, just like their empty _fields tuple.
445+
for name, cls in ast.__dict__.items():
446+
if isinstance(cls, type) and issubclass(cls, ast.AST):
447+
with self.subTest(cls=name):
448+
self.assertIsInstance(cls._field_types, dict)
449+
self.assertEqual(set(cls._field_types), set(cls._fields))
450+
self.assertEqual(ast.AST._field_types, {})
451+
self.assertEqual(ast.mod._field_types, {})
452+
441453
def test_dynamic_attr(self):
442454
for name, item in ast.__dict__.items():
443455
# constructor has a different signature
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Add ``_field_types`` to :class:`ast.AST` and to the abstract :mod:`ast`
2+
classes such as :class:`ast.mod` and :class:`ast.stmt`, where it is an empty
3+
dictionary. The attribute was previously only set on classes with fields.

‎Parser/asdl_c.py‎

Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1134,10 +1134,14 @@ def visitModule(self, mod):
11341134
res = -1;
11351135
goto cleanup;
11361136
}
1137-
if (field_types == NULL) {
1138-
// Probably a user-defined subclass of AST that lacks _field_types.
1137+
if (field_types == NULL ||
1138+
(PyDict_Check(field_types) && PyDict_GET_SIZE(field_types) == 0))
1139+
{
1140+
// Probably a user-defined subclass of AST that lacks _field_types,
1141+
// or that inherits the empty dictionary of an abstract AST class.
11391142
// This will continue to work as it did before 3.13; i.e., attributes
11401143
// that are not passed in simply do not exist on the instance.
1144+
Py_XDECREF(field_types);
11411145
goto cleanup;
11421146
}
11431147
remaining_list = PySequence_List(remaining_fields);
@@ -1690,7 +1694,7 @@ def visitModule(self, mod):
16901694
make_type(struct ast_state *state, const char *type, PyObject* base,
16911695
const char* const* fields, int num_fields, const char *doc)
16921696
{
1693-
PyObject *fnames, *result;
1697+
PyObject *fnames, *field_types, *result;
16941698
int i;
16951699
fnames = PyTuple_New(num_fields);
16961700
if (!fnames) return NULL;
@@ -1710,6 +1714,23 @@ def visitModule(self, mod):
17101714
state->ast,
17111715
state->__doc__, doc);
17121716
Py_DECREF(fnames);
1717+
if (result == NULL) {
1718+
return NULL;
1719+
}
1720+
// Every AST class owns a _field_types dictionary. Classes without fields,
1721+
// such as the abstract sum types, simply have an empty one. The
1722+
// dictionaries of concrete classes are filled in by add_ast_annotations().
1723+
field_types = PyDict_New();
1724+
if (field_types == NULL) {
1725+
Py_DECREF(result);
1726+
return NULL;
1727+
}
1728+
if (PyObject_SetAttrString(result, "_field_types", field_types) < 0) {
1729+
Py_DECREF(field_types);
1730+
Py_DECREF(result);
1731+
return NULL;
1732+
}
1733+
Py_DECREF(field_types);
17131734
return result;
17141735
}
17151736
@@ -1839,16 +1860,26 @@ def visitModule(self, mod):
18391860
18401861
static int add_ast_fields(struct ast_state *state)
18411862
{
1842-
PyObject *empty_tuple;
1863+
PyObject *empty_tuple, *empty_dict;
18431864
empty_tuple = PyTuple_New(0);
1844-
if (!empty_tuple ||
1845-
PyObject_SetAttrString(state->AST_type, "_fields", empty_tuple) < 0 ||
1865+
if (!empty_tuple) {
1866+
return -1;
1867+
}
1868+
empty_dict = PyDict_New();
1869+
if (empty_dict == NULL) {
1870+
Py_DECREF(empty_tuple);
1871+
return -1;
1872+
}
1873+
if (PyObject_SetAttrString(state->AST_type, "_fields", empty_tuple) < 0 ||
1874+
PyObject_SetAttrString(state->AST_type, "_field_types", empty_dict) < 0 ||
18461875
PyObject_SetAttrString(state->AST_type, "__match_args__", empty_tuple) < 0 ||
18471876
PyObject_SetAttrString(state->AST_type, "_attributes", empty_tuple) < 0) {
1848-
Py_XDECREF(empty_tuple);
1877+
Py_DECREF(empty_tuple);
1878+
Py_DECREF(empty_dict);
18491879
return -1;
18501880
}
18511881
Py_DECREF(empty_tuple);
1882+
Py_DECREF(empty_dict);
18521883
return 0;
18531884
}
18541885

‎Python/Python-ast.c‎

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

0 commit comments

Comments
 (0)