ActiveState Code

Recipe 551779: Introspecting call arguments


This recipe implements in pure Python the algorithm used by the interpreter for binding the values passed as parameters in a function call to the formal arguments of the function.

This is useful for decorators that want to take into account this binding when wrapping a function (e.g. for type checking).

Python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
from itertools import izip
from inspect import getargspec, ismethod


def getcallargs(func, *args, **kwds):
    '''Get the actual value bounded to each formal parameter when calling
    `func(*args,**kwds)`.

    It works for methods too (bounded, unbounded, staticmethods, classmethods).

    @rtype: dict
    @returns: A mapping of every formal parameter (including *varargs
    and **kwargs if present) of the function to the respective bounded value.

    Examples::
        >>> def func(a, b='foo', c=None, *x, **y):
        ...     pass
        >>> sorted(getcallargs(func, 5).items())
        [('a', 5), ('b', 'foo'), ('c', None), ('x', ()), ('y', {})]
        >>> sorted(getcallargs(func, 5, 'bar').items())
        [('a', 5), ('b', 'bar'), ('c', None), ('x', ()), ('y', {})]
        >>> sorted(getcallargs(func, 5, c=['a', 'b']).items())
        [('a', 5), ('b', 'foo'), ('c', ['a', 'b']), ('x', ()), ('y', {})]
        >>> sorted(getcallargs(func, 5, 6, 7, 8).items())
        [('a', 5), ('b', 6), ('c', 7), ('x', (8,)), ('y', {})]
        >>> sorted(getcallargs(func, 5, z=3, b=2).items())
        [('a', 5), ('b', 2), ('c', None), ('x', ()), ('y', {'z': 3})]
    '''
    arg2value = {}
    f_name = func.func_name
    spec_args, varargs, varkw, defaults = getargspec(func)
    # handle methods
    if ismethod(func):
        # implicit 'self' (or 'cls' for classmethods) argument: func.im_self
        if func.im_self is not None:
            arg2value[spec_args.pop(0)] = func.im_self
        elif not args or not isinstance(args[0], func.im_class):
            got = args and ('%s instance' % type(args[0]).__name__) or '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_args = len(args)
    has_kwds = bool(kwds)
    num_spec_args = len(spec_args)
    num_defaults = len(defaults or ())
    # get the expected arguments passed positionally
    arg2value.update(izip(spec_args,args))
    # get the expected arguments passed by name
    for arg in spec_args:
        if arg in kwds:
            if arg in arg2value:
                raise TypeError("%s() got multiple values for keyword "
                                "argument '%s'" % (f_name,arg))
            else:
                arg2value[arg] = kwds.pop(arg)
    # fill in any missing values with the defaults
    if defaults:
        for arg,val in izip(spec_args[-num_defaults:],defaults):
            if arg not in arg2value:
                arg2value[arg] = val
    # ensure that all required args have a value
    for arg in spec_args:
        if arg not in arg2value:
            num_required = num_spec_args - num_defaults
            raise TypeError('%s() takes at least %d %s argument%s (%d given)'
                            % (f_name, num_required,
                               has_kwds and 'non-keyword ' or '',
                               num_required>1 and 's' or '', num_args))
    # handle any remaining named arguments
    if varkw:
        arg2value[varkw] = kwds
    elif kwds:
        raise TypeError("%s() got an unexpected keyword argument '%s'" %
                        (f_name, iter(kwds).next()))
    # handle any remaining positional arguments
    if varargs:
        if num_args > num_spec_args:
            arg2value[varargs] = args[-(num_args-num_spec_args):]
        else:
            arg2value[varargs] = ()
    elif num_spec_args < num_args:
        raise TypeError('%s() takes %s %d argument%s (%d given)' %
                        (f_name, defaults and 'at most' or 'exactly',
                         num_spec_args, num_spec_args>1 and 's' or '', num_args))
    return arg2value

Sign in to comment