summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Lib/inspect.py56
-rw-r--r--Lib/test/test_inspect.py13
-rw-r--r--Misc/NEWS6
-rw-r--r--Modules/_sre.c68
-rw-r--r--Modules/_testcapimodule.c12
-rwxr-xr-xTools/clinic/clinic.py28
6 files changed, 152 insertions, 31 deletions
diff --git a/Lib/inspect.py b/Lib/inspect.py
index 7c954eb..c9d10dc 100644
--- a/Lib/inspect.py
+++ b/Lib/inspect.py
@@ -1974,18 +1974,60 @@ class Signature:
parameters = []
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):
- name = name_node.arg
-
- if isinstance(default_node, ast.Num):
- default = default.n
- elif isinstance(default_node, ast.NameConstant):
- default = default_node.value
+ name = parse_node(name_node)
+ if name is invalid:
+ return None
+ if default_node:
+ o = parse_node(default_node)
+ if o is invalid:
+ return None
+ default = o if o is not invalid else default
parameters.append(Parameter(name, kind, default=default, annotation=empty))
# 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)
# *args
diff --git a/Lib/test/test_inspect.py b/Lib/test/test_inspect.py
index 520bf0e..9dc5475 100644
--- a/Lib/test/test_inspect.py
+++ b/Lib/test/test_inspect.py
@@ -15,6 +15,7 @@ try:
from concurrent.futures import ThreadPoolExecutor
except ImportError:
ThreadPoolExecutor = None
+import _testcapi
from test.support import run_unittest, TESTFN, DirsOnSysPath
from test.support import MISSING_C_DOCSTRINGS
@@ -1593,9 +1594,19 @@ class TestSignatureObject(unittest.TestCase):
@unittest.skipIf(MISSING_C_DOCSTRINGS,
"Signature information for builtins requires docstrings")
def test_signature_on_builtins(self):
+ # min doesn't have a signature (yet)
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))
+ 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):
with self.assertRaisesRegex(TypeError, 'is not a callable object'):
diff --git a/Misc/NEWS b/Misc/NEWS
index 82bac3a..9e7dd2e 100644
--- a/Misc/NEWS
+++ b/Misc/NEWS
@@ -13,11 +13,17 @@ Core and Builtins
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.
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
now more accurate.
diff --git a/Modules/_sre.c b/Modules/_sre.c
index 55a86c2..6be2e59 100644
--- a/Modules/_sre.c
+++ b/Modules/_sre.c
@@ -526,21 +526,58 @@ sre_search(SRE_STATE* state, SRE_CODE* pattern)
return sre_ucs4_search(state, pattern);
}
-static PyObject*
-pattern_match(PatternObject* self, PyObject* args, PyObject* kw)
+/*[clinic]
+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;
Py_ssize_t status;
+ PyObject *string;
- PyObject* string;
- 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);
+ string = state_init(&state, (PatternObject *)self, pattern, pos, endpos);
if (!string)
return NULL;
@@ -556,7 +593,7 @@ pattern_match(PatternObject* self, PyObject* args, PyObject* kw)
state_fini(&state);
- return pattern_new_match(self, &state, status);
+ return (PyObject *)pattern_new_match(self, &state, status);
}
static PyObject*
@@ -1254,10 +1291,6 @@ done:
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,
"fullmatch(string[, pos[, endpos]]) -> match object or None.\n\
Matches against all of the string");
@@ -1295,8 +1328,7 @@ PyDoc_STRVAR(pattern_subn_doc,
PyDoc_STRVAR(pattern_doc, "Compiled regular expression objects");
static PyMethodDef pattern_methods[] = {
- {"match", (PyCFunction) pattern_match, METH_VARARGS|METH_KEYWORDS,
- pattern_match_doc},
+ PATTERN_MATCH_METHODDEF
{"fullmatch", (PyCFunction) pattern_fullmatch, METH_VARARGS|METH_KEYWORDS,
pattern_fullmatch_doc},
{"search", (PyCFunction) pattern_search, METH_VARARGS|METH_KEYWORDS,
diff --git a/Modules/_testcapimodule.c b/Modules/_testcapimodule.c
index a0cffde..b8fe8d5 100644
--- a/Modules/_testcapimodule.c
+++ b/Modules/_testcapimodule.c
@@ -2869,6 +2869,15 @@ PyDoc_STRVAR(docstring_with_signature_and_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
typedef struct {
PyThread_type_lock start_event;
@@ -3087,6 +3096,9 @@ static PyMethodDef TestMethods[] = {
{"docstring_with_signature_and_extra_newlines",
(PyCFunction)test_with_docstring, METH_NOARGS,
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
{"call_in_temporary_c_thread", call_in_temporary_c_thread, METH_O,
PyDoc_STR("set_error_class(error_class) -> None")},
diff --git a/Tools/clinic/clinic.py b/Tools/clinic/clinic.py
index 5351b6d..3b167ea 100755
--- a/Tools/clinic/clinic.py
+++ b/Tools/clinic/clinic.py
@@ -1362,15 +1362,15 @@ class CConverter(metaclass=CConverterAutoRegister):
# Only used by format units ending with '#'.
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.name = name
if default is not unspecified:
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.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:
fail(function.fullname + " argument " + name + " specified a 'doc_default' without having a 'default'")
if annotation != unspecified:
@@ -2315,18 +2315,36 @@ class DSLParser:
function_args = module.body[0].args
parameter = function_args.args[0]
+ py_default = None
+
+ parameter_name = parameter.arg
+ name, legacy, kwargs = self.parse_converter(parameter.annotation)
+
if function_args.defaults:
expr = function_args.defaults[0]
# mild hack: explicitly support NULL as a default value
if isinstance(expr, ast.Name) and expr.id == '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:
value = ast.literal_eval(expr)
else:
value = unspecified
- parameter_name = parameter.arg
- name, legacy, kwargs = self.parse_converter(parameter.annotation)
dict = legacy_converters if legacy else converters
legacy_str = "legacy " if legacy else ""
if name not in dict: