Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions Lib/test/clinic.test.c
Original file line number Diff line number Diff line change
Expand Up @@ -5769,6 +5769,67 @@ Test___init___impl(TestObj *self, PyObject *a, int group_right_1,
/*[clinic end generated code: output=2bbb8ea60e8f57a6 input=10f5d0f1e8e466ef]*/


/*[clinic input]
group_and_optional_parameter
[
a: object
b: object
]
c: object = None
/
The optional parameter can be omitted with or without the group.
[clinic start generated code]*/

PyDoc_STRVAR(group_and_optional_parameter__doc__,
"group_and_optional_parameter([a, b,] c=None)\n"
"The optional parameter can be omitted with or without the group.");

#define GROUP_AND_OPTIONAL_PARAMETER_METHODDEF \
{"group_and_optional_parameter", (PyCFunction)group_and_optional_parameter, METH_VARARGS, group_and_optional_parameter__doc__},

static PyObject *
group_and_optional_parameter_impl(PyObject *module, int group_left_1,
PyObject *a, PyObject *b, PyObject *c);

static PyObject *
group_and_optional_parameter(PyObject *module, PyObject *args)
{
PyObject *return_value = NULL;
int group_left_1 = 0;
PyObject *a = NULL;
PyObject *b = NULL;
PyObject *c = Py_None;

switch (PyTuple_GET_SIZE(args)) {
case 0:
case 1:
if (!PyArg_ParseTuple(args, "|O:group_and_optional_parameter", &c)) {
goto exit;
}
break;
case 2:
case 3:
if (!PyArg_ParseTuple(args, "OO|O:group_and_optional_parameter", &a, &b, &c)) {
goto exit;
}
group_left_1 = 1;
break;
default:
PyErr_SetString(PyExc_TypeError, "group_and_optional_parameter requires 0 to 3 arguments");
goto exit;
}
return_value = group_and_optional_parameter_impl(module, group_left_1, a, b, c);

exit:
return return_value;
}

static PyObject *
group_and_optional_parameter_impl(PyObject *module, int group_left_1,
PyObject *a, PyObject *b, PyObject *c)
/*[clinic end generated code: output=3faea69eafd5bbbe input=7f0fbb6124f5a972]*/


/*[clinic input]
Test._pyarg_parsestackandkeywords
cls: defining_class
Expand Down
39 changes: 39 additions & 0 deletions Lib/test/test_clinic.py
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,24 @@ def __init__(self):
"""
self.expect_failure(block, err, lineno=8)

def test_ambiguous_group_and_optional_parameters(self):
err = ("Function 'my_test_func' has an ambiguous group configuration: "
"a call with 2 argument(s) can be parsed in more than one way.")
block = """
/*[clinic input]
my_test_func

[
a: object
b: object
]
c: object = None
d: object = None
/
[clinic start generated code]*/
"""
self.expect_failure(block, err)

def test_star_after_vararg(self):
err = "'my_test_func' uses '*' more than once."
block = """
Expand Down Expand Up @@ -3865,6 +3883,27 @@ def test_varpos_kwonly_req_opt(self):
self.assertEqual(fn(1, a=2, b=3), ((1,), 2, 3, False))
self.assertEqual(fn(1, a=2, b=3, c=4), ((1,), 2, 3, 4))

def test_group_and_opt(self):
# fn([a, b,] c=None)
fn = ac_tester.group_and_opt
self.assertEqual(fn(), (False, None, None, None))
self.assertEqual(fn(1), (False, None, None, 1))
self.assertEqual(fn(1, 2), (True, 1, 2, None))
self.assertEqual(fn(1, 2, 3), (True, 1, 2, 3))
self.assertRaises(TypeError, fn, 1, 2, 3, 4)
self.assertRaises(TypeError, fn, c=1)

def test_group_and_two_opt(self):
# fn([a, b, c,] d=None, e=None)
fn = ac_tester.group_and_two_opt
self.assertEqual(fn(), (False, None, None, None, None, None))
self.assertEqual(fn(1), (False, None, None, None, 1, None))
self.assertEqual(fn(1, 2), (False, None, None, None, 1, 2))
self.assertEqual(fn(1, 2, 3), (True, 1, 2, 3, None, None))
self.assertEqual(fn(1, 2, 3, 4), (True, 1, 2, 3, 4, None))
self.assertEqual(fn(1, 2, 3, 4, 5), (True, 1, 2, 3, 4, 5))
self.assertRaises(TypeError, fn, 1, 2, 3, 4, 5, 6)

def test_gh_32092_oob(self):
ac_tester.gh_32092_oob(1, 2, 3, 4, kw1=5, kw2=6)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Fix Argument Clinic support of parameters with a default value used together
with optional groups.
Such parameters were always required in the generated parsing code.
48 changes: 48 additions & 0 deletions Modules/_testclinic.c
Original file line number Diff line number Diff line change
Expand Up @@ -1237,6 +1237,52 @@ posonly_poskw_varpos_array_impl(PyObject *module, PyObject *a, PyObject *b,
}


/*[clinic input]
group_and_opt

[
a: object
b: object
]
c: object = None
/

[clinic start generated code]*/

static PyObject *
group_and_opt_impl(PyObject *module, int group_left_1, PyObject *a,
PyObject *b, PyObject *c)
/*[clinic end generated code: output=23413ec545526111 input=8a84d8f44bc8bd0b]*/
{
return pack_arguments_newref(4, group_left_1 ? Py_True : Py_False,
a, b, c);
}


/*[clinic input]
group_and_two_opt

[
a: object
b: object
c: object
]
d: object = None
e: object = None
/

[clinic start generated code]*/

static PyObject *
group_and_two_opt_impl(PyObject *module, int group_left_1, PyObject *a,
PyObject *b, PyObject *c, PyObject *d, PyObject *e)
/*[clinic end generated code: output=1427c4b3c35f24ff input=cdda98eec1e365ea]*/
{
return pack_arguments_newref(6, group_left_1 ? Py_True : Py_False,
a, b, c, d, e);
}



/*[clinic input]
gh_32092_oob
Expand Down Expand Up @@ -2455,6 +2501,8 @@ static PyMethodDef tester_methods[] = {
POSONLY_VARPOS_ARRAY_METHODDEF
POSONLY_REQ_OPT_VARPOS_ARRAY_METHODDEF
POSONLY_POSKW_VARPOS_ARRAY_METHODDEF
GROUP_AND_OPT_METHODDEF
GROUP_AND_TWO_OPT_METHODDEF

GH_32092_OOB_METHODDEF
GH_32092_KW_PASS_METHODDEF
Expand Down
92 changes: 91 additions & 1 deletion Modules/clinic/_testclinic.c.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Tools/c-analyzer/cpython/_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,7 @@ def format_tsv_lines(lines):
_abs('Modules/_remote_debugging/debug_offsets_validation.h'): (25_000, 1000),
_abs('Modules/_remote_debugging/*.h'): (20_000, 1000),
_abs('Modules/_testcapimodule.c'): (20_000, 400),
_abs('Modules/_testclinic.c'): (20_000, 400),
_abs('Modules/expat/expat.h'): (10_000, 400),
_abs('Objects/stringlib/unicode_format.h'): (10_000, 400),
_abs('Objects/typeobject.c'): (380_000, 13_000),
Expand Down
39 changes: 33 additions & 6 deletions Tools/clinic/libclinic/clanguage.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from libclinic.codegen import CRenderData, TemplateDict, CodeGen
from libclinic.language import Language
from libclinic.function import (
Module, Class, Function, Parameter,
Module, Class, Function, Parameter, ParamTuple,
permute_optional_groups,
GETTER, SETTER, METHOD_INIT)
from libclinic.converters import self_converter
Expand Down Expand Up @@ -304,14 +304,34 @@ def render_option_group_parsing(
count_min = sys.maxsize
count_max = -1

# Trailing parameters with a default value which are not in any group
# can be omitted, so a subset matches a range of argument counts.
subsets: list[tuple[ParamTuple, int]] = []
for subset in permute_optional_groups(left, required, right):
first_optional = len(subset)
for p in reversed(subset):
if p.group or not p.is_optional():
break
first_optional -= 1
subsets.append((subset, first_optional))

seen: set[int] = set()
for subset, first_optional in subsets:
for count in range(first_optional, len(subset) + 1):
if count in seen:
fail(f"Function {f.full_name!r} has an ambiguous group "
f"configuration: a call with {count} argument(s) "
f"can be parsed in more than one way.")
seen.add(count)

if limited_capi:
nargs = 'PyTuple_Size(args)'
else:
nargs = 'PyTuple_GET_SIZE(args)'
out.append(f"switch ({nargs}) {{\n")
for subset in permute_optional_groups(left, required, right):
for subset, first_optional in subsets:
count = len(subset)
count_min = min(count_min, count)
count_min = min(count_min, first_optional)
count_max = max(count_max, count)

if count == 0:
Expand All @@ -322,9 +342,11 @@ def render_option_group_parsing(

group_ids = {p.group for p in subset} # eliminate duplicates
d: dict[str, str | int] = {}
d['count'] = count
d['name'] = f.name
d['format_units'] = "".join(p.converter.format_unit for p in subset)
format_units = [p.converter.format_unit for p in subset]
if first_optional < count:
format_units.insert(first_optional, '|')
d['format_units'] = "".join(format_units)

parse_arguments: list[str] = []
for p in subset:
Expand All @@ -337,8 +359,13 @@ def render_option_group_parsing(
for g in group_ids
])

d['cases'] = "\n".join([
f" case {n}:"
for n in range(first_optional, count + 1)
])

s = """\
case {count}:
{cases}
if (!PyArg_ParseTuple(args, "{format_units}:{name}", {parse_arguments})) {{
goto exit;
}}
Expand Down
Loading