4

How can I check if a function can always be called with the same arguments as another function? For example, b can be called with all arguments provided to a.

def a(a, b, c=None):
    pass

def b(a, *args, d=4,**kwargs): 
    pass

The reason I want this is that I have a base function:

def f(a, b):
    print('f', a, b)

and a list of callbacks:

def g(b, a):
    print('g', a, b)

def h(*args, **kwargs):
    print('h', args, kwargs)    

funcs = [g, h]

and a wrapper function that accepts anything:

def call(*args, **kwargs):
    f(*args, **kwargs)
    for func in funcs:
        func(*args, **kwargs)

Now I want to check if all callbacks will accept the arguments provided to call(), assuming they're valid for f(). For performance reasons, I don't want to check the arguments every time call() is called, but rather check each callback before adding it to the list of callbacks. For example, those calls are ok:

call(1, 2)
call(a=1, b=3)

But this one should fail because g has arguments in wrong order:

call(1, b=3)
5
  • 1
    inspect.signature would be the starting point… though "can be called" and "will do useful things/won't throw other errors with the given values" is another topic you won't be able to answer. Commented Jul 20, 2017 at 9:43
  • I tried it before but there are some corner cases when calling functions. Is there some specification how arguments are bound to parameters in python? Commented Jul 20, 2017 at 9:52
  • 2
    Elaborating on those corner cases may help. If you've already done some research, show it so we're not needlessly repeating it. Commented Jul 20, 2017 at 9:53
  • For instance: def a(a, b, *args, d): print(a, b, args, d), def b(a, d, *args, **kwargs): print(args, d, kwargs) Commented Jul 20, 2017 at 9:58
  • inspect.getfullargspec(a) gives some insight -> d is actually parsed as a keyword argument with no default, rather than a positional argument. So the order by whch args are consumed is still defined positional arguments, optional positional arguments, keyword arguments, and then **kw keyword arguments, with the added caveat that keyword arguments with no defaults are required. Commented Jul 20, 2017 at 10:35

2 Answers 2

2

This took a bit of fun research, but i think i've covered the corner cases. A number of them arise to keep things compatible with python 2 while new syntax being added.

Most problematic part is the fact that some named (keyword) parameters can be passed in as positional argument or be required based on order passed in.

For more see comments.

Below code will ensure that function b can be called using any possible combination of valid arguments to function a. (does not imply inverse). Uncomment/add try except block to get true/valse result and not an AssertionError.

import inspect

def check_arg_spec(a,b):
    """

    attrs of FullArgSpec object:

        sp.args = pos or legacy keyword arguments, w/ keyword at back
        sp.varargs = *args
        sp.varkw = **kwargs
        sp.defaults = default values for legacy keyword arguments @ 
        sp.args
        sp.kwdonly = keyword arguments follow *args or *, must be passed in by name
        sp.kwdonlydefaults = {name: default_val, .... }
        sp.annotatons -> currently not in use, except as standard flag for outside applications

    Consume order:

    (1) Positional arguments
    (2) legacy keyword argument = default (can be filled by both keyword or positional parameter)
    [
        (3) *args
        [
          (4) keyword only arguments [=default]
        ]
    ]
    (5) **kwds

    """
    a_sp = inspect.getfullargspec(a) 
    b_sp = inspect.getfullargspec(b)
    kwdfb = b_sp.kwonlydefaults or {}
    kwdfa = a_sp.kwonlydefaults or {}
    kwddefb = b_sp.defaults or []
    kwddefa = a_sp.defaults or []

    # try:
    akwd = a_sp.kwonlyargs
    if len(kwddefa):
        akwd += a_sp.args[-len(kwddefa):]
    bkwd = b_sp.kwonlyargs
    if len(kwddefb):
        bkwd += b_sp.args[-len(kwddefb):]


    # all required arguments in b must have name match in a spec.
    for bkey in (set(b_sp.args) ^ set(bkwd)) & set(b_sp.args) :
        assert bkey in a_sp.args


    # all required positional in b can be met by a
    assert (len(a_sp.args)-len(kwddefb)) >= (len(b_sp.args)-len(kwddefb))

    # if a has  *args spec, so must b
    assert not ( a_sp.varargs and b_sp.varargs is None )


    # if a does not take *args, max number of pos args passed to a is len(a_sp.args). b must accept at least this many positional args unless it can consume *args
    if b_sp.varargs is None:
        # if neither a nor b accepts *args, check that total number of pos plus py2 style keyword arguments for sg of b is more than a can send its way. 
        assert len(a_sp.args) <= len(b_sp.args)


    #  Keyword only arguments of b -> they are required, must be present in a.
    akws = set(a_sp.kwonlyargs) | set(a_sp.args[-len(kwddefa):])

    for nmreq in (set(b_sp.kwonlyargs)^set(kwdfb)) & set(b_sp.kwonlyargs):
         assert nmreq in akws

     # if a and b both accept an arbitrary number of positional arguments or if b can but a cannot, no more checks neccessary here

    # if a accepts optional arbitrary, **kwds, then so must b
    assert not (a_sp.varkw and b_sp.varkw is None)

    if b_sp.varkw is None:
        # neither a nor b can consume arbitrary keyword arguments
        # then b must be able to consume all keywords that a can be called w/th.
        for akw in akwd:
            assert akw in bkwd

          # if b accepts **kwds, but not a, then there is no need to check further
          # if both accept **kwds, then also no need to check further

    #     return True 
    # 
    # except AssertionError:
    # 
    #       return False
Sign up to request clarification or add additional context in comments.

6 Comments

better? it could use improvement but cover all the cases i can think of.
Fails in this case: def f(a, b): pass, def g(c, d): pass check_arg_spec(f, g) and check_arg_spec(g, f) doesn't fail. But I can call f(a=1, b=2), but not g(a=1, b=2)
see line i added and commented out , start w/ "# require all names". Now assume that all positional arguments [legacy, non keyword only] are also be named arguments. Using this assumption, neither check(f,g) nor check(g,f) will succeed. For the most part although its possible, convention is to treat positional arguments as positional arguments.
thanks for pointing that out though, didnt really think about it that way.
This fixes the above case but what about def f(a=1): pass, def g(a, **kwargs): pass. check_arg_spec(f, g) doesn't fail but I can call f() but not g(). Thanks for the help!
|
1

Not sure what you are really looking for and I'm pretty sure your issue could be solved in a better way, but anyway:

from inspect import getargspec

def foo(a, b, c=None):
    pass

def bar(a, d=4, *args, **kwargs):
    pass

def same_args(func1, func2):
    return list(set(getargspec(func1)[0]).intersection(set(getargspec(func2)[0])))

print same_args(foo, bar)
# => ['a']

same_args just check arguments from func1 and func2 and returns a new list with only same arguments in both func1 and func2.

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.