Index: Lib/inspect.py =================================================================== --- Lib/inspect.py (revision 79099) +++ Lib/inspect.py (working copy) @@ -17,7 +17,7 @@ getmodule() - determine the module that an object came from getclasstree() - arrange classes so as to represent their hierarchy - getargspec(), getargvalues() - get info about function arguments + getargspec(), getargvalues(), getcallargs() - get info about function arguments formatargspec(), formatargvalues() - format an argument spec getouterframes(), getinnerframes() - get info about frames currentframe() - get the current stack frame @@ -884,6 +884,98 @@ specs.append(formatvarkw(varkw) + formatvalue(locals[varkw])) return '(' + string.join(specs, ', ') + ')' +def getcallargs(func, *positional, **named): + """Get the mapping of arguments to values when calling func(*positional, **named). + + A dict is returned, with keys the function argument names (including the + names of the * and ** arguments, if any), and values the respective bound + values from 'positional' and 'named'.""" + args, varargs, varkw, defaults = getargspec(func) + f_name = func.__name__ + arg2value = {} + + # the following closures are basically because of tuple parameter unpacking + assigned_tuple_params = [] + def assign(arg, value): + if isinstance(arg, str): + arg2value[arg] = value + else: + assigned_tuple_params.append(arg) + value = iter(value) + for i,subarg in enumerate(arg): + try: subvalue = next(value) + except StopIteration: + raise ValueError('need more than %d %s to unpack' % + (i, 'values' if i>1 else 'value')) + assign(subarg,subvalue) + try: next(value) + except StopIteration: pass + else: raise ValueError('too many values to unpack') + def is_assigned(arg): + if isinstance(arg,str): + return arg in arg2value + return arg in assigned_tuple_params + #------- handle methods (bound and unbound) -------------------------------- + if ismethod(func): + # implicit 'self' (or 'cls' for classmethods) argument + if func.im_self is not None: + positional = (func.im_self,) + positional + elif not positional or not isinstance(positional[0], func.im_class): + got = ('%s instance' % type(positional[0]).__name__ if positional + else 'nothing') + raise TypeError('unbound method %s() must be called with %s ' + 'instance as first argument (got %s instead)' % + (f_name, func.im_class.__name__, got)) + num_pos = len(positional) + has_named = bool(named) + num_args = len(args) + num_defaults = len(defaults or ()) + #------- assign the positional arguments ----------------------------------- + for arg,value in zip(args,positional): + assign(arg, value) + if varargs: + if num_pos > num_args: + assign(varargs, positional[-(num_pos-num_args):]) + else: + assign(varargs, ()) + elif 0 < num_args < num_pos: + raise TypeError('%s() takes %s %d %s%s (%d given)' % ( + f_name, 'at most' if defaults else 'exactly', num_args, + 'non-keyword ' if has_named else '', + 'arguments' if num_args>1 else 'argument', num_pos)) + elif num_args==0 and (num_pos or has_named): + raise TypeError('%s() takes no arguments (%d given)' % + (f_name, num_pos+len(named))) + #------- assign the named arguments ---------------------------------------- + for arg in args: + if isinstance(arg,str) and arg in named: + if is_assigned(arg): + raise TypeError("%s() got multiple values for keyword " + "argument '%s'" % (f_name,arg)) + else: + assign(arg, named.pop(arg)) + if defaults: # fill in any missing values with the defaults + for arg,value in zip(args[-num_defaults:],defaults): + if not is_assigned(arg): + assign(arg, value) + if varkw: + assign(varkw, named) + elif named: + unexpected = next(iter(named)) + if isinstance(unexpected, unicode): + unexpected = unexpected.encode('ascii','replace') + raise TypeError("%s() got an unexpected keyword argument '%s'" % + (f_name, unexpected)) + #------- ensure that all required args have a value ------------------------ + unassigned = sum(1 for arg in args if not is_assigned(arg)) + if unassigned: + num_required = num_args - num_defaults + raise TypeError('%s() takes %s %d %s%s (%d given)' % ( + f_name, 'at least' if defaults else 'exactly', num_required, + 'non-keyword ' if has_named else '', + 'arguments' if num_required>1 else 'argument', num_required-unassigned)) + return arg2value + # -------------------------------------------------- stack frame extraction Traceback = namedtuple('Traceback', 'filename lineno function code_context index') Index: Lib/test/test_inspect.py =================================================================== --- Lib/test/test_inspect.py (revision 79099) +++ Lib/test/test_inspect.py (working copy) @@ -1,3 +1,4 @@ +import re import sys import types import unittest @@ -3,4 +4,6 @@ import inspect import datetime +from UserList import UserList +from UserDict import UserDict from test.test_support import run_unittest @@ -551,10 +554,199 @@ self.assertIn(('m1', 'method', D), attrs, 'missing plain method') self.assertIn(('datablob', 'data', A), attrs, 'missing data') +class TestGetcallargsFunctions(unittest.TestCase): + # tuple parameters are named '.1', '.2', etc. + is_tuplename = re.compile(r'^\.\d+$').match + + def assertEqualCallArgs(self, func, call_params_string, locals=None): + locals = dict(locals or {}, func=func) + self.assertEqual( + eval('func(%s)' % call_params_string, None, locals), + eval('inspect.getcallargs(func, %s)' % call_params_string, None, locals)) + + def assertEqualException(self, func, call_param_string, locals=None): + locals = dict(locals or {}, func=func) + try: + eval('func(%s)' % call_param_string, None, locals) + except Exception, ex1: + pass + else: + self.fail('Exception not raised') + try: + eval('inspect.getcallargs(func, %s)' % call_param_string, None, locals) + except Exception, ex2: + pass + else: + self.fail('Exception not raised') + self.assertEqual(str(ex1), str(ex2)) + + def makeCallable(self, signature): + # create a function that returns its locals(), excluding the autogenerated + # '.1', '.2', etc. tuple param names (if any) + return eval( + 'lambda %s: dict(i for i in locals().items() if not is_tuplename(i[0]))' + % signature, {'is_tuplename':self.is_tuplename}) + + def test_plain(self): + f = self.makeCallable('a, b=1') + self.assertEqualCallArgs(f, '2') + self.assertEqualCallArgs(f, '2, 3') + self.assertEqualCallArgs(f, 'a=2') + self.assertEqualCallArgs(f, 'b=3, a=2') + self.assertEqualCallArgs(f, '2, b=3') + # expand *iterable / **mapping + self.assertEqualCallArgs(f, '*(2,)') + self.assertEqualCallArgs(f, '*[2]') + self.assertEqualCallArgs(f, '*(2, 3)') + self.assertEqualCallArgs(f, '*[2, 3]') + self.assertEqualCallArgs(f, '**{"a":2}') + self.assertEqualCallArgs(f, 'b=3, **{"a":2}') + self.assertEqualCallArgs(f, '2, **{"b":3}') + self.assertEqualCallArgs(f, '**{"b":3, "a":2}') + # expand UserList / UserDict + self.assertEqualCallArgs(f, '*UserList([2])') + self.assertEqualCallArgs(f, '*UserList([2, 3])') + self.assertEqualCallArgs(f, '**UserDict(a=2)') + self.assertEqualCallArgs(f, '2, **UserDict(b=3)') + self.assertEqualCallArgs(f, 'b=2, **UserDict(a=3)') + # unicode keyword args + self.assertEqualCallArgs(f, '**{u"a":2}') + self.assertEqualCallArgs(f, 'b=3, **{u"a":2}') + self.assertEqualCallArgs(f, '2, **{u"b":3}') + self.assertEqualCallArgs(f, '**{u"b":3, u"a":2}') + + def test_varargs(self): + f = self.makeCallable('a, b=1, *c') + self.assertEqualCallArgs(f, '2') + self.assertEqualCallArgs(f, '2, 3') + self.assertEqualCallArgs(f, '2, 3, 4') + self.assertEqualCallArgs(f, '*(2,3,4)') + self.assertEqualCallArgs(f, '2, *[3,4]') + self.assertEqualCallArgs(f, '2, 3, *UserList([4])') + + def test_varkw(self): + f = self.makeCallable('a, b=1, **c') + self.assertEqualCallArgs(f, 'a=2') + self.assertEqualCallArgs(f, '2, b=3, c=4') + self.assertEqualCallArgs(f, 'b=3, a=2, c=4') + self.assertEqualCallArgs(f, 'c=4, **{"a":2, "b":3}') + self.assertEqualCallArgs(f, '2, c=4, **{"b":3}') + self.assertEqualCallArgs(f, 'b=2, **{"a":3, "c":4}') + self.assertEqualCallArgs(f, '**UserDict(a=2, b=3, c=4)') + self.assertEqualCallArgs(f, '2, c=4, **UserDict(b=3)') + self.assertEqualCallArgs(f, 'b=2, **UserDict(a=3, c=4)') + # unicode keyword args + self.assertEqualCallArgs(f, 'c=4, **{u"a":2, u"b":3}') + self.assertEqualCallArgs(f, '2, c=4, **{u"b":3}') + self.assertEqualCallArgs(f, 'b=2, **{u"a":3, u"c":4}') + + def test_tupleargs(self): + f = self.makeCallable('(b,c), (d,(e,f))=(0,[1,2])') + self.assertEqualCallArgs(f, '(2,3)') + self.assertEqualCallArgs(f, '[2,3]') + self.assertEqualCallArgs(f, 'UserList([2,3])') + self.assertEqualCallArgs(f, '(2,3), (4,(5,6))') + self.assertEqualCallArgs(f, '(2,3), (4,[5,6])') + self.assertEqualCallArgs(f, '(2,3), [4,UserList([5,6])]') + + def test_whole_enchilada(self): + f = self.makeCallable('a, b=2, (c,(d,e))=(3,[4,5]), *f, **g') + self.assertEqualCallArgs(f, '2, 3, (4,[5,6]), 7') + self.assertEqualCallArgs(f, '2, 3, *[(4,[5,6]), 7], x=8') + self.assertEqualCallArgs(f, '2, 3, x=8, *[(4,[5,6]), 7]') + self.assertEqualCallArgs(f, '2, x=8, *[3, (4,[5,6]), 7], y=9') + self.assertEqualCallArgs(f, 'x=8, *[2, 3, (4,[5,6])], y=9') + self.assertEqualCallArgs(f, 'x=8, *UserList([2, 3, (4,[5,6])]), **{"y":9, "z":10}') + self.assertEqualCallArgs(f, '2, x=8, *UserList([3, (4,[5,6])]), **UserDict(y=9, z=10)') + + def test_errors(self): + f0 = self.makeCallable('') + f1 = self.makeCallable('a, b') + f2 = self.makeCallable('a, b=1') + # f0 takes no arguments + self.assertEqualException(f0, '1') + self.assertEqualException(f0, 'x=1') + self.assertEqualException(f0, '1,x=1') + # f1 takes exactly 2 arguments + self.assertEqualException(f1, '') + self.assertEqualException(f1, '1') + # f1 takes exactly 2 non-keyword arguments + self.assertEqualException(f1, 'a=2') + # XXX: Python (wrongly) reports 0 given arguments instead of 1 + ##self.assertEqualException(f1, 'b=3') + # f2 takes at least 1 argument + self.assertEqualException(f2, '') + # f2 takes at least 1 non-keyword argument + self.assertEqualException(f2, 'b=3') + for f in f1,f2: + # f1/f2 takes exactly/at most 2 arguments + self.assertEqualException(f, '2,3,4') + # f takes exactly/at most 2 non-keyword arguments + self.assertEqualException(f, '1,2,3,a=1') + self.assertEqualException(f, '2,3,4,c=5') + self.assertEqualException(f, '2,3,4,a=1,c=5') + # f got an unexpected keyword argument + self.assertEqualException(f, 'c=2') + self.assertEqualException(f, '2,c=3') + self.assertEqualException(f, '2,3,c=4') + self.assertEqualException(f, '2,c=4,b=3') + self.assertEqualException(f, '**{u"\u03c0\u03b9": 4}') + # f got multiple values for keyword argument + self.assertEqualException(f, '1, a=2') + self.assertEqualException(f, '1, **{"a":2}') + self.assertEqualException(f, '1, 2, b=3') + # XXX: Python inconsistency + # - for functions and bound methods: unexpected keyword 'c' + # - for unbound methods: multiple values for keyword 'a' + #self.assertEqualException(f, '1, c=3, a=2') + f = self.makeCallable('(a,b)=(0,1)') + self.assertEqualException(f, '1') + self.assertEqualException(f, '[1]') + self.assertEqualException(f, '(1,2,3)') + +class TestGetcallargsMethods(TestGetcallargsFunctions): + def setUp(self): + class Foo(object): pass + self.cls = Foo + self.inst = Foo() + + def makeCallable(self, signature): + assert 'self' not in signature + self.cls.method = super(TestGetcallargsMethods,self).makeCallable('self, '+signature) + return self.inst.method + +class TestGetcallargsUnboundMethods(TestGetcallargsMethods): + def makeCallable(self, signature): + super(TestGetcallargsUnboundMethods, self).makeCallable(signature) + return self.cls.method + + def assertEqualCallArgs(self, func, call_params_string, locals=None): + return super(TestGetcallargsUnboundMethods,self).assertEqualCallArgs( + *self._getAssertEqualParams(func, call_params_string, locals)) + + def assertEqualException(self, func, call_params_string, locals=None): + return super(TestGetcallargsUnboundMethods,self).assertEqualException( + *self._getAssertEqualParams(func, call_params_string, locals)) + + def _getAssertEqualParams(self, func, call_params_string, locals=None): + assert 'inst' not in call_params_string + locals = dict(locals or {}, inst=self.inst) + return (func, 'inst,'+call_params_string, locals) + + def test_self_param(self): + # test that it fails if 'self' is not an instance of the class + superAssertEqualException = super(TestGetcallargsUnboundMethods,self).assertEqualException + f = self.makeCallable('a, b') + superAssertEqualException(f, '') + superAssertEqualException(f, '1') + superAssertEqualException(f, 'self=s', {'s':self.inst}) + superAssertEqualException(f, '1, self=s', {'s':self.inst}) + def test_main(): - run_unittest(TestDecorators, TestRetrievingSourceCode, TestOneliners, - TestBuggyCases, - TestInterpreterStack, TestClassesAndFunctions, TestPredicates) + run_unittest( + TestDecorators, TestRetrievingSourceCode, TestOneliners, TestBuggyCases, + TestInterpreterStack, TestClassesAndFunctions, TestPredicates, + TestGetcallargsFunctions, TestGetcallargsMethods, TestGetcallargsUnboundMethods) if __name__ == "__main__": test_main() Index: Doc/library/inspect.rst =================================================================== --- Doc/library/inspect.rst (revision 79099) +++ Doc/library/inspect.rst (working copy) @@ -504,6 +504,29 @@ metatype is in use, cls will be the first element of the tuple. +.. function:: getcallargs(func[, *args][, **kwds]) + + Get the mapping of arguments to values when calling ``func(*args, **kwds)``. + + A dict is returned, with keys the function argument names (including the + names of the ``*`` and ``**`` arguments, if any), and values the respective + bound values from ``args`` and ``kwds``. For example:: + + >>> from inspect import getcallargs + >>> def f(a, b=1, *pos, **named): + ... pass + >>> getcallargs(f, 1, 2, 3) + {'a': 1, 'named': {}, 'b': 2, 'pos': (3,)} + >>> getcallargs(f, a=2, x=4) + {'a': 2, 'named': {'x': 4}, 'b': 1, 'pos': ()} + >>> getcallargs(f) + Traceback (most recent call last): + ... + TypeError: f() takes at least 1 argument (0 given) + + .. versionadded:: 2.7 + + .. _inspect-stack: The interpreter stack