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_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/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..3ce2cddeb5f0766 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,16 @@ get_attrib_from_keywords(PyObject *kwds) return NULL; } Py_SETREF(attrib, _PyDict_CopyAsDict(attrib)); + if (attrib == NULL) { + return NULL; + } + if (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 +414,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 +457,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 +481,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 +4472,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 +4482,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]*/ 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/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):] 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: