https://github.com/python/cpython/commit/7dddb4e667b5eb76cbe11755051ec139b0f437a9
commit: 7dddb4e667b5eb76cbe11755051ec139b0f437a9
branch: main
author: Jelle Zijlstra <jelle.zijls...@gmail.com>
committer: JelleZijlstra <jelle.zijls...@gmail.com>
date: 2025-05-10T09:17:38-07:00
summary:

gh-133783: Fix __replace__ on AST nodes for optional attributes (#133797)

files:
A Misc/NEWS.d/next/Library/2025-05-09-19-05-24.gh-issue-133783.1voCnR.rst
M Lib/test/test_ast/test_ast.py
M Parser/asdl_c.py
M Python/Python-ast.c

diff --git a/Lib/test/test_ast/test_ast.py b/Lib/test/test_ast/test_ast.py
index 02628868db008c..0776559b9003db 100644
--- a/Lib/test/test_ast/test_ast.py
+++ b/Lib/test/test_ast/test_ast.py
@@ -1315,6 +1315,15 @@ def test_replace_reject_missing_field(self):
         self.assertIs(repl.id, 'y')
         self.assertIs(repl.ctx, context)
 
+    def test_replace_accept_missing_field_with_default(self):
+        node = ast.FunctionDef(name="foo", args=ast.arguments())
+        self.assertIs(node.returns, None)
+        self.assertEqual(node.decorator_list, [])
+        node2 = copy.replace(node, name="bar")
+        self.assertEqual(node2.name, "bar")
+        self.assertIs(node2.returns, None)
+        self.assertEqual(node2.decorator_list, [])
+
     def test_replace_reject_known_custom_instance_fields_commits(self):
         node = ast.parse('x').body[0].value
         node.extra = extra = object()  # add instance 'extra' field
diff --git 
a/Misc/NEWS.d/next/Library/2025-05-09-19-05-24.gh-issue-133783.1voCnR.rst 
b/Misc/NEWS.d/next/Library/2025-05-09-19-05-24.gh-issue-133783.1voCnR.rst
new file mode 100644
index 00000000000000..62e742df17954e
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2025-05-09-19-05-24.gh-issue-133783.1voCnR.rst
@@ -0,0 +1,3 @@
+Fix bug with applying :func:`copy.replace` to :mod:`ast` objects. Attributes
+that default to ``None`` were incorrectly treated as required for manually
+created AST nodes.
diff --git a/Parser/asdl_c.py b/Parser/asdl_c.py
index 09e014534fbabc..22dcfe1b0d99bf 100755
--- a/Parser/asdl_c.py
+++ b/Parser/asdl_c.py
@@ -1244,6 +1244,32 @@ def visitModule(self, mod):
             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);
diff --git a/Python/Python-ast.c b/Python/Python-ast.c
index df035568f84be1..f7625ab1205bdc 100644
--- a/Python/Python-ast.c
+++ b/Python/Python-ast.c
@@ -5528,6 +5528,32 @@ ast_type_replace_check(PyObject *self,
             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);

_______________________________________________
Python-checkins mailing list -- python-checkins@python.org
To unsubscribe send an email to python-checkins-le...@python.org
https://mail.python.org/mailman3/lists/python-checkins.python.org/
Member address: arch...@mail-archive.com

Reply via email to