diff --git a/Lib/inspect.py b/Lib/inspect.py --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -1975,13 +1975,39 @@ parameters = [] empty = Parameter.empty + def parse_name(node): + id = node.id + ctx = node.ctx + if (not isinstance(ctx, ast.Load)) or (id not in sys.modules): + return None + return sys.modules[id] + + def parse_attribute(node): + if not isinstance(node.ctx, ast.Load): + return None + value = node.value + attr = node.attr + if isinstance(value, ast.Attribute): + o = parse_attribute(value) + elif isinstance(value, ast.Name): + o = parse_name(value) + else: + return None + if not (o and hasattr(o, attr)): + return None + return getattr(o, attr) + def p(name_node, default_node, default=empty): name = name_node.arg if isinstance(default_node, ast.Num): - default = default.n + default = default_node.n elif isinstance(default_node, ast.NameConstant): default = default_node.value + elif isinstance(default_node, ast.Attribute): + print(ast.dump(default_node)) + default = parse_attribute(default_node) + print("DEF", repr(default)) parameters.append(Parameter(name, kind, default=default, annotation=empty)) # non-keyword-only parameters diff --git a/Misc/NEWS b/Misc/NEWS --- a/Misc/NEWS +++ b/Misc/NEWS @@ -13,9 +13,15 @@ Library ------- +- Issue #20144: inspect.Signature now supports parsing simple symbolic + constants as parameter default values in __text_signature__. + 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 --- a/Modules/_sre.c +++ b/Modules/_sre.c @@ -526,21 +526,58 @@ 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; - 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); + PyObject *string; + + string = state_init(&state, (PatternObject *)self, pattern, pos, endpos); if (!string) return NULL; @@ -556,7 +593,7 @@ state_fini(&state); - return pattern_new_match(self, &state, status); + return (PyObject *)pattern_new_match(self, &state, status); } static PyObject* @@ -1254,10 +1291,6 @@ 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_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/Tools/clinic/clinic.py b/Tools/clinic/clinic.py --- a/Tools/clinic/clinic.py +++ b/Tools/clinic/clinic.py @@ -1362,15 +1362,15 @@ # 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,37 @@ 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(list(reversed(a))) + value = None + c_default = kwargs.get("c_default") + if not (isinstance(c_default, str) and c_default): + print("kwargs", kwargs, "c_default", repr(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: