Skip to content
Closed
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
13 changes: 10 additions & 3 deletions Doc/c-api/arg.rst
Original file line number Diff line number Diff line change
Expand Up @@ -384,12 +384,19 @@ inside nested parentheses. They are:
``$``
:c:func:`PyArg_ParseTupleAndKeywords` only:
Indicates that the remaining arguments in the Python argument list are
keyword-only. Currently, all keyword-only arguments must also be optional
arguments, so ``|`` must always be specified before ``$`` in the format
string.
keyword-only. By default, arguments following ``$`` are optional and
``|`` must be specified before ``$``. For required, keyword-only arguments,
specify ``@`` after ``$``.

.. versionadded:: 3.3

``@``
:c:func:`PyArg_ParseTupleAndKeywords` only:
Indicates that the remaining arguments in the Python argument list are
required keyword-only arguments. Must be specified after ``|`` and ``$``.

.. versionadded:: 3.8

``:``
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
1 change: 1 addition & 0 deletions Include/modsupport.h
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,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 required_kwonly_start; /* first required kwonly argument */
PyObject *kwtuple; /* tuple of keyword parameter names */
struct _PyArg_Parser *next;
} _PyArg_Parser;
Expand Down
183 changes: 181 additions & 2 deletions Lib/test/test_getargs2.py
Original file line number Diff line number Diff line change
Expand Up @@ -661,6 +661,185 @@ def test_surrogate_keyword(self):
"'\udc80' is an invalid keyword argument for this function"):
getargs_keyword_only(1, 2, **{'\uDC80': 10})

class RequiredKeywordOnly_TestCase(unittest.TestCase):
from _testcapi import getargs_required_keyword_only as getargs

def test_basic_args(self):
self.assertEqual(
self.getargs(1, kw_required=2),
(1, -1, 2, -1)
)
self.assertEqual(
self.getargs(1, 2, kw_required=3),
(1, 2, 3, -1)
)
self.assertEqual(
self.getargs(1, 2, kw_required=3, kw_optional=4),
(1, 2, 3, 4)
)
self.assertEqual(
self.getargs(1, arg2=2, kw_required=3, kw_optional=4),
(1, 2, 3, 4)
)
self.assertEqual(
self.getargs(1, arg2=2, kw_required=3),
(1, 2, 3, -1)
)
self.assertEqual(
self.getargs(arg1=1, arg2=2, kw_required=3, kw_optional=4),
(1, 2, 3, 4)
)
self.assertEqual(
self.getargs(arg1=1, arg2=2, kw_required=3),
(1, 2, 3, -1)
)
self.assertEqual(
self.getargs(arg1=1, kw_required=3, kw_optional=4),
(1, -1, 3, 4)
)
self.assertEqual(
self.getargs(arg1=1, kw_required=3),
(1, -1, 3, -1)
)

def test_required_positional_args(self):
# required positional arg missing
with self.assertRaisesRegex(TypeError,
r"function missing required argument 'arg1' \(pos 1\)"):
self.getargs()

with self.assertRaisesRegex(TypeError,
r"function missing required argument 'arg1' \(pos 1\)"):
self.getargs(kw_required=3)

with self.assertRaisesRegex(TypeError,
r"function missing required argument 'arg1' \(pos 1\)"):
self.getargs(arg2=2, kw_required=4)

def test_required_keyword_args(self):
# required keyword arg missing
with self.assertRaisesRegex(TypeError,
r"function missing required keyword-only argument 'kw_required'"):
self.getargs(1)

with self.assertRaisesRegex(TypeError,
r"function missing required keyword-only argument 'kw_required'"):
self.getargs(1, 2)

with self.assertRaisesRegex(TypeError,
r"function missing required keyword-only argument 'kw_required'"):
self.getargs(1, arg2=2)

with self.assertRaisesRegex(TypeError,
r"function missing required keyword-only argument 'kw_required'"):
self.getargs(1, arg2=2, kw_optional=3)

with self.assertRaisesRegex(TypeError,
r"function missing required keyword-only argument 'kw_required'"):
self.getargs(arg1=1, arg2=2, kw_optional=3)

def test_keyword_only(self):
with self.assertRaisesRegex(TypeError,
r"function takes at most 2 positional arguments \(3 given\)"):
self.getargs(1, 2, 3)

class RequiredKeywordOnly2_TestCase(unittest.TestCase):
from _testcapi import getargs_required_keyword_only2 as getargs

def test_basic_args(self):
self.assertEqual(
self.getargs(1, kw_required=3),
(1, 3, -1)
)
self.assertEqual(
self.getargs(1, 4, kw_required=3),
(1, 3, 4)
)
self.assertEqual(
self.getargs(arg1=1, kw_required=3),
(1, 3, -1)
)

def test_required_positional_args(self):
# required positional arg missing
with self.assertRaisesRegex(TypeError,
r"function missing required argument 'arg1' \(pos 1\)"):
self.getargs()

with self.assertRaisesRegex(TypeError,
r"function missing required argument 'arg1' \(pos 1\)"):
self.getargs(kw_required=3)

with self.assertRaisesRegex(TypeError,
r"function missing required argument 'arg1' \(pos 1\)"):
self.getargs(arg2=2, kw_required=4)

def test_required_keyword_args(self):
# required keyword arg missing
with self.assertRaisesRegex(TypeError,
r"function missing required keyword-only argument 'kw_required'"):
self.getargs(1)

with self.assertRaisesRegex(TypeError,
r"function missing required keyword-only argument 'kw_required'"):
self.getargs(1, 2)

with self.assertRaisesRegex(TypeError,
r"function missing required keyword-only argument 'kw_required'"):
self.getargs(1, arg2=2)

with self.assertRaisesRegex(TypeError,
r"function missing required keyword-only argument 'kw_required'"):
self.getargs(arg1=1, arg2=2)

def test_keyword_only(self):
with self.assertRaisesRegex(TypeError,
r"function takes at most 2 positional arguments \(3 given\)"):
self.getargs(1, 2, 3)

class RequiredKeywordOnly3_TestCase(unittest.TestCase):
from _testcapi import getargs_required_keyword_only3 as getargs

def test_basic_args(self):
self.assertEqual(
self.getargs(1, kw_required=2),
(1, 2)
)
self.assertEqual(
self.getargs(arg1=1, kw_required=2),
(1, 2)
)

def test_required_positional_args(self):
# required positional arg missing
with self.assertRaisesRegex(TypeError,
r"function missing required argument 'arg1' \(pos 1\)"):
self.getargs()

with self.assertRaisesRegex(TypeError,
r"function missing required argument 'arg1' \(pos 1\)"):
self.getargs(kw_required=3)

def test_required_keyword_args(self):
# required keyword arg missing
with self.assertRaisesRegex(TypeError,
r"function missing required keyword-only argument 'kw_required'"):
self.getargs(1)

def test_keyword_only(self):
with self.assertRaisesRegex(TypeError,
r"function takes at most 1 positional argument \(2 given\)"):
self.getargs(1, 2)

# Run all the same required keywords tests against the "fast" versions
class RequiredKeywordOnlyFast_TestCase(RequiredKeywordOnly_TestCase):
from _testcapi import getargs_required_keyword_only_fast as getargs

class RequiredKeywordOnlyFast2_TestCase(RequiredKeywordOnly2_TestCase):
from _testcapi import getargs_required_keyword_only_fast2 as getargs

class RequiredKeywordOnlyFast3_TestCase(RequiredKeywordOnly3_TestCase):
from _testcapi import getargs_required_keyword_only_fast3 as getargs

class PositionalOnlyAndKeywords_TestCase(unittest.TestCase):
from _testcapi import getargs_positional_only_and_keywords as getargs
Expand Down Expand Up @@ -1001,8 +1180,8 @@ def test_skipitem(self):

# skip parentheses, the error reporting is inconsistent about them
# skip 'e', it's always a two-character code
# skip '|' and '$', they don't represent arguments anyway
if c in '()e|$':
# skip '|', '$', and '@', they don't represent arguments anyway
if c in '()e|$@':
continue

# test the format unit when not skipped
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Support required keyword-only arguments in `PyArg_ParseTupleAndKeywords`.
114 changes: 114 additions & 0 deletions Modules/_testcapimodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -1063,6 +1063,102 @@ getargs_positional_only_and_keywords(PyObject *self, PyObject *args, PyObject *k
return Py_BuildValue("iii", required, optional, keyword);
}

static PyObject *
getargs_required_keyword_only(PyObject *self, PyObject *args, PyObject *kwargs)
{
static char *keywords[] = {
"arg1", "arg2", "kw_optional", "kw_required", NULL};
int arg1 = -1;
int arg2 = -1;
int required = -1;
int optional = -1;

if (!PyArg_ParseTupleAndKeywords(args, kwargs, "i|i$i@i", keywords,
&arg1, &arg2, &optional, &required))
return NULL;
return Py_BuildValue("iiii", arg1, arg2, required, optional);
}

static PyObject *
getargs_required_keyword_only_fast(
PyObject *self, PyObject *args, PyObject *kwargs)
{
static const char * const keywords[] = {
"arg1", "arg2", "kw_optional", "kw_required", NULL};
static _PyArg_Parser parser = {"i|i$i@i", keywords, 0};
int arg1 = -1;
int arg2 = -1;
int required = -1;
int optional = -1;

if (!_PyArg_ParseTupleAndKeywordsFast(args, kwargs, &parser,
&arg1, &arg2, &optional, &required))
return NULL;
return Py_BuildValue("iiii", arg1, arg2, required, optional);
}

static PyObject *
getargs_required_keyword_only2(PyObject *self, PyObject *args, PyObject *kwargs)
{
static char *keywords[] = {
"arg1", "arg2", "kw_required", NULL};
int arg1 = -1;
int arg2 = -1;
int required = -1;

if (!PyArg_ParseTupleAndKeywords(args, kwargs, "i|i$@i", keywords,
&arg1, &arg2, &required))
return NULL;
return Py_BuildValue("iii", arg1, required, arg2);
}

static PyObject *
getargs_required_keyword_only_fast2(
PyObject *self, PyObject *args, PyObject *kwargs)
{
static const char * const keywords[] = {
"arg1", "arg2", "kw_required", NULL};
static _PyArg_Parser parser = {"i|i$@i", keywords, 0};
int arg1 = -1;
int arg2 = -1;
int required = -1;

if (!_PyArg_ParseTupleAndKeywordsFast(args, kwargs, &parser,
&arg1, &arg2, &required))
return NULL;
return Py_BuildValue("iii", arg1, required, arg2);
}

static PyObject *
getargs_required_keyword_only3(PyObject *self, PyObject *args, PyObject *kwargs)
{
static char *keywords[] = {
"arg1", "kw_required", NULL};
int arg1 = -1;
int required = -1;

if (!PyArg_ParseTupleAndKeywords(args, kwargs, "i|$@i", keywords,
&arg1, &required))
return NULL;
return Py_BuildValue("ii", arg1, required);
}

static PyObject *
getargs_required_keyword_only_fast3(
PyObject *self, PyObject *args, PyObject *kwargs)
{
static const char * const keywords[] = {
"arg1", "kw_required", NULL};
static _PyArg_Parser parser = {"i|$@i", keywords, 0};
int arg1 = -1;
int required = -1;

if (!_PyArg_ParseTupleAndKeywordsFast(args, kwargs, &parser,
&arg1, &required))
return NULL;
return Py_BuildValue("ii", arg1, required);
}

/* Functions to call PyArg_ParseTuple with integer format codes,
and return the result.
*/
Expand Down Expand Up @@ -4777,6 +4873,24 @@ static PyMethodDef TestMethods[] = {
{"getargs_positional_only_and_keywords",
(PyCFunction)(void(*)(void))getargs_positional_only_and_keywords,
METH_VARARGS|METH_KEYWORDS},
{"getargs_required_keyword_only",
(PyCFunction)(void(*)(void))getargs_required_keyword_only,
METH_VARARGS|METH_KEYWORDS},
{"getargs_required_keyword_only2",
(PyCFunction)(void(*)(void))getargs_required_keyword_only2,
METH_VARARGS|METH_KEYWORDS},
{"getargs_required_keyword_only3",
(PyCFunction)(void(*)(void))getargs_required_keyword_only3,
METH_VARARGS|METH_KEYWORDS},
{"getargs_required_keyword_only_fast",
(PyCFunction)(void(*)(void))getargs_required_keyword_only_fast,
METH_VARARGS|METH_KEYWORDS},
{"getargs_required_keyword_only_fast2",
(PyCFunction)(void(*)(void))getargs_required_keyword_only_fast2,
METH_VARARGS|METH_KEYWORDS},
{"getargs_required_keyword_only_fast3",
(PyCFunction)(void(*)(void))getargs_required_keyword_only_fast3,
METH_VARARGS|METH_KEYWORDS},
{"getargs_b", getargs_b, METH_VARARGS},
{"getargs_B", getargs_B, METH_VARARGS},
{"getargs_h", getargs_h, METH_VARARGS},
Expand Down
Loading