Skip to content
Draft
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
15 changes: 15 additions & 0 deletions Doc/c-api/arg.rst
Original file line number Diff line number Diff line change
Expand Up @@ -402,18 +402,33 @@ inside nested parentheses. They are:
For example, the format string ``"OO|OO"`` corresponds to the Python
signature ``f(a, b, c=None, d=None)``.

:c:func:`PyArg_ParseTupleAndKeywords` only:
after ``%`` it indicates that the remaining keyword-only arguments are optional.

``$``
:c:func:`PyArg_ParseTupleAndKeywords` only:
Indicates that the remaining arguments in the Python argument list are
keyword-only.
They are optional if ``|`` was specified before ``$``, and required otherwise.
``|`` cannot be specified after ``$``.
Use ``%`` to mix required and optional keyword-only arguments.
For example, the format string ``"O|O$O"`` corresponds to the Python
signature ``f(a, b=None, *, c=None)``,
and the format string ``"OO$OO"`` corresponds to ``f(a, b, *, c, d)``.

.. versionadded:: 3.3

``%``
:c:func:`PyArg_ParseTupleAndKeywords` only:
Indicates that the remaining arguments in the Python argument list are
keyword-only and required.
They become optional after ``|``.
Unlike ``$``, it does not depend on whether ``|`` was specified before it.
For example, the format string ``"O|O%O|O"`` corresponds to the Python
signature ``f(a, b=None, *, c, d=None)``.

.. versionadded:: next

``:``
The list of format units ends here; the string after the colon is used as the
function name in error messages (the "associated value" of the exception that
Expand Down
8 changes: 7 additions & 1 deletion Doc/whatsnew/3.16.rst
Original file line number Diff line number Diff line change
Expand Up @@ -845,7 +845,13 @@ C API changes
New features
------------

* TODO
* :c:func:`PyArg_ParseTupleAndKeywords` now supports required keyword-only
arguments mixed with optional arguments.
The new format unit ``%`` starts the keyword-only arguments which are
required until ``|``.
For example, the format string ``"O|O%O|O"`` corresponds to the Python
signature ``f(a, b=None, *, c, d=None)``.
(Contributed by Serhiy Storchaka in :gh:`78416`.)

Porting to Python 3.16
----------------------
Expand Down
1 change: 1 addition & 0 deletions Include/cpython/modsupport.h
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ typedef struct _PyArg_Parser {
int pos; /* number of positional-only arguments */
int min; /* minimal number of arguments */
int max; /* maximal number of positional arguments */
int minkw; /* index of the first optional keyword-only argument */
PyObject *kwtuple; /* tuple of keyword parameter names */
struct _PyArg_Parser *next;
} _PyArg_Parser;
Expand Down
79 changes: 77 additions & 2 deletions Lib/test/test_capi/test_getargs.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import string
import sys
import unittest
from functools import partial
from test import support
from test.support import import_helper
from test.support import script_helper
Expand Down Expand Up @@ -825,6 +826,80 @@ def __hash__(self):
getargs_keyword_only(1, 2, **{BadStr("monster"): 666})


class RequiredKeywordOnly_TestCase(unittest.TestCase):
# '%' marks the start of the keyword-only arguments, which are required
# until '|', so that required and optional ones can be mixed.

def parse(self, format, args, kwargs, keywords=('a', 'b', 'c', 'd')):
return _testcapi.parse_tuple_and_keywords(args, kwargs,
format, list(keywords))

def test_required_after_optional(self):
# f(a, b=None, *, c, d=None)
parse = partial(self.parse, "O|O%O|O")
self.assertEqual(parse((1, 2), {'c': 3, 'd': 4}), (1, 2, 3, 4))
self.assertEqual(parse((1,), {'c': 3}), (1, None, 3, None))
with self.assertRaisesRegex(TypeError, "missing required argument 'c'"):
parse((1,), {})
with self.assertRaisesRegex(TypeError, "missing required argument 'c'"):
parse((1,), {'d': 4})
with self.assertRaisesRegex(TypeError, "at most 2 positional"):
parse((1, 2, 3), {'c': 3})

def test_all_keyword_only_required(self):
# f(a, b=None, *, c, d)
parse = partial(self.parse, "O|O%OO")
self.assertEqual(parse((1,), {'c': 3, 'd': 4}), (1, None, 3, 4))
with self.assertRaisesRegex(TypeError, "missing required argument 'd'"):
parse((1,), {'c': 3})

def test_all_positional_required(self):
# f(a, b, *, c, d=None)
parse = partial(self.parse, "OO%O|O")
self.assertEqual(parse((1, 2), {'c': 3}), (1, 2, 3, None))
with self.assertRaisesRegex(TypeError, "missing required argument 'c'"):
parse((1, 2), {})

def test_cached_parser(self):
# The same format, parsed once and cached in a _PyArg_Parser.
f = _testcapi.getargs_fast_required_kwonly
self.assertEqual(f(1, 2, c=3, d=4), (1, 2, 3, 4))
self.assertEqual(f(1, c=3), (1, None, 3, None))
with self.assertRaisesRegex(TypeError, "missing required argument 'c'"):
f(1)
with self.assertRaisesRegex(TypeError, "missing required argument 'c'"):
f(1, d=4)

def test_invalid_format(self):
for format, msg in (
("O%O%O", r"\$ specified twice"),
("O%O$O", r"\$ specified twice"),
("O%O|O|O", r"\| specified twice"),
("O$O|O", r"\$ before \|"),
):
with self.subTest(format=format):
n = format.count('O')
npos = len(format) - len(format.lstrip('O'))
args = tuple(range(npos))
kwargs = {'abcd'[i]: i for i in range(npos, n)}
with self.assertRaisesRegex(SystemError, msg):
self.parse(format, args, kwargs, 'abcd'[:n])

def test_unchanged_meaning_of_dollar(self):
# '$' still inherits the state of the positional arguments.
parse = partial(self.parse, "O|O$O", keywords=('a', 'b', 'c'))
self.assertEqual(parse((1,), {}), (1, None, None))
parse = partial(self.parse, "OO$O", keywords=('a', 'b', 'c'))
self.assertEqual(parse((1, 2), {'c': 3}), (1, 2, 3))
with self.assertRaisesRegex(TypeError, "missing required argument 'c'"):
parse((1, 2), {})
# The same with a cached parser.
f = _testcapi.getargs_fast_kwonly
self.assertEqual(f(1, 2, c=3, d=4), (1, 2, 3, 4))
with self.assertRaisesRegex(TypeError, "missing required argument 'c'"):
f(1, 2)


class PositionalOnlyAndKeywords_TestCase(unittest.TestCase):
from _testcapi import getargs_positional_only_and_keywords as getargs

Expand Down Expand Up @@ -1164,8 +1239,8 @@ def test_skipitem(self):

# skip parentheses, the error reporting is inconsistent about them
# skip 'e' and 'w', they're always two-character codes
# skip '|' and '$', they don't represent arguments anyway
if c in '()ew|$':
# skip '|', '$' and '%', they don't represent arguments anyway
if c in '()ew|$%':
continue

# test the format unit when not skipped
Expand Down
77 changes: 77 additions & 0 deletions Lib/test/test_clinic.py
Original file line number Diff line number Diff line change
Expand Up @@ -2207,6 +2207,36 @@ def test_depr_slash_duplicate2(self):
err = "Function 'bar': '/ [from 3.14]' must precede '/ [from 3.15]'"
self.expect_failure(block, err, lineno=5)

def test_required_keyword_only_after_optional(self):
function = self.parse_function("""
module foo
foo.bar
a: int
b: int = 0
*
c: int
d: int = 0
Docstring.
""")
_, a, b, c, d = function.parameters.values()
self.assertFalse(a.converter.is_optional())
self.assertTrue(b.converter.is_optional())
self.assertFalse(c.converter.is_optional())
self.assertTrue(d.converter.is_optional())

def test_optional_before_required_keyword_only(self):
block = """
module foo
foo.bar
*
a: int = 0
b: int
Docstring.
"""
err = ("Can't have a parameter without a default ('b') "
"after a parameter with a default!")
self.expect_failure(block, err, lineno=4)

def test_single_slash(self):
block = """
module foo
Expand Down Expand Up @@ -4664,6 +4694,41 @@ def test_limited_capi_float(self):
self.assertIn("float f;", generated)
self.assertIn("f = (float) PyFloat_AsDouble", generated)

def test_limited_capi_required_keyword_only(self):
block = self.wrap_clinic_input("""
func
a: object
b: object = None
*
c: object
d: object = None
""")
generated = self.clinic.parse(block)
# '$' cannot express this, only '%' can.
self.assertIn('"O|O%O|O:func"', generated)

def test_limited_capi_optional_after_required_keyword_only(self):
block = self.wrap_clinic_input("""
func
a: object
*
b: object
c: object = None
""")
generated = self.clinic.parse(block)
self.assertIn('"O%O|O:func"', generated)

def test_limited_capi_keyword_only(self):
# '$' is still used if it can express the signature.
block = self.wrap_clinic_input("""
func
a: object
*
b: object
""")
generated = self.clinic.parse(block)
self.assertIn('"O$O:func"', generated)

def test_limited_capi_double(self):
block = self.wrap_clinic_input("""
func
Expand Down Expand Up @@ -4720,6 +4785,18 @@ def test_my_double_sum(self):
with self.assertRaises(TypeError):
func(1., "2")

def test_required_kwonly(self):
# test a required keyword-only parameter after an optional one
func = _testclinic_limited.required_kwonly
self.assertEqual(func(1, 2, c=3, d=4), (1, 2, 3, 4))
self.assertEqual(func(1, c=3), (1, None, 3, None))
with self.assertRaisesRegex(TypeError, "argument 'c'"):
func(1, 2)
with self.assertRaisesRegex(TypeError, "argument 'c'"):
func(1, 2, d=4)
with self.assertRaises(TypeError):
func(1, 2, 3)

def test_get_file_descriptor(self):
# test 'file descriptor' converter: call PyObject_AsFileDescriptor()
get_fd = _testclinic_limited.get_file_descriptor
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
:c:func:`PyArg_ParseTupleAndKeywords` now supports required keyword-only arguments mixed with optional arguments.
The new format unit ``%`` starts the keyword-only arguments which are required until ``|``.
For example, the format string ``"O|O%O|O"`` corresponds to the Python signature ``f(a, b=None, *, c, d=None)``.
40 changes: 40 additions & 0 deletions Modules/_testcapi/getargs.c
Original file line number Diff line number Diff line change
Expand Up @@ -765,6 +765,42 @@ gh_99240_clear_args(PyObject *self, PyObject *args)
Py_RETURN_NONE;
}

/* f(a, b=None, *, c, d=None) parsed with a cached _PyArg_Parser. */
static PyObject *
getargs_fast_required_kwonly(PyObject *self, PyObject *args, PyObject *kwargs)
{
static const char * const keywords[] = {"a", "b", "c", "d", NULL};
static _PyArg_Parser parser = {
.format = "O|O%O|O:getargs_fast_required_kwonly",
.keywords = keywords,
};
PyObject *a, *b = Py_None, *c, *d = Py_None;
if (!_PyArg_ParseTupleAndKeywordsFast(args, kwargs, &parser,
&a, &b, &c, &d))
{
return NULL;
}
return Py_BuildValue("OOOO", a, b, c, d);
}

/* f(a, b, *, c, d) parsed with a cached _PyArg_Parser. */
static PyObject *
getargs_fast_kwonly(PyObject *self, PyObject *args, PyObject *kwargs)
{
static const char * const keywords[] = {"a", "b", "c", "d", NULL};
static _PyArg_Parser parser = {
.format = "OO$OO:getargs_fast_kwonly",
.keywords = keywords,
};
PyObject *a, *b, *c, *d;
if (!_PyArg_ParseTupleAndKeywordsFast(args, kwargs, &parser,
&a, &b, &c, &d))
{
return NULL;
}
return Py_BuildValue("OOOO", a, b, c, d);
}

static PyMethodDef test_methods[] = {
{"get_args", get_args, METH_VARARGS},
{"get_kwargs", _PyCFunction_CAST(get_kwargs), METH_VARARGS|METH_KEYWORDS},
Expand Down Expand Up @@ -809,6 +845,10 @@ static PyMethodDef test_methods[] = {
{"getargs_z_hash", getargs_z_hash, METH_VARARGS},
{"getargs_z_star", getargs_z_star, METH_VARARGS},
{"parse_tuple_and_keywords", parse_tuple_and_keywords, METH_VARARGS},
{"getargs_fast_required_kwonly",
_PyCFunction_CAST(getargs_fast_required_kwonly), METH_VARARGS|METH_KEYWORDS},
{"getargs_fast_kwonly", _PyCFunction_CAST(getargs_fast_kwonly),
METH_VARARGS|METH_KEYWORDS},
{"gh_99240_clear_args", gh_99240_clear_args, METH_VARARGS},
{"test_w_code_invalid", test_w_code_invalid, METH_NOARGS},
{NULL},
Expand Down
22 changes: 22 additions & 0 deletions Modules/_testclinic_limited.c
Original file line number Diff line number Diff line change
Expand Up @@ -129,9 +129,31 @@ static PyMethodDef tester_methods[] = {
MY_FLOAT_SUM_METHODDEF
MY_DOUBLE_SUM_METHODDEF
GET_FILE_DESCRIPTOR_METHODDEF
REQUIRED_KWONLY_METHODDEF
{NULL, NULL}
};

/*[clinic input]
required_kwonly

a: object
b: object = None
*
c: object
d: object = None

Mix an optional parameter with a required keyword-only one.
[clinic start generated code]*/

static PyObject *
required_kwonly_impl(PyObject *module, PyObject *a, PyObject *b, PyObject *c,
PyObject *d)
/*[clinic end generated code: output=8e9a974afa614cd6 input=297ec6487f6373dd]*/
{
return Py_BuildValue("OOOO", a, b, c, d);
}


static struct PyModuleDef _testclinic_module = {
PyModuleDef_HEAD_INIT,
.m_name = "_testclinic_limited",
Expand Down
34 changes: 33 additions & 1 deletion Modules/clinic/_testclinic_limited.c.h

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

Loading
Loading