gh-64490: Argument Clinic: Add support for `**kwds` (#138344)

This adds a scaffold of support, initially only working with
strictly positional-only arguments. The FASTCALL calling
convention is not yet supported.
This commit is contained in:
Adam Turner 2025-09-18 14:31:42 +01:00 committed by GitHub
parent 594bdde9df
commit 1ebd726c9b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 600 additions and 12 deletions

View file

@ -357,6 +357,32 @@ class ClinicWholeFileTest(TestCase):
"""
self.expect_failure(block, err, lineno=6)
def test_double_star_after_var_keyword(self):
err = "Function 'my_test_func' has an invalid parameter declaration (**kwargs?): '**kwds: dict'"
block = """
/*[clinic input]
my_test_func
pos_arg: object
**kwds: dict
**
[clinic start generated code]*/
"""
self.expect_failure(block, err, lineno=5)
def test_var_keyword_after_star(self):
err = "Function 'my_test_func' has an invalid parameter declaration: '**'"
block = """
/*[clinic input]
my_test_func
pos_arg: object
**
**kwds: dict
[clinic start generated code]*/
"""
self.expect_failure(block, err, lineno=5)
def test_module_already_got_one(self):
err = "Already defined module 'm'!"
block = """
@ -748,6 +774,16 @@ class ClinicWholeFileTest(TestCase):
""")
self.clinic.parse(raw)
def test_var_keyword_non_dict(self):
err = "'var_keyword_object' is not a valid converter"
block = """
/*[clinic input]
my_test_func
**kwds: object
[clinic start generated code]*/
"""
self.expect_failure(block, err, lineno=4)
class ParseFileUnitTest(TestCase):
def expect_parsing_failure(
@ -1608,6 +1644,11 @@ class ClinicParserTest(TestCase):
[
a: object
]
""", """
with_kwds
[
**kwds: dict
]
""")
err = (
"You cannot use optional groups ('[' and ']') unless all "
@ -1991,6 +2032,44 @@ class ClinicParserTest(TestCase):
err = "Function 'bar': '/' must precede '*'"
self.expect_failure(block, err)
def test_slash_after_var_keyword(self):
block = """
module foo
foo.bar
x: int
y: int
**kwds: dict
z: int
/
"""
err = "Function 'bar' has an invalid parameter declaration (**kwargs?): '**kwds: dict'"
self.expect_failure(block, err)
def test_star_after_var_keyword(self):
block = """
module foo
foo.bar
x: int
y: int
**kwds: dict
z: int
*
"""
err = "Function 'bar' has an invalid parameter declaration (**kwargs?): '**kwds: dict'"
self.expect_failure(block, err)
def test_parameter_after_var_keyword(self):
block = """
module foo
foo.bar
x: int
y: int
**kwds: dict
z: int
"""
err = "Function 'bar' has an invalid parameter declaration (**kwargs?): '**kwds: dict'"
self.expect_failure(block, err)
def test_depr_star_must_come_after_slash(self):
block = """
module foo
@ -2079,6 +2158,16 @@ class ClinicParserTest(TestCase):
"""
self.expect_failure(block, err, lineno=3)
def test_parameters_no_more_than_one_var_keyword(self):
err = "Encountered parameter line when not expecting parameters: **var_keyword_2: dict"
block = """
module foo
foo.bar
**var_keyword_1: dict
**var_keyword_2: dict
"""
self.expect_failure(block, err, lineno=3)
def test_function_not_at_column_0(self):
function = self.parse_function("""
module foo
@ -2513,6 +2602,14 @@ class ClinicParserTest(TestCase):
"""
self.expect_failure(block, err, lineno=1)
def test_var_keyword_cannot_take_default_value(self):
err = "Function 'fn' has an invalid parameter declaration:"
block = """
fn
**kwds: dict = None
"""
self.expect_failure(block, err, lineno=1)
def test_default_is_not_of_correct_type(self):
err = ("int_converter: default value 2.5 for field 'a' "
"is not of type 'int'")
@ -2610,6 +2707,43 @@ class ClinicParserTest(TestCase):
"""
self.expect_failure(block, err, lineno=2)
def test_var_keyword_with_pos_or_kw(self):
block = """
module foo
foo.bar
x: int
**kwds: dict
"""
err = "Function 'bar' has an invalid parameter declaration (**kwargs?): '**kwds: dict'"
self.expect_failure(block, err)
def test_var_keyword_with_kw_only(self):
block = """
module foo
foo.bar
x: int
/
*
y: int
**kwds: dict
"""
err = "Function 'bar' has an invalid parameter declaration (**kwargs?): '**kwds: dict'"
self.expect_failure(block, err)
def test_var_keyword_with_pos_or_kw_and_kw_only(self):
block = """
module foo
foo.bar
x: int
/
y: int
*
z: int
**kwds: dict
"""
err = "Function 'bar' has an invalid parameter declaration (**kwargs?): '**kwds: dict'"
self.expect_failure(block, err)
def test_allow_negative_accepted_by_py_ssize_t_converter_only(self):
errmsg = re.escape("converter_init() got an unexpected keyword argument 'allow_negative'")
unsupported_converters = [converter_name for converter_name in converters.keys()
@ -3954,6 +4088,49 @@ class ClinicFunctionalTest(unittest.TestCase):
check("a", b="b", c="c", d="d", e="e", f="f", g="g")
self.assertRaises(TypeError, fn, a="a", b="b", c="c", d="d", e="e", f="f", g="g")
def test_lone_kwds(self):
with self.assertRaises(TypeError):
ac_tester.lone_kwds(1, 2)
self.assertEqual(ac_tester.lone_kwds(), ({},))
self.assertEqual(ac_tester.lone_kwds(y='y'), ({'y': 'y'},))
kwds = {'y': 'y', 'z': 'z'}
self.assertEqual(ac_tester.lone_kwds(y='y', z='z'), (kwds,))
self.assertEqual(ac_tester.lone_kwds(**kwds), (kwds,))
def test_kwds_with_pos_only(self):
with self.assertRaises(TypeError):
ac_tester.kwds_with_pos_only()
with self.assertRaises(TypeError):
ac_tester.kwds_with_pos_only(y='y')
with self.assertRaises(TypeError):
ac_tester.kwds_with_pos_only(1, y='y')
self.assertEqual(ac_tester.kwds_with_pos_only(1, 2), (1, 2, {}))
self.assertEqual(ac_tester.kwds_with_pos_only(1, 2, y='y'), (1, 2, {'y': 'y'}))
kwds = {'y': 'y', 'z': 'z'}
self.assertEqual(ac_tester.kwds_with_pos_only(1, 2, y='y', z='z'), (1, 2, kwds))
self.assertEqual(ac_tester.kwds_with_pos_only(1, 2, **kwds), (1, 2, kwds))
def test_kwds_with_stararg(self):
self.assertEqual(ac_tester.kwds_with_stararg(), ((), {}))
self.assertEqual(ac_tester.kwds_with_stararg(1, 2), ((1, 2), {}))
self.assertEqual(ac_tester.kwds_with_stararg(y='y'), ((), {'y': 'y'}))
args = (1, 2)
kwds = {'y': 'y', 'z': 'z'}
self.assertEqual(ac_tester.kwds_with_stararg(1, 2, y='y', z='z'), (args, kwds))
self.assertEqual(ac_tester.kwds_with_stararg(*args, **kwds), (args, kwds))
def test_kwds_with_pos_only_and_stararg(self):
with self.assertRaises(TypeError):
ac_tester.kwds_with_pos_only_and_stararg()
with self.assertRaises(TypeError):
ac_tester.kwds_with_pos_only_and_stararg(y='y')
self.assertEqual(ac_tester.kwds_with_pos_only_and_stararg(1, 2), (1, 2, (), {}))
self.assertEqual(ac_tester.kwds_with_pos_only_and_stararg(1, 2, y='y'), (1, 2, (), {'y': 'y'}))
args = ('lobster', 'thermidor')
kwds = {'y': 'y', 'z': 'z'}
self.assertEqual(ac_tester.kwds_with_pos_only_and_stararg(1, 2, 'lobster', 'thermidor', y='y', z='z'), (1, 2, args, kwds))
self.assertEqual(ac_tester.kwds_with_pos_only_and_stararg(1, 2, *args, **kwds), (1, 2, args, kwds))
class LimitedCAPIOutputTests(unittest.TestCase):

View file

@ -2308,6 +2308,88 @@ depr_multi_impl(PyObject *module, PyObject *a, PyObject *b, PyObject *c,
#undef _SAVED_PY_VERSION
/*[clinic input]
output pop
[clinic start generated code]*/
/*[clinic end generated code: output=da39a3ee5e6b4b0d input=e7c7c42daced52b0]*/
/*[clinic input]
output push
destination kwarg new file '{dirname}/clinic/_testclinic_kwds.c.h'
output everything kwarg
output docstring_prototype suppress
output parser_prototype suppress
output impl_definition block
[clinic start generated code]*/
/*[clinic end generated code: output=da39a3ee5e6b4b0d input=02965b54b3981cc4]*/
#include "clinic/_testclinic_kwds.c.h"
/*[clinic input]
lone_kwds
**kwds: dict
[clinic start generated code]*/
static PyObject *
lone_kwds_impl(PyObject *module, PyObject *kwds)
/*[clinic end generated code: output=572549c687a0432e input=6ef338b913ecae17]*/
{
return pack_arguments_newref(1, kwds);
}
/*[clinic input]
kwds_with_pos_only
a: object
b: object
/
**kwds: dict
[clinic start generated code]*/
static PyObject *
kwds_with_pos_only_impl(PyObject *module, PyObject *a, PyObject *b,
PyObject *kwds)
/*[clinic end generated code: output=573096d3a7efcce5 input=da081a5d9ae8878a]*/
{
return pack_arguments_newref(3, a, b, kwds);
}
/*[clinic input]
kwds_with_stararg
*args: tuple
**kwds: dict
[clinic start generated code]*/
static PyObject *
kwds_with_stararg_impl(PyObject *module, PyObject *args, PyObject *kwds)
/*[clinic end generated code: output=d4b0064626a25208 input=1be404572d685859]*/
{
return pack_arguments_newref(2, args, kwds);
}
/*[clinic input]
kwds_with_pos_only_and_stararg
a: object
b: object
/
*args: tuple
**kwds: dict
[clinic start generated code]*/
static PyObject *
kwds_with_pos_only_and_stararg_impl(PyObject *module, PyObject *a,
PyObject *b, PyObject *args,
PyObject *kwds)
/*[clinic end generated code: output=af7df7640c792246 input=2fe330c7981f0829]*/
{
return pack_arguments_newref(4, a, b, args, kwds);
}
/*[clinic input]
output pop
[clinic start generated code]*/
@ -2404,6 +2486,12 @@ static PyMethodDef tester_methods[] = {
DEPR_KWD_NOINLINE_METHODDEF
DEPR_KWD_MULTI_METHODDEF
DEPR_MULTI_METHODDEF
LONE_KWDS_METHODDEF
KWDS_WITH_POS_ONLY_METHODDEF
KWDS_WITH_STARARG_METHODDEF
KWDS_WITH_POS_ONLY_AND_STARARG_METHODDEF
{NULL, NULL}
};

184
Modules/clinic/_testclinic_kwds.c.h generated Normal file
View file

@ -0,0 +1,184 @@
/*[clinic input]
preserve
[clinic start generated code]*/
#if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE)
# include "pycore_gc.h" // PyGC_Head
#endif
#include "pycore_abstract.h" // _PyNumber_Index()
#include "pycore_long.h" // _PyLong_UnsignedShort_Converter()
#include "pycore_modsupport.h" // _PyArg_CheckPositional()
#include "pycore_runtime.h" // _Py_ID()
#include "pycore_tuple.h" // _PyTuple_FromArray()
PyDoc_STRVAR(lone_kwds__doc__,
"lone_kwds($module, /, **kwds)\n"
"--\n"
"\n");
#define LONE_KWDS_METHODDEF \
{"lone_kwds", _PyCFunction_CAST(lone_kwds), METH_VARARGS|METH_KEYWORDS, lone_kwds__doc__},
static PyObject *
lone_kwds_impl(PyObject *module, PyObject *kwds);
static PyObject *
lone_kwds(PyObject *module, PyObject *args, PyObject *kwargs)
{
PyObject *return_value = NULL;
PyObject *__clinic_kwds = NULL;
if (!_PyArg_NoPositional("lone_kwds", args)) {
goto exit;
}
if (kwargs == NULL) {
__clinic_kwds = PyDict_New();
if (__clinic_kwds == NULL) {
goto exit;
}
}
else {
__clinic_kwds = Py_NewRef(kwargs);
}
return_value = lone_kwds_impl(module, __clinic_kwds);
exit:
/* Cleanup for kwds */
Py_XDECREF(__clinic_kwds);
return return_value;
}
PyDoc_STRVAR(kwds_with_pos_only__doc__,
"kwds_with_pos_only($module, a, b, /, **kwds)\n"
"--\n"
"\n");
#define KWDS_WITH_POS_ONLY_METHODDEF \
{"kwds_with_pos_only", _PyCFunction_CAST(kwds_with_pos_only), METH_VARARGS|METH_KEYWORDS, kwds_with_pos_only__doc__},
static PyObject *
kwds_with_pos_only_impl(PyObject *module, PyObject *a, PyObject *b,
PyObject *kwds);
static PyObject *
kwds_with_pos_only(PyObject *module, PyObject *args, PyObject *kwargs)
{
PyObject *return_value = NULL;
PyObject *a;
PyObject *b;
PyObject *__clinic_kwds = NULL;
if (!_PyArg_CheckPositional("kwds_with_pos_only", PyTuple_GET_SIZE(args), 2, 2)) {
goto exit;
}
a = PyTuple_GET_ITEM(args, 0);
b = PyTuple_GET_ITEM(args, 1);
if (kwargs == NULL) {
__clinic_kwds = PyDict_New();
if (__clinic_kwds == NULL) {
goto exit;
}
}
else {
__clinic_kwds = Py_NewRef(kwargs);
}
return_value = kwds_with_pos_only_impl(module, a, b, __clinic_kwds);
exit:
/* Cleanup for kwds */
Py_XDECREF(__clinic_kwds);
return return_value;
}
PyDoc_STRVAR(kwds_with_stararg__doc__,
"kwds_with_stararg($module, /, *args, **kwds)\n"
"--\n"
"\n");
#define KWDS_WITH_STARARG_METHODDEF \
{"kwds_with_stararg", _PyCFunction_CAST(kwds_with_stararg), METH_VARARGS|METH_KEYWORDS, kwds_with_stararg__doc__},
static PyObject *
kwds_with_stararg_impl(PyObject *module, PyObject *args, PyObject *kwds);
static PyObject *
kwds_with_stararg(PyObject *module, PyObject *args, PyObject *kwargs)
{
PyObject *return_value = NULL;
PyObject *__clinic_args = NULL;
PyObject *__clinic_kwds = NULL;
__clinic_args = Py_NewRef(args);
if (kwargs == NULL) {
__clinic_kwds = PyDict_New();
if (__clinic_kwds == NULL) {
goto exit;
}
}
else {
__clinic_kwds = Py_NewRef(kwargs);
}
return_value = kwds_with_stararg_impl(module, __clinic_args, __clinic_kwds);
exit:
/* Cleanup for args */
Py_XDECREF(__clinic_args);
/* Cleanup for kwds */
Py_XDECREF(__clinic_kwds);
return return_value;
}
PyDoc_STRVAR(kwds_with_pos_only_and_stararg__doc__,
"kwds_with_pos_only_and_stararg($module, a, b, /, *args, **kwds)\n"
"--\n"
"\n");
#define KWDS_WITH_POS_ONLY_AND_STARARG_METHODDEF \
{"kwds_with_pos_only_and_stararg", _PyCFunction_CAST(kwds_with_pos_only_and_stararg), METH_VARARGS|METH_KEYWORDS, kwds_with_pos_only_and_stararg__doc__},
static PyObject *
kwds_with_pos_only_and_stararg_impl(PyObject *module, PyObject *a,
PyObject *b, PyObject *args,
PyObject *kwds);
static PyObject *
kwds_with_pos_only_and_stararg(PyObject *module, PyObject *args, PyObject *kwargs)
{
PyObject *return_value = NULL;
PyObject *a;
PyObject *b;
PyObject *__clinic_args = NULL;
PyObject *__clinic_kwds = NULL;
if (!_PyArg_CheckPositional("kwds_with_pos_only_and_stararg", PyTuple_GET_SIZE(args), 2, PY_SSIZE_T_MAX)) {
goto exit;
}
a = PyTuple_GET_ITEM(args, 0);
b = PyTuple_GET_ITEM(args, 1);
__clinic_args = PyTuple_GetSlice(args, 2, PY_SSIZE_T_MAX);
if (!__clinic_args) {
goto exit;
}
if (kwargs == NULL) {
__clinic_kwds = PyDict_New();
if (__clinic_kwds == NULL) {
goto exit;
}
}
else {
__clinic_kwds = Py_NewRef(kwargs);
}
return_value = kwds_with_pos_only_and_stararg_impl(module, a, b, __clinic_args, __clinic_kwds);
exit:
/* Cleanup for args */
Py_XDECREF(__clinic_args);
/* Cleanup for kwds */
Py_XDECREF(__clinic_kwds);
return return_value;
}
/*[clinic end generated code: output=e4dea1070e003f5d input=a9049054013a1b77]*/

View file

@ -84,6 +84,7 @@ CLINIC_PREFIXED_ARGS: Final = frozenset(
"argsbuf",
"fastargs",
"kwargs",
"kwds",
"kwnames",
"nargs",
"noptargs",

View file

@ -274,7 +274,7 @@ class CConverter(metaclass=CConverterAutoRegister):
data.modifications.append('/* modifications for ' + name + ' */\n' + modifications.rstrip())
# keywords
if parameter.is_vararg():
if parameter.is_variable_length():
pass
elif parameter.is_positional_only():
data.keywords.append('')

View file

@ -1300,3 +1300,37 @@ class varpos_array_converter(VarPosCConverter):
{paramname} = {start};
{self.length_name} = {size};
"""
# Converters for var-keyword parameters.
class VarKeywordCConverter(CConverter):
format_unit = ''
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:
raise NotImplementedError
class var_keyword_dict_converter(VarKeywordCConverter):
type = 'PyObject *'
c_default = 'NULL'
def cleanup(self) -> str:
return f'Py_XDECREF({self.parser_name});\n'
def parse_var_keyword(self) -> str:
param_name = self.parser_name
return f"""
if (kwargs == NULL) {{{{
{param_name} = PyDict_New();
if ({param_name} == NULL) {{{{
goto exit;
}}}}
}}}}
else {{{{
{param_name} = Py_NewRef(kwargs);
}}}}
"""

View file

@ -246,6 +246,7 @@ class IndentStack:
class DSLParser:
function: Function | None
state: StateKeeper
expecting_parameters: bool
keyword_only: bool
positional_only: bool
deprecated_positional: VersionTuple | None
@ -285,6 +286,7 @@ class DSLParser:
def reset(self) -> None:
self.function = None
self.state = self.state_dsl_start
self.expecting_parameters = True
self.keyword_only = False
self.positional_only = False
self.deprecated_positional = None
@ -876,6 +878,10 @@ class DSLParser:
def parse_parameter(self, line: str) -> None:
assert self.function is not None
if not self.expecting_parameters:
fail('Encountered parameter line when not expecting '
f'parameters: {line}')
match self.parameter_state:
case ParamState.START | ParamState.REQUIRED:
self.to_required()
@ -909,27 +915,40 @@ class DSLParser:
if len(function_args.args) > 1:
fail(f"Function {self.function.name!r} has an "
f"invalid parameter declaration (comma?): {line!r}")
if function_args.kwarg:
fail(f"Function {self.function.name!r} has an "
f"invalid parameter declaration (**kwargs?): {line!r}")
is_vararg = is_var_keyword = False
if function_args.vararg:
self.check_previous_star()
self.check_remaining_star()
is_vararg = True
parameter = function_args.vararg
elif function_args.kwarg:
# If the existing parameters are all positional only or ``*args``
# (var-positional), then we allow ``**kwds`` (var-keyword).
# Currently, pos-or-keyword or keyword-only arguments are not
# allowed with the ``**kwds`` converter.
has_non_positional_param = any(
p.is_positional_or_keyword() or p.is_keyword_only()
for p in self.function.parameters.values()
)
if has_non_positional_param:
fail(f"Function {self.function.name!r} has an "
f"invalid parameter declaration (**kwargs?): {line!r}")
is_var_keyword = True
parameter = function_args.kwarg
else:
is_vararg = False
parameter = function_args.args[0]
parameter_name = parameter.arg
name, legacy, kwargs = self.parse_converter(parameter.annotation)
if is_vararg:
name = 'varpos_' + name
name = f'varpos_{name}'
elif is_var_keyword:
name = f'var_keyword_{name}'
value: object
if not function_args.defaults:
if is_vararg:
if is_vararg or is_var_keyword:
value = NULL
else:
if self.parameter_state is ParamState.OPTIONAL:
@ -1065,6 +1084,8 @@ class DSLParser:
kind: inspect._ParameterKind
if is_vararg:
kind = inspect.Parameter.VAR_POSITIONAL
elif is_var_keyword:
kind = inspect.Parameter.VAR_KEYWORD
elif self.keyword_only:
kind = inspect.Parameter.KEYWORD_ONLY
else:
@ -1118,6 +1139,8 @@ class DSLParser:
if is_vararg:
self.keyword_only = True
if is_var_keyword:
self.expecting_parameters = False
@staticmethod
def parse_converter(
@ -1159,6 +1182,9 @@ class DSLParser:
The 'version' parameter signifies the future version from which
the marker will take effect (None means it is already in effect).
"""
if not self.expecting_parameters:
fail("Encountered '*' when not expecting parameters")
if version is None:
self.check_previous_star()
self.check_remaining_star()
@ -1214,6 +1240,9 @@ class DSLParser:
The 'version' parameter signifies the future version from which
the marker will take effect (None means it is already in effect).
"""
if not self.expecting_parameters:
fail("Encountered '/' when not expecting parameters")
if version is None:
if self.deprecated_keyword:
fail(f"Function {function.name!r}: '/' must precede '/ [from ...]'")
@ -1450,11 +1479,13 @@ class DSLParser:
if p.is_vararg():
p_lines.append("*")
added_star = True
if p.is_var_keyword():
p_lines.append("**")
name = p.converter.signature_name or p.name
p_lines.append(name)
if not p.is_vararg() and p.converter.is_optional():
if not p.is_variable_length() and p.converter.is_optional():
p_lines.append('=')
value = p.converter.py_default
if not value:
@ -1583,8 +1614,11 @@ class DSLParser:
for p in reversed(self.function.parameters.values()):
if self.keyword_only:
if (p.kind == inspect.Parameter.KEYWORD_ONLY or
p.kind == inspect.Parameter.VAR_POSITIONAL):
if p.kind in {
inspect.Parameter.KEYWORD_ONLY,
inspect.Parameter.VAR_POSITIONAL,
inspect.Parameter.VAR_KEYWORD
}:
return
elif self.deprecated_positional:
if p.deprecated_positional == self.deprecated_positional:

View file

@ -220,9 +220,18 @@ class Parameter:
def is_positional_only(self) -> bool:
return self.kind == inspect.Parameter.POSITIONAL_ONLY
def is_positional_or_keyword(self) -> bool:
return self.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD
def is_vararg(self) -> bool:
return self.kind == inspect.Parameter.VAR_POSITIONAL
def is_var_keyword(self) -> bool:
return self.kind == inspect.Parameter.VAR_KEYWORD
def is_variable_length(self) -> bool:
return self.is_vararg() or self.is_var_keyword()
def is_optional(self) -> bool:
return not self.is_vararg() and (self.default is not unspecified)

View file

@ -36,7 +36,7 @@ def declare_parser(
num_keywords = len([
p for p in f.parameters.values()
if not p.is_positional_only() and not p.is_vararg()
if p.is_positional_or_keyword() or p.is_keyword_only()
])
condition = '#if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE)'
@ -220,6 +220,7 @@ class ParseArgsCodeGen:
max_pos: int = 0
min_kw_only: int = 0
varpos: Parameter | None = None
var_keyword: Parameter | None = None
docstring_prototype: str
docstring_definition: str
@ -255,13 +256,24 @@ class ParseArgsCodeGen:
del self.parameters[i]
break
for i, p in enumerate(self.parameters):
if p.is_var_keyword():
self.var_keyword = p
del self.parameters[i]
break
self.converters = [p.converter for p in self.parameters]
if self.func.critical_section:
self.codegen.add_include('pycore_critical_section.h',
'Py_BEGIN_CRITICAL_SECTION()')
# Use fastcall if not disabled, except if in a __new__ or
# __init__ method, or if there is a **kwargs parameter.
if self.func.disable_fastcall:
self.fastcall = False
elif self.var_keyword is not None:
self.fastcall = False
else:
self.fastcall = not self.is_new_or_init()
@ -469,6 +481,12 @@ class ParseArgsCodeGen:
fastcall=self.fastcall,
limited_capi=self.limited_capi)
def _parse_kwarg(self) -> str:
assert self.var_keyword is not None
c = self.var_keyword.converter
assert isinstance(c, libclinic.converters.VarKeywordCConverter)
return c.parse_var_keyword()
def parse_pos_only(self) -> None:
if self.fastcall:
# positional-only, but no option groups
@ -564,6 +582,8 @@ class ParseArgsCodeGen:
parser_code.append("skip_optional:")
if self.varpos:
parser_code.append(libclinic.normalize_snippet(self._parse_vararg(), indent=4))
elif self.var_keyword:
parser_code.append(libclinic.normalize_snippet(self._parse_kwarg(), indent=4))
else:
for parameter in self.parameters:
parameter.converter.use_converter()
@ -590,6 +610,45 @@ class ParseArgsCodeGen:
""", indent=4)]
self.parser_body(*parser_code)
def parse_var_keyword(self) -> None:
self.flags = "METH_VARARGS|METH_KEYWORDS"
self.parser_prototype = PARSER_PROTOTYPE_KEYWORD
nargs = 'PyTuple_GET_SIZE(args)'
parser_code = []
max_args = NO_VARARG if self.varpos else self.max_pos
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)) {{
goto exit;
}}
""", indent=4))
elif self.min_pos or max_args != NO_VARARG:
self.codegen.add_include('pycore_modsupport.h',
'_PyArg_CheckPositional()')
parser_code.append(libclinic.normalize_snippet(f"""
if (!_PyArg_CheckPositional("{{name}}", {nargs}, {self.min_pos}, {max_args})) {{{{
goto exit;
}}}}
""", indent=4))
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
parser_code.append(libclinic.normalize_snippet(parse_arg, indent=4))
if self.varpos:
parser_code.append(libclinic.normalize_snippet(self._parse_vararg(), indent=4))
if self.var_keyword:
parser_code.append(libclinic.normalize_snippet(self._parse_kwarg(), indent=4))
self.parser_body(*parser_code)
def parse_general(self, clang: CLanguage) -> None:
parsearg: str | None
deprecated_positionals: dict[int, Parameter] = {}
@ -921,12 +980,14 @@ class ParseArgsCodeGen:
# previous call to parser_body. this is used for an awful hack.
self.parser_body_fields: tuple[str, ...] = ()
if not self.parameters and not self.varpos:
if not self.parameters and not self.varpos and not self.var_keyword:
self.parse_no_args()
elif self.use_meth_o():
self.parse_one_arg()
elif self.has_option_groups():
self.parse_option_groups()
elif self.var_keyword is not None:
self.parse_var_keyword()
elif (not self.requires_defining_class
and self.pos_only == len(self.parameters)):
self.parse_pos_only()