Skip to content
Merged
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
45 changes: 35 additions & 10 deletions Tools/clinic/libclinic/clanguage.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
from __future__ import annotations
import itertools
import sys
import textwrap
from typing import TYPE_CHECKING, Literal, Final
from operator import attrgetter
Expand All @@ -12,7 +11,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 All @@ -21,6 +20,20 @@
from libclinic.app import Clinic


def count_required(subset: ParamTuple) -> int:
"""Return the number of arguments which cannot be omitted.

A parameter in an optional group is passed together with its group,
so only trailing parameters with a default value can be omitted.
"""
count = len(subset)
for p in reversed(subset):
if p.group or not p.is_optional():
break
count -= 1
return count


def c_id(name: str) -> str:
if len(name) == 1 and ord(name) < 256:
if name.isalnum():
Expand Down Expand Up @@ -301,18 +314,26 @@ def render_option_group_parsing(
assert group is not None
group.append(p)

count_min = sys.maxsize
count_max = -1
# Map the number of arguments to the subset which accepts it.
subsets: dict[int, ParamTuple] = {}
for subset in permute_optional_groups(left, required, right):
for count in range(count_required(subset), len(subset) + 1):
if count in subsets:
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.")
subsets[count] = subset

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):
count = len(subset)
count_min = min(count_min, count)
count_max = max(count_max, count)
for count, subset in sorted(subsets.items()):
if count < len(subset):
# The omitted parameters are parsed by the following case.
out.append(f" case {count}:\n")
continue

if count == 0:
out.append(""" case 0:
Expand All @@ -324,7 +345,11 @@ def render_option_group_parsing(
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]
n_required = count_required(subset)
if n_required < count:
format_units.insert(n_required, '|')
d['format_units'] = "".join(format_units)

parse_arguments: list[str] = []
for p in subset:
Expand All @@ -351,7 +376,7 @@ def render_option_group_parsing(

out.append(" default:\n")
s = ' PyErr_SetString(PyExc_TypeError, "{} requires {} to {} arguments");\n'
out.append(s.format(f.full_name, count_min, count_max))
out.append(s.format(f.full_name, min(subsets), max(subsets)))
out.append(' goto exit;\n')
out.append("}")

Expand Down
Loading