]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
[3.14] gh-133783: Fix __replace__ on AST nodes for optional attributes (GH-133797...
authorMiss Islington (bot) <31488909+miss-islington@users.noreply.github.com>
Sat, 10 May 2025 16:44:07 +0000 (18:44 +0200)
committerGitHub <noreply@github.com>
Sat, 10 May 2025 16:44:07 +0000 (16:44 +0000)
gh-133783: Fix __replace__ on AST nodes for optional attributes (GH-133797)
(cherry picked from commit 7dddb4e667b5eb76cbe11755051ec139b0f437a9)

Co-authored-by: Jelle Zijlstra <jelle.zijlstra@gmail.com>
Lib/test/test_ast/test_ast.py
Misc/NEWS.d/next/Library/2025-05-09-19-05-24.gh-issue-133783.1voCnR.rst [new file with mode: 0644]
Parser/asdl_c.py
Python/Python-ast.c

index 02628868db008cd3094cb4c92083839e754e4e3d..0776559b9003db77e9fc8a5262b57977a3761799 100644 (file)
@@ -1315,6 +1315,15 @@ class CopyTests(unittest.TestCase):
         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 (file)
index 0000000..62e742d
--- /dev/null
@@ -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.
index 09e014534fbabc3a25c79275efaf7c9e6bd3b630..22dcfe1b0d99bf7af0d1d8298508a5af37e8d2c2 100755 (executable)
@@ -1244,6 +1244,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);
index df035568f84be1cd1987e8bf2abf291a70f0607d..f7625ab1205bdc4cb8824aa44b9fc26444901477 100644 (file)
@@ -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);