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: