Index: Lib/inspect.py =================================================================== --- Lib/inspect.py (revision 51512) +++ Lib/inspect.py (working copy) @@ -857,3 +857,218 @@ def trace(context=1): """Return a list of records for the stack below the current exception.""" return getinnerframes(sys.exc_info()[2], context) + + +class Parameter(object): + + """Represent a parameter in a function signature.""" + + def __init__(self, name, position, has_default, *args): + self.name = name + self.position = position + if not has_default: + self.has_default = False + else: + self.has_default = True + if len(args) != 1: + raise ValueError("Parameter requires a default value to be " + "specified when has_default has been set " + "to True") + self.default_value = args[0] + + @classmethod + def __tuple2param(self, tuple_): + if not isinstance(tuple_, tuple): + return str(tuple_) + elif len(tuple_) == 1: + return "(" + str(tuple_[0]) +",)" + else: + return ('(' + + ', '.join(self.__tuple2param(x) for x in tuple_) + + ')') + + def __str__(self): + """Return the string representation of the parameter as it would look + in a function's signature.""" + if isinstance(self.name, tuple): + result = self.__tuple2param(self.name) + else: + result = self.name + if self.has_default: + result+= "=" + str(self.default_value) + return result + + def __repr__(self): + """Return the string required to create an equivalent instance of this + parameter.""" + result = "%s(%r, %r, %r" % (self.__class__.__name__, + self.name, self.position, self.has_default) + if self.has_default: + result +=", %r" % self.default_value + result += ")" + return result + + +class Signature(object): + + """Object to represent the signature of a function/method.""" + + def __init__(self, func): + """Initialize from a function or method object.""" + if hasattr(func, 'im_func'): + func = func.im_func + self.name = func.__name__ + + argspec = getargspec(func) + + self.var_args = argspec[1] if (argspec[1] is not None) else '' + self.var_kw_args = argspec[2] if (argspec[2] is not None) else '' + + arg_count = len(argspec[0]) + defaults_start = (arg_count - len(argspec[3]) + if argspec[3] else arg_count) + parameters = [] + for index, arg_name in enumerate(argspec[0]): + if isinstance(arg_name, list): + arg_name = self.__list2tuple(arg_name) + + if index >= defaults_start: + parameters.append(Parameter(arg_name, index, True, + argspec[3][index - defaults_start])) + else: + parameters.append(Parameter(arg_name, index, False)) + + self.parameters = tuple(parameters) + + @classmethod + def __list2tuple(cls, list_): + if not isinstance(list_, list): + return list_ + else: + return tuple(cls.__list2tuple(x) for x in list_) + + def __str__(self): + """String representation of a signature as one might write it in source + code.""" + result = "%s(" % self.name + result += ", ".join(str(param) for param in self.parameters) + if self.var_args: + if self.parameters: + result +=", " + result += "*%s" % self.var_args + if self.var_kw_args: + if self.parameters or self.var_args: + result += ", " + result += "**%s" % self.var_kw_args + result += ")" + return result + + @classmethod + def __tuple_bind_ok(cls, tuple_, arg): + """Verify that 'arg' will unpack properly to be used with 'tuple_'.""" + try: + if len(tuple_) != len(arg): + return False + except TypeError: + raise TypeError("cannot determine the length of the argument") + if (hasattr(arg, '__iter__') and hasattr(arg, 'next') and + callable(arg.__iter__) and callable(arg.next)): + raise IndexError("do not want to mutate iterator") + for tuple_item, arg_item in zip(tuple_, arg): + if isinstance(tuple_item, tuple): + if not cls.__tuple_bind_ok(tuple_item, arg_item): + return False + return True + + @classmethod + def __positional_bind(cls, parameter, arg, bindings): + """Bind 'argument' to 'parameter' in 'bindings' if it is a legitimate + binding. + + A binding can be illegitimate if the parameter is a tuple and the + argument will either unpack improperly or it cannot be determined if it + will without possibly mutating the object (e.g., if it is an + iterator). + + """ + if isinstance(parameter, tuple): + if not cls.__tuple_bind_ok(parameter, arg): + raise TypeError("cannot unpack argument for %s" % parameter) + bindings[parameter] = arg + + def bind(self, *args, **kwargs): + """Return a dictionary mapping function arguments to their parameter + variables.""" + bindings = {} + arg_names_seq = [param.name for param in self.parameters] + arg_names_set = set(arg_names_seq) + arg_names_cnt = len(arg_names_set) + required_args = set(param.name for param in self.parameters + if not param.has_default) + required_args_cnt = len(required_args) + # *args. + if self.var_args: + bindings[self.var_args] = args[arg_names_cnt:] + args = args[:arg_names_cnt] + if len(args) > arg_names_cnt: + raise TypeError("too many positional arguments provided") + for arg_name, value in zip(arg_names_seq, args): + self.__positional_bind(arg_name, value, bindings) + # Keyword arguments. + var_kw_args = {} + for key, value in kwargs.items(): + if key not in arg_names_set: + if not self.var_kw_args: + raise TypeError("unexpected keyword argument: %r" % key) + else: + var_kw_args[key] = value + else: + if key in bindings: + raise TypeError("got multiple values for argument %r" % + key) + else: + bindings[key] = value + del kwargs[key] + if kwargs: + raise TypeError("too many keyword arguments provided") + # **kwargs. + if self.var_kw_args: + bindings[self.var_kw_args] = var_kw_args + # Default values. + for param in self.parameters: + if param.has_default: + if param.name not in bindings: + bindings[param.name] = param.default_value + + # Make sure all required arguments are bound to. + for bound in bindings.iterkeys(): + try: + required_args.remove(bound) + except KeyError: + pass + else: + if required_args: + raise TypeError("too few arguments provided") + return bindings + + +def getsignature(func): + """Return a Signature object for the function or method. + + If possible, return the existing value stored in __signature__. If that + attribute does not exist, then try to store the Signature object at that + attribute if possible (but is not required). + + """ + if hasattr(func, 'im_func'): + func = func.im_func + sig = Signature(func) + if not hasattr(func, '__signature__'): + try: + func.__signature__ = sig + except AttributeError: + pass + else: + sig = func.__signature__ + + return sig Index: Lib/test/test_inspect.py =================================================================== --- Lib/test/test_inspect.py (revision 51512) +++ Lib/test/test_inspect.py (working copy) @@ -148,11 +148,15 @@ self.assertEqual(classes, [('FesteringGob', mod.FesteringGob), ('MalodorousPervert', mod.MalodorousPervert), + ('NewStyleHasMethod', mod.NewStyleHasMethod), ('ParrotDroppings', mod.ParrotDroppings), - ('StupidGit', mod.StupidGit)]) + ('StupidGit', mod.StupidGit), + ]) tree = inspect.getclasstree([cls[1] for cls in classes], 1) self.assertEqual(tree, - [(mod.ParrotDroppings, ()), + [(object, ()), + [(mod.NewStyleHasMethod, (object,))], + (mod.ParrotDroppings, ()), (mod.StupidGit, ()), [(mod.MalodorousPervert, (mod.StupidGit,)), [(mod.FesteringGob, (mod.MalodorousPervert, @@ -163,8 +167,19 @@ def test_getfunctions(self): functions = inspect.getmembers(mod, inspect.isfunction) - self.assertEqual(functions, [('eggs', mod.eggs), - ('spam', mod.spam)]) + self.assertEqual(functions, [ + ('all_args', mod.all_args), + ('default_args', mod.default_args), + ('default_tuple_args', + mod.default_tuple_args), + ('eggs', mod.eggs), + ('no_args', mod.no_args), + ('no_default_args', mod.no_default_args), + ('spam', mod.spam), + ('tuple_args', mod.tuple_args), + ('var_args', mod.var_args), + ('var_kw_args', mod.var_kw_args), + ]) def test_getdoc(self): self.assertEqual(inspect.getdoc(mod), 'A module docstring.') @@ -462,10 +477,262 @@ self.assert_(('m1', 'method', D) in attrs, 'missing plain method') self.assert_(('datablob', 'data', A) in attrs, 'missing data') + +class ParameterObjectTests(unittest.TestCase): + + """Test the Parameter object.""" + + def test_name(self): + # Test that 'name' attribute works. + # Must test both using a string and a tuple of strings. + name = "test" + param = inspect.Parameter(name, 0, False) + self.failUnlessEqual(param.name, name) + name = ('a', ('b',)) + param = inspect.Parameter(name, 0, False) + self.failUnlessEqual(param.name, name) + + def test_position(self): + # Test the 'position' attribute. + pos = 42 + param = inspect.Parameter("_", pos, False) + self.failUnlessEqual(param.position, pos) + + def test_has_default(self): + # Test the 'has_default' attribute. + # Testing that 'default_value' is not set is handled in the testing of + # that attribute. + param = inspect.Parameter('_', 0, True, None) + self.failUnlessEqual(param.has_default, True) + param = inspect.Parameter('_', 0, False) + self.failUnlessEqual(param.has_default, False) + self.failUnlessRaises(TypeError, inspect.Parameter, + ('_', 0, False, 'extra arg')) + self.failUnlessRaises(TypeError, inspect.Parameter, + ('_', 0, True)) + self.failUnlessRaises(TypeError, inspect.Parameter, + ('_', 0, True, 'default', 'extra')) + + def test_default_value(self): + # Test the 'default_value' attribute. + # Make sure that if has_default is set to False that default_value is + # not defined. + param = inspect.Parameter('_', 0, False) + self.failUnless(not hasattr(param, 'default_value')) + default = 42 + param = inspect.Parameter('_', 0, True, default) + self.failUnlessEqual(param.default_value, default) + + def test_str(self): + # Test __str__(). + name = "X" + param = inspect.Parameter(name, 0, False) + self.failUnlessEqual(name, str(param)) + default_value = 42 + param = inspect.Parameter(name, 0, True, default_value) + self.failUnlessEqual("%s=%s" % (name, default_value), str(param)) + + def test_repr(self): + # Test __repr__(). + name = "X" + pos = 0 + param = inspect.Parameter(name, pos, False) + self.failUnlessEqual("Parameter(%r, %r, False)" % (name, pos), + repr(param)) + default_value = 42 + param = inspect.Parameter(name, pos, True, default_value) + self.failUnlessEqual("Parameter(%r, %r, True, %r)" % + (name, pos, default_value), + repr(param)) + + +class SignatureObjectTests(unittest.TestCase): + + def test_no_args(self): + # Test a function with no arguments. + sig = inspect.Signature(mod.no_args) + self.failUnlessEqual('no_args', sig.name) + self.failUnless(not sig.var_args) + self.failUnless(not sig.var_kw_args) + self.failUnlessEqual(0, len(sig.parameters)) + + def test_var_args(self): + # Test the var_args attribute. + sig = inspect.Signature(mod.var_args) + self.failUnlessEqual('args', sig.var_args) + self.failUnlessEqual(0, len(sig.parameters)) + sig = inspect.Signature(mod.no_args) + self.failUnlessEqual('', sig.var_args) + + def test_var_kw_args(self): + # Test the var_kw_args attribute. + sig = inspect.Signature(mod.var_kw_args) + self.failUnlessEqual('var_kw_args', sig.name) + self.failUnlessEqual('kwargs', sig.var_kw_args) + self.failUnlessEqual(0, len(sig.parameters)) + sig = inspect.Signature(mod.no_args) + self.failUnlessEqual('', sig.var_kw_args) + + def test_parameter_positional(self): + sig = inspect.Signature(mod.no_default_args) + self.failUnlessEqual('no_default_args', sig.name) + param = sig.parameters[0] + self.failUnlessEqual('a', param.name) + self.failUnlessEqual(0, param.position) + self.failUnless(not param.has_default) + self.failUnless(not hasattr(param, 'default_value')) + + def test_parameter_default(self): + sig = inspect.Signature(mod.default_args) + self.failUnlessEqual('default_args', sig.name) + param = sig.parameters[0] + self.failUnlessEqual('a', param.name) + self.failUnlessEqual(0, param.position) + self.failUnless(param.has_default) + self.failUnlessEqual(42, param.default_value) + + def test_parameter_tuple(self): + sig = inspect.Signature(mod.tuple_args) + self.failUnlessEqual('tuple_args', sig.name) + param = sig.parameters[0] + self.failUnless(isinstance(param.name, tuple)) + self.failUnlessEqual(('a', ('b',)), param.name) + self.failUnlessEqual(0, param.position) + self.failUnless(not param.has_default) + self.failUnless(not hasattr(param, 'default_value')) + + def test_parameter_tuple_default(self): + sig = inspect.Signature(mod.default_tuple_args) + self.failUnlessEqual('default_tuple_args', sig.name) + param = sig.parameters[0] + self.failUnlessEqual(('a', ('b',)), param.name) + self.failUnlessEqual(0, param.position) + self.failUnless(param.has_default) + self.failUnlessEqual((1, (2,)), param.default_value) + + def test_positioning(self): + sig = inspect.Signature(mod.all_args) + param = sig.parameters[2] + self.failUnlessEqual('d', param.name) + + def test_getsignature(self): + def fresh_func(): + pass + self.failUnless(not hasattr(fresh_func, '__signature__')) + sig = inspect.getsignature(fresh_func) + self.failUnlessEqual(sig, fresh_func.__signature__) + sig2 = inspect.getsignature(fresh_func) + self.failUnlessEqual(sig, sig2) + class FreshClass(object): + def fresh_method(self): + pass + sig = inspect.getsignature(FreshClass.fresh_method) + self.failUnlessEqual(sig, FreshClass.fresh_method.im_func.__signature__) + + def test_str(self): + # Test __str__(). + sig = inspect.Signature(mod.no_args) + self.failUnlessEqual("no_args()", str(sig)) + sig = inspect.Signature(mod.var_args) + self.failUnlessEqual("var_args(*args)", str(sig)) + sig = inspect.Signature(mod.var_kw_args) + self.failUnlessEqual("var_kw_args(**kwargs)", str(sig)) + sig = inspect.Signature(mod.default_args) + self.failUnlessEqual("default_args(a=42)", str(sig)) + sig = inspect.Signature(mod.no_default_args) + self.failUnlessEqual("no_default_args(a)", str(sig)) + sig = inspect.Signature(mod.tuple_args) + self.failUnlessEqual("tuple_args((a, (b,)))", str(sig)) + sig = inspect.Signature(mod.default_tuple_args) + self.failUnlessEqual("default_tuple_args((a, (b,))=(1, (2,)))", + str(sig)) + sig = inspect.Signature(mod.all_args) + self.failUnlessEqual("all_args(a, (b, (c,)), d=0, " + "(e, (f,))=(1, (2,)), *g, **h)", + str(sig)) + +class SignatureBindTests(unittest.TestCase): + + """Test Signature.bind().""" + + def test_no_parameters(self): + sig = inspect.Signature(mod.no_args) + binding = sig.bind() + self.failUnlessEqual({}, binding) + self.failUnlessRaises(TypeError, sig.bind, 42) + self.failUnlessRaises(TypeError, sig.bind, a=0) + + def test_var_parameters(self): + sig = inspect.Signature(mod.var_args) + binding = sig.bind(0, 1, 2) + self.failUnlessEqual({'args':(0, 1, 2)}, binding) + binding = sig.bind() + self.failUnlessEqual({'args':tuple()}, binding) + self.failUnlessRaises(TypeError, sig.bind, a=0) + + def test_var_kw_parameters(self): + sig = inspect.Signature(mod.var_kw_args) + binding = sig.bind(a=0) + self.failUnlessEqual({'kwargs':{'a':0}}, binding) + binding = sig.bind() + self.failUnlessEqual({'kwargs':{}}, binding) + self.failUnlessRaises(TypeError, sig.bind, 42) + + def test_positional_parameters(self): + sig = inspect.Signature(mod.no_default_args) + binding = sig.bind(42) + self.failUnlessEqual({'a':42}, binding) + binding = sig.bind(a=42) + self.failUnlessEqual({'a':42}, binding) + self.failUnlessRaises(TypeError, sig.bind) + self.failUnlessRaises(TypeError, sig.bind, 0, 1) + self.failUnlessRaises(TypeError, sig.bind, b=0) + + def test_keyword_parameters(self): + sig = inspect.Signature(mod.default_args) + binding = sig.bind(0) + self.failUnlessEqual({'a':0}, binding) + binding = sig.bind() + self.failUnlessEqual({'a':42}, binding) + binding = sig.bind(a=0) + self.failUnlessEqual({'a':0}, binding) + self.failUnlessRaises(TypeError, sig.bind, 0, 1) + self.failUnlessRaises(TypeError, sig.bind, a=0, b=1) + self.failUnlessRaises(TypeError, sig.bind, b=1) + + def test_tuple_parameter(self): + sig = inspect.Signature(mod.tuple_args) + binding = sig.bind((1, (2,))) + self.failUnlessEqual({('a', ('b',)):(1, (2,))}, binding) + arg = (1, ((2,),)) + binding = sig.bind(arg) + self.failUnlessEqual({('a', ('b',)):arg}, binding) + self.failUnlessRaises(TypeError, sig.bind, (1,2,3)) + self.failUnlessRaises(TypeError, sig.bind, (1, 2)) + + def test_default_tuple_parameter(self): + sig = inspect.Signature(mod.default_tuple_args) + binding = sig.bind() + self.failUnlessEqual({('a', ('b',)):(1, (2,))}, binding) + arg = (0, (1,)) + binding = sig.bind(arg) + self.failUnlessEqual({('a', ('b',)):arg}, binding) + + def test_all_parameter_types(self): + sig = inspect.Signature(mod.all_args) + binding = sig.bind(0, (1, (2,)), 3, (4, (5,)), 6, i=7) + expected = {'a':0, ('b', ('c',)):(1, (2,)), 'd':3, + ('e', ('f',)):(4, (5,)), 'g':(6,), 'h':{'i':7}} + self.failUnlessEqual(expected, binding) + + def test_main(): run_unittest(TestDecorators, TestRetrievingSourceCode, TestOneliners, TestBuggyCases, - TestInterpreterStack, TestClassesAndFunctions, TestPredicates) + TestInterpreterStack, TestClassesAndFunctions, TestPredicates, + ParameterObjectTests, SignatureObjectTests, + SignatureBindTests, + ) if __name__ == "__main__": test_main() Index: Lib/test/inspect_fodder.py =================================================================== --- Lib/test/inspect_fodder.py (revision 51512) +++ Lib/test/inspect_fodder.py (working copy) @@ -54,3 +54,32 @@ class FesteringGob(MalodorousPervert, ParrotDroppings): pass + + # line 58 +def no_args(): + pass + +def var_args(*args): + pass + +def var_kw_args(**kwargs): + pass + +def default_args(a=42): + pass + +def no_default_args(a): + pass + +def tuple_args((a, (b,))): + pass + +def default_tuple_args((a, (b,))=(1, (2,))): + pass + +def all_args(a, (b, (c,)), d=0, (e, (f,))=(1, (2,)), *g, **h): + pass + +class NewStyleHasMethod(object): + def meth(self): + pass