Peeking into the internals of Python's 'functools.wraps' decorator
The functools.wraps decorator allows you to keep your function's identity intact after it's been wrapped by a decorator. Whenever a function is wrapped by a decorator, identity properties like - function name, docstring, annotations of it get replaced by those of the wrapper function. Consider this example:
Here, I've defined a simple logging decorator that wraps the add function. The function add has its own type annotations and docstring. So, you'd expect the docstring and name* of the add function to be printed when the above snippet gets executed. However, running the script prints the following instead:
This is surprising and probably not something you want. If you pay attention to the function wrapper in the log decorator, you'll see that the identity properties of the wrapper function replace the identity properties of the wrapped function add. This can easily be avoided by decorating the wrapper function inside the log decorator with the functools.wraps decorator:
Now, running the script will return the expected output:
I wanted to take a peek into how the functools.wraps decorator works internally. Turns out that the implementation is quite straightforward. Here's the entire implementation from the functools.py module. For brevity's sake, I've stripped out the comments and added type annotations:
The bulk of the work is done in the update_wrapper function. It copies the identity properties defined in WRAPPER_ASSIGNMENTS and WRAPPER_UPDATES - from the wrapped function over to the wrapper function. Here, the wrapped function is the decorated one (add function) and the wrapper function is the eponymous function inside the log decorator.
Since you've already seen that whenever you try to introspect the identity properties of a wrapped function, the wrapper function obfuscates them and returns its own properties. However, if the identity properties are copied over from the wrapped to the wrapper function, your inspection will return the expected result. The update_wrapper function is doing exactly that.
The wraps function just binds the input arguments with the update_wrapper function using the partial function defined in the same module. This allows us to use the wraps function as a decorator.
You can also directly use the update_wrapper function to get the same result should you choose to do so. Here's how to do it:
Further reading
Discussion in the ATmosphere