diff --git a/Lib/inspect.py b/Lib/inspect.py --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -1974,18 +1974,60 @@ 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 isinstance(value, ast.Name): + name = o + if name not in sys.modules: + return invalid + o = sys.modules[name] + + attr = node.attr + if not (o and hasattr(o, attr)): + return invalid + return getattr(o, attr) + + 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 --- a/Lib/test/test_inspect.py +++ b/Lib/test/test_inspect.py @@ -1593,9 +1593,14 @@ @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) self.assertTrue(isinstance(signature, inspect.Signature)) + p = signature.parameters + self.assertEqual(p['dir_fd'].default, None) + self.assertEqual(p['follow_symlinks'].default, True) def test_signature_on_non_function(self): with self.assertRaisesRegex(TypeError, 'is not a callable object'): 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/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: