Skip to content

Commit b051ae8

Browse files
committed
Argument Clinic: Add support for **kwds
This adds a scaffold of support, initially only working with strictly positional-only arguments. The FASTCALL calling convention is not yet supported.
1 parent 2a54acf commit b051ae8

File tree

7 files changed

+364
-16
lines changed

7 files changed

+364
-16
lines changed

Modules/_testclinic.c

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2303,6 +2303,88 @@ depr_multi_impl(PyObject *module, PyObject *a, PyObject *b, PyObject *c,
23032303
#undef _SAVED_PY_VERSION
23042304

23052305

2306+
/*[clinic input]
2307+
output pop
2308+
[clinic start generated code]*/
2309+
/*[clinic end generated code: output=da39a3ee5e6b4b0d input=e7c7c42daced52b0]*/
2310+
2311+
2312+
/*[clinic input]
2313+
output push
2314+
destination kwarg new file '{dirname}/clinic/_testclinic_kwds.c.h'
2315+
output everything kwarg
2316+
output docstring_prototype suppress
2317+
output parser_prototype suppress
2318+
output impl_definition block
2319+
[clinic start generated code]*/
2320+
/*[clinic end generated code: output=da39a3ee5e6b4b0d input=02965b54b3981cc4]*/
2321+
2322+
#include "clinic/_testclinic_kwds.c.h"
2323+
2324+
2325+
/*[clinic input]
2326+
lone_kwds
2327+
**kwds: dict
2328+
[clinic start generated code]*/
2329+
2330+
static PyObject *
2331+
lone_kwds_impl(PyObject *module, PyObject *kwds)
2332+
/*[clinic end generated code: output=572549c687a0432e input=6ef338b913ecae17]*/
2333+
{
2334+
Py_RETURN_NONE;
2335+
}
2336+
2337+
2338+
/*[clinic input]
2339+
kwds_with_pos_only
2340+
a: object
2341+
b: object
2342+
/
2343+
**kwds: dict
2344+
[clinic start generated code]*/
2345+
2346+
static PyObject *
2347+
kwds_with_pos_only_impl(PyObject *module, PyObject *a, PyObject *b,
2348+
PyObject *kwds)
2349+
/*[clinic end generated code: output=573096d3a7efcce5 input=da081a5d9ae8878a]*/
2350+
{
2351+
Py_RETURN_NONE;
2352+
}
2353+
2354+
2355+
/*[clinic input]
2356+
kwds_with_stararg
2357+
*args: tuple
2358+
**kwds: dict
2359+
[clinic start generated code]*/
2360+
2361+
static PyObject *
2362+
kwds_with_stararg_impl(PyObject *module, PyObject *args, PyObject *kwds)
2363+
/*[clinic end generated code: output=d4b0064626a25208 input=1be404572d685859]*/
2364+
{
2365+
Py_RETURN_NONE;
2366+
}
2367+
2368+
2369+
/*[clinic input]
2370+
kwds_with_pos_only_and_stararg
2371+
a: object
2372+
b: object
2373+
/
2374+
*args: tuple
2375+
**kwds: dict
2376+
[clinic start generated code]*/
2377+
2378+
static PyObject *
2379+
kwds_with_pos_only_and_stararg_impl(PyObject *module, PyObject *a,
2380+
PyObject *b, PyObject *args,
2381+
PyObject *kwds)
2382+
/*[clinic end generated code: output=af7df7640c792246 input=2fe330c7981f0829]*/
2383+
{
2384+
Py_RETURN_NONE;
2385+
}
2386+
2387+
23062388
/*[clinic input]
23072389
output pop
23082390
[clinic start generated code]*/
@@ -2399,6 +2481,12 @@ static PyMethodDef tester_methods[] = {
23992481
DEPR_KWD_NOINLINE_METHODDEF
24002482
DEPR_KWD_MULTI_METHODDEF
24012483
DEPR_MULTI_METHODDEF
2484+
2485+
LONE_KWDS_METHODDEF
2486+
KWDS_WITH_POS_ONLY_METHODDEF
2487+
KWDS_WITH_STARARG_METHODDEF
2488+
KWDS_WITH_POS_ONLY_AND_STARARG_METHODDEF
2489+
24022490
{NULL, NULL}
24032491
};
24042492

Modules/clinic/_testclinic_kwds.c.h

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

Tools/clinic/libclinic/converter.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -274,7 +274,7 @@ def _render_non_self(
274274
data.modifications.append('/* modifications for ' + name + ' */\n' + modifications.rstrip())
275275

276276
# keywords
277-
if parameter.is_vararg():
277+
if parameter.is_vararg() or parameter.is_var_keyword():
278278
pass
279279
elif parameter.is_positional_only():
280280
data.keywords.append('')

Tools/clinic/libclinic/converters.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1279,3 +1279,30 @@ def parse_vararg(self, *, pos_only: int, min_pos: int, max_pos: int,
12791279
{paramname} = {start};
12801280
{self.length_name} = {size};
12811281
"""
1282+
1283+
1284+
# Converters for var-keyword parameters.
1285+
1286+
class VarKeywordCConverter(CConverter):
1287+
format_unit = ''
1288+
1289+
def parse_arg(self, argname: str, displayname: str, *, limited_capi: bool) -> str | None:
1290+
raise AssertionError('should never be called')
1291+
1292+
def parse_var_keyword(self) -> str:
1293+
raise NotImplementedError
1294+
1295+
1296+
class var_keyword_dict_converter(VarKeywordCConverter):
1297+
type = 'PyObject *'
1298+
format_unit = ''
1299+
c_default = 'NULL'
1300+
1301+
def cleanup(self) -> str:
1302+
return f'Py_XDECREF({self.parser_name});\n'
1303+
1304+
def parse_var_keyword(self) -> str:
1305+
param_name = self.parser_name
1306+
return f"""
1307+
{param_name} = (kwargs != NULL) ? Py_NewRef(kwargs) : PyDict_New();
1308+
"""

Tools/clinic/libclinic/dsl_parser.py

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -909,27 +909,37 @@ def parse_parameter(self, line: str) -> None:
909909
if len(function_args.args) > 1:
910910
fail(f"Function {self.function.name!r} has an "
911911
f"invalid parameter declaration (comma?): {line!r}")
912-
if function_args.kwarg:
913-
fail(f"Function {self.function.name!r} has an "
914-
f"invalid parameter declaration (**kwargs?): {line!r}")
915912

913+
is_vararg = is_var_keyword = False
916914
if function_args.vararg:
917915
self.check_previous_star()
918916
self.check_remaining_star()
919917
is_vararg = True
920918
parameter = function_args.vararg
919+
elif function_args.kwarg:
920+
# If the existing parameters are all positional only or ``*args``
921+
# (var-positional), then we allow ``**kwds`` (var-keyword).
922+
# Currently, pos-or-keyword or keyword-only arguments are not
923+
# allowed with the ``**kwds`` converter.
924+
if not all(p.is_positional_only() or p.is_vararg()
925+
for p in self.function.parameters.values()):
926+
fail(f"Function {self.function.name!r} has an "
927+
f"invalid parameter declaration (**kwargs?): {line!r}")
928+
is_var_keyword = True
929+
parameter = function_args.kwarg
921930
else:
922-
is_vararg = False
923931
parameter = function_args.args[0]
924932

925933
parameter_name = parameter.arg
926934
name, legacy, kwargs = self.parse_converter(parameter.annotation)
927935
if is_vararg:
928-
name = 'varpos_' + name
936+
name = f'varpos_{name}'
937+
elif is_var_keyword:
938+
name = f'var_keyword_{name}'
929939

930940
value: object
931941
if not function_args.defaults:
932-
if is_vararg:
942+
if is_vararg or is_var_keyword:
933943
value = NULL
934944
else:
935945
if self.parameter_state is ParamState.OPTIONAL:
@@ -1065,6 +1075,8 @@ def bad_node(self, node: ast.AST) -> None:
10651075
kind: inspect._ParameterKind
10661076
if is_vararg:
10671077
kind = inspect.Parameter.VAR_POSITIONAL
1078+
elif is_var_keyword:
1079+
kind = inspect.Parameter.VAR_KEYWORD
10681080
elif self.keyword_only:
10691081
kind = inspect.Parameter.KEYWORD_ONLY
10701082
else:
@@ -1116,7 +1128,7 @@ def bad_node(self, node: ast.AST) -> None:
11161128
key = f"{parameter_name}_as_{c_name}" if c_name else parameter_name
11171129
self.function.parameters[key] = p
11181130

1119-
if is_vararg:
1131+
if is_vararg or is_var_keyword:
11201132
self.keyword_only = True
11211133

11221134
@staticmethod
@@ -1450,11 +1462,16 @@ def add_parameter(text: str) -> None:
14501462
if p.is_vararg():
14511463
p_lines.append("*")
14521464
added_star = True
1465+
if p.is_var_keyword():
1466+
p_lines.append("**")
14531467

14541468
name = p.converter.signature_name or p.name
14551469
p_lines.append(name)
14561470

1457-
if not p.is_vararg() and p.converter.is_optional():
1471+
if (
1472+
not (p.is_vararg() or p.is_var_keyword())
1473+
and p.converter.is_optional()
1474+
):
14581475
p_lines.append('=')
14591476
value = p.converter.py_default
14601477
if not value:
@@ -1583,8 +1600,11 @@ def check_remaining_star(self, lineno: int | None = None) -> None:
15831600

15841601
for p in reversed(self.function.parameters.values()):
15851602
if self.keyword_only:
1586-
if (p.kind == inspect.Parameter.KEYWORD_ONLY or
1587-
p.kind == inspect.Parameter.VAR_POSITIONAL):
1603+
if p.kind in {
1604+
inspect.Parameter.KEYWORD_ONLY,
1605+
inspect.Parameter.VAR_POSITIONAL,
1606+
inspect.Parameter.VAR_KEYWORD
1607+
}:
15881608
return
15891609
elif self.deprecated_positional:
15901610
if p.deprecated_positional == self.deprecated_positional:

Tools/clinic/libclinic/function.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,9 @@ def is_positional_only(self) -> bool:
223223
def is_vararg(self) -> bool:
224224
return self.kind == inspect.Parameter.VAR_POSITIONAL
225225

226+
def is_var_keyword(self) -> bool:
227+
return self.kind == inspect.Parameter.VAR_KEYWORD
228+
226229
def is_optional(self) -> bool:
227230
return not self.is_vararg() and (self.default is not unspecified)
228231

0 commit comments

Comments
 (0)