cpython: 69c0aa8a8185 (original) (raw)
Mercurial > cpython
changeset 101854:69c0aa8a8185
Issue #26282: PyArg_ParseTupleAndKeywords() and Argument Clinic now support positional-only and keyword parameters in the same function. [#26282]
Serhiy Storchaka storchaka@gmail.com | |
---|---|
date | Thu, 09 Jun 2016 16:30:29 +0300 |
parents | a3060775b8ad |
children | a5a013ca5687 |
files | Doc/c-api/arg.rst Doc/glossary.rst Doc/whatsnew/3.6.rst Lib/test/test_capi.py Lib/test/test_getargs2.py Misc/NEWS Modules/_testcapimodule.c Python/getargs.c Tools/clinic/clinic.py |
diffstat | 9 files changed, 210 insertions(+), 72 deletions(-)[+] [-] Doc/c-api/arg.rst 11 Doc/glossary.rst 2 Doc/whatsnew/3.6.rst 5 Lib/test/test_capi.py 25 Lib/test/test_getargs2.py 33 Misc/NEWS 12 Modules/_testcapimodule.c 18 Python/getargs.c 125 Tools/clinic/clinic.py 51 |
line wrap: on
line diff
--- a/Doc/c-api/arg.rst +++ b/Doc/c-api/arg.rst @@ -406,8 +406,15 @@ API Functions .. c:function:: int PyArg_ParseTupleAndKeywords(PyObject *args, PyObject *kw, const char *format, char *keywords[], ...) Parse the parameters of a function that takes both positional and keyword
- parameters into local variables. Returns true on success; on failure, it
- returns false and raises the appropriate exception.
- parameters into local variables. The keywords argument is a
- NULL-terminated array of keyword parameter names. Empty names denote
- :ref:
positional-only parameters <positional-only_parameter>
. - Returns true on success; on failure, it returns false and raises the
- appropriate exception. +
- .. versionchanged:: 3.6
Added support for :ref:`positional-only parameters[](#l1.16)
<positional-only_parameter>`.[](#l1.17)
.. c:function:: int PyArg_VaParseTupleAndKeywords(PyObject *args, PyObject *kw, const char *format, char *keywords[], va_list vargs)
--- a/Doc/glossary.rst +++ b/Doc/glossary.rst @@ -718,6 +718,8 @@ Glossary def func(foo, bar=None): ...
.. _positional-only_parameter:[](#l2.7)
+
* :dfn:positional-only
: specifies an argument that can be supplied only
by position. Python has no syntax for defining positional-only
parameters. However, some built-in functions have positional-only
--- a/Doc/whatsnew/3.6.rst +++ b/Doc/whatsnew/3.6.rst @@ -503,6 +503,11 @@ Build and C API Changes
- New :c:func:
Py_FinalizeEx
API which indicates if flushing buffered data failed (:issue:5319
). +* :c:func:PyArg_ParseTupleAndKeywords
now supports :ref:`positional-only
- parameters `. Positional-only parameters are
- defined by empty names.
- (Contributed by Serhit Storchaka in :issue:
26282
). + Deprecated ==========
--- a/Lib/test/test_capi.py +++ b/Lib/test/test_capi.py @@ -527,6 +527,31 @@ class SkipitemTest(unittest.TestCase): self.assertRaises(ValueError, _testcapi.parse_tuple_and_keywords, (), {}, b'', [42])
parse((1, 2, 3), {}, b'OOO', ['', '', 'a'])[](#l4.10)
parse((1, 2), {'a': 3}, b'OOO', ['', '', 'a'])[](#l4.11)
with self.assertRaisesRegex(TypeError,[](#l4.12)
'Function takes at least 2 positional arguments \(1 given\)'):[](#l4.13)
parse((1,), {'a': 3}, b'OOO', ['', '', 'a'])[](#l4.14)
parse((1,), {}, b'O|OO', ['', '', 'a'])[](#l4.15)
with self.assertRaisesRegex(TypeError,[](#l4.16)
'Function takes at least 1 positional arguments \(0 given\)'):[](#l4.17)
parse((), {}, b'O|OO', ['', '', 'a'])[](#l4.18)
parse((1, 2), {'a': 3}, b'OO$O', ['', '', 'a'])[](#l4.19)
with self.assertRaisesRegex(TypeError,[](#l4.20)
'Function takes exactly 2 positional arguments \(1 given\)'):[](#l4.21)
parse((1,), {'a': 3}, b'OO$O', ['', '', 'a'])[](#l4.22)
parse((1,), {}, b'O|O$O', ['', '', 'a'])[](#l4.23)
with self.assertRaisesRegex(TypeError,[](#l4.24)
'Function takes at least 1 positional arguments \(0 given\)'):[](#l4.25)
parse((), {}, b'O|O$O', ['', '', 'a'])[](#l4.26)
with self.assertRaisesRegex(SystemError, 'Empty parameter name after \$'):[](#l4.27)
parse((1,), {}, b'O|$OO', ['', '', 'a'])[](#l4.28)
with self.assertRaisesRegex(SystemError, 'Empty keyword'):[](#l4.29)
parse((1,), {}, b'O|OO', ['', 'a', ''])[](#l4.30)
+ @unittest.skipUnless(threading, 'Threading required for this test.') class TestThreadState(unittest.TestCase):
--- a/Lib/test/test_getargs2.py +++ b/Lib/test/test_getargs2.py @@ -658,6 +658,39 @@ class KeywordOnly_TestCase(unittest.Test getargs_keyword_only(1, 2, **{'\uDC80': 10}) +class PositionalOnlyAndKeywords_TestCase(unittest.TestCase):
- def test_positional_args(self):
# using all possible positional args[](#l5.11)
self.assertEqual(self.getargs(1, 2, 3), (1, 2, 3))[](#l5.12)
- def test_mixed_args(self):
# positional and keyword args[](#l5.15)
self.assertEqual(self.getargs(1, 2, keyword=3), (1, 2, 3))[](#l5.16)
- def test_optional_args(self):
# missing optional args[](#l5.19)
self.assertEqual(self.getargs(1, 2), (1, 2, -1))[](#l5.20)
self.assertEqual(self.getargs(1, keyword=3), (1, -1, 3))[](#l5.21)
- def test_required_args(self):
self.assertEqual(self.getargs(1), (1, -1, -1))[](#l5.24)
# required positional arg missing[](#l5.25)
with self.assertRaisesRegex(TypeError,[](#l5.26)
"Function takes at least 1 positional arguments \(0 given\)"):[](#l5.27)
self.getargs()[](#l5.28)
with self.assertRaisesRegex(TypeError,[](#l5.30)
"Function takes at least 1 positional arguments \(0 given\)"):[](#l5.31)
self.getargs(keyword=3)[](#l5.32)
- def test_empty_keyword(self):
with self.assertRaisesRegex(TypeError,[](#l5.35)
"'' is an invalid keyword argument for this function"):[](#l5.36)
self.getargs(1, 2, **{'': 666})[](#l5.37)
+ + class Bytes_TestCase(unittest.TestCase): def test_c(self): from _testcapi import getargs_c
--- a/Misc/NEWS +++ b/Misc/NEWS @@ -201,6 +201,18 @@ Misc
- Issue #17500, and https://github.com/python/pythondotorg/issues/945: Remove unused and outdated icons. +C API +----- + +- Issue #26282: PyArg_ParseTupleAndKeywords() now supports positional-only
- parameters. + +Tools/Demos +----------- + +- Issue #26282: Argument Clinic now supports positional-only and keyword
- parameters in the same function. + What's New in Python 3.6.0 alpha 1? ===================================
--- a/Modules/_testcapimodule.c +++ b/Modules/_testcapimodule.c @@ -1028,6 +1028,21 @@ getargs_keyword_only(PyObject self, PyO return Py_BuildValue("iii", required, optional, keyword_only); } +/ test PyArg_ParseTupleAndKeywords positional-only arguments */ +static PyObject * +getargs_positional_only_and_keywords(PyObject *self, PyObject *args, PyObject *kwargs) +{
- static char *keywords[] = {"", "", "keyword", NULL};
- int required = -1;
- int optional = -1;
- int keyword = -1;
- if (!PyArg_ParseTupleAndKeywords(args, kwargs, "i|ii", keywords,
&required, &optional, &keyword))[](#l7.17)
return NULL;[](#l7.18)
- return Py_BuildValue("iii", required, optional, keyword);
+} + /* Functions to call PyArg_ParseTuple with integer format codes, and return the result. */ @@ -3963,6 +3978,9 @@ static PyMethodDef TestMethods[] = { METH_VARARGS|METH_KEYWORDS}, {"getargs_keyword_only", (PyCFunction)getargs_keyword_only, METH_VARARGS|METH_KEYWORDS},
- {"getargs_positional_only_and_keywords",
(PyCFunction)getargs_positional_only_and_keywords,[](#l7.30)
{"getargs_b", getargs_b, METH_VARARGS}, {"getargs_B", getargs_B, METH_VARARGS}, {"getargs_h", getargs_h, METH_VARARGS},METH_VARARGS|METH_KEYWORDS},[](#l7.31)
--- a/Python/getargs.c +++ b/Python/getargs.c @@ -1443,7 +1443,8 @@ vgetargskeywords(PyObject *args, PyObjec const char *fname, *msg, *custom_msg, *keyword; int min = INT_MAX; int max = INT_MAX;
- int i, pos, len;
- int skip = 0; Py_ssize_t nargs, nkeywords; PyObject *current_arg; freelistentry_t static_entries[STATIC_FREELIST_ENTRIES]; @@ -1471,9 +1472,17 @@ vgetargskeywords(PyObject *args, PyObjec custom_msg++; }
- /* scan kwlist and count the number of positional-only parameters */
- for (pos = 0; kwlist[pos] && !*kwlist[pos]; pos++) {
- } /* scan kwlist and get greatest possible nbr of args */
- for (len = pos; kwlist[len]; len++) {
if (!*kwlist[len]) {[](#l8.24)
PyErr_SetString(PyExc_SystemError,[](#l8.25)
"Empty keyword parameter name");[](#l8.26)
return cleanreturn(0, &freelist);[](#l8.27)
}[](#l8.28)
- }
if (len > STATIC_FREELIST_ENTRIES) { freelist.entries = PyMem_NEW(freelistentry_t, len); @@ -1526,6 +1535,14 @@ vgetargskeywords(PyObject *args, PyObjec max = i; format++;
if (max < pos) {[](#l8.37)
PyErr_SetString(PyExc_SystemError,[](#l8.38)
"Empty parameter name after $");[](#l8.39)
return cleanreturn(0, &freelist);[](#l8.40)
}[](#l8.41)
if (skip) {[](#l8.42)
break;[](#l8.43)
}[](#l8.44) if (max < nargs) {[](#l8.45) PyErr_Format(PyExc_TypeError,[](#l8.46) "Function takes %s %d positional arguments"[](#l8.47)
@@ -1541,48 +1558,59 @@ vgetargskeywords(PyObject *args, PyObjec "format specifiers (%d)", len, i); return cleanreturn(0, &freelist); }
current_arg = NULL;[](#l8.52)
if (nkeywords) {[](#l8.53)
current_arg = PyDict_GetItemString(keywords, keyword);[](#l8.54)
}[](#l8.55)
if (current_arg) {[](#l8.56)
--nkeywords;[](#l8.57)
if (i < nargs) {[](#l8.58)
/* arg present in tuple and in dict */[](#l8.59)
PyErr_Format(PyExc_TypeError,[](#l8.60)
"Argument given by name ('%s') "[](#l8.61)
"and position (%d)",[](#l8.62)
keyword, i+1);[](#l8.63)
return cleanreturn(0, &freelist);[](#l8.64)
if (!skip) {[](#l8.65)
current_arg = NULL;[](#l8.66)
if (nkeywords && i >= pos) {[](#l8.67)
current_arg = PyDict_GetItemString(keywords, keyword);[](#l8.68)
if (!current_arg && PyErr_Occurred()) {[](#l8.69)
return cleanreturn(0, &freelist);[](#l8.70)
}[](#l8.71)
}[](#l8.72)
if (current_arg) {[](#l8.73)
--nkeywords;[](#l8.74)
if (i < nargs) {[](#l8.75)
/* arg present in tuple and in dict */[](#l8.76)
PyErr_Format(PyExc_TypeError,[](#l8.77)
"Argument given by name ('%s') "[](#l8.78)
"and position (%d)",[](#l8.79)
keyword, i+1);[](#l8.80)
return cleanreturn(0, &freelist);[](#l8.81)
}[](#l8.82)
}[](#l8.83)
else if (i < nargs)[](#l8.84)
current_arg = PyTuple_GET_ITEM(args, i);[](#l8.85)
if (current_arg) {[](#l8.87)
msg = convertitem(current_arg, &format, p_va, flags,[](#l8.88)
levels, msgbuf, sizeof(msgbuf), &freelist);[](#l8.89)
if (msg) {[](#l8.90)
seterror(i+1, msg, levels, fname, custom_msg);[](#l8.91)
return cleanreturn(0, &freelist);[](#l8.92)
}[](#l8.93)
continue;[](#l8.94)
}[](#l8.95)
if (i < min) {[](#l8.97)
if (i < pos) {[](#l8.98)
assert (min == INT_MAX);[](#l8.99)
assert (max == INT_MAX);[](#l8.100)
skip = 1;[](#l8.101)
}[](#l8.102)
else {[](#l8.103)
PyErr_Format(PyExc_TypeError, "Required argument "[](#l8.104)
"'%s' (pos %d) not found",[](#l8.105)
keyword, i+1);[](#l8.106)
return cleanreturn(0, &freelist);[](#l8.107)
}[](#l8.108)
}[](#l8.109)
/* current code reports success when all required args[](#l8.110)
* fulfilled and no keyword args left, with no further[](#l8.111)
* validation. XXX Maybe skip this in debug build ?[](#l8.112)
*/[](#l8.113)
if (!nkeywords && !skip) {[](#l8.114)
return cleanreturn(1, &freelist);[](#l8.115) }[](#l8.116) }[](#l8.117)
else if (nkeywords && PyErr_Occurred())[](#l8.118)
return cleanreturn(0, &freelist);[](#l8.119)
else if (i < nargs)[](#l8.120)
current_arg = PyTuple_GET_ITEM(args, i);[](#l8.121)
if (current_arg) {[](#l8.123)
msg = convertitem(current_arg, &format, p_va, flags,[](#l8.124)
levels, msgbuf, sizeof(msgbuf), &freelist);[](#l8.125)
if (msg) {[](#l8.126)
seterror(i+1, msg, levels, fname, custom_msg);[](#l8.127)
return cleanreturn(0, &freelist);[](#l8.128)
}[](#l8.129)
continue;[](#l8.130)
}[](#l8.131)
if (i < min) {[](#l8.133)
PyErr_Format(PyExc_TypeError, "Required argument "[](#l8.134)
"'%s' (pos %d) not found",[](#l8.135)
keyword, i+1);[](#l8.136)
return cleanreturn(0, &freelist);[](#l8.137)
}[](#l8.138)
/* current code reports success when all required args[](#l8.139)
* fulfilled and no keyword args left, with no further[](#l8.140)
* validation. XXX Maybe skip this in debug build ?[](#l8.141)
*/[](#l8.142)
if (!nkeywords)[](#l8.143)
return cleanreturn(1, &freelist);[](#l8.144)
/* We are into optional args, skip thru to any remaining * keyword args */ @@ -1594,6 +1622,15 @@ vgetargskeywords(PyObject *args, PyObjec } }
- if (skip) {
PyErr_Format(PyExc_TypeError,[](#l8.153)
"Function takes %s %d positional arguments"[](#l8.154)
" (%d given)",[](#l8.155)
(Py_MIN(pos, min) < i) ? "at least" : "exactly",[](#l8.156)
Py_MIN(pos, min), nargs);[](#l8.157)
return cleanreturn(0, &freelist);[](#l8.158)
- }
+ if (!IS_END_OF_FORMAT(*format) && (*format != '|') && (*format != '$')) { PyErr_Format(PyExc_SystemError, "more argument specifiers than keyword list entries " @@ -1613,7 +1650,7 @@ vgetargskeywords(PyObject *args, PyObjec return cleanreturn(0, &freelist); } for (i = 0; i < len; i++) {
if (!PyUnicode_CompareWithASCIIString(key, kwlist[i])) {[](#l8.168)
if (*kwlist[i] && !PyUnicode_CompareWithASCIIString(key, kwlist[i])) {[](#l8.169) match = 1;[](#l8.170) break;[](#l8.171) }[](#l8.172)
--- a/Tools/clinic/clinic.py +++ b/Tools/clinic/clinic.py @@ -644,7 +644,7 @@ class CLanguage(Language): default_return_converter = (not f.return_converter or f.return_converter.type == 'PyObject *')
positional = parameters and (parameters[-1].kind == inspect.Parameter.POSITIONAL_ONLY)[](#l9.7)
positional = parameters and parameters[-1].is_positional_only()[](#l9.8) all_boring_objects = False # yes, this will be false if there are 0 parameters, it's fine[](#l9.9) first_optional = len(parameters)[](#l9.10) for i, p in enumerate(parameters):[](#l9.11)
@@ -661,7 +661,7 @@ class CLanguage(Language): new_or_init = f.kind in (METHOD_NEW, METHOD_INIT) meth_o = (len(parameters) == 1 and
parameters[0].kind == inspect.Parameter.POSITIONAL_ONLY and[](#l9.16)
parameters[0].is_positional_only() and[](#l9.17) not converters[0].is_optional() and[](#l9.18) not new_or_init)[](#l9.19)
@@ -1075,7 +1075,7 @@ class CLanguage(Language): last_group = 0 first_optional = len(selfless)
positional = selfless and selfless[-1].kind == inspect.Parameter.POSITIONAL_ONLY[](#l9.25)
positional = selfless and selfless[-1].is_positional_only()[](#l9.26) new_or_init = f.kind in (METHOD_NEW, METHOD_INIT)[](#l9.27) default_return_converter = (not f.return_converter or[](#l9.28) f.return_converter.type == 'PyObject *')[](#l9.29)
@@ -2367,7 +2367,10 @@ class CConverter(metaclass=CConverterAut data.modifications.append('/* modifications for ' + name + ' */\n' + modifications.rstrip()) # keywords
data.keywords.append(parameter.name)[](#l9.34)
if parameter.is_positional_only():[](#l9.35)
data.keywords.append('')[](#l9.36)
else:[](#l9.37)
data.keywords.append(parameter.name)[](#l9.38)
# format_units if self.is_optional() and '|' not in data.format_units: @@ -3192,6 +3195,7 @@ class DSLParser: self.state = self.state_dsl_start self.parameter_indent = None self.keyword_only = False
self.positional_only = False[](#l9.46) self.group = 0[](#l9.47) self.parameter_state = self.ps_start[](#l9.48) self.seen_positional_with_default = False[](#l9.49)
@@ -3570,8 +3574,8 @@ class DSLParser: # "parameter_state". (Previously the code was a miasma of ifs and # separate boolean state variables.) The states are: #
[ [ a, b, ] c, ] d, e, f=3, [ g, h, [ i ] ] <- line
# 01 2 3 4 5 6 <- state transitions
0: ps_start. before we've seen anything. legal transitions are to 1 or 3.
1: ps_left_square_before. left square brackets before required parameters.
@@ -3582,9 +3586,8 @@ class DSLParser: # now must have default values. # 5: ps_group_after. in a group, after required parameters. # 6: ps_right_square_after. right square brackets after required parameters.
ps_start, ps_left_square_before, ps_group_before, ps_required, [](#l9.66) 7: ps_seen_slash. seen slash.
- ps_optional, ps_group_after, ps_right_square_after, ps_seen_slash = range(8)
def state_parameters_start(self, line): if self.ignore_line(line): @@ -3863,9 +3866,6 @@ class DSLParser: return name, False, kwargs def parse_special_symbol(self, symbol):
if self.parameter_state == self.ps_seen_slash:[](#l9.76)
fail("Function " + self.function.name + " specifies " + symbol + " after /, which is unsupported.")[](#l9.77)
- if symbol == '': if self.keyword_only: fail("Function " + self.function.name + " uses '' more than once.") @@ -3892,13 +3892,15 @@ class DSLParser: else: fail("Function " + self.function.name + " has an unsupported group configuration. (Unexpected state " + str(self.parameter_state) + ".c)") elif symbol == '/':
if self.positional_only:[](#l9.86)
fail("Function " + self.function.name + " uses '/' more than once.")[](#l9.87)
self.positional_only = True[](#l9.88) # ps_required and ps_optional are allowed here, that allows positional-only without option groups[](#l9.89) # to work (and have default values!)[](#l9.90) if (self.parameter_state not in (self.ps_required, self.ps_optional, self.ps_right_square_after, self.ps_group_before)) or self.group:[](#l9.91) fail("Function " + self.function.name + " has an unsupported group configuration. (Unexpected state " + str(self.parameter_state) + ".d)")[](#l9.92) if self.keyword_only:[](#l9.93) fail("Function " + self.function.name + " mixes keyword-only and positional-only parameters, which is unsupported.")[](#l9.94)
self.parameter_state = self.ps_seen_slash[](#l9.95) # fixup preceding parameters[](#l9.96) for p in self.function.parameters.values():[](#l9.97) if (p.kind != inspect.Parameter.POSITIONAL_OR_KEYWORD and not isinstance(p.converter, self_converter)):[](#l9.98)
@@ -3986,23 +3988,20 @@ class DSLParser: # populate "right_bracket_count" field for every parameter assert parameters, "We should always have a self parameter. " + repr(f) assert isinstance(parameters[0].converter, self_converter)
# self is always positional-only.[](#l9.103)
assert parameters[0].is_positional_only()[](#l9.104) parameters[0].right_bracket_count = 0[](#l9.105)
parameters_after_self = parameters[1:][](#l9.106)
if parameters_after_self:[](#l9.107)
# for now, the only way Clinic supports positional-only parameters[](#l9.108)
# is if all of them are positional-only...[](#l9.109)
#[](#l9.110)
# ... except for self! self is always positional-only.[](#l9.111)
positional_only_parameters = [p.kind == inspect.Parameter.POSITIONAL_ONLY for p in parameters_after_self][](#l9.113)
if parameters_after_self[0].kind == inspect.Parameter.POSITIONAL_ONLY:[](#l9.114)
assert all(positional_only_parameters)[](#l9.115)
for p in parameters:[](#l9.116)
p.right_bracket_count = abs(p.group)[](#l9.117)
positional_only = True[](#l9.118)
for p in parameters[1:]:[](#l9.119)
if not p.is_positional_only():[](#l9.120)
positional_only = False[](#l9.121)
else:[](#l9.122)
assert positional_only[](#l9.123)
if positional_only:[](#l9.124)
p.right_bracket_count = abs(p.group)[](#l9.125) else:[](#l9.126) # don't put any right brackets around non-positional-only parameters, ever.[](#l9.127)
for p in parameters_after_self:[](#l9.128)
p.right_bracket_count = 0[](#l9.129)
p.right_bracket_count = 0[](#l9.130)