Skip to content

Commit 907ee1f

Browse files
bpo-36290: Fix keytword collision handling in AST node constructors (GH-12382)
(cherry picked from commit c73914a) Co-authored-by: Rémi Lapeyre <[email protected]>
1 parent 1ae0fd8 commit 907ee1f

File tree

5 files changed

+66
-6
lines changed

5 files changed

+66
-6
lines changed

Lib/ast.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -483,6 +483,13 @@ def __instancecheck__(cls, inst):
483483
return type.__instancecheck__(cls, inst)
484484

485485
def _new(cls, *args, **kwargs):
486+
for key in kwargs:
487+
if key not in cls._fields:
488+
# arbitrary keyword arguments are accepted
489+
continue
490+
pos = cls._fields.index(key)
491+
if pos < len(args):
492+
raise TypeError(f"{cls.__name__} got multiple values for argument {key!r}")
486493
if cls in _const_types:
487494
return Constant(*args, **kwargs)
488495
return Constant.__new__(cls, *args, **kwargs)

Lib/test/test_ast.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -387,6 +387,15 @@ def test_classattrs(self):
387387
self.assertRaises(TypeError, ast.Num, 1, None, 2)
388388
self.assertRaises(TypeError, ast.Num, 1, None, 2, lineno=0)
389389

390+
# Arbitrary keyword arguments are supported
391+
self.assertEqual(ast.Constant(1, foo='bar').foo, 'bar')
392+
self.assertEqual(ast.Num(1, foo='bar').foo, 'bar')
393+
394+
with self.assertRaisesRegex(TypeError, "Num got multiple values for argument 'n'"):
395+
ast.Num(1, n=2)
396+
with self.assertRaisesRegex(TypeError, "Constant got multiple values for argument 'value'"):
397+
ast.Constant(1, value=2)
398+
390399
self.assertEqual(ast.Num(42).n, 42)
391400
self.assertEqual(ast.Num(4.25).n, 4.25)
392401
self.assertEqual(ast.Num(4.25j).n, 4.25j)
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
AST nodes are now raising :exc:`TypeError` on conflicting keyword arguments.
2+
Patch contributed by Rémi Lapeyre.

Parser/asdl_c.py

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -665,8 +665,9 @@ def visitModule(self, mod):
665665
}
666666
if (fields) {
667667
numfields = PySequence_Size(fields);
668-
if (numfields == -1)
668+
if (numfields == -1) {
669669
goto cleanup;
670+
}
670671
}
671672
672673
res = 0; /* if no error occurs, this stays 0 to the end */
@@ -687,15 +688,35 @@ def visitModule(self, mod):
687688
}
688689
res = PyObject_SetAttr(self, name, PyTuple_GET_ITEM(args, i));
689690
Py_DECREF(name);
690-
if (res < 0)
691+
if (res < 0) {
691692
goto cleanup;
693+
}
692694
}
693695
if (kw) {
694696
i = 0; /* needed by PyDict_Next */
695697
while (PyDict_Next(kw, &i, &key, &value)) {
698+
int contains = PySequence_Contains(fields, key);
699+
if (contains == -1) {
700+
res = -1;
701+
goto cleanup;
702+
} else if (contains == 1) {
703+
Py_ssize_t p = PySequence_Index(fields, key);
704+
if (p == -1) {
705+
res = -1;
706+
goto cleanup;
707+
}
708+
if (p < PyTuple_GET_SIZE(args)) {
709+
PyErr_Format(PyExc_TypeError,
710+
"%.400s got multiple values for argument '%U'",
711+
Py_TYPE(self)->tp_name, key);
712+
res = -1;
713+
goto cleanup;
714+
}
715+
}
696716
res = PyObject_SetAttr(self, key, value);
697-
if (res < 0)
717+
if (res < 0) {
698718
goto cleanup;
719+
}
699720
}
700721
}
701722
cleanup:

Python/Python-ast.c

Lines changed: 24 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)