From d8343200bad438057ec60e68cb7a278e30edf64a Mon Sep 17 00:00:00 2001 From: Brian Schubert Date: Sat, 9 Aug 2025 18:48:02 -0400 Subject: [PATCH 01/14] Promote deprecation warnings to errors --- Lib/test/test_ast/test_ast.py | 61 ++++++++---------- Parser/asdl_c.py | 118 +++++++++++++++++++++++++++------- Python/Python-ast.c | 118 +++++++++++++++++++++++++++------- 3 files changed, 213 insertions(+), 84 deletions(-) diff --git a/Lib/test/test_ast/test_ast.py b/Lib/test/test_ast/test_ast.py index 1e6f60074308e2..838cb6852ff293 100644 --- a/Lib/test/test_ast/test_ast.py +++ b/Lib/test/test_ast/test_ast.py @@ -458,14 +458,13 @@ def test_field_attr_writable(self): self.assertEqual(x._fields, 666) def test_classattrs(self): - with self.assertWarns(DeprecationWarning): + msg = "Constant.__init__ missing 1 required positional argument: 'value'" + with self.assertRaisesRegex(TypeError, re.escape(msg)): x = ast.Constant() - self.assertEqual(x._fields, ('value', 'kind')) - - with self.assertRaises(AttributeError): - x.value x = ast.Constant(42) + self.assertEqual(x._fields, ('value', 'kind')) + self.assertEqual(x.value, 42) with self.assertRaises(AttributeError): @@ -485,9 +484,10 @@ def test_classattrs(self): self.assertRaises(TypeError, ast.Constant, 1, None, 2) self.assertRaises(TypeError, ast.Constant, 1, None, 2, lineno=0) - # Arbitrary keyword arguments are supported (but deprecated) - with self.assertWarns(DeprecationWarning): - self.assertEqual(ast.Constant(1, foo='bar').foo, 'bar') + # Arbitrary keyword arguments are not supported + msg = "Constant.__init__ got an unexpected keyword argument 'foo'" + with self.assertRaisesRegex(TypeError, re.escape(msg)): + ast.Constant(1, foo='bar') with self.assertRaisesRegex(TypeError, "Constant got multiple values for argument 'value'"): ast.Constant(1, value=2) @@ -528,23 +528,24 @@ def test_module(self): self.assertEqual(x.body, body) def test_nodeclasses(self): - # Zero arguments constructor explicitly allowed (but deprecated) - with self.assertWarns(DeprecationWarning): + # Zero arguments constructor is not allowed + msg = "missing 3 required positional arguments: 'left', 'op', and 'right'" + with self.assertRaisesRegex(TypeError, re.escape(msg)): x = ast.BinOp() - self.assertEqual(x._fields, ('left', 'op', 'right')) - - # Random attribute allowed too - x.foobarbaz = 5 - self.assertEqual(x.foobarbaz, 5) n1 = ast.Constant(1) n3 = ast.Constant(3) addop = ast.Add() x = ast.BinOp(n1, addop, n3) + self.assertEqual(x._fields, ('left', 'op', 'right')) self.assertEqual(x.left, n1) self.assertEqual(x.op, addop) self.assertEqual(x.right, n3) + # Random attribute allowed too + x.foobarbaz = 5 + self.assertEqual(x.foobarbaz, 5) + x = ast.BinOp(1, 2, 3) self.assertEqual(x.left, 1) self.assertEqual(x.op, 2) @@ -568,10 +569,9 @@ def test_nodeclasses(self): self.assertEqual(x.right, 3) self.assertEqual(x.lineno, 0) - # Random kwargs also allowed (but deprecated) - with self.assertWarns(DeprecationWarning): + # Random kwargs are not allowed + with self.assertRaisesRegex(TypeError, "unexpected keyword argument 'foobarbaz'"): x = ast.BinOp(1, 2, 3, foobarbaz=42) - self.assertEqual(x.foobarbaz, 42) def test_no_fields(self): # this used to fail because Sub._fields was None @@ -3209,11 +3209,10 @@ def test_FunctionDef(self): args = ast.arguments() self.assertEqual(args.args, []) self.assertEqual(args.posonlyargs, []) - with self.assertWarnsRegex(DeprecationWarning, + with self.assertRaisesRegex(TypeError, r"FunctionDef\.__init__ missing 1 required positional argument: 'name'"): node = ast.FunctionDef(args=args) - self.assertNotHasAttr(node, "name") - self.assertEqual(node.decorator_list, []) + node = ast.FunctionDef(name='foo', args=args) self.assertEqual(node.name, 'foo') self.assertEqual(node.decorator_list, []) @@ -3231,7 +3230,7 @@ def test_expr_context(self): self.assertEqual(name3.id, "x") self.assertIsInstance(name3.ctx, ast.Del) - with self.assertWarnsRegex(DeprecationWarning, + with self.assertRaisesRegex(TypeError, r"Name\.__init__ missing 1 required positional argument: 'id'"): name3 = ast.Name() @@ -3272,8 +3271,8 @@ class MyAttrs(ast.AST): self.assertEqual(obj.a, 1) self.assertEqual(obj.b, 2) - with self.assertWarnsRegex(DeprecationWarning, - r"MyAttrs.__init__ got an unexpected keyword argument 'c'."): + with self.assertRaisesRegex(TypeError, + r"MyAttrs.__init__ got an unexpected keyword argument 'c'"): obj = MyAttrs(c=3) def test_fields_and_types_no_default(self): @@ -3281,11 +3280,10 @@ class FieldsAndTypesNoDefault(ast.AST): _fields = ('a',) _field_types = {'a': int} - with self.assertWarnsRegex(DeprecationWarning, - r"FieldsAndTypesNoDefault\.__init__ missing 1 required positional argument: 'a'\."): + with self.assertRaisesRegex(TypeError, + 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) @@ -3296,13 +3294,8 @@ class MoreFieldsThanTypes(ast.AST): a: int | None = None b: int | None = None - with self.assertWarnsRegex( - DeprecationWarning, - r"Field 'b' is missing from MoreFieldsThanTypes\._field_types" - ): + with self.assertRaisesRegex(TypeError, "Field 'b' is missing"): obj = MoreFieldsThanTypes() - self.assertIs(obj.a, None) - self.assertIs(obj.b, None) obj = MoreFieldsThanTypes(a=1, b=2) self.assertEqual(obj.a, 1) diff --git a/Parser/asdl_c.py b/Parser/asdl_c.py index dba20226c3283a..aee5526fc756e2 100755 --- a/Parser/asdl_c.py +++ b/Parser/asdl_c.py @@ -873,6 +873,69 @@ def visitModule(self, mod): return 0; } +/* + * Format the names in the set 'missing' into a natural language list, + * sorted in the order in which they appear in 'fields'. + * + * Similar to format_missing from 'Python/ceval.c'. + * + * Parameters + + * missing Set of missing field names to render. + * fields Sequence of AST node field names (self._fields). + */ +static PyObject * +format_missing(PyObject *missing, PyObject *fields) +{ + Py_ssize_t num_fields, num_total, num_left; + num_fields = PySequence_Size(fields); + if (num_fields == -1) { + return NULL; + } + num_total = num_left = PySet_GET_SIZE(missing); + PyObject *name_str = PyUnicode_FromString(""); + // Iterate all AST node fields in order so that the missing positional + // arguments are rendered in the order in which __init__ expects them. + for (Py_ssize_t i = 0; i < num_fields; i++) { + PyObject *name = PySequence_GetItem(fields, i); + if (!name) { + Py_DECREF(name_str); + return NULL; + } + int contains = PySet_Contains(missing, name); + if (contains == -1) { + Py_DECREF(name_str); + Py_DECREF(name); + return NULL; + } + else if (contains == 1) { + const char* fmt = NULL; + if (num_left == 1) { + fmt = "'%U'"; + } + else if (num_total == 2) { + fmt = "'%U' and "; + } + else if (num_left == 2) { + fmt = "'%U', and "; + } + else { + fmt = "'%U', "; + } + num_left--; + PyObject *tmp = PyUnicode_FromFormat(fmt, name); + if (!tmp) { + Py_DECREF(name_str); + Py_DECREF(name); + return NULL; + } + name_str = PyUnicode_Concat(name_str, tmp); + } + Py_DECREF(name); + } + return name_str; +} + static int ast_type_init(PyObject *self, PyObject *args, PyObject *kw) { @@ -963,16 +1026,11 @@ def visitModule(self, mod): goto cleanup; } else if (contains == 0) { - if (PyErr_WarnFormat( - PyExc_DeprecationWarning, 1, - "%.400s.__init__ got an unexpected keyword argument '%U'. " - "Support for arbitrary keyword arguments is deprecated " - "and will be removed in Python 3.15.", - Py_TYPE(self)->tp_name, key - ) < 0) { - res = -1; - goto cleanup; - } + PyErr_Format(PyExc_TypeError, + "%T.__init__ got an unexpected keyword " + "argument '%U'", self, key); + res = -1; + goto cleanup; } } res = PyObject_SetAttr(self, key, value); @@ -982,7 +1040,7 @@ def visitModule(self, mod): } } Py_ssize_t size = PySet_Size(remaining_fields); - PyObject *field_types = NULL, *remaining_list = NULL; + PyObject *field_types = NULL, *remaining_list = NULL, *missing_names = NULL; if (size > 0) { if (PyObject_GetOptionalAttr((PyObject*)Py_TYPE(self), &_Py_ID(_field_types), &field_types) < 0) { @@ -999,6 +1057,10 @@ def visitModule(self, mod): if (!remaining_list) { goto set_remaining_cleanup; } + missing_names = PySet_New(NULL); + if (!missing_names) { + goto set_remaining_cleanup; + } for (Py_ssize_t i = 0; i < size; i++) { PyObject *name = PyList_GET_ITEM(remaining_list, i); PyObject *type = PyDict_GetItemWithError(field_types, name); @@ -1007,14 +1069,10 @@ def visitModule(self, mod): goto set_remaining_cleanup; } else { - if (PyErr_WarnFormat( - PyExc_DeprecationWarning, 1, - "Field '%U' is missing from %.400s._field_types. " - "This will become an error in Python 3.15.", - name, Py_TYPE(self)->tp_name - ) < 0) { - goto set_remaining_cleanup; - } + PyErr_Format(PyExc_TypeError, + "Field '%U' is missing from %T._field_types", + name, self); + goto set_remaining_cleanup; } } else if (_PyUnion_Check(type)) { @@ -1042,16 +1100,25 @@ def visitModule(self, mod): } else { // simple field (e.g., identifier) - if (PyErr_WarnFormat( - PyExc_DeprecationWarning, 1, - "%.400s.__init__ missing 1 required positional argument: '%U'. " - "This will become an error in Python 3.15.", - Py_TYPE(self)->tp_name, name - ) < 0) { + res = PySet_Add(missing_names, name); + if (res < 0) { goto set_remaining_cleanup; } } } + Py_ssize_t num_missing = PySet_GET_SIZE(missing_names); + if (num_missing > 0) { + PyObject* name_str = format_missing(missing_names, fields); + if (!name_str) { + goto set_remaining_cleanup; + } + PyErr_Format(PyExc_TypeError, + "%T.__init__ missing %d required positional argument%s: %U", + self, num_missing, num_missing == 1 ? "" : "s", name_str); + Py_DECREF(name_str); + goto set_remaining_cleanup; + } + Py_DECREF(missing_names); Py_DECREF(remaining_list); Py_DECREF(field_types); } @@ -1061,6 +1128,7 @@ def visitModule(self, mod): Py_XDECREF(remaining_fields); return res; set_remaining_cleanup: + Py_XDECREF(missing_names); Py_XDECREF(remaining_list); Py_XDECREF(field_types); res = -1; diff --git a/Python/Python-ast.c b/Python/Python-ast.c index 660bc598a4862c..1363a5b73a058b 100644 --- a/Python/Python-ast.c +++ b/Python/Python-ast.c @@ -5157,6 +5157,69 @@ ast_clear(PyObject *op) return 0; } +/* + * Format the names in the set 'missing' into a natural language list, + * sorted in the order in which they appear in 'fields'. + * + * Similar to format_missing from 'Python/ceval.c'. + * + * Parameters + + * missing Set of missing field names to render. + * fields Sequence of AST node field names (self._fields). + */ +static PyObject * +format_missing(PyObject *missing, PyObject *fields) +{ + Py_ssize_t num_fields, num_total, num_left; + num_fields = PySequence_Size(fields); + if (num_fields == -1) { + return NULL; + } + num_total = num_left = PySet_GET_SIZE(missing); + PyObject *name_str = PyUnicode_FromString(""); + // Iterate all AST node fields in order so that the missing positional + // arguments are rendered in the order in which __init__ expects them. + for (Py_ssize_t i = 0; i < num_fields; i++) { + PyObject *name = PySequence_GetItem(fields, i); + if (!name) { + Py_DECREF(name_str); + return NULL; + } + int contains = PySet_Contains(missing, name); + if (contains == -1) { + Py_DECREF(name_str); + Py_DECREF(name); + return NULL; + } + else if (contains == 1) { + const char* fmt = NULL; + if (num_left == 1) { + fmt = "'%U'"; + } + else if (num_total == 2) { + fmt = "'%U' and "; + } + else if (num_left == 2) { + fmt = "'%U', and "; + } + else { + fmt = "'%U', "; + } + num_left--; + PyObject *tmp = PyUnicode_FromFormat(fmt, name); + if (!tmp) { + Py_DECREF(name_str); + Py_DECREF(name); + return NULL; + } + name_str = PyUnicode_Concat(name_str, tmp); + } + Py_DECREF(name); + } + return name_str; +} + static int ast_type_init(PyObject *self, PyObject *args, PyObject *kw) { @@ -5247,16 +5310,11 @@ ast_type_init(PyObject *self, PyObject *args, PyObject *kw) goto cleanup; } else if (contains == 0) { - if (PyErr_WarnFormat( - PyExc_DeprecationWarning, 1, - "%.400s.__init__ got an unexpected keyword argument '%U'. " - "Support for arbitrary keyword arguments is deprecated " - "and will be removed in Python 3.15.", - Py_TYPE(self)->tp_name, key - ) < 0) { - res = -1; - goto cleanup; - } + PyErr_Format(PyExc_TypeError, + "%T.__init__ got an unexpected keyword " + "argument '%U'", self, key); + res = -1; + goto cleanup; } } res = PyObject_SetAttr(self, key, value); @@ -5266,7 +5324,7 @@ ast_type_init(PyObject *self, PyObject *args, PyObject *kw) } } Py_ssize_t size = PySet_Size(remaining_fields); - PyObject *field_types = NULL, *remaining_list = NULL; + PyObject *field_types = NULL, *remaining_list = NULL, *missing_names = NULL; if (size > 0) { if (PyObject_GetOptionalAttr((PyObject*)Py_TYPE(self), &_Py_ID(_field_types), &field_types) < 0) { @@ -5283,6 +5341,10 @@ ast_type_init(PyObject *self, PyObject *args, PyObject *kw) if (!remaining_list) { goto set_remaining_cleanup; } + missing_names = PySet_New(NULL); + if (!missing_names) { + goto set_remaining_cleanup; + } for (Py_ssize_t i = 0; i < size; i++) { PyObject *name = PyList_GET_ITEM(remaining_list, i); PyObject *type = PyDict_GetItemWithError(field_types, name); @@ -5291,14 +5353,10 @@ ast_type_init(PyObject *self, PyObject *args, PyObject *kw) goto set_remaining_cleanup; } else { - if (PyErr_WarnFormat( - PyExc_DeprecationWarning, 1, - "Field '%U' is missing from %.400s._field_types. " - "This will become an error in Python 3.15.", - name, Py_TYPE(self)->tp_name - ) < 0) { - goto set_remaining_cleanup; - } + PyErr_Format(PyExc_TypeError, + "Field '%U' is missing from %T._field_types", + name, self); + goto set_remaining_cleanup; } } else if (_PyUnion_Check(type)) { @@ -5326,16 +5384,25 @@ ast_type_init(PyObject *self, PyObject *args, PyObject *kw) } else { // simple field (e.g., identifier) - if (PyErr_WarnFormat( - PyExc_DeprecationWarning, 1, - "%.400s.__init__ missing 1 required positional argument: '%U'. " - "This will become an error in Python 3.15.", - Py_TYPE(self)->tp_name, name - ) < 0) { + res = PySet_Add(missing_names, name); + if (res < 0) { goto set_remaining_cleanup; } } } + Py_ssize_t num_missing = PySet_GET_SIZE(missing_names); + if (num_missing > 0) { + PyObject* name_str = format_missing(missing_names, fields); + if (!name_str) { + goto set_remaining_cleanup; + } + PyErr_Format(PyExc_TypeError, + "%T.__init__ missing %d required positional argument%s: %U", + self, num_missing, num_missing == 1 ? "" : "s", name_str); + Py_DECREF(name_str); + goto set_remaining_cleanup; + } + Py_DECREF(missing_names); Py_DECREF(remaining_list); Py_DECREF(field_types); } @@ -5345,6 +5412,7 @@ ast_type_init(PyObject *self, PyObject *args, PyObject *kw) Py_XDECREF(remaining_fields); return res; set_remaining_cleanup: + Py_XDECREF(missing_names); Py_XDECREF(remaining_list); Py_XDECREF(field_types); res = -1; From de5564a645c3b4b0c36f400c0404b3f8a430e1dc Mon Sep 17 00:00:00 2001 From: Brian Schubert Date: Sat, 9 Aug 2025 18:52:51 -0400 Subject: [PATCH 02/14] Remove obsolete checks for copy.replace support --- Lib/test/test_ast/test_ast.py | 6 +- Parser/asdl_c.py | 179 ---------------------------------- Python/Python-ast.c | 179 ---------------------------------- 3 files changed, 3 insertions(+), 361 deletions(-) diff --git a/Lib/test/test_ast/test_ast.py b/Lib/test/test_ast/test_ast.py index 838cb6852ff293..4c0b944ee2cdc6 100644 --- a/Lib/test/test_ast/test_ast.py +++ b/Lib/test/test_ast/test_ast.py @@ -1412,7 +1412,7 @@ def test_replace_reject_missing_field(self): self.assertRaises(AttributeError, getattr, node, 'id') self.assertIs(node.ctx, context) - msg = "Name.__replace__ missing 1 keyword argument: 'id'." + msg = "ast.Name.__init__ missing 1 required positional argument: 'id'" with self.assertRaisesRegex(TypeError, re.escape(msg)): copy.replace(node) # assert that there is no side-effect @@ -1449,7 +1449,7 @@ def test_replace_reject_known_custom_instance_fields_commits(self): # explicit rejection of known instance fields self.assertHasAttr(node, 'extra') - msg = "Name.__replace__ got an unexpected keyword argument 'extra'." + msg = "ast.Name.__init__ got an unexpected keyword argument 'extra'" with self.assertRaisesRegex(TypeError, re.escape(msg)): copy.replace(node, extra=1) # assert that there is no side-effect @@ -1463,7 +1463,7 @@ def test_replace_reject_unknown_instance_fields(self): # explicit rejection of unknown extra fields self.assertRaises(AttributeError, getattr, node, 'unknown') - msg = "Name.__replace__ got an unexpected keyword argument 'unknown'." + msg = "ast.Name.__init__ got an unexpected keyword argument 'unknown'" with self.assertRaisesRegex(TypeError, re.escape(msg)): copy.replace(node, unknown=1) # assert that there is no side-effect diff --git a/Parser/asdl_c.py b/Parser/asdl_c.py index aee5526fc756e2..0fe2175ca040cc 100755 --- a/Parser/asdl_c.py +++ b/Parser/asdl_c.py @@ -1212,182 +1212,6 @@ def visitModule(self, mod): return result; } -/* - * Perform the following validations: - * - * - All keyword arguments are known 'fields' or 'attributes'. - * - No field or attribute would be left unfilled after copy.replace(). - * - * On success, this returns 1. Otherwise, set a TypeError - * exception and returns -1 (no exception is set if some - * other internal errors occur). - * - * Parameters - * - * self The AST node instance. - * dict The AST node instance dictionary (self.__dict__). - * fields The list of fields (self._fields). - * attributes The list of attributes (self._attributes). - * kwargs Keyword arguments passed to ast_type_replace(). - * - * The 'dict', 'fields', 'attributes' and 'kwargs' arguments can be NULL. - * - * Note: this function can be removed in 3.15 since the verification - * will be done inside the constructor. - */ -static inline int -ast_type_replace_check(PyObject *self, - PyObject *dict, - PyObject *fields, - PyObject *attributes, - PyObject *kwargs) -{ - // While it is possible to make some fast paths that would avoid - // allocating objects on the stack, this would cost us readability. - // For instance, if 'fields' and 'attributes' are both empty, and - // 'kwargs' is not empty, we could raise a TypeError immediately. - PyObject *expecting = PySet_New(fields); - if (expecting == NULL) { - return -1; - } - if (attributes) { - if (_PySet_Update(expecting, attributes) < 0) { - Py_DECREF(expecting); - return -1; - } - } - // Any keyword argument that is neither a field nor attribute is rejected. - // We first need to check whether a keyword argument is accepted or not. - // If all keyword arguments are accepted, we compute the required fields - // and attributes. A field or attribute is not needed if: - // - // 1) it is given in 'kwargs', or - // 2) it already exists on 'self'. - if (kwargs) { - Py_ssize_t pos = 0; - PyObject *key, *value; - while (PyDict_Next(kwargs, &pos, &key, &value)) { - int rc = PySet_Discard(expecting, key); - if (rc < 0) { - Py_DECREF(expecting); - return -1; - } - if (rc == 0) { - PyErr_Format(PyExc_TypeError, - "%.400s.__replace__ got an unexpected keyword " - "argument '%U'.", Py_TYPE(self)->tp_name, key); - Py_DECREF(expecting); - return -1; - } - } - } - // check that the remaining fields or attributes would be filled - if (dict) { - Py_ssize_t pos = 0; - PyObject *key, *value; - while (PyDict_Next(dict, &pos, &key, &value)) { - // Mark fields or attributes that are found on the instance - // as non-mandatory. If they are not given in 'kwargs', they - // will be shallow-coied; otherwise, they would be replaced - // (not in this function). - if (PySet_Discard(expecting, key) < 0) { - Py_DECREF(expecting); - return -1; - } - } - if (attributes) { - // Some attributes may or may not be present at runtime. - // In particular, now that we checked whether 'kwargs' - // is correct or not, we allow any attribute to be missing. - // - // Note that fields must still be entirely determined when - // calling the constructor later. - PyObject *unused = PyObject_CallMethodOneArg(expecting, - &_Py_ID(difference_update), - attributes); - if (unused == NULL) { - Py_DECREF(expecting); - return -1; - } - Py_DECREF(unused); - } - } - - // Discard fields from 'expecting' that default to None - PyObject *field_types = NULL; - if (PyObject_GetOptionalAttr((PyObject*)Py_TYPE(self), - &_Py_ID(_field_types), - &field_types) < 0) - { - Py_DECREF(expecting); - return -1; - } - if (field_types != NULL) { - Py_ssize_t pos = 0; - PyObject *field_name, *field_type; - while (PyDict_Next(field_types, &pos, &field_name, &field_type)) { - if (_PyUnion_Check(field_type)) { - // optional field - if (PySet_Discard(expecting, field_name) < 0) { - Py_DECREF(expecting); - Py_DECREF(field_types); - return -1; - } - } - } - Py_DECREF(field_types); - } - - // Now 'expecting' contains the fields or attributes - // that would not be filled inside ast_type_replace(). - Py_ssize_t m = PySet_GET_SIZE(expecting); - if (m > 0) { - PyObject *names = PyList_New(m); - if (names == NULL) { - Py_DECREF(expecting); - return -1; - } - Py_ssize_t i = 0, pos = 0; - PyObject *item; - Py_hash_t hash; - while (_PySet_NextEntry(expecting, &pos, &item, &hash)) { - PyObject *name = PyObject_Repr(item); - if (name == NULL) { - Py_DECREF(expecting); - Py_DECREF(names); - return -1; - } - // steal the reference 'name' - PyList_SET_ITEM(names, i++, name); - } - Py_DECREF(expecting); - if (PyList_Sort(names) < 0) { - Py_DECREF(names); - return -1; - } - PyObject *sep = PyUnicode_FromString(", "); - if (sep == NULL) { - Py_DECREF(names); - return -1; - } - PyObject *str_names = PyUnicode_Join(sep, names); - Py_DECREF(sep); - Py_DECREF(names); - if (str_names == NULL) { - return -1; - } - PyErr_Format(PyExc_TypeError, - "%.400s.__replace__ missing %ld keyword argument%s: %U.", - Py_TYPE(self)->tp_name, m, m == 1 ? "" : "s", str_names); - Py_DECREF(str_names); - return -1; - } - else { - Py_DECREF(expecting); - return 1; - } -} - /* * Python equivalent: * @@ -1477,9 +1301,6 @@ def visitModule(self, mod): if (PyObject_GetOptionalAttr(self, state->__dict__, &dict) < 0) { goto cleanup; } - if (ast_type_replace_check(self, dict, fields, attributes, kwargs) < 0) { - goto cleanup; - } empty_tuple = PyTuple_New(0); if (empty_tuple == NULL) { goto cleanup; diff --git a/Python/Python-ast.c b/Python/Python-ast.c index 1363a5b73a058b..08984057d74d5c 100644 --- a/Python/Python-ast.c +++ b/Python/Python-ast.c @@ -5496,182 +5496,6 @@ ast_type_reduce(PyObject *self, PyObject *unused) return result; } -/* - * Perform the following validations: - * - * - All keyword arguments are known 'fields' or 'attributes'. - * - No field or attribute would be left unfilled after copy.replace(). - * - * On success, this returns 1. Otherwise, set a TypeError - * exception and returns -1 (no exception is set if some - * other internal errors occur). - * - * Parameters - * - * self The AST node instance. - * dict The AST node instance dictionary (self.__dict__). - * fields The list of fields (self._fields). - * attributes The list of attributes (self._attributes). - * kwargs Keyword arguments passed to ast_type_replace(). - * - * The 'dict', 'fields', 'attributes' and 'kwargs' arguments can be NULL. - * - * Note: this function can be removed in 3.15 since the verification - * will be done inside the constructor. - */ -static inline int -ast_type_replace_check(PyObject *self, - PyObject *dict, - PyObject *fields, - PyObject *attributes, - PyObject *kwargs) -{ - // While it is possible to make some fast paths that would avoid - // allocating objects on the stack, this would cost us readability. - // For instance, if 'fields' and 'attributes' are both empty, and - // 'kwargs' is not empty, we could raise a TypeError immediately. - PyObject *expecting = PySet_New(fields); - if (expecting == NULL) { - return -1; - } - if (attributes) { - if (_PySet_Update(expecting, attributes) < 0) { - Py_DECREF(expecting); - return -1; - } - } - // Any keyword argument that is neither a field nor attribute is rejected. - // We first need to check whether a keyword argument is accepted or not. - // If all keyword arguments are accepted, we compute the required fields - // and attributes. A field or attribute is not needed if: - // - // 1) it is given in 'kwargs', or - // 2) it already exists on 'self'. - if (kwargs) { - Py_ssize_t pos = 0; - PyObject *key, *value; - while (PyDict_Next(kwargs, &pos, &key, &value)) { - int rc = PySet_Discard(expecting, key); - if (rc < 0) { - Py_DECREF(expecting); - return -1; - } - if (rc == 0) { - PyErr_Format(PyExc_TypeError, - "%.400s.__replace__ got an unexpected keyword " - "argument '%U'.", Py_TYPE(self)->tp_name, key); - Py_DECREF(expecting); - return -1; - } - } - } - // check that the remaining fields or attributes would be filled - if (dict) { - Py_ssize_t pos = 0; - PyObject *key, *value; - while (PyDict_Next(dict, &pos, &key, &value)) { - // Mark fields or attributes that are found on the instance - // as non-mandatory. If they are not given in 'kwargs', they - // will be shallow-coied; otherwise, they would be replaced - // (not in this function). - if (PySet_Discard(expecting, key) < 0) { - Py_DECREF(expecting); - return -1; - } - } - if (attributes) { - // Some attributes may or may not be present at runtime. - // In particular, now that we checked whether 'kwargs' - // is correct or not, we allow any attribute to be missing. - // - // Note that fields must still be entirely determined when - // calling the constructor later. - PyObject *unused = PyObject_CallMethodOneArg(expecting, - &_Py_ID(difference_update), - attributes); - if (unused == NULL) { - Py_DECREF(expecting); - return -1; - } - Py_DECREF(unused); - } - } - - // Discard fields from 'expecting' that default to None - PyObject *field_types = NULL; - if (PyObject_GetOptionalAttr((PyObject*)Py_TYPE(self), - &_Py_ID(_field_types), - &field_types) < 0) - { - Py_DECREF(expecting); - return -1; - } - if (field_types != NULL) { - Py_ssize_t pos = 0; - PyObject *field_name, *field_type; - while (PyDict_Next(field_types, &pos, &field_name, &field_type)) { - if (_PyUnion_Check(field_type)) { - // optional field - if (PySet_Discard(expecting, field_name) < 0) { - Py_DECREF(expecting); - Py_DECREF(field_types); - return -1; - } - } - } - Py_DECREF(field_types); - } - - // Now 'expecting' contains the fields or attributes - // that would not be filled inside ast_type_replace(). - Py_ssize_t m = PySet_GET_SIZE(expecting); - if (m > 0) { - PyObject *names = PyList_New(m); - if (names == NULL) { - Py_DECREF(expecting); - return -1; - } - Py_ssize_t i = 0, pos = 0; - PyObject *item; - Py_hash_t hash; - while (_PySet_NextEntry(expecting, &pos, &item, &hash)) { - PyObject *name = PyObject_Repr(item); - if (name == NULL) { - Py_DECREF(expecting); - Py_DECREF(names); - return -1; - } - // steal the reference 'name' - PyList_SET_ITEM(names, i++, name); - } - Py_DECREF(expecting); - if (PyList_Sort(names) < 0) { - Py_DECREF(names); - return -1; - } - PyObject *sep = PyUnicode_FromString(", "); - if (sep == NULL) { - Py_DECREF(names); - return -1; - } - PyObject *str_names = PyUnicode_Join(sep, names); - Py_DECREF(sep); - Py_DECREF(names); - if (str_names == NULL) { - return -1; - } - PyErr_Format(PyExc_TypeError, - "%.400s.__replace__ missing %ld keyword argument%s: %U.", - Py_TYPE(self)->tp_name, m, m == 1 ? "" : "s", str_names); - Py_DECREF(str_names); - return -1; - } - else { - Py_DECREF(expecting); - return 1; - } -} - /* * Python equivalent: * @@ -5761,9 +5585,6 @@ ast_type_replace(PyObject *self, PyObject *args, PyObject *kwargs) if (PyObject_GetOptionalAttr(self, state->__dict__, &dict) < 0) { goto cleanup; } - if (ast_type_replace_check(self, dict, fields, attributes, kwargs) < 0) { - goto cleanup; - } empty_tuple = PyTuple_New(0); if (empty_tuple == NULL) { goto cleanup; From 4f4466f3ccabd103679439617782279109d22ef3 Mon Sep 17 00:00:00 2001 From: Brian Schubert Date: Sat, 9 Aug 2025 19:02:10 -0400 Subject: [PATCH 03/14] Add whatsnew and news entries --- Doc/whatsnew/3.15.rst | 10 ++++++++++ .../2025-08-09-19-00-36.gh-issue-137600.p_p6OU.rst | 5 +++++ 2 files changed, 15 insertions(+) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2025-08-09-19-00-36.gh-issue-137600.p_p6OU.rst diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index 9f01b52f1aff3b..b310643f3bf6f5 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -418,6 +418,16 @@ hashlib Removed ======= +ast +--- + +* The constructors of node types in the :mod:`ast` module now raise a + :exc:`TypeError` when a required argument is omitted or when a + keyword-argument that does not map to a field on the AST node is passed. + These cases had previously raised a :exc:`DeprecationWarning` since Python 3.13. + (Contributed by Brian Schubert and Jelle Zijlstra in :gh:`137600` and :gh:`105858`.) + + ctypes ------ diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-08-09-19-00-36.gh-issue-137600.p_p6OU.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-08-09-19-00-36.gh-issue-137600.p_p6OU.rst new file mode 100644 index 00000000000000..72f1bd0de58da1 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2025-08-09-19-00-36.gh-issue-137600.p_p6OU.rst @@ -0,0 +1,5 @@ +The constructors of node types in the :mod:`ast` module now raise a +:exc:`TypeError` when a required argument is omitted or when a +keyword-argument that does not map to a field on the AST node is passed. +These cases had previously raised a :exc:`DeprecationWarning` since Python +3.13. Patch by Brian Schubert. From de883a58c70a4d3b011aceb5f3f8c8a7787ca8d1 Mon Sep 17 00:00:00 2001 From: Brian Schubert Date: Sat, 9 Aug 2025 19:21:42 -0400 Subject: [PATCH 04/14] Fix lints --- Parser/asdl_c.py | 8 ++++---- Python/Python-ast.c | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Parser/asdl_c.py b/Parser/asdl_c.py index 0fe2175ca040cc..354ed252879214 100755 --- a/Parser/asdl_c.py +++ b/Parser/asdl_c.py @@ -874,18 +874,18 @@ def visitModule(self, mod): } /* - * Format the names in the set 'missing' into a natural language list, + * Format the names in the set 'missing' into a natural language list, * sorted in the order in which they appear in 'fields'. * * Similar to format_missing from 'Python/ceval.c'. * * Parameters - + * missing Set of missing field names to render. * fields Sequence of AST node field names (self._fields). */ static PyObject * -format_missing(PyObject *missing, PyObject *fields) +format_missing(PyObject *missing, PyObject *fields) { Py_ssize_t num_fields, num_total, num_left; num_fields = PySequence_Size(fields); @@ -894,7 +894,7 @@ def visitModule(self, mod): } num_total = num_left = PySet_GET_SIZE(missing); PyObject *name_str = PyUnicode_FromString(""); - // Iterate all AST node fields in order so that the missing positional + // Iterate all AST node fields in order so that the missing positional // arguments are rendered in the order in which __init__ expects them. for (Py_ssize_t i = 0; i < num_fields; i++) { PyObject *name = PySequence_GetItem(fields, i); diff --git a/Python/Python-ast.c b/Python/Python-ast.c index 08984057d74d5c..97e2d2186607eb 100644 --- a/Python/Python-ast.c +++ b/Python/Python-ast.c @@ -5158,18 +5158,18 @@ ast_clear(PyObject *op) } /* - * Format the names in the set 'missing' into a natural language list, + * Format the names in the set 'missing' into a natural language list, * sorted in the order in which they appear in 'fields'. * * Similar to format_missing from 'Python/ceval.c'. * * Parameters - + * missing Set of missing field names to render. * fields Sequence of AST node field names (self._fields). */ static PyObject * -format_missing(PyObject *missing, PyObject *fields) +format_missing(PyObject *missing, PyObject *fields) { Py_ssize_t num_fields, num_total, num_left; num_fields = PySequence_Size(fields); @@ -5178,7 +5178,7 @@ format_missing(PyObject *missing, PyObject *fields) } num_total = num_left = PySet_GET_SIZE(missing); PyObject *name_str = PyUnicode_FromString(""); - // Iterate all AST node fields in order so that the missing positional + // Iterate all AST node fields in order so that the missing positional // arguments are rendered in the order in which __init__ expects them. for (Py_ssize_t i = 0; i < num_fields; i++) { PyObject *name = PySequence_GetItem(fields, i); From 39cdf209146fb81ca20b8e8f116df0282a40f133 Mon Sep 17 00:00:00 2001 From: Brian Schubert Date: Sat, 9 Aug 2025 20:41:49 -0400 Subject: [PATCH 05/14] Handle error, fix leaked ref --- Parser/asdl_c.py | 13 ++++++++++--- Python/Python-ast.c | 13 ++++++++++--- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/Parser/asdl_c.py b/Parser/asdl_c.py index 354ed252879214..ac9c95484f6e9d 100755 --- a/Parser/asdl_c.py +++ b/Parser/asdl_c.py @@ -923,13 +923,20 @@ def visitModule(self, mod): fmt = "'%U', "; } num_left--; - PyObject *tmp = PyUnicode_FromFormat(fmt, name); - if (!tmp) { + PyObject *tail = PyUnicode_FromFormat(fmt, name); + if (!tail) { Py_DECREF(name_str); Py_DECREF(name); return NULL; } - name_str = PyUnicode_Concat(name_str, tmp); + PyObject *tmp = PyUnicode_Concat(name_str, tail); + Py_DECREF(name_str); + Py_DECREF(tail); + if (!tmp) { + Py_DECREF(name); + return NULL; + } + name_str = tmp; } Py_DECREF(name); } diff --git a/Python/Python-ast.c b/Python/Python-ast.c index 97e2d2186607eb..c67829ba26e275 100644 --- a/Python/Python-ast.c +++ b/Python/Python-ast.c @@ -5207,13 +5207,20 @@ format_missing(PyObject *missing, PyObject *fields) fmt = "'%U', "; } num_left--; - PyObject *tmp = PyUnicode_FromFormat(fmt, name); - if (!tmp) { + PyObject *tail = PyUnicode_FromFormat(fmt, name); + if (!tail) { Py_DECREF(name_str); Py_DECREF(name); return NULL; } - name_str = PyUnicode_Concat(name_str, tmp); + PyObject *tmp = PyUnicode_Concat(name_str, tail); + Py_DECREF(name_str); + Py_DECREF(tail); + if (!tmp) { + Py_DECREF(name); + return NULL; + } + name_str = tmp; } Py_DECREF(name); } From 1d3a3b5a5c315def0eab334eecc4d6f58e116547 Mon Sep 17 00:00:00 2001 From: Brian Schubert Date: Sat, 9 Aug 2025 20:53:12 -0400 Subject: [PATCH 06/14] Minor tidy --- Parser/asdl_c.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Parser/asdl_c.py b/Parser/asdl_c.py index ac9c95484f6e9d..1cfe81227fb85f 100755 --- a/Parser/asdl_c.py +++ b/Parser/asdl_c.py @@ -880,7 +880,7 @@ def visitModule(self, mod): * Similar to format_missing from 'Python/ceval.c'. * * Parameters - + * * missing Set of missing field names to render. * fields Sequence of AST node field names (self._fields). */ @@ -1034,8 +1034,8 @@ def visitModule(self, mod): } else if (contains == 0) { PyErr_Format(PyExc_TypeError, - "%T.__init__ got an unexpected keyword " - "argument '%U'", self, key); + "%T.__init__ got an unexpected keyword argument '%U'", + self, key); res = -1; goto cleanup; } @@ -1115,7 +1115,7 @@ def visitModule(self, mod): } Py_ssize_t num_missing = PySet_GET_SIZE(missing_names); if (num_missing > 0) { - PyObject* name_str = format_missing(missing_names, fields); + PyObject *name_str = format_missing(missing_names, fields); if (!name_str) { goto set_remaining_cleanup; } From c0663383f70efe6a4ae5478d9575e65ad8529447 Mon Sep 17 00:00:00 2001 From: Brian Schubert Date: Sat, 9 Aug 2025 21:18:32 -0400 Subject: [PATCH 07/14] Minor tidy --- Doc/whatsnew/3.15.rst | 2 +- .../2025-08-09-19-00-36.gh-issue-137600.p_p6OU.rst | 2 +- Python/Python-ast.c | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index b310643f3bf6f5..e86c7450c4aa3a 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -421,7 +421,7 @@ Removed ast --- -* The constructors of node types in the :mod:`ast` module now raise a +* The constructors of node types in the :mod:`ast` module now raise a :exc:`TypeError` when a required argument is omitted or when a keyword-argument that does not map to a field on the AST node is passed. These cases had previously raised a :exc:`DeprecationWarning` since Python 3.13. diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-08-09-19-00-36.gh-issue-137600.p_p6OU.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-08-09-19-00-36.gh-issue-137600.p_p6OU.rst index 72f1bd0de58da1..330d646f67a26b 100644 --- a/Misc/NEWS.d/next/Core_and_Builtins/2025-08-09-19-00-36.gh-issue-137600.p_p6OU.rst +++ b/Misc/NEWS.d/next/Core_and_Builtins/2025-08-09-19-00-36.gh-issue-137600.p_p6OU.rst @@ -1,4 +1,4 @@ -The constructors of node types in the :mod:`ast` module now raise a +The constructors of node types in the :mod:`ast` module now raise a :exc:`TypeError` when a required argument is omitted or when a keyword-argument that does not map to a field on the AST node is passed. These cases had previously raised a :exc:`DeprecationWarning` since Python diff --git a/Python/Python-ast.c b/Python/Python-ast.c index c67829ba26e275..e3698868dd27cc 100644 --- a/Python/Python-ast.c +++ b/Python/Python-ast.c @@ -5164,7 +5164,7 @@ ast_clear(PyObject *op) * Similar to format_missing from 'Python/ceval.c'. * * Parameters - + * * missing Set of missing field names to render. * fields Sequence of AST node field names (self._fields). */ @@ -5318,8 +5318,8 @@ ast_type_init(PyObject *self, PyObject *args, PyObject *kw) } else if (contains == 0) { PyErr_Format(PyExc_TypeError, - "%T.__init__ got an unexpected keyword " - "argument '%U'", self, key); + "%T.__init__ got an unexpected keyword argument '%U'", + self, key); res = -1; goto cleanup; } @@ -5399,7 +5399,7 @@ ast_type_init(PyObject *self, PyObject *args, PyObject *kw) } Py_ssize_t num_missing = PySet_GET_SIZE(missing_names); if (num_missing > 0) { - PyObject* name_str = format_missing(missing_names, fields); + PyObject *name_str = format_missing(missing_names, fields); if (!name_str) { goto set_remaining_cleanup; } From 48267a327766ad0807332917696dc2f3bf5287f2 Mon Sep 17 00:00:00 2001 From: Brian Schubert Date: Sun, 10 Aug 2025 09:17:18 -0400 Subject: [PATCH 08/14] Switch to PyUnicodeWriter --- Parser/asdl_c.py | 34 ++++++++++++++-------------------- Python/Python-ast.c | 34 ++++++++++++++-------------------- 2 files changed, 28 insertions(+), 40 deletions(-) diff --git a/Parser/asdl_c.py b/Parser/asdl_c.py index 1cfe81227fb85f..694445d42bac59 100755 --- a/Parser/asdl_c.py +++ b/Parser/asdl_c.py @@ -877,7 +877,7 @@ def visitModule(self, mod): * Format the names in the set 'missing' into a natural language list, * sorted in the order in which they appear in 'fields'. * - * Similar to format_missing from 'Python/ceval.c'. + * Similar to format_missing() from 'Python/ceval.c'. * * Parameters * @@ -893,20 +893,21 @@ def visitModule(self, mod): return NULL; } num_total = num_left = PySet_GET_SIZE(missing); - PyObject *name_str = PyUnicode_FromString(""); + PyUnicodeWriter *writer = PyUnicodeWriter_Create(0); + if (writer == NULL) { + goto error; + } // Iterate all AST node fields in order so that the missing positional // arguments are rendered in the order in which __init__ expects them. for (Py_ssize_t i = 0; i < num_fields; i++) { PyObject *name = PySequence_GetItem(fields, i); - if (!name) { - Py_DECREF(name_str); - return NULL; + if (name == NULL) { + goto error; } int contains = PySet_Contains(missing, name); if (contains == -1) { - Py_DECREF(name_str); Py_DECREF(name); - return NULL; + goto error; } else if (contains == 1) { const char* fmt = NULL; @@ -923,24 +924,17 @@ def visitModule(self, mod): fmt = "'%U', "; } num_left--; - PyObject *tail = PyUnicode_FromFormat(fmt, name); - if (!tail) { - Py_DECREF(name_str); + if (PyUnicodeWriter_Format(writer, fmt, name) < 0) { Py_DECREF(name); - return NULL; - } - PyObject *tmp = PyUnicode_Concat(name_str, tail); - Py_DECREF(name_str); - Py_DECREF(tail); - if (!tmp) { - Py_DECREF(name); - return NULL; + goto error; } - name_str = tmp; } Py_DECREF(name); } - return name_str; + return PyUnicodeWriter_Finish(writer); +error: + PyUnicodeWriter_Discard(writer); + return NULL; } static int diff --git a/Python/Python-ast.c b/Python/Python-ast.c index e3698868dd27cc..2f63a25375504f 100644 --- a/Python/Python-ast.c +++ b/Python/Python-ast.c @@ -5161,7 +5161,7 @@ ast_clear(PyObject *op) * Format the names in the set 'missing' into a natural language list, * sorted in the order in which they appear in 'fields'. * - * Similar to format_missing from 'Python/ceval.c'. + * Similar to format_missing() from 'Python/ceval.c'. * * Parameters * @@ -5177,20 +5177,21 @@ format_missing(PyObject *missing, PyObject *fields) return NULL; } num_total = num_left = PySet_GET_SIZE(missing); - PyObject *name_str = PyUnicode_FromString(""); + PyUnicodeWriter *writer = PyUnicodeWriter_Create(0); + if (writer == NULL) { + goto error; + } // Iterate all AST node fields in order so that the missing positional // arguments are rendered in the order in which __init__ expects them. for (Py_ssize_t i = 0; i < num_fields; i++) { PyObject *name = PySequence_GetItem(fields, i); - if (!name) { - Py_DECREF(name_str); - return NULL; + if (name == NULL) { + goto error; } int contains = PySet_Contains(missing, name); if (contains == -1) { - Py_DECREF(name_str); Py_DECREF(name); - return NULL; + goto error; } else if (contains == 1) { const char* fmt = NULL; @@ -5207,24 +5208,17 @@ format_missing(PyObject *missing, PyObject *fields) fmt = "'%U', "; } num_left--; - PyObject *tail = PyUnicode_FromFormat(fmt, name); - if (!tail) { - Py_DECREF(name_str); + if (PyUnicodeWriter_Format(writer, fmt, name) < 0) { Py_DECREF(name); - return NULL; - } - PyObject *tmp = PyUnicode_Concat(name_str, tail); - Py_DECREF(name_str); - Py_DECREF(tail); - if (!tmp) { - Py_DECREF(name); - return NULL; + goto error; } - name_str = tmp; } Py_DECREF(name); } - return name_str; + return PyUnicodeWriter_Finish(writer); +error: + PyUnicodeWriter_Discard(writer); + return NULL; } static int From fa270ff6fac7224ddcabc8bc7a7cb86f3dc021e4 Mon Sep 17 00:00:00 2001 From: Brian Schubert Date: Sun, 10 Aug 2025 09:30:07 -0400 Subject: [PATCH 09/14] Misc updates from review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com> --- Doc/whatsnew/3.15.rst | 2 +- Lib/test/test_ast/test_ast.py | 15 +++++++++------ 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index e86c7450c4aa3a..872b107ca1cbf5 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -421,7 +421,7 @@ Removed ast --- -* The constructors of node types in the :mod:`ast` module now raise a +* The constructors of AST node types in the :mod:`ast` module now raise a :exc:`TypeError` when a required argument is omitted or when a keyword-argument that does not map to a field on the AST node is passed. These cases had previously raised a :exc:`DeprecationWarning` since Python 3.13. diff --git a/Lib/test/test_ast/test_ast.py b/Lib/test/test_ast/test_ast.py index 4c0b944ee2cdc6..81f79b03390fe2 100644 --- a/Lib/test/test_ast/test_ast.py +++ b/Lib/test/test_ast/test_ast.py @@ -545,6 +545,7 @@ def test_nodeclasses(self): # Random attribute allowed too x.foobarbaz = 5 self.assertEqual(x.foobarbaz, 5) + self.assertEqual(x._fields, ('left', 'op', 'right')) x = ast.BinOp(1, 2, 3) self.assertEqual(x.left, 1) @@ -570,7 +571,8 @@ def test_nodeclasses(self): self.assertEqual(x.lineno, 0) # Random kwargs are not allowed - with self.assertRaisesRegex(TypeError, "unexpected keyword argument 'foobarbaz'"): + msg = "ast.BinOp.__init__ got an unexpected keyword argument 'foobarbaz'" + with self.assertRaisesRegex(TypeError, re.escape(msg)): x = ast.BinOp(1, 2, 3, foobarbaz=42) def test_no_fields(self): @@ -3271,8 +3273,8 @@ class MyAttrs(ast.AST): self.assertEqual(obj.a, 1) self.assertEqual(obj.b, 2) - with self.assertRaisesRegex(TypeError, - r"MyAttrs.__init__ got an unexpected keyword argument 'c'"): + msg = "MyAttrs.__init__ got an unexpected keyword argument 'c'" + with self.assertRaisesRegex(TypeError, re.escape(msg)): obj = MyAttrs(c=3) def test_fields_and_types_no_default(self): @@ -3280,8 +3282,8 @@ class FieldsAndTypesNoDefault(ast.AST): _fields = ('a',) _field_types = {'a': int} - with self.assertRaisesRegex(TypeError, - r"FieldsAndTypesNoDefault\.__init__ missing 1 required positional argument: 'a'"): + msg = "FieldsAndTypesNoDefault.__init__ missing 1 required positional argument: 'a'" + with self.assertRaisesRegex(TypeError, re.escape(msg)): obj = FieldsAndTypesNoDefault() obj = FieldsAndTypesNoDefault(a=1) @@ -3294,7 +3296,8 @@ class MoreFieldsThanTypes(ast.AST): a: int | None = None b: int | None = None - with self.assertRaisesRegex(TypeError, "Field 'b' is missing"): + msg = "Field 'b' is missing from test.test_ast.test_ast.ASTConstructorTests.test_incomplete_field_types..MoreFieldsThanTypes._field_types" + with self.assertRaisesRegex(TypeError, re.escape(msg)): obj = MoreFieldsThanTypes() obj = MoreFieldsThanTypes(a=1, b=2) From 606708a63dc30cd52481c09627a666b4cbfa968d Mon Sep 17 00:00:00 2001 From: Brian Schubert Date: Sun, 10 Aug 2025 09:40:54 -0400 Subject: [PATCH 10/14] Use fully qualified type names more consistently, more `msg =` in tests --- Lib/test/test_ast/test_ast.py | 17 +++++++++-------- Parser/asdl_c.py | 4 ++-- Python/Python-ast.c | 4 ++-- 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/Lib/test/test_ast/test_ast.py b/Lib/test/test_ast/test_ast.py index 81f79b03390fe2..a5acf14b9caeb3 100644 --- a/Lib/test/test_ast/test_ast.py +++ b/Lib/test/test_ast/test_ast.py @@ -458,7 +458,7 @@ def test_field_attr_writable(self): self.assertEqual(x._fields, 666) def test_classattrs(self): - msg = "Constant.__init__ missing 1 required positional argument: 'value'" + msg = "ast.Constant.__init__ missing 1 required positional argument: 'value'" with self.assertRaisesRegex(TypeError, re.escape(msg)): x = ast.Constant() @@ -485,11 +485,12 @@ def test_classattrs(self): self.assertRaises(TypeError, ast.Constant, 1, None, 2, lineno=0) # Arbitrary keyword arguments are not supported - msg = "Constant.__init__ got an unexpected keyword argument 'foo'" + msg = "ast.Constant.__init__ got an unexpected keyword argument 'foo'" with self.assertRaisesRegex(TypeError, re.escape(msg)): ast.Constant(1, foo='bar') - with self.assertRaisesRegex(TypeError, "Constant got multiple values for argument 'value'"): + msg = "ast.Constant got multiple values for argument 'value'" + with self.assertRaisesRegex(TypeError, re.escape(msg)): ast.Constant(1, value=2) self.assertEqual(ast.Constant(42).value, 42) @@ -529,7 +530,7 @@ def test_module(self): def test_nodeclasses(self): # Zero arguments constructor is not allowed - msg = "missing 3 required positional arguments: 'left', 'op', and 'right'" + msg = "ast.BinOp.__init__ missing 3 required positional arguments: 'left', 'op', and 'right'" with self.assertRaisesRegex(TypeError, re.escape(msg)): x = ast.BinOp() @@ -3211,8 +3212,8 @@ def test_FunctionDef(self): args = ast.arguments() self.assertEqual(args.args, []) self.assertEqual(args.posonlyargs, []) - with self.assertRaisesRegex(TypeError, - r"FunctionDef\.__init__ missing 1 required positional argument: 'name'"): + msg = "ast.FunctionDef.__init__ missing 1 required positional argument: 'name'" + with self.assertRaisesRegex(TypeError, re.escape(msg)): node = ast.FunctionDef(args=args) node = ast.FunctionDef(name='foo', args=args) @@ -3232,8 +3233,8 @@ def test_expr_context(self): self.assertEqual(name3.id, "x") self.assertIsInstance(name3.ctx, ast.Del) - with self.assertRaisesRegex(TypeError, - r"Name\.__init__ missing 1 required positional argument: 'id'"): + msg = "ast.Name.__init__ missing 1 required positional argument: 'id'" + with self.assertRaisesRegex(TypeError, re.escape(msg)): name3 = ast.Name() def test_custom_subclass_with_no_fields(self): diff --git a/Parser/asdl_c.py b/Parser/asdl_c.py index 694445d42bac59..1b63c336630fc4 100755 --- a/Parser/asdl_c.py +++ b/Parser/asdl_c.py @@ -1006,8 +1006,8 @@ def visitModule(self, mod): } if (p == 0) { PyErr_Format(PyExc_TypeError, - "%.400s got multiple values for argument '%U'", - Py_TYPE(self)->tp_name, key); + "%T got multiple values for argument '%U'", + self, key); res = -1; goto cleanup; } diff --git a/Python/Python-ast.c b/Python/Python-ast.c index 2f63a25375504f..35ef7bfa498072 100644 --- a/Python/Python-ast.c +++ b/Python/Python-ast.c @@ -5290,8 +5290,8 @@ ast_type_init(PyObject *self, PyObject *args, PyObject *kw) } if (p == 0) { PyErr_Format(PyExc_TypeError, - "%.400s got multiple values for argument '%U'", - Py_TYPE(self)->tp_name, key); + "%T got multiple values for argument '%U'", + self, key); res = -1; goto cleanup; } From f5b258235b8d74b2cd339d00c6638dce385ba7df Mon Sep 17 00:00:00 2001 From: Brian Schubert Date: Fri, 15 Aug 2025 11:17:02 -0400 Subject: [PATCH 11/14] Update docs --- Doc/library/ast.rst | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Doc/library/ast.rst b/Doc/library/ast.rst index 319b2c81505f48..ebc738808a37ee 100644 --- a/Doc/library/ast.rst +++ b/Doc/library/ast.rst @@ -158,8 +158,7 @@ Node classes Previous versions of Python allowed the creation of AST nodes that were missing required fields. Similarly, AST node constructors allowed arbitrary keyword arguments that were set as attributes of the AST node, even if they did not - match any of the fields of the AST node. This behavior is deprecated and will - be removed in Python 3.15. + match any of the fields of the AST node. These cases now raise a :exc:`TypeError`. .. note:: The descriptions of the specific node classes displayed here From 92ac6d1f65b704114eb167e4e9531c5df8b7f843 Mon Sep 17 00:00:00 2001 From: Brian Schubert Date: Mon, 18 Aug 2025 17:18:29 -0400 Subject: [PATCH 12/14] Review suggestions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com> --- Doc/whatsnew/3.15.rst | 2 +- Lib/test/test_ast/test_ast.py | 6 +++--- .../2025-08-09-19-00-36.gh-issue-137600.p_p6OU.rst | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index 872b107ca1cbf5..5ec5af7741926f 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -423,7 +423,7 @@ ast * The constructors of AST node types in the :mod:`ast` module now raise a :exc:`TypeError` when a required argument is omitted or when a - keyword-argument that does not map to a field on the AST node is passed. + keyword argument that does not map to a field on the AST node is passed. These cases had previously raised a :exc:`DeprecationWarning` since Python 3.13. (Contributed by Brian Schubert and Jelle Zijlstra in :gh:`137600` and :gh:`105858`.) diff --git a/Lib/test/test_ast/test_ast.py b/Lib/test/test_ast/test_ast.py index a5acf14b9caeb3..76e0c0e231f084 100644 --- a/Lib/test/test_ast/test_ast.py +++ b/Lib/test/test_ast/test_ast.py @@ -1408,7 +1408,7 @@ def test_replace_ignore_known_custom_instance_fields(self): self.assertRaises(AttributeError, getattr, repl, 'extra') def test_replace_reject_missing_field(self): - # case: warn if deleted field is not replaced + # case: raise if deleted field is not replaced node = ast.parse('x').body[0].value context = node.ctx del node.id @@ -3297,8 +3297,8 @@ class MoreFieldsThanTypes(ast.AST): a: int | None = None b: int | None = None - msg = "Field 'b' is missing from test.test_ast.test_ast.ASTConstructorTests.test_incomplete_field_types..MoreFieldsThanTypes._field_types" - with self.assertRaisesRegex(TypeError, re.escape(msg)): + msg = "Field 'b' is missing from .*\.MoreFieldsThanTypes\._field_types" + with self.assertRaisesRegex(TypeError, msg): obj = MoreFieldsThanTypes() obj = MoreFieldsThanTypes(a=1, b=2) diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-08-09-19-00-36.gh-issue-137600.p_p6OU.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-08-09-19-00-36.gh-issue-137600.p_p6OU.rst index 330d646f67a26b..c78918fb8cb2ed 100644 --- a/Misc/NEWS.d/next/Core_and_Builtins/2025-08-09-19-00-36.gh-issue-137600.p_p6OU.rst +++ b/Misc/NEWS.d/next/Core_and_Builtins/2025-08-09-19-00-36.gh-issue-137600.p_p6OU.rst @@ -1,5 +1,5 @@ The constructors of node types in the :mod:`ast` module now raise a :exc:`TypeError` when a required argument is omitted or when a -keyword-argument that does not map to a field on the AST node is passed. +keyword argument that does not map to a field on the AST node is passed. These cases had previously raised a :exc:`DeprecationWarning` since Python 3.13. Patch by Brian Schubert. From 6ccb623b96515f80cb09ae670f15d92feddfdd4c Mon Sep 17 00:00:00 2001 From: Brian Schubert Date: Mon, 18 Aug 2025 17:24:59 -0400 Subject: [PATCH 13/14] Tidy tests --- Lib/test/test_ast/test_ast.py | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/Lib/test/test_ast/test_ast.py b/Lib/test/test_ast/test_ast.py index 76e0c0e231f084..9e46beae83d4dc 100644 --- a/Lib/test/test_ast/test_ast.py +++ b/Lib/test/test_ast/test_ast.py @@ -459,8 +459,7 @@ def test_field_attr_writable(self): def test_classattrs(self): msg = "ast.Constant.__init__ missing 1 required positional argument: 'value'" - with self.assertRaisesRegex(TypeError, re.escape(msg)): - x = ast.Constant() + self.assertRaisesRegex(TypeError, re.escape(msg), ast.Constant) x = ast.Constant(42) self.assertEqual(x._fields, ('value', 'kind')) @@ -531,8 +530,7 @@ def test_module(self): def test_nodeclasses(self): # Zero arguments constructor is not allowed msg = "ast.BinOp.__init__ missing 3 required positional arguments: 'left', 'op', and 'right'" - with self.assertRaisesRegex(TypeError, re.escape(msg)): - x = ast.BinOp() + self.assertRaisesRegex(TypeError, re.escape(msg), ast.BinOp) n1 = ast.Constant(1) n3 = ast.Constant(3) @@ -574,7 +572,7 @@ def test_nodeclasses(self): # Random kwargs are not allowed msg = "ast.BinOp.__init__ got an unexpected keyword argument 'foobarbaz'" with self.assertRaisesRegex(TypeError, re.escape(msg)): - x = ast.BinOp(1, 2, 3, foobarbaz=42) + ast.BinOp(1, 2, 3, foobarbaz=42) def test_no_fields(self): # this used to fail because Sub._fields was None @@ -3214,7 +3212,7 @@ def test_FunctionDef(self): self.assertEqual(args.posonlyargs, []) msg = "ast.FunctionDef.__init__ missing 1 required positional argument: 'name'" with self.assertRaisesRegex(TypeError, re.escape(msg)): - node = ast.FunctionDef(args=args) + ast.FunctionDef(args=args) node = ast.FunctionDef(name='foo', args=args) self.assertEqual(node.name, 'foo') @@ -3234,8 +3232,7 @@ def test_expr_context(self): self.assertIsInstance(name3.ctx, ast.Del) msg = "ast.Name.__init__ missing 1 required positional argument: 'id'" - with self.assertRaisesRegex(TypeError, re.escape(msg)): - name3 = ast.Name() + self.assertRaisesRegex(TypeError, re.escape(msg), ast.Name) def test_custom_subclass_with_no_fields(self): class NoInit(ast.AST): @@ -3276,7 +3273,7 @@ class MyAttrs(ast.AST): msg = "MyAttrs.__init__ got an unexpected keyword argument 'c'" with self.assertRaisesRegex(TypeError, re.escape(msg)): - obj = MyAttrs(c=3) + MyAttrs(c=3) def test_fields_and_types_no_default(self): class FieldsAndTypesNoDefault(ast.AST): @@ -3284,8 +3281,7 @@ class FieldsAndTypesNoDefault(ast.AST): _field_types = {'a': int} msg = "FieldsAndTypesNoDefault.__init__ missing 1 required positional argument: 'a'" - with self.assertRaisesRegex(TypeError, re.escape(msg)): - obj = FieldsAndTypesNoDefault() + self.assertRaisesRegex(TypeError, re.escape(msg), FieldsAndTypesNoDefault) obj = FieldsAndTypesNoDefault(a=1) self.assertEqual(obj.a, 1) @@ -3298,8 +3294,7 @@ class MoreFieldsThanTypes(ast.AST): b: int | None = None msg = "Field 'b' is missing from .*\.MoreFieldsThanTypes\._field_types" - with self.assertRaisesRegex(TypeError, msg): - obj = MoreFieldsThanTypes() + self.assertRaisesRegex(TypeError, msg, MoreFieldsThanTypes) obj = MoreFieldsThanTypes(a=1, b=2) self.assertEqual(obj.a, 1) From b0e1c6710620131c9fa63eb65fe4bd0ef6206249 Mon Sep 17 00:00:00 2001 From: Brian Schubert Date: Mon, 18 Aug 2025 17:31:53 -0400 Subject: [PATCH 14/14] Use raw string for regex --- Lib/test/test_ast/test_ast.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_ast/test_ast.py b/Lib/test/test_ast/test_ast.py index 9e46beae83d4dc..f21c96ed35fded 100644 --- a/Lib/test/test_ast/test_ast.py +++ b/Lib/test/test_ast/test_ast.py @@ -3293,7 +3293,7 @@ class MoreFieldsThanTypes(ast.AST): a: int | None = None b: int | None = None - msg = "Field 'b' is missing from .*\.MoreFieldsThanTypes\._field_types" + msg = r"Field 'b' is missing from .*\.MoreFieldsThanTypes\._field_types" self.assertRaisesRegex(TypeError, msg, MoreFieldsThanTypes) obj = MoreFieldsThanTypes(a=1, b=2)