Skip to content

gh-137600: Promote ast node constructor deprecation warnings to errors #137601

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
Open
Prev Previous commit
Next Next commit
Remove obsolete checks for copy.replace support
  • Loading branch information
brianschubert committed Aug 9, 2025
commit de5564a645c3b4b0c36f400c0404b3f8a430e1dc
6 changes: 3 additions & 3 deletions Lib/test/test_ast/test_ast.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
179 changes: 0 additions & 179 deletions Parser/asdl_c.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
*
Expand Down Expand Up @@ -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;
Expand Down
Loading