From 4c71d51a4b7989fc8754ba512c40e21666f9db0d Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Thu, 28 Mar 2024 04:30:31 -0600 Subject: gh-117266: Fix crashes on user-created AST subclasses (GH-117276) Fix crashes on user-created AST subclasses --- Lib/test/test_ast.py | 41 ++++++++++++++++++++++ .../2024-03-26-17-22-38.gh-issue-117266.Kwh79O.rst | 2 ++ Parser/asdl_c.py | 15 ++++++-- Python/Python-ast.c | 15 ++++++-- 4 files changed, 69 insertions(+), 4 deletions(-) create mode 100644 Misc/NEWS.d/next/Core and Builtins/2024-03-26-17-22-38.gh-issue-117266.Kwh79O.rst diff --git a/Lib/test/test_ast.py b/Lib/test/test_ast.py index 7cecf31..3929e4e 100644 --- a/Lib/test/test_ast.py +++ b/Lib/test/test_ast.py @@ -2916,6 +2916,47 @@ class ASTConstructorTests(unittest.TestCase): self.assertEqual(node.name, 'foo') self.assertEqual(node.decorator_list, []) + def test_custom_subclass(self): + class NoInit(ast.AST): + pass + + obj = NoInit() + self.assertIsInstance(obj, NoInit) + self.assertEqual(obj.__dict__, {}) + + class Fields(ast.AST): + _fields = ('a',) + + with self.assertWarnsRegex(DeprecationWarning, + r"Fields provides _fields but not _field_types."): + obj = Fields() + with self.assertRaises(AttributeError): + obj.a + obj = Fields(a=1) + self.assertEqual(obj.a, 1) + + class FieldsAndTypes(ast.AST): + _fields = ('a',) + _field_types = {'a': int | None} + a: int | None = None + + obj = FieldsAndTypes() + self.assertIs(obj.a, None) + obj = FieldsAndTypes(a=1) + self.assertEqual(obj.a, 1) + + class FieldsAndTypesNoDefault(ast.AST): + _fields = ('a',) + _field_types = {'a': int} + + with self.assertWarnsRegex(DeprecationWarning, + r"FieldsAndTypesNoDefault\.__init__ missing 1 required positional argument: 'a'\."): + obj = FieldsAndTypesNoDefault() + with self.assertRaises(AttributeError): + obj.a + obj = FieldsAndTypesNoDefault(a=1) + self.assertEqual(obj.a, 1) + @support.cpython_only class ModuleStateTests(unittest.TestCase): diff --git a/Misc/NEWS.d/next/Core and Builtins/2024-03-26-17-22-38.gh-issue-117266.Kwh79O.rst b/Misc/NEWS.d/next/Core and Builtins/2024-03-26-17-22-38.gh-issue-117266.Kwh79O.rst new file mode 100644 index 0000000..5055954 --- /dev/null +++ b/Misc/NEWS.d/next/Core and Builtins/2024-03-26-17-22-38.gh-issue-117266.Kwh79O.rst @@ -0,0 +1,2 @@ +Fix crashes for certain user-created subclasses of :class:`ast.AST`. Such +classes are now expected to set the ``_field_types`` attribute. diff --git a/Parser/asdl_c.py b/Parser/asdl_c.py index 59cc391..c4df2c5 100755 --- a/Parser/asdl_c.py +++ b/Parser/asdl_c.py @@ -973,11 +973,22 @@ ast_type_init(PyObject *self, PyObject *args, PyObject *kw) Py_ssize_t size = PySet_Size(remaining_fields); PyObject *field_types = NULL, *remaining_list = NULL; if (size > 0) { - if (!PyObject_GetOptionalAttr((PyObject*)Py_TYPE(self), &_Py_ID(_field_types), - &field_types)) { + if (PyObject_GetOptionalAttr((PyObject*)Py_TYPE(self), &_Py_ID(_field_types), + &field_types) < 0) { res = -1; goto cleanup; } + if (field_types == NULL) { + if (PyErr_WarnFormat( + PyExc_DeprecationWarning, 1, + "%.400s provides _fields but not _field_types. " + "This will become an error in Python 3.15.", + Py_TYPE(self)->tp_name + ) < 0) { + res = -1; + } + goto cleanup; + } remaining_list = PySequence_List(remaining_fields); if (!remaining_list) { goto set_remaining_cleanup; diff --git a/Python/Python-ast.c b/Python/Python-ast.c index 7b591dd..60b4626 100644 --- a/Python/Python-ast.c +++ b/Python/Python-ast.c @@ -5119,11 +5119,22 @@ ast_type_init(PyObject *self, PyObject *args, PyObject *kw) Py_ssize_t size = PySet_Size(remaining_fields); PyObject *field_types = NULL, *remaining_list = NULL; if (size > 0) { - if (!PyObject_GetOptionalAttr((PyObject*)Py_TYPE(self), &_Py_ID(_field_types), - &field_types)) { + if (PyObject_GetOptionalAttr((PyObject*)Py_TYPE(self), &_Py_ID(_field_types), + &field_types) < 0) { res = -1; goto cleanup; } + if (field_types == NULL) { + if (PyErr_WarnFormat( + PyExc_DeprecationWarning, 1, + "%.400s provides _fields but not _field_types. " + "This will become an error in Python 3.15.", + Py_TYPE(self)->tp_name + ) < 0) { + res = -1; + } + goto cleanup; + } remaining_list = PySequence_List(remaining_fields); if (!remaining_list) { goto set_remaining_cleanup; -- cgit v0.12