add inspect.getcallargs, which binds function arguments like a normal call #3135

Patch by George Sakkis
This commit is contained in:
Benjamin Peterson 2010-03-30 17:58:13 +00:00
parent ec71794cb8
commit 7e213255ce
4 changed files with 306 additions and 4 deletions

View File

@ -504,6 +504,32 @@ Classes and functions
metatype is in use, cls will be the first element of the tuple. metatype is in use, cls will be the first element of the tuple.
.. function:: getcallargs(func[, *args][, **kwds])
Bind the *args* and *kwds* to the argument names of the Python function or
method *func*, as if it was called with them. For bound methods, bind also the
first argument (typically named ``self``) to the associated instance. A dict
is returned, mapping the argument names (including the names of the ``*`` and
``**`` arguments, if any) to their values from *args* and *kwds*. In case of
invoking *func* incorrectly, i.e. whenever ``func(*args, **kwds)`` would raise
an exception because of incompatible signature, an exception of the same type
and the same or similar message is raised. 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: .. _inspect-stack:
The interpreter stack The interpreter stack

View File

@ -17,7 +17,7 @@ Here are some of the useful functions provided by this module:
getmodule() - determine the module that an object came from getmodule() - determine the module that an object came from
getclasstree() - arrange classes so as to represent their hierarchy 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 formatargspec(), formatargvalues() - format an argument spec
getouterframes(), getinnerframes() - get info about frames getouterframes(), getinnerframes() - get info about frames
currentframe() - get the current stack frame currentframe() - get the current stack frame
@ -884,6 +884,89 @@ def formatargvalues(args, varargs, varkw, locals,
specs.append(formatvarkw(varkw) + formatvalue(locals[varkw])) specs.append(formatvarkw(varkw) + formatvalue(locals[varkw]))
return '(' + string.join(specs, ', ') + ')' return '(' + string.join(specs, ', ') + ')'
def getcallargs(func, *positional, **named):
"""Get the mapping of arguments to values.
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
if ismethod(func) and func.im_self is not None:
# implicit 'self' (or 'cls' for classmethods) argument
positional = (func.im_self,) + positional
num_pos = len(positional)
num_total = num_pos + len(named)
num_args = len(args)
num_defaults = len(defaults) if defaults else 0
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 (%d given)' % (
f_name, 'at most' if defaults else 'exactly', num_args,
'arguments' if num_args > 1 else 'argument', num_total))
elif num_args == 0 and num_total:
raise TypeError('%s() takes no arguments (%d given)' %
(f_name, num_total))
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(sys.getdefaultencoding(), 'replace')
raise TypeError("%s() got an unexpected keyword argument '%s'" %
(f_name, unexpected))
unassigned = num_args - len([arg for arg in args if is_assigned(arg)])
if unassigned:
num_required = num_args - num_defaults
raise TypeError('%s() takes %s %d %s (%d given)' % (
f_name, 'at least' if defaults else 'exactly', num_required,
'arguments' if num_required > 1 else 'argument', num_total))
return arg2value
# -------------------------------------------------- stack frame extraction # -------------------------------------------------- stack frame extraction
Traceback = namedtuple('Traceback', 'filename lineno function code_context index') Traceback = namedtuple('Traceback', 'filename lineno function code_context index')

View File

@ -1,8 +1,11 @@
import re
import sys import sys
import types import types
import unittest import unittest
import inspect import inspect
import datetime import datetime
from UserList import UserList
from UserDict import UserDict
from test.test_support import run_unittest, check_py3k_warnings from test.test_support import run_unittest, check_py3k_warnings
@ -557,10 +560,197 @@ class TestClassesAndFunctions(unittest.TestCase):
self.assertIn(('m1', 'method', D), attrs, 'missing plain method') self.assertIn(('m1', 'method', D), attrs, 'missing plain method')
self.assertIn(('datablob', 'data', A), attrs, 'missing data') 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, locs=None):
locs = dict(locs or {}, func=func)
r1 = eval('func(%s)' % call_params_string, None, locs)
r2 = eval('inspect.getcallargs(func, %s)' % call_params_string, None,
locs)
self.assertEqual(r1, r2)
def assertEqualException(self, func, call_param_string, locs=None):
locs = dict(locs or {}, func=func)
try:
eval('func(%s)' % call_param_string, None, locs)
except Exception, ex1:
pass
else:
self.fail('Exception not raised')
try:
eval('inspect.getcallargs(func, %s)' % call_param_string, None,
locs)
except Exception, ex2:
pass
else:
self.fail('Exception not raised')
self.assertIs(type(ex1), type(ex2))
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)."""
code = ("lambda %s: dict(i for i in locals().items() "
"if not is_tuplename(i[0]))")
return eval(code % 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_multiple_features(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')
self.assertEqualException(f1, 'a=2')
self.assertEqualException(f1, 'b=3')
# f2 takes at least 1 argument
self.assertEqualException(f2, '')
self.assertEqualException(f2, 'b=3')
for f in f1, f2:
# f1/f2 takes exactly/at most 2 arguments
self.assertEqualException(f, '2, 3, 4')
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
mk = super(TestGetcallargsMethods, self).makeCallable
self.cls.method = mk('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, locs=None):
return super(TestGetcallargsUnboundMethods, self).assertEqualCallArgs(
*self._getAssertEqualParams(func, call_params_string, locs))
def assertEqualException(self, func, call_params_string, locs=None):
return super(TestGetcallargsUnboundMethods, self).assertEqualException(
*self._getAssertEqualParams(func, call_params_string, locs))
def _getAssertEqualParams(self, func, call_params_string, locs=None):
assert 'inst' not in call_params_string
locs = dict(locs or {}, inst=self.inst)
return (func, 'inst,' + call_params_string, locs)
def test_main(): def test_main():
run_unittest(TestDecorators, TestRetrievingSourceCode, TestOneliners, run_unittest(
TestBuggyCases, TestDecorators, TestRetrievingSourceCode, TestOneliners, TestBuggyCases,
TestInterpreterStack, TestClassesAndFunctions, TestPredicates) TestInterpreterStack, TestClassesAndFunctions, TestPredicates,
TestGetcallargsFunctions, TestGetcallargsMethods,
TestGetcallargsUnboundMethods)
if __name__ == "__main__": if __name__ == "__main__":
test_main() test_main()

View File

@ -32,6 +32,9 @@ Core and Builtins
Library Library
------- -------
- Issue #3135: Add inspect.getcallargs, which binds arguments to a function like
a normal call.
- Backwards incompatible change: Unicode codepoints line tabulation (0x0B) and - Backwards incompatible change: Unicode codepoints line tabulation (0x0B) and
form feed (0x0C) are now considered linebreaks, as specified in Unicode form feed (0x0C) are now considered linebreaks, as specified in Unicode
Standard Annex #14. See issue #7643. Standard Annex #14. See issue #7643.