From 0461451e4bbb33a47edb9a789a823575bd6a7c1c Mon Sep 17 00:00:00 2001 From: Cody Maloney Date: Thu, 29 Oct 2026 18:02:55 -0700 Subject: [PATCH 1/4] gh-87613: Support **kwargs in Argument Clinic vectorcall `@vectorcall` with `**kwargs` always delegated to the keyword helper, which only reads a kwargs dict, so it couldn't be generated. The vectorcall now parses positionals with the shared positional emitter and builds the dict from kwnames only when there are keywords, otherwise passing NULL, as the tuple parser does. The `*args` and `**kwargs` parsers take the calling convention explicitly rather than reading `self.fastcall`, which describes the Python-visible entry point having stack style args, not the vectorcall. Co-Authored-By: Claude Opus 5.5 --- Tools/clinic/libclinic/converters.py | 16 ++++-- Tools/clinic/libclinic/parse_args.py | 79 ++++++++++++---------------- 2 files changed, 46 insertions(+), 49 deletions(-) diff --git a/Tools/clinic/libclinic/converters.py b/Tools/clinic/libclinic/converters.py index e3571eb597f24af..014486f7f469def 100644 --- a/Tools/clinic/libclinic/converters.py +++ b/Tools/clinic/libclinic/converters.py @@ -1437,7 +1437,7 @@ class VarKeywordCConverter(CConverter): def parse_arg(self, argname: str, displayname: str, *, limited_capi: bool) -> str | None: raise AssertionError('should never be called') - def parse_var_keyword(self) -> str: + def parse_var_keyword(self, *, stackargs: bool) -> str: raise NotImplementedError @@ -1448,6 +1448,16 @@ class var_keyword_dict_converter(VarKeywordCConverter): def cleanup(self) -> str: return f'Py_XDECREF({self.parser_name});\n' - def parse_var_keyword(self) -> str: + def parse_var_keyword(self, *, stackargs: bool) -> str: # NULL when there are no keywords: no call allocates an empty dict. - return f"{self.parser_name} = Py_XNewRef(kwargs);\n" + param_name = self.parser_name + if stackargs: + return f""" + if (kwnames != NULL) {{{{ + {param_name} = _PyStack_AsDict(args + nargs, kwnames); + if ({param_name} == NULL) {{{{ + goto exit; + }}}} + }}}} + """ + return f"{param_name} = Py_XNewRef(kwargs);\n" diff --git a/Tools/clinic/libclinic/parse_args.py b/Tools/clinic/libclinic/parse_args.py index 0d62e230b4a56fe..4608947cb7fff7c 100644 --- a/Tools/clinic/libclinic/parse_args.py +++ b/Tools/clinic/libclinic/parse_args.py @@ -933,11 +933,11 @@ def _parse_vararg(self, *, fastcall: bool) -> str: fastcall=fastcall, limited_capi=self.limited_capi) - def _parse_kwarg(self) -> str: + def _parse_kwarg(self, *, stackargs: bool) -> str: assert self.var_keyword is not None c = self.var_keyword.converter assert isinstance(c, libclinic.converters.VarKeywordCConverter) - return c.parse_var_keyword() + return c.parse_var_keyword(stackargs=stackargs) def _check_positional(self, nargs: str, *, indent: int = 4) -> list[str]: @@ -967,9 +967,8 @@ def _parse_positional_args( ) -> list[str] | None: """Emit per-parameter positional argument parsing. - Shared by parse_pos_only() and the vectorcall paths. Returns the - code snippets, or None if a converter doesn't support parse_arg - (the caller must fall back to a tuple/stack parser). + Returns the code snippets, or None if a converter doesn't support + parse_arg (the caller must fall back to a tuple/stack parser). """ parser_code: list[str] = [] for i, p in enumerate(self.parameters): @@ -998,9 +997,9 @@ def _parse_positional_args( if self.varpos: parser_code.append(libclinic.normalize_snippet( self._parse_vararg(fastcall=fastcall), indent=4)) - elif self.var_keyword: - parser_code.append(libclinic.normalize_snippet(self._parse_kwarg(), - indent=4)) + if self.var_keyword: + parser_code.append(libclinic.normalize_snippet( + self._parse_kwarg(stackargs=fastcall), indent=4)) return parser_code def select_positional_convention(self) -> tuple[str, str]: @@ -1112,48 +1111,38 @@ def parse_pos_only(self) -> None: """, indent=4)) self.parser_body(*parser_code) - def parse_var_keyword(self) -> None: + def parse_var_keyword(self, *, vectorcall: bool = False) -> None: self.flags = "METH_VARARGS|METH_KEYWORDS" - self.parser_prototype = PARSER_PROTOTYPE_KEYWORD - nargs = 'PyTuple_GET_SIZE(args)' + # The vectorcall gets an array of arguments and kwnames, not a tuple + # and a dict: use the converters' fastcall convention, which handles + # that. + if vectorcall: + nargs, argname_fmt = 'nargs', 'args[%d]' + check, arg = '_PyArg_NoPositionalStack', 'nargs' + parser_code = self._vectorcall_type_check() + else: + self.parser_prototype = PARSER_PROTOTYPE_KEYWORD + nargs, argname_fmt = 'PyTuple_GET_SIZE(args)', 'PyTuple_GET_ITEM(args, %d)' + check, arg = '_PyArg_NoPositional', 'args' + parser_code = [] - parser_code = [] if self.varpos is None and self.min_pos == self.max_pos == 0: - self.codegen.add_include('pycore_modsupport.h', - '_PyArg_NoPositional()') - parser_code.append(libclinic.normalize_snippet(""" - if (!_PyArg_NoPositional("{name}", args)) {{ + self.codegen.add_include('pycore_modsupport.h', f'{check}()') + parser_code.append(libclinic.normalize_snippet(f""" + if (!{check}("{{name}}", {arg})) {{{{ goto exit; - }} + }}}} """, indent=4)) else: parser_code.extend(self._check_positional(nargs)) - has_optional = False - for i, p in enumerate(self.parameters): - parse_arg = p.converter.parse_arg( - f'PyTuple_GET_ITEM(args, {i})', - p.get_displayname(i+1), - limited_capi=self.limited_capi, - ) - assert parse_arg is not None - if has_optional or p.is_optional(): - has_optional = True - parser_code.append(libclinic.normalize_snippet(""" - if (%s < %d) {{ - goto skip_optional; - }} - """, indent=4) % (nargs, i + 1)) - parser_code.append(libclinic.normalize_snippet(parse_arg, indent=4)) - if has_optional: - parser_code.append("skip_optional:") - - if self.varpos: - parser_code.append(libclinic.normalize_snippet( - self._parse_vararg(fastcall=self.fastcall), indent=4)) - if self.var_keyword: - parser_code.append(libclinic.normalize_snippet(self._parse_kwarg(), indent=4)) - self.parser_body(*parser_code) + pos_code = self._parse_positional_args( + argname_fmt=argname_fmt, nargs=nargs, + limited_capi=self.limited_capi, fastcall=vectorcall) + assert pos_code is not None + parser_code.extend(pos_code) + body = self.vectorcall_body if vectorcall else self.parser_body + body(*parser_code) def use_converters(self) -> None: """Prepare for parsing all arguments by a single call. @@ -1456,7 +1445,7 @@ def handle_new_or_init(self) -> None: if self.func.kind is METHOD_INIT: self.return_value_declaration = "int {parser_retval} = -1;" - if self.func.vectorcall and 'METH_KEYWORDS' in self.flags: + if self.parser_prototype == PARSER_PROTOTYPE_KEYWORD_HELPER: self._new_or_init_delegate_to_helper() else: self._new_or_init_parser_body() @@ -1624,8 +1613,6 @@ def _vectorcall_positional(self, *, arity_checked: Already have a number of arguments check. """ - # The vectorcall gets an array of arguments, not a tuple: use the - # converters' fastcall convention, which handles that. pos_code = self._parse_positional_args( argname_fmt='args[%d]', nargs='nargs', limited_capi=False, fastcall=True) @@ -1781,7 +1768,7 @@ def parse_vectorcall(self) -> None: if not self.parameters and not self.varpos and not self.var_keyword: self.parse_vectorcall_no_args() elif self.var_keyword is not None: - self.parse_vectorcall_kw_required() + self.parse_var_keyword(vectorcall=True) elif self.pos_only == len(self.parameters): self.parse_vectorcall_pos_only() elif any(p.is_keyword_only() for p in self.parameters) or self.varpos: From 0f32147918d5c1b3c4d5ceeb5299210ec6a2e178 Mon Sep 17 00:00:00 2001 From: Cody Maloney Date: Sat, 7 Nov 2026 17:24:33 -0800 Subject: [PATCH 2/4] gh-87613: Use Argument Clinic vectorcall for xml.etree.ElementTree.Element xml.etree.ElementTree.Element gains a generated vectorcall. Keys that are not strings in `**extra` now raise TypeError on a normal call, as the Python implementation already did, because vectorcall keywords must be strings; an explicit `__init__` call and subclasses still accept them. `@text_signature` keeps the documented `(tag, /, attrib={}, **extra)`: `attrib` can still be passed by keyword, which the impl pops from `extra`. Merging the keywords into the attributes now copies them with PyDict_Copy() instead of PyDict_New() plus PyDict_Update(), and pops `attrib` with `_Py_ID(attrib)` rather than creating a string per call; SubElement shares that helper. pyperf microbenchmark (PGO+LTO, run serially): | Benchmark | base | Element | |------------------------|:-------:|:---------------------:| | E('a', x='1') | 116 ns | 76.3 ns: 1.52x faster | | E('a') | 42.6 ns | 28.2 ns: 1.51x faster | | E('a', attrib=d) | 122 ns | 82.8 ns: 1.47x faster | | E('a', d) | 73.8 ns | 53.6 ns: 1.38x faster | | SE(E('p'), 'a', x='1') | 161 ns | 120 ns: 1.35x faster | | E('a', d, y='2') | 102 ns | 80.3 ns: 1.27x faster | | S('a') | 49.4 ns | 39.8 ns: 1.24x faster | Benchmark code: ```python import pyperf runner = pyperf.Runner() setup = ("from _elementtree import Element as E, SubElement as SE\n" "d = {'x': '1'}\n" "class S(E): pass") for stmt in ["E('a')", "E('a', d)", "E('a', x='1')", "E('a', d, y='2')", "E('a', attrib=d)", "S('a')", "SE(E('p'), 'a', x='1')"]: runner.timeit(stmt, stmt, setup) ``` Co-Authored-By: Claude Opus 5.5 --- .../pycore_global_objects_fini_generated.h | 1 + Include/internal/pycore_global_strings.h | 1 + .../internal/pycore_runtime_init_generated.h | 1 + .../internal/pycore_unicodeobject_generated.h | 1 + Lib/test/test_xml_etree.py | 4 + ...6-11-03-12-33-23.gh-issue-87613.7lKjHs.rst | 3 + Modules/_elementtree.c | 96 ++++++++-------- Modules/clinic/_elementtree.c.h | 104 +++++++++++++++++- 8 files changed, 161 insertions(+), 50 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-11-03-12-33-23.gh-issue-87613.7lKjHs.rst diff --git a/Include/internal/pycore_global_objects_fini_generated.h b/Include/internal/pycore_global_objects_fini_generated.h index 483a1dc58791cd5..fa662d39d95985f 100644 --- a/Include/internal/pycore_global_objects_fini_generated.h +++ b/Include/internal/pycore_global_objects_fini_generated.h @@ -315,6 +315,7 @@ _PyStaticObjects_CheckAll(PyInterpreterState *interp) { _PyStaticObject_CheckUnicodeSingleton((PyObject *)&_Py_ID(ast), "ast", 3); _PyStaticObject_CheckUnicodeSingleton((PyObject *)&_Py_ID(athrow), "athrow", 6); _PyStaticObject_CheckUnicodeSingleton((PyObject *)&_Py_ID(attr), "attr", 4); + _PyStaticObject_CheckUnicodeSingleton((PyObject *)&_Py_ID(attrib), "attrib", 6); _PyStaticObject_CheckUnicodeSingleton((PyObject *)&_Py_ID(attribute), "attribute", 9); _PyStaticObject_CheckUnicodeSingleton((PyObject *)&_Py_ID(autocommit), "autocommit", 10); _PyStaticObject_CheckUnicodeSingleton((PyObject *)&_Py_ID(backtick), "backtick", 8); diff --git a/Include/internal/pycore_global_strings.h b/Include/internal/pycore_global_strings.h index 949e19196dc5279..3038184fc287b5a 100644 --- a/Include/internal/pycore_global_strings.h +++ b/Include/internal/pycore_global_strings.h @@ -327,6 +327,7 @@ struct _Py_global_strings { STRUCT_FOR_ID(ast) STRUCT_FOR_ID(athrow) STRUCT_FOR_ID(attr) + STRUCT_FOR_ID(attrib) STRUCT_FOR_ID(attribute) STRUCT_FOR_ID(autocommit) STRUCT_FOR_ID(backtick) diff --git a/Include/internal/pycore_runtime_init_generated.h b/Include/internal/pycore_runtime_init_generated.h index 4ee954942521ae1..d5e724a2c5f74df 100644 --- a/Include/internal/pycore_runtime_init_generated.h +++ b/Include/internal/pycore_runtime_init_generated.h @@ -1602,6 +1602,7 @@ extern "C" { INIT_ID(ast), \ INIT_ID(athrow), \ INIT_ID(attr), \ + INIT_ID(attrib), \ INIT_ID(attribute), \ INIT_ID(autocommit), \ INIT_ID(backtick), \ diff --git a/Include/internal/pycore_unicodeobject_generated.h b/Include/internal/pycore_unicodeobject_generated.h index 3aad3345abc4d07..01765d8a849029d 100644 --- a/Include/internal/pycore_unicodeobject_generated.h +++ b/Include/internal/pycore_unicodeobject_generated.h @@ -279,6 +279,7 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) { offsetof(struct _Py_global_strings, identifiers._py_ast._ascii.ob_base), offsetof(struct _Py_global_strings, identifiers._py_athrow._ascii.ob_base), offsetof(struct _Py_global_strings, identifiers._py_attr._ascii.ob_base), + offsetof(struct _Py_global_strings, identifiers._py_attrib._ascii.ob_base), offsetof(struct _Py_global_strings, identifiers._py_attribute._ascii.ob_base), offsetof(struct _Py_global_strings, identifiers._py_autocommit._ascii.ob_base), offsetof(struct _Py_global_strings, identifiers._py_backtick._ascii.ob_base), diff --git a/Lib/test/test_xml_etree.py b/Lib/test/test_xml_etree.py index f0cf7dffab0e8a9..92e4583c117dcfa 100644 --- a/Lib/test/test_xml_etree.py +++ b/Lib/test/test_xml_etree.py @@ -3022,6 +3022,10 @@ def f(): class BasicElementTest(ElementTestCase, unittest.TestCase): + def test_extra_keys_must_be_strings(self): + with self.assertRaisesRegex(TypeError, "keywords must be strings"): + ET.Element("tag", **{ET.QName("key"): "value"}) + def test___init__(self): tag = "foo" attrib = { "zix": "wyp" } diff --git a/Misc/NEWS.d/next/Library/2026-11-03-12-33-23.gh-issue-87613.7lKjHs.rst b/Misc/NEWS.d/next/Library/2026-11-03-12-33-23.gh-issue-87613.7lKjHs.rst new file mode 100644 index 000000000000000..c508b8ab1ffd0e5 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-11-03-12-33-23.gh-issue-87613.7lKjHs.rst @@ -0,0 +1,3 @@ +Calling :class:`xml.etree.ElementTree.Element` with non-string keys in +*extra* now raises :exc:`TypeError`, as the Python implementation already +did. Pass such keys in *attrib* instead. diff --git a/Modules/_elementtree.c b/Modules/_elementtree.c index 983a00593b85d89..093f02b7d2a7ce8 100644 --- a/Modules/_elementtree.c +++ b/Modules/_elementtree.c @@ -19,6 +19,7 @@ #include "pycore_ceval.h" // _Py_EnterRecursiveCall() #include "pycore_dict.h" // _PyDict_CopyAsDict() #include "pycore_pyhash.h" // _Py_HashSecret +#include "pycore_runtime.h" // _Py_ID() #include "pycore_tuple.h" // _PyTuple_FromPair #include "pycore_weakref.h" // FT_CLEAR_WEAKREFS() @@ -377,7 +378,7 @@ static PyObject* get_attrib_from_keywords(PyObject *kwds) { PyObject *attrib; - if (PyDict_PopString(kwds, "attrib", &attrib) < 0) { + if (PyDict_Pop(kwds, &_Py_ID(attrib), &attrib) < 0) { return NULL; } @@ -393,16 +394,13 @@ get_attrib_from_keywords(PyObject *kwds) return NULL; } Py_SETREF(attrib, _PyDict_CopyAsDict(attrib)); + if (attrib != NULL && PyDict_Update(attrib, kwds) < 0) { + Py_DECREF(attrib); + return NULL; + } + return attrib; } - else { - attrib = PyDict_New(); - } - - if (attrib != NULL && PyDict_Update(attrib, kwds) < 0) { - Py_DECREF(attrib); - return NULL; - } - return attrib; + return PyDict_Copy(kwds); } /*[clinic input] @@ -413,15 +411,37 @@ class _elementtree.XMLParser "XMLParserObject *" "clinic_state()->XMLParser_Type [clinic start generated code]*/ /*[clinic end generated code: output=da39a3ee5e6b4b0d input=6c83ea832d2b0ef1]*/ +/*[clinic input] +@vectorcall +@text_signature "(tag, /, attrib={}, **extra)" +_elementtree.Element.__init__ + + tag: object + attrib: object(c_default="NULL") = {} + / + **extra: dict + +An XML element. + +This class is the reference implementation of the Element interface. + +An element's length is its number of subelements. That means if you +want to check if an element is truly empty, you should check BOTH +its length AND its text attribute. + +*tag* is the element name. *attrib* is an optional dictionary +containing element attributes. *extra* are additional element +attributes given as keyword arguments. + +Example form: + text...tail +[clinic start generated code]*/ + static int -element_init(PyObject *self, PyObject *args, PyObject *kwds) +_elementtree_Element___init___impl(ElementObject *self, PyObject *tag, + PyObject *attrib, PyObject *extra) +/*[clinic end generated code: output=d10be095275d5d1a input=a66a3eacbc30aa64]*/ { - PyObject *tag; - PyObject *attrib = NULL; - ElementObject *self_elem; - - if (!PyArg_ParseTuple(args, "O|O:Element", &tag, &attrib)) - return -1; if (attrib != NULL && !PyAnyDict_Check(attrib)) { PyErr_Format(PyExc_TypeError, "Element() argument 2 must be dict or frozendict, not %T", @@ -434,23 +454,21 @@ element_init(PyObject *self, PyObject *args, PyObject *kwds) attrib = _PyDict_CopyAsDict(attrib); if (!attrib) return -1; - if (kwds) { - if (PyDict_Update(attrib, kwds) < 0) { + if (extra) { + if (PyDict_Update(attrib, extra) < 0) { Py_DECREF(attrib); return -1; } } - } else if (kwds) { + } else if (extra) { /* have keywords args */ - attrib = get_attrib_from_keywords(kwds); + attrib = get_attrib_from_keywords(extra); if (!attrib) return -1; } - self_elem = (ElementObject *)self; - if (attrib != NULL && !is_empty_dict(attrib)) { - if (create_extra(self_elem, attrib) < 0) { + if (create_extra(self, attrib) < 0) { Py_DECREF(attrib); return -1; } @@ -460,10 +478,10 @@ element_init(PyObject *self, PyObject *args, PyObject *kwds) Py_XDECREF(attrib); /* Replace the objects already pointed to by tag, text and tail. */ - Py_XSETREF(self_elem->tag, Py_NewRef(tag)); + Py_XSETREF(self->tag, Py_NewRef(tag)); - _set_joined_ptr(&self_elem->text, Py_NewRef(Py_None)); - _set_joined_ptr(&self_elem->tail, Py_NewRef(Py_None)); + _set_joined_ptr(&self->text, Py_NewRef(Py_None)); + _set_joined_ptr(&self->tail, Py_NewRef(Py_None)); return 0; } @@ -4451,27 +4469,8 @@ static PyGetSetDef element_getsetlist[] = { {NULL}, }; -PyDoc_STRVAR(element_doc, -"Element(tag, attrib={}, **extra)\n" -"--\n" -"\n" -"An XML element.\n" -"\n" -"This class is the reference implementation of the Element interface.\n" -"\n" -"An element's length is its number of subelements. That means if you\n" -"want to check if an element is truly empty, you should check BOTH\n" -"its length AND its text attribute.\n" -"\n" -"*tag* is the element name. *attrib* is an optional dictionary\n" -"containing element attributes. *extra* are additional element\n" -"attributes given as keyword arguments.\n" -"\n" -"Example form:\n" -" text...tail"); - static PyType_Slot element_slots[] = { - {Py_tp_doc, (void *)element_doc}, + {Py_tp_doc, (void *)_elementtree_Element___init____doc__}, {Py_tp_dealloc, element_dealloc}, {Py_tp_repr, element_repr}, {Py_tp_getattro, PyObject_GenericGetAttr}, @@ -4480,7 +4479,8 @@ static PyType_Slot element_slots[] = { {Py_tp_methods, element_methods}, {Py_tp_members, element_members}, {Py_tp_getset, element_getsetlist}, - {Py_tp_init, element_init}, + {Py_tp_init, _elementtree_Element___init__}, + {Py_tp_vectorcall, _elementtree_Element_vectorcall}, {Py_tp_alloc, PyType_GenericAlloc}, {Py_tp_new, element_new}, {Py_sq_length, element_length}, diff --git a/Modules/clinic/_elementtree.c.h b/Modules/clinic/_elementtree.c.h index afc9daa5288f2c5..3aaacafe69d1664 100644 --- a/Modules/clinic/_elementtree.c.h +++ b/Modules/clinic/_elementtree.c.h @@ -4,12 +4,112 @@ preserve #if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE) # include "pycore_gc.h" // PyGC_Head -# include "pycore_runtime.h" // _Py_ID() #endif #include "pycore_abstract.h" // _PyNumber_Index() #include "pycore_modsupport.h" // _PyArg_CheckPositional() +#include "pycore_runtime.h" // _Py_SINGLETON() #include "pycore_unicodeobject.h" // _PyUnicode_AsUTF8NoNUL() +PyDoc_STRVAR(_elementtree_Element___init____doc__, +"Element(tag, /, attrib={}, **extra)\n" +"--\n" +"\n" +"An XML element.\n" +"\n" +"This class is the reference implementation of the Element interface.\n" +"\n" +"An element\'s length is its number of subelements. That means if you\n" +"want to check if an element is truly empty, you should check BOTH\n" +"its length AND its text attribute.\n" +"\n" +"*tag* is the element name. *attrib* is an optional dictionary\n" +"containing element attributes. *extra* are additional element\n" +"attributes given as keyword arguments.\n" +"\n" +"Example form:\n" +" text...tail"); + +static int +_elementtree_Element___init___impl(ElementObject *self, PyObject *tag, + PyObject *attrib, PyObject *extra); + +static int +_elementtree_Element___init__(PyObject *self, PyObject *args, PyObject *kwargs) +{ + int return_value = -1; + PyObject *tag; + PyObject *attrib = NULL; + PyObject *extra = NULL; + + if (!_PyArg_CheckPositional("Element", PyTuple_GET_SIZE(args), 1, 2)) { + goto exit; + } + tag = PyTuple_GET_ITEM(args, 0); + if (PyTuple_GET_SIZE(args) < 2) { + goto skip_optional; + } + attrib = PyTuple_GET_ITEM(args, 1); +skip_optional: + extra = Py_XNewRef(kwargs); + return_value = _elementtree_Element___init___impl((ElementObject *)self, tag, attrib, extra); + +exit: + /* Cleanup for extra */ + Py_XDECREF(extra); + + return return_value; +} + +static PyObject * +_elementtree_Element_vectorcall(PyObject *type, PyObject *const *args, + size_t nargsf, PyObject *kwnames) +{ + PyObject *return_value = NULL; + Py_ssize_t nargs = PyVectorcall_NARGS(nargsf); + PyObject *self; + int _result; + PyObject *tag; + PyObject *attrib = NULL; + PyObject *extra = NULL; + + assert(_PyType_CAST(type)->tp_init == _elementtree_Element___init__); + /* Make sure the type object is immutable: the generated + * vectorcall doesn't deal e.g. with users reassigning __init__. */ + assert(PyType_HasFeature(_PyType_CAST(type), Py_TPFLAGS_IMMUTABLETYPE)); + if (!_PyArg_CheckPositional("Element", nargs, 1, 2)) { + goto exit; + } + tag = args[0]; + if (nargs < 2) { + goto skip_optional; + } + attrib = args[1]; +skip_optional: + if (kwnames != NULL) { + extra = _PyStack_AsDict(args + nargs, kwnames); + if (extra == NULL) { + goto exit; + } + } + self = _PyType_CAST(type)->tp_new(_PyType_CAST(type), + (PyObject *)&_Py_SINGLETON(tuple_empty), NULL); + if (self == NULL) { + goto exit; + } + _result = _elementtree_Element___init___impl((ElementObject *)self, tag, attrib, extra); + if (_result != 0) { + Py_DECREF(self); + goto exit; + } + return_value = self; + +exit: + /* Cleanup for extra */ + Py_XDECREF(extra); + + return return_value; +} + PyDoc_STRVAR(_elementtree_SubElement__doc__, "SubElement($module, parent, tag, attrib={}, /, **extra)\n" "--\n" @@ -1474,4 +1574,4 @@ _elementtree_XMLParser__setevents(PyObject *self, PyObject *const *args, Py_ssiz exit: return return_value; } -/*[clinic end generated code: output=d454d27e90eb399e input=a9049054013a1b77]*/ +/*[clinic end generated code: output=e018eb31bcaee24f input=a9049054013a1b77]*/ From e49ac0926d184e9d3610abcd1f7d731d6d79854b Mon Sep 17 00:00:00 2001 From: Cody Maloney Date: Sat, 7 Nov 2026 17:57:43 -0800 Subject: [PATCH 3/4] gh-87613: Check the attrib copy explicitly in get_attrib_from_keywords Co-Authored-By: Claude Opus 5.5 --- Modules/_elementtree.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Modules/_elementtree.c b/Modules/_elementtree.c index 093f02b7d2a7ce8..3ce2cddeb5f0766 100644 --- a/Modules/_elementtree.c +++ b/Modules/_elementtree.c @@ -394,7 +394,10 @@ get_attrib_from_keywords(PyObject *kwds) return NULL; } Py_SETREF(attrib, _PyDict_CopyAsDict(attrib)); - if (attrib != NULL && PyDict_Update(attrib, kwds) < 0) { + if (attrib == NULL) { + return NULL; + } + if (PyDict_Update(attrib, kwds) < 0) { Py_DECREF(attrib); return NULL; } From 285bfddbd20e3b6152f8c03c545f4daa50ea6cab Mon Sep 17 00:00:00 2001 From: Cody Maloney Date: Wed, 28 Oct 2026 02:32:27 -0700 Subject: [PATCH 4/4] gh-87613: Allow 'as' on Argument Clinic **kwargs parameters `*args as name` already set the C name; `**kwargs as name` failed to parse because the 'as' regex only allowed a single leading star. Co-Authored-By: Claude Opus 5.5 --- Lib/test/test_clinic.py | 9 +++++++++ Tools/clinic/libclinic/dsl_parser.py | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_clinic.py b/Lib/test/test_clinic.py index 4e0b0137e9faa0b..94194a3166a5b8f 100644 --- a/Lib/test/test_clinic.py +++ b/Lib/test/test_clinic.py @@ -1847,6 +1847,15 @@ def test_c_name(self): """) self.assertEqual("os_stat_fn", function.c_basename) + def test_var_keyword_c_name(self): + function = self.parse_function(""" + module foo + foo.bar + **kwargs as kwds: dict + """) + p = function.parameters['kwargs_as_kwds'] + self.assertEqual("kwds", p.converter.name) + def test_base_invalid_syntax(self): block = """ module os diff --git a/Tools/clinic/libclinic/dsl_parser.py b/Tools/clinic/libclinic/dsl_parser.py index d4fcf4d6b350c6d..8426042c069d637 100644 --- a/Tools/clinic/libclinic/dsl_parser.py +++ b/Tools/clinic/libclinic/dsl_parser.py @@ -996,7 +996,7 @@ def parse_parameter(self, line: str) -> None: # handle "as" for parameters too c_name = None - m = re.match(r'(?:\* *)?\w+( +as +(\w+))', line) + m = re.match(r'(?:\*{1,2} *)?\w+( +as +(\w+))', line) if m: c_name = m[2] line = line[:m.start(1)] + line[m.end(1):]