1

(Python 3) First of all, I feel my title isn't quite what it should be, so if you stick through the question and come up with a better title, please feel free to edit it.

I have recently learned about Python Decorators and Python Annotations, and so I wrote two little functions to test what I have recently learned. One of them, called wraps is supposed to mimic the behaviour of the functools wraps, while the other, called ensure_types is supposed to check, for a given function and through its annotations, if the arguments passed to some function are the correct ones. This is the code I have for those functions:

def wraps(original_func):
    """Update the decorated function with some important attributes from the
    one that was decorated so as not to lose good information"""
    def update_attrs(new_func):
        # Update the __annotations__
        for key, value in original_func.__annotations__.items():
            new_func.__annotations__[key] = value
        # Update the __dict__
        for key, value in original_func.__dict__.items():
            new_func.__dict__[key] = value
        # Copy the __name__
        new_func.__name__ = original_func.__name__
        # Copy the docstring (__doc__)
        new_func.__doc__ = original_func.__doc__
        return new_func
    return update_attrs # return the decorator

def ensure_types(f):
    """Uses f.__annotations__ to check the expected types for the function's
    arguments. Raises a TypeError if there is no match.
    If an argument has no annotation, object is returned and so, regardless of
    the argument passed, isinstance(arg, object) evaluates to True"""
    @wraps(f) # say that test_types is wrapping f
    def test_types(*args, **kwargs):
        # Loop through the positional args, get their name and check the type
        for i in range(len(args)):
            # function.__code__.co_varnames is a tuple with the names of the
            ##arguments in the order they are in the function def statement
            var_name = f.__code__.co_varnames[i]
            if not(isinstance(args[i], f.__annotations__.get(var_name, object))):
                raise TypeError("Bad type for function argument named '{}'".format(var_name))
        # Loop through the named args, get their value and check the type
        for key in kwargs.keys():
            if not(isinstance(kwargs[key], f.__annotations__.get(key, object))):
                raise TypeError("Bad type for function argument named '{}'".format(key))
        return f(*args, **kwargs)
    return test_types

Supposedly, everything is alright until now. Both the wraps and the ensure_types are supposed to be used as decorators. The problem comes when I defined a third decorator, debug_dec that is supposed to print to the console when a function is called and its arguments. The function:

def debug_dec(f):
    """Does some annoying printing for debugging purposes"""
    @wraps(f)
    def profiler(*args, **kwargs):
        print("{} function called:".format(f.__name__))
        print("\tArgs: {}".format(args))
        print("\tKwargs: {}".format(kwargs))
        return f(*args, **kwargs)
    return profiler

That also works cooly. The problem comes when I try to use debug_dec and ensure_types at the same time.

@ensure_types
@debug_dec
def testing(x: str, y: str = "lol"):
    print(x)
    print(y)

testing("hahaha", 3) # raises no TypeError as expected

But if I change the order with which the decorators are called, it works just fine. Can someone please help me understand what is going wrong, and if is there any way of solving the problem besides swapping those two lines?

EDIT If I add the lines:

print(testing.__annotations__)
print(testing.__code__.co_varnames)

The output is as follows:

#{'y': <class 'str'>, 'x': <class 'str'>}
#('args', 'kwargs', 'i', 'var_name', 'key')
RGS
  • 964
  • 2
  • 10
  • 27
  • what are `x:` and `y:` in `testing(x: str, y: str = "lol")` – Padraic Cunningham Aug 08 '14 at 19:04
  • @PadraicCunningham Those are function annotations. It lets you write things about your function's arguments and your function's return value, that is stored in `your_function_name.__annotations__`. That is mainly used by 3rd party modules and so to do some specific parsing regarding the function and whatnot. – RGS Aug 08 '14 at 19:16
  • @PadraicCunningham Check this SO answer (and the question, and the other answers): http://stackoverflow.com/a/3038096/2828287 – RGS Aug 08 '14 at 19:18
  • you are using python 3 I presume then – Padraic Cunningham Aug 08 '14 at 19:28
  • @PadraicCunningham You presume well... I tought that those `print()` made it clear, but I'll make it more explicit! – RGS Aug 08 '14 at 19:32
  • `print()` is valid syntax in both 2 and 3 obviously with different uses or the same if you import from future. – Padraic Cunningham Aug 08 '14 at 19:33

1 Answers1

3

Although wraps maintains the annotations, it doesn't maintain the function signature. You see this when you print out the co_varnames. Since ensure_types does its checking by comparing the names of the arguments with the names in the annotation dict, it fails to match them up, because the wrapped function has no arguments named x and y (it just accepts generic *args and **kwargs).

You could try using the decorator module, which lets you write decorators that act like functools.wrap but also preserve the function signature (including annotations).

There is probably also a way to make it work "manually", but it would be a bit of a pain. Basically what you would have to do is have wraps store the original functions argspec (the names of its arguments), then have ensure_dict use this stored argspec instead of the wrapper's argspec in checking the types. Essentially your decorators would pass the argspec in parallel with the wrapped functions. However, using decorator is probably easier.

BrenBarn
  • 242,874
  • 37
  • 412
  • 384
  • But `debug_dec` was defined to use `wraps`, so why doesn't the `wraps` function keep the annotations for the debugger function? – RGS Aug 08 '14 at 18:53
  • @RSerrao: See my edited answer. The problem is that `wraps` doesn't preserve the function signature, so the argument names aren't there to be matched up with the annotations. – BrenBarn Aug 08 '14 at 19:08
  • Ok thanks. That does make sense. I think that for now I'll just swap the order of the decorator usage. – RGS Aug 08 '14 at 19:14