diff --git a/Lib/test/test_clinic.py b/Lib/test/test_clinic.py index 93c284e58764f4..c75e47c68adb18 100644 --- a/Lib/test/test_clinic.py +++ b/Lib/test/test_clinic.py @@ -4295,6 +4295,18 @@ def test_kwds_with_pos_only(self): 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_optional_pos_only(self): + with self.assertRaises(TypeError): + ac_tester.kwds_with_optional_pos_only() + with self.assertRaises(TypeError): + ac_tester.kwds_with_optional_pos_only(y='y') + self.assertEqual(ac_tester.kwds_with_optional_pos_only(1), (1, None, {})) + self.assertEqual(ac_tester.kwds_with_optional_pos_only(1, 2), (1, 2, {})) + self.assertEqual(ac_tester.kwds_with_optional_pos_only(1, y='y'), + (1, None, {'y': 'y'})) + self.assertEqual(ac_tester.kwds_with_optional_pos_only(1, 2, y='y'), + (1, 2, {'y': 'y'})) + def test_kwds_with_stararg(self): self.assertEqual(ac_tester.kwds_with_stararg(), ((), {})) self.assertEqual(ac_tester.kwds_with_stararg(1, 2), ((1, 2), {})) diff --git a/Misc/NEWS.d/next/Tools-Demos/2026-08-22-11-00-00.gh-issue-156219.Wq2nRt.rst b/Misc/NEWS.d/next/Tools-Demos/2026-08-22-11-00-00.gh-issue-156219.Wq2nRt.rst new file mode 100644 index 00000000000000..2b1002da5d0cec --- /dev/null +++ b/Misc/NEWS.d/next/Tools-Demos/2026-08-22-11-00-00.gh-issue-156219.Wq2nRt.rst @@ -0,0 +1,2 @@ +Fix Argument Clinic generating code which reads an optional positional +argument which was not passed, if the function has a ``**kwds`` parameter. diff --git a/Modules/_testclinic.c b/Modules/_testclinic.c index 66a375589ba38e..3b0fc223142a3e 100644 --- a/Modules/_testclinic.c +++ b/Modules/_testclinic.c @@ -2358,6 +2358,23 @@ kwds_with_pos_only_impl(PyObject *module, PyObject *a, PyObject *b, } +/*[clinic input] +kwds_with_optional_pos_only + a: object + b: object = None + / + **kwds: dict +[clinic start generated code]*/ + +static PyObject * +kwds_with_optional_pos_only_impl(PyObject *module, PyObject *a, PyObject *b, + PyObject *kwds) +/*[clinic end generated code: output=25a8458f5acc1a07 input=0b18b9e1670904ec]*/ +{ + return pack_arguments_newref(3, a, b, kwds); +} + + /*[clinic input] kwds_with_stararg *args: tuple @@ -2490,6 +2507,7 @@ static PyMethodDef tester_methods[] = { LONE_KWDS_METHODDEF KWDS_WITH_POS_ONLY_METHODDEF + KWDS_WITH_OPTIONAL_POS_ONLY_METHODDEF KWDS_WITH_STARARG_METHODDEF KWDS_WITH_POS_ONLY_AND_STARARG_METHODDEF diff --git a/Modules/clinic/_testclinic_kwds.c.h b/Modules/clinic/_testclinic_kwds.c.h index 86cad50c56cf55..475bb12120c8f8 100644 --- a/Modules/clinic/_testclinic_kwds.c.h +++ b/Modules/clinic/_testclinic_kwds.c.h @@ -92,6 +92,53 @@ kwds_with_pos_only(PyObject *module, PyObject *args, PyObject *kwargs) return return_value; } +PyDoc_STRVAR(kwds_with_optional_pos_only__doc__, +"kwds_with_optional_pos_only($module, a, b=None, /, **kwds)\n" +"--\n" +"\n"); + +#define KWDS_WITH_OPTIONAL_POS_ONLY_METHODDEF \ + {"kwds_with_optional_pos_only", _PyCFunction_CAST(kwds_with_optional_pos_only), METH_VARARGS|METH_KEYWORDS, kwds_with_optional_pos_only__doc__}, + +static PyObject * +kwds_with_optional_pos_only_impl(PyObject *module, PyObject *a, PyObject *b, + PyObject *kwds); + +static PyObject * +kwds_with_optional_pos_only(PyObject *module, PyObject *args, PyObject *kwargs) +{ + PyObject *return_value = NULL; + PyObject *a; + PyObject *b = Py_None; + PyObject *__clinic_kwds = NULL; + + if (!_PyArg_CheckPositional("kwds_with_optional_pos_only", PyTuple_GET_SIZE(args), 1, 2)) { + goto exit; + } + a = PyTuple_GET_ITEM(args, 0); + if (PyTuple_GET_SIZE(args) < 2) { + goto skip_optional; + } + b = PyTuple_GET_ITEM(args, 1); +skip_optional: + if (kwargs == NULL) { + __clinic_kwds = PyDict_New(); + if (__clinic_kwds == NULL) { + goto exit; + } + } + else { + __clinic_kwds = Py_NewRef(kwargs); + } + return_value = kwds_with_optional_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" @@ -181,4 +228,4 @@ kwds_with_pos_only_and_stararg(PyObject *module, PyObject *args, PyObject *kwarg return return_value; } -/*[clinic end generated code: output=3e5251b10aa44382 input=a9049054013a1b77]*/ +/*[clinic end generated code: output=d4e257c529010ae1 input=a9049054013a1b77]*/ diff --git a/Tools/clinic/libclinic/parse_args.py b/Tools/clinic/libclinic/parse_args.py index bca87ecd75100c..5f7199bdba695c 100644 --- a/Tools/clinic/libclinic/parse_args.py +++ b/Tools/clinic/libclinic/parse_args.py @@ -634,6 +634,7 @@ def parse_var_keyword(self) -> None: }}}} """, indent=4)) + has_optional = False for i, p in enumerate(self.parameters): parse_arg = p.converter.parse_arg( f'PyTuple_GET_ITEM(args, {i})', @@ -641,7 +642,16 @@ def parse_var_keyword(self) -> None: 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(), indent=4))