Welcome, guest | Sign In | My Account | Store | Cart

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).

Edit (2008/5/10): Added a second returned value, missing_args: a tuple of the formal parameters whose value was not provided (i.e. those using the respective default value). This is useful in cases where one want to distinguish f() from f(None) given "def f(x=None):".

Python, 90 lines
 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
86
87
88
89
90
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).

    @returns: `(bindings, missing_args)`, where:
        - `bindings` is a mapping of every formal parameter (including *varargs
           and **kwargs if present) of the function to the respective bounded value.
        - `missing_args` is a tuple of the formal parameters whose value was not
           provided (i.e. using the respective default value)

    Examples::
        >>> def func(a, b='foo', c=None, *x, **y):
        ...     pass
        >>> getcallargs(func, 5)
        ({'a': 5, 'y': {}, 'c': None, 'b': 'foo', 'x': ()}, ('b', 'c'))
        >>> getcallargs(func, 5, 'foo')
        ({'a': 5, 'y': {}, 'c': None, 'b': 'foo', 'x': ()}, ('c',))
        >>> getcallargs(func, 5, c=['a', 'b'])
        ({'a': 5, 'y': {}, 'c': ['a', 'b'], 'b': 'foo', 'x': ()}, ('b',))
        >>> getcallargs(func, 5, 6, 7, 8)
        ({'a': 5, 'y': {}, 'c': 7, 'b': 6, 'x': (8,)}, ())
        >>> getcallargs(func, 5, z=3, b=2)
        ({'a': 5, 'y': {'z': 3}, 'c': None, 'b': 2, 'x': ()}, ('c',))
    '''
    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
    missing = []
    if defaults:
        for arg,val in izip(spec_args[-num_defaults:],defaults):
            if arg not in arg2value:
                arg2value[arg] = val
                missing.append(arg)
    # 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, tuple(missing)


if __name__ == '__main__':
    import doctest
    doctest.testmod()

1 comment

Josh 8 years, 6 months ago  # | flag

There are a couple of things you forgot to import

Created by George Sakkis on Tue, 18 Mar 2008 (PSF)
Python recipes (4591)
George Sakkis's recipes (26)

Required Modules

Other Information and Tasks