diff --git a/Doc/library/ast.rst b/Doc/library/ast.rst index e30dadc9733d37..84799fe31fd52c 100644 --- a/Doc/library/ast.rst +++ b/Doc/library/ast.rst @@ -75,8 +75,9 @@ Node classes .. attribute:: _field_types - The :attr:`!_field_types` attribute on each concrete class is a dictionary - mapping field names (as also listed in :attr:`_fields`) to their types. + The :attr:`!_field_types` attribute on each class is a dictionary mapping + field names (as also listed in :attr:`_fields`) to their types. Classes + without fields, such as the abstract classes, have an empty dictionary. .. doctest:: @@ -85,6 +86,10 @@ Node classes .. versionadded:: 3.13 + .. versionchanged:: 3.16 + The attribute is now also available on abstract classes and on other + classes without fields. + .. attribute:: lineno col_offset end_lineno diff --git a/Lib/test/test_ast/test_ast.py b/Lib/test/test_ast/test_ast.py index b1483ad6bf2d40..8d5646107a0df5 100644 --- a/Lib/test/test_ast/test_ast.py +++ b/Lib/test/test_ast/test_ast.py @@ -438,6 +438,18 @@ def test_field_attr_existence(self): if isinstance(x, ast.AST): self.assertIs(type(x._fields), tuple) + def test_field_types_attr_existence(self): + # gh-135797: every AST class has a _field_types dictionary whose keys + # are the names listed in _fields. Abstract classes have no fields + # and thus an empty dictionary, just like their empty _fields tuple. + for name, cls in ast.__dict__.items(): + if isinstance(cls, type) and issubclass(cls, ast.AST): + with self.subTest(cls=name): + self.assertIsInstance(cls._field_types, dict) + self.assertEqual(set(cls._field_types), set(cls._fields)) + self.assertEqual(ast.AST._field_types, {}) + self.assertEqual(ast.mod._field_types, {}) + def test_dynamic_attr(self): for name, item in ast.__dict__.items(): # constructor has a different signature diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-10-17-02-02-31.gh-issue-135797.Qs7vKd.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-10-17-02-02-31.gh-issue-135797.Qs7vKd.rst new file mode 100644 index 00000000000000..042c7990a18473 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-10-17-02-02-31.gh-issue-135797.Qs7vKd.rst @@ -0,0 +1,3 @@ +Add an empty ``_field_types`` dictionary to :class:`ast.AST` and to the +abstract :mod:`ast` classes that have no fields, such as ``ast.mod`` and +``ast.stmt``. The attribute was previously only set on classes with fields. diff --git a/Parser/asdl_c.py b/Parser/asdl_c.py index 2b83e97353bbf1..7e50bd06abee8f 100755 --- a/Parser/asdl_c.py +++ b/Parser/asdl_c.py @@ -1134,10 +1134,14 @@ def visitModule(self, mod): res = -1; goto cleanup; } - if (field_types == NULL) { - // Probably a user-defined subclass of AST that lacks _field_types. + if (field_types == NULL || + (PyDict_Check(field_types) && PyDict_GET_SIZE(field_types) == 0)) + { + // Probably a user-defined subclass of AST that lacks _field_types, + // or that inherits the empty dictionary of an abstract AST class. // This will continue to work as it did before 3.13; i.e., attributes // that are not passed in simply do not exist on the instance. + Py_XDECREF(field_types); goto cleanup; } remaining_list = PySequence_List(remaining_fields); @@ -1690,7 +1694,7 @@ def visitModule(self, mod): make_type(struct ast_state *state, const char *type, PyObject* base, const char* const* fields, int num_fields, const char *doc) { - PyObject *fnames, *result; + PyObject *fnames, *field_types, *result; int i; fnames = PyTuple_New(num_fields); if (!fnames) return NULL; @@ -1710,6 +1714,23 @@ def visitModule(self, mod): state->ast, state->__doc__, doc); Py_DECREF(fnames); + if (result == NULL) { + return NULL; + } + // Every AST class owns a _field_types dictionary. Classes without fields, + // such as the abstract sum types, simply have an empty one. The + // dictionaries of concrete classes are filled in by add_ast_annotations(). + field_types = PyDict_New(); + if (field_types == NULL) { + Py_DECREF(result); + return NULL; + } + if (PyObject_SetAttrString(result, "_field_types", field_types) < 0) { + Py_DECREF(field_types); + Py_DECREF(result); + return NULL; + } + Py_DECREF(field_types); return result; } @@ -1839,16 +1860,26 @@ def visitModule(self, mod): static int add_ast_fields(struct ast_state *state) { - PyObject *empty_tuple; + PyObject *empty_tuple, *empty_dict; empty_tuple = PyTuple_New(0); - if (!empty_tuple || - PyObject_SetAttrString(state->AST_type, "_fields", empty_tuple) < 0 || + if (!empty_tuple) { + return -1; + } + empty_dict = PyDict_New(); + if (empty_dict == NULL) { + Py_DECREF(empty_tuple); + return -1; + } + if (PyObject_SetAttrString(state->AST_type, "_fields", empty_tuple) < 0 || + PyObject_SetAttrString(state->AST_type, "_field_types", empty_dict) < 0 || PyObject_SetAttrString(state->AST_type, "__match_args__", empty_tuple) < 0 || PyObject_SetAttrString(state->AST_type, "_attributes", empty_tuple) < 0) { - Py_XDECREF(empty_tuple); + Py_DECREF(empty_tuple); + Py_DECREF(empty_dict); return -1; } Py_DECREF(empty_tuple); + Py_DECREF(empty_dict); return 0; } diff --git a/Python/Python-ast.c b/Python/Python-ast.c index 0d89f7e87f1fa2..90dffab175c4bd 100644 --- a/Python/Python-ast.c +++ b/Python/Python-ast.c @@ -1635,10 +1635,14 @@ ast_type_init(PyObject *self, PyObject *args, PyObject *kw) res = -1; goto cleanup; } - if (field_types == NULL) { - // Probably a user-defined subclass of AST that lacks _field_types. + if (field_types == NULL || + (PyDict_Check(field_types) && PyDict_GET_SIZE(field_types) == 0)) + { + // Probably a user-defined subclass of AST that lacks _field_types, + // or that inherits the empty dictionary of an abstract AST class. // This will continue to work as it did before 3.13; i.e., attributes // that are not passed in simply do not exist on the instance. + Py_XDECREF(field_types); goto cleanup; } remaining_list = PySequence_List(remaining_fields); @@ -2191,7 +2195,7 @@ static PyObject * make_type(struct ast_state *state, const char *type, PyObject* base, const char* const* fields, int num_fields, const char *doc) { - PyObject *fnames, *result; + PyObject *fnames, *field_types, *result; int i; fnames = PyTuple_New(num_fields); if (!fnames) return NULL; @@ -2211,6 +2215,23 @@ make_type(struct ast_state *state, const char *type, PyObject* base, state->ast, state->__doc__, doc); Py_DECREF(fnames); + if (result == NULL) { + return NULL; + } + // Every AST class owns a _field_types dictionary. Classes without fields, + // such as the abstract sum types, simply have an empty one. The + // dictionaries of concrete classes are filled in by add_ast_annotations(). + field_types = PyDict_New(); + if (field_types == NULL) { + Py_DECREF(result); + return NULL; + } + if (PyObject_SetAttrString(result, "_field_types", field_types) < 0) { + Py_DECREF(field_types); + Py_DECREF(result); + return NULL; + } + Py_DECREF(field_types); return result; } @@ -2340,16 +2361,26 @@ static int obj2ast_int(struct ast_state* Py_UNUSED(state), PyObject* obj, int* o static int add_ast_fields(struct ast_state *state) { - PyObject *empty_tuple; + PyObject *empty_tuple, *empty_dict; empty_tuple = PyTuple_New(0); - if (!empty_tuple || - PyObject_SetAttrString(state->AST_type, "_fields", empty_tuple) < 0 || + if (!empty_tuple) { + return -1; + } + empty_dict = PyDict_New(); + if (empty_dict == NULL) { + Py_DECREF(empty_tuple); + return -1; + } + if (PyObject_SetAttrString(state->AST_type, "_fields", empty_tuple) < 0 || + PyObject_SetAttrString(state->AST_type, "_field_types", empty_dict) < 0 || PyObject_SetAttrString(state->AST_type, "__match_args__", empty_tuple) < 0 || PyObject_SetAttrString(state->AST_type, "_attributes", empty_tuple) < 0) { - Py_XDECREF(empty_tuple); + Py_DECREF(empty_tuple); + Py_DECREF(empty_dict); return -1; } Py_DECREF(empty_tuple); + Py_DECREF(empty_dict); return 0; }