Search code examples
pythonpython-3.xexceptionsignaturepython-decorators

How can I invoke Python's built-in function argument verification without calling the function?


Python obviously has a way to verify whether a function call has valid arguments (correct number of positional arguments, correct keyword arguments, etc). The following is a basic example of what I mean:

def test_func(x, y, z=None):
    print(x, y, z)

test_func(2)  # Raises a "missing positional argument" TypeError
test_func(1, 2, 3, a=5)  # Raises an "unexpected keyword argument" TypeError

Is there a way that I can use this argument verification, without actually calling the function?

I'm basically trying to write a decorator that does some preprocessing steps based on the function arguments before calling the wrapped function itself, such as:

def preprocess(func):
    def wrapper(*args, **kwargs):
        # Verify *args and **kwargs are valid for the original function.
        # I want the exact behavior of calling func() in the case of bad arguments,
        # but without actually calling func() if the arguments are ok.

        preprocess_stuff(*args, **kwargs)
        func(*args, **kwargs)
    return wrapper

I want my wrapper function to verify that the arguments would be valid if used on the wrapped function before doing any preprocessing work.

I would like to take advantage of the checks Python already does every time you call a function and the various exceptions it will raise. I just do not want to actually call the function, because the function may not be idempotent. Writing my own checks and exceptions feels like reinventing the wheel.


Solution

  • You can't invoke the actual built-in argument verification for a function without calling the function, but you can use something pretty close.

    The inspect module has a function signature(), which returns a Signature object representing the argument signature of the function passed to it. That Signature object has a bind() method which attempts to create a BoundArguments object using the arguments passed to it. If those arguments don't match the signature, a TypeError is raised.

    While this mostly behaves like the built-in argument binding logic, it has a few differences. For example, it can't always determine the signature of functions written in C, and its interaction with decorators will depend on whether they use functools.wraps (or something else that sets the __wrapped__ attribute). That said, since the real argument binding logic is inaccessible, inspect.signature is the best alternative.

    We can use all this to create your decorator:

    import functools
    import inspect
    
    def preprocess(func):
        sig = inspect.signature(func)
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            try:
                sig.bind(*args, **kwargs)
            except TypeError:
                pass  # bad arguments; skip preprocessing
            else:
                print("Preprocessing: args=%r, kwargs=%r" % (args, kwargs))
                # ... etc.
            return func(*args, **kwargs)
        return wrapper
    

    Usage:

    @preprocess
    def test_func(x, y, z=None):
        print(x, y, z)
    

    >>> test_func(2)
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
      File "<stdin>", line 10, in wrapper
    TypeError: test_func() missing 1 required positional argument: 'y'
    

    >>> test_func(1, 2, 3, a=5)
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
      File "<stdin>", line 10, in wrapper
    TypeError: test_func() got an unexpected keyword argument 'a'
    

    >>> test_func(1, 2)
    Preprocessing: args=(1, 2), kwargs={}
    1 2 None
    

    Note that, if bad arguments are supplied, you do in fact want to call the function, because you "want the exact behavior of calling func() in the case of bad arguments" (to quote your comment), and the only way of getting the exact behaviour of calling an arbitrary function (even if that behaviour is to immediately fail) is to actually call it. What you don't want to do in such cases is the preprocessing, which the decorator above achieves for you.