Issue #20144: Argument Clinic now supports simple constants as parameter

default values.  inspect.Signature correspondingly supports them in
__text_signature__ fields for builtins.
This commit is contained in:
Larry Hastings 2014-01-07 11:53:01 -08:00
parent 0bce6e7462
commit 16c5191ab3
6 changed files with 152 additions and 31 deletions

View File

@ -1974,18 +1974,60 @@ class Signature:
parameters = [] parameters = []
empty = Parameter.empty empty = Parameter.empty
invalid = object()
def parse_attribute(node):
if not isinstance(node.ctx, ast.Load):
return None
value = node.value
o = parse_node(value)
if o is invalid:
return invalid
if isinstance(value, ast.Name):
name = o
if name not in sys.modules:
return invalid
o = sys.modules[name]
return getattr(o, node.attr, invalid)
def parse_node(node):
if isinstance(node, ast.arg):
if node.annotation != None:
raise ValueError("Annotations are not currently supported")
return node.arg
if isinstance(node, ast.Num):
return node.n
if isinstance(node, ast.Str):
return node.s
if isinstance(node, ast.NameConstant):
return node.value
if isinstance(node, ast.Attribute):
return parse_attribute(node)
if isinstance(node, ast.Name):
if not isinstance(node.ctx, ast.Load):
return invalid
return node.id
return invalid
def p(name_node, default_node, default=empty): def p(name_node, default_node, default=empty):
name = name_node.arg name = parse_node(name_node)
if name is invalid:
if isinstance(default_node, ast.Num): return None
default = default.n if default_node:
elif isinstance(default_node, ast.NameConstant): o = parse_node(default_node)
default = default_node.value if o is invalid:
return None
default = o if o is not invalid else default
parameters.append(Parameter(name, kind, default=default, annotation=empty)) parameters.append(Parameter(name, kind, default=default, annotation=empty))
# non-keyword-only parameters # non-keyword-only parameters
for name, default in reversed(list(itertools.zip_longest(reversed(f.args.args), reversed(f.args.defaults), fillvalue=None))): args = reversed(f.args.args)
defaults = reversed(f.args.defaults)
iter = itertools.zip_longest(args, defaults, fillvalue=None)
for name, default in reversed(list(iter)):
p(name, default) p(name, default)
# *args # *args

View File

@ -15,6 +15,7 @@ try:
from concurrent.futures import ThreadPoolExecutor from concurrent.futures import ThreadPoolExecutor
except ImportError: except ImportError:
ThreadPoolExecutor = None ThreadPoolExecutor = None
import _testcapi
from test.support import run_unittest, TESTFN, DirsOnSysPath from test.support import run_unittest, TESTFN, DirsOnSysPath
from test.support import MISSING_C_DOCSTRINGS from test.support import MISSING_C_DOCSTRINGS
@ -1593,9 +1594,19 @@ class TestSignatureObject(unittest.TestCase):
@unittest.skipIf(MISSING_C_DOCSTRINGS, @unittest.skipIf(MISSING_C_DOCSTRINGS,
"Signature information for builtins requires docstrings") "Signature information for builtins requires docstrings")
def test_signature_on_builtins(self): def test_signature_on_builtins(self):
# min doesn't have a signature (yet)
self.assertEqual(inspect.signature(min), None) self.assertEqual(inspect.signature(min), None)
signature = inspect.signature(os.stat)
signature = inspect.signature(_testcapi.docstring_with_signature_with_defaults)
self.assertTrue(isinstance(signature, inspect.Signature)) self.assertTrue(isinstance(signature, inspect.Signature))
def p(name): return signature.parameters[name].default
self.assertEqual(p('s'), 'avocado')
self.assertEqual(p('d'), 3.14)
self.assertEqual(p('i'), 35)
self.assertEqual(p('c'), sys.maxsize)
self.assertEqual(p('n'), None)
self.assertEqual(p('t'), True)
self.assertEqual(p('f'), False)
def test_signature_on_non_function(self): def test_signature_on_non_function(self):
with self.assertRaisesRegex(TypeError, 'is not a callable object'): with self.assertRaisesRegex(TypeError, 'is not a callable object'):

View File

@ -13,11 +13,17 @@ Core and Builtins
Library Library
------- -------
- Issue #20144: inspect.Signature now supports parsing simple symbolic
constants as parameter default values in __text_signature__.
- Issue #20072: Fixed multiple errors in tkinter with wantobjects is False. - Issue #20072: Fixed multiple errors in tkinter with wantobjects is False.
Tools/Demos Tools/Demos
----------- -----------
- Issue #20144: Argument Clinic now supports simple symbolic constants
as parameter default values.
- Issue #20143: The line numbers reported in Argument Clinic errors are - Issue #20143: The line numbers reported in Argument Clinic errors are
now more accurate. now more accurate.

View File

@ -526,21 +526,58 @@ sre_search(SRE_STATE* state, SRE_CODE* pattern)
return sre_ucs4_search(state, pattern); return sre_ucs4_search(state, pattern);
} }
static PyObject* /*[clinic]
pattern_match(PatternObject* self, PyObject* args, PyObject* kw) module _sre
class _sre.SRE_Pattern
_sre.SRE_Pattern.match as pattern_match
self: self(type="PatternObject *")
pattern: object
pos: Py_ssize_t = 0
endpos: Py_ssize_t(c_default="PY_SSIZE_T_MAX") = sys.maxsize
Matches zero or more characters at the beginning of the string.
[clinic]*/
PyDoc_STRVAR(pattern_match__doc__,
"match(pattern, pos=0, endpos=sys.maxsize)\n"
"Matches zero or more characters at the beginning of the string.");
#define PATTERN_MATCH_METHODDEF \
{"match", (PyCFunction)pattern_match, METH_VARARGS|METH_KEYWORDS, pattern_match__doc__},
static PyObject *
pattern_match_impl(PatternObject *self, PyObject *pattern, Py_ssize_t pos, Py_ssize_t endpos);
static PyObject *
pattern_match(PyObject *self, PyObject *args, PyObject *kwargs)
{
PyObject *return_value = NULL;
static char *_keywords[] = {"pattern", "pos", "endpos", NULL};
PyObject *pattern;
Py_ssize_t pos = 0;
Py_ssize_t endpos = PY_SSIZE_T_MAX;
if (!PyArg_ParseTupleAndKeywords(args, kwargs,
"O|nn:match", _keywords,
&pattern, &pos, &endpos))
goto exit;
return_value = pattern_match_impl((PatternObject *)self, pattern, pos, endpos);
exit:
return return_value;
}
static PyObject *
pattern_match_impl(PatternObject *self, PyObject *pattern, Py_ssize_t pos, Py_ssize_t endpos)
/*[clinic checksum: 63e59c5f3019efe6c1f3acdec42b2d3595e14a09]*/
{ {
SRE_STATE state; SRE_STATE state;
Py_ssize_t status; Py_ssize_t status;
PyObject *string;
PyObject* string; string = state_init(&state, (PatternObject *)self, pattern, pos, endpos);
Py_ssize_t start = 0;
Py_ssize_t end = PY_SSIZE_T_MAX;
static char* kwlist[] = { "pattern", "pos", "endpos", NULL };
if (!PyArg_ParseTupleAndKeywords(args, kw, "O|nn:match", kwlist,
&string, &start, &end))
return NULL;
string = state_init(&state, self, string, start, end);
if (!string) if (!string)
return NULL; return NULL;
@ -556,7 +593,7 @@ pattern_match(PatternObject* self, PyObject* args, PyObject* kw)
state_fini(&state); state_fini(&state);
return pattern_new_match(self, &state, status); return (PyObject *)pattern_new_match(self, &state, status);
} }
static PyObject* static PyObject*
@ -1254,10 +1291,6 @@ done:
return result; return result;
} }
PyDoc_STRVAR(pattern_match_doc,
"match(string[, pos[, endpos]]) -> match object or None.\n\
Matches zero or more characters at the beginning of the string");
PyDoc_STRVAR(pattern_fullmatch_doc, PyDoc_STRVAR(pattern_fullmatch_doc,
"fullmatch(string[, pos[, endpos]]) -> match object or None.\n\ "fullmatch(string[, pos[, endpos]]) -> match object or None.\n\
Matches against all of the string"); Matches against all of the string");
@ -1295,8 +1328,7 @@ PyDoc_STRVAR(pattern_subn_doc,
PyDoc_STRVAR(pattern_doc, "Compiled regular expression objects"); PyDoc_STRVAR(pattern_doc, "Compiled regular expression objects");
static PyMethodDef pattern_methods[] = { static PyMethodDef pattern_methods[] = {
{"match", (PyCFunction) pattern_match, METH_VARARGS|METH_KEYWORDS, PATTERN_MATCH_METHODDEF
pattern_match_doc},
{"fullmatch", (PyCFunction) pattern_fullmatch, METH_VARARGS|METH_KEYWORDS, {"fullmatch", (PyCFunction) pattern_fullmatch, METH_VARARGS|METH_KEYWORDS,
pattern_fullmatch_doc}, pattern_fullmatch_doc},
{"search", (PyCFunction) pattern_search, METH_VARARGS|METH_KEYWORDS, {"search", (PyCFunction) pattern_search, METH_VARARGS|METH_KEYWORDS,

View File

@ -2869,6 +2869,15 @@ PyDoc_STRVAR(docstring_with_signature_and_extra_newlines,
"This docstring has a valid signature and some extra newlines." "This docstring has a valid signature and some extra newlines."
); );
PyDoc_STRVAR(docstring_with_signature_with_defaults,
"docstring_with_signature_with_defaults(s='avocado', d=3.14, i=35, c=sys.maxsize, n=None, t=True, f=False)\n"
"\n"
"\n"
"\n"
"This docstring has a valid signature with parameters,\n"
"and the parameters take defaults of varying types."
);
#ifdef WITH_THREAD #ifdef WITH_THREAD
typedef struct { typedef struct {
PyThread_type_lock start_event; PyThread_type_lock start_event;
@ -3087,6 +3096,9 @@ static PyMethodDef TestMethods[] = {
{"docstring_with_signature_and_extra_newlines", {"docstring_with_signature_and_extra_newlines",
(PyCFunction)test_with_docstring, METH_NOARGS, (PyCFunction)test_with_docstring, METH_NOARGS,
docstring_with_signature_and_extra_newlines}, docstring_with_signature_and_extra_newlines},
{"docstring_with_signature_with_defaults",
(PyCFunction)test_with_docstring, METH_NOARGS,
docstring_with_signature_with_defaults},
#ifdef WITH_THREAD #ifdef WITH_THREAD
{"call_in_temporary_c_thread", call_in_temporary_c_thread, METH_O, {"call_in_temporary_c_thread", call_in_temporary_c_thread, METH_O,
PyDoc_STR("set_error_class(error_class) -> None")}, PyDoc_STR("set_error_class(error_class) -> None")},

View File

@ -1362,15 +1362,15 @@ class CConverter(metaclass=CConverterAutoRegister):
# Only used by format units ending with '#'. # Only used by format units ending with '#'.
length = False length = False
def __init__(self, name, function, default=unspecified, *, doc_default=None, required=False, annotation=unspecified, **kwargs): def __init__(self, name, function, default=unspecified, *, doc_default=None, c_default=None, py_default=None, required=False, annotation=unspecified, **kwargs):
self.function = function self.function = function
self.name = name self.name = name
if default is not unspecified: if default is not unspecified:
self.default = default self.default = default
self.py_default = py_repr(default) self.py_default = py_default if py_default is not None else py_repr(default)
self.doc_default = doc_default if doc_default is not None else self.py_default self.doc_default = doc_default if doc_default is not None else self.py_default
self.c_default = c_repr(default) self.c_default = c_default if c_default is not None else c_repr(default)
elif doc_default is not None: elif doc_default is not None:
fail(function.fullname + " argument " + name + " specified a 'doc_default' without having a 'default'") fail(function.fullname + " argument " + name + " specified a 'doc_default' without having a 'default'")
if annotation != unspecified: if annotation != unspecified:
@ -2315,18 +2315,36 @@ class DSLParser:
function_args = module.body[0].args function_args = module.body[0].args
parameter = function_args.args[0] parameter = function_args.args[0]
py_default = None
parameter_name = parameter.arg
name, legacy, kwargs = self.parse_converter(parameter.annotation)
if function_args.defaults: if function_args.defaults:
expr = function_args.defaults[0] expr = function_args.defaults[0]
# mild hack: explicitly support NULL as a default value # mild hack: explicitly support NULL as a default value
if isinstance(expr, ast.Name) and expr.id == 'NULL': if isinstance(expr, ast.Name) and expr.id == 'NULL':
value = NULL value = NULL
elif isinstance(expr, ast.Attribute):
a = []
n = expr
while isinstance(n, ast.Attribute):
a.append(n.attr)
n = n.value
if not isinstance(n, ast.Name):
fail("Malformed default value (looked like a Python constant)")
a.append(n.id)
py_default = ".".join(reversed(a))
value = None
c_default = kwargs.get("c_default")
if not (isinstance(c_default, str) and c_default):
fail("When you specify a named constant (" + repr(py_default) + ") as your default value,\nyou MUST specify a valid c_default.")
kwargs["py_default"] = py_default
else: else:
value = ast.literal_eval(expr) value = ast.literal_eval(expr)
else: else:
value = unspecified value = unspecified
parameter_name = parameter.arg
name, legacy, kwargs = self.parse_converter(parameter.annotation)
dict = legacy_converters if legacy else converters dict = legacy_converters if legacy else converters
legacy_str = "legacy " if legacy else "" legacy_str = "legacy " if legacy else ""
if name not in dict: if name not in dict: