gh-156219: Fix Argument Clinic for an optional argument with **kwds (GH-156220)

serhiy-storchaka <[email protected]>
Newsgroups gmane.comp.python.cvs
Message-ID <[email protected]>
https://github.com/python/cpython/commit/4b9a1caa32dd41f4ba2699bd071fcfe0659db5ab
commit: 4b9a1caa32dd41f4ba2699bd071fcfe0659db5ab
branch: main
author: Serhiy Storchaka <[email protected]>
committer: serhiy-storchaka <[email protected]>
date: 2026-08-22T08:04:50Z
summary:

gh-156219: Fix Argument Clinic for an optional argument with **kwds (GH-156220)

The generated code read an optional positional argument even if it was
not passed.

files:
A Misc/NEWS.d/next/Tools-Demos/2026-08-22-11-00-00.gh-issue-156219.Wq2nRt.rst
M Lib/test/test_clinic.py
M Modules/_testclinic.c
M Modules/clinic/_testclinic_kwds.c.h
M Tools/clinic/libclinic/parse_args.py

diff --git a/Lib/test/test_clinic.py b/Lib/test/test_clinic.py
index d3e4454348d023..43a1a52874e019 100644
--- a/Lib/test/test_clinic.py
+++ b/Lib/test/test_clinic.py
@@ -4981,6 +4981,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 ad4e34f640e530..95209cf8127041 100644
--- a/Modules/_testclinic.c
+++ b/Modules/_testclinic.c
@@ -2474,6 +2474,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
@@ -2611,6 +2628,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 37d7cb7ffabe51..f2ea51e3f4e8d1 100644
--- a/Tools/clinic/libclinic/parse_args.py
+++ b/Tools/clinic/libclinic/parse_args.py
@@ -655,6 +655,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})',
@@ -662,7 +663,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))

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.