From d8343200bad438057ec60e68cb7a278e30edf64a Mon Sep 17 00:00:00 2001 From: Brian Schubert Date: Sat, 9 Aug 2025 18:48:02 -0400 Subject: [PATCH 01/10] 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/10] 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/10] 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/10] 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/10] 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/10] 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/10] 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/10] 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/10] 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/10] 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; }