Python runtime type check that function passed to a decorator has correct signature

Viewed 127

I have a python decorator that expects the signature of the passed function to be a User and a Point, and it returns a function with the same signature:

@dataclass
class User:
    name: str

@dataclass
class Point:
    x: float
    y: float
    z: float


def logging_decorator(f: Callable[[User, Point], bool]) -> Callable[[User, Point], bool]:
    print('decorating function...')
    def decorated(a: User, b: Point) -> bool:
        print(f'User {a.name} calling with points {b.x},{b.y},{b.z}')
        ret = f(a, b)
        print(f'returned {ret}')
        return ret
    return decorated

At the point it prints decorating function..., I want it to check that the function which has been passed in has the correct signature, and give a sensible error message if it does not.

Here is an example of decorating two functions, one with the correct signature, and one with an incorrect one:

@logging_decorator
def good_f(a: User, b: Point) -> bool:
    return True

@logging_decorator
def bad_f(a: str, b: str) -> str:
    return "Hello"

How can I add checks to the logging_decorator so it checks that the function being decorated has the correct argument types?

I have tried using the typing_inspect and typeguard packages, but cannot find the functionality I need.

I am using Python 3.8.3

1 Answers

Use the inspect module in the standard library. With inspect.signature, you can write function-validation code like this:

import inspect

def validate_func(func):
    def good_func(a: User, b: Point) -> bool: ...
    good_func_signature = inspect.signature(good_func)
    received_signature = inspect.signature(func)
    if received_signature != good_func_signature:
        raise TypeError(
            f'Invalid function signature: '
            f'expected "{good_func_signature}", '
            f'got "{received_signature}"'
        )

(You can put this logic inside the decorator for your use case; I only wrote it as a separate function to keep it separate from your code.)

In usage:

>>> validate_func(good_f)
>>> validate_func(lambda a, b: a)
Traceback (most recent call last):
  File "<string>", line 1, in <module>
  File "<string>", line 6, in validate_func
TypeError: Invalid function signature: expected "(a:__main__.User, b:__main__.Point) -> bool", got "(a, b)"
>>> validate_func(bad_f)
Traceback (most recent call last):
  File "<string>", line 1, in <module>
  File "<string>", line 6, in validate_func
TypeError: Invalid function signature: expected "(a:__main__.User, b:__main__.Point) -> bool", got "(a:str, b:str) -> str"

If you want to, you can do far more detailed and fine-grained comparisons between signatures than just a simple equality check. You can use the return value from inspect.signature to compare:

  • The number of parameters
  • The names of the parameters
  • The kinds of the parameters (positional-only, positional-or-keyword, or keyword-only)
  • Whether the parameters have annotations
  • What the annotations of the parameters are
  • Whether the parameters have default values
  • What the default values of the parameters are
  • Whether the function has a return annotation
  • What the return annotation of the function is

You can find the documentation for inspect.signature here.

Related