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:
parent
0bce6e7462
commit
16c5191ab3
|
@ -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
|
||||||
|
|
|
@ -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'):
|
||||||
|
|
|
@ -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.
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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")},
|
||||||
|
|
|
@ -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:
|
||||||
|
|
Loading…
Reference in New Issue