Access to documentation of a decorated function? - python

I am developing an API through which I am passing to the user list of functionalities of a module with the documentations of each function. In order to access the documentation I used to do:
def foo(*args, **kwargs):
"""
Foo documentation is here!
"""
return None
print(foo.__doc__)
# Foo documentation is here!
Now that I added a decorator for some of those functions, the __doc__ returns None since the decorator function doesn't have any documentation.
def decor_func(func):
def wrap(*args, **kwargs):
return func(*args, **kwargs)
return wrap
#decor_func
def foo(*args, **kwargs):
"""
Foo documentation is here!
"""
return None
print(foo.__doc__)
# None
Is there any way that I can have access to decorated function's documentation?

You can update the __doc__ attribute of the wrap function:
def decor_func(func):
def wrap(*args, **kwargs):
return func(*args, **kwargs)
# Set the decorated function `__doc__` attribute
wrap.__doc__ = func.__doc__
return wrap
#decor_func
def foo(*args, **kwargs):
"""
Foo documentation is here!
"""
return None
print(foo.__doc__)
# Foo documentation is here!
However, the best approach is to use functools.wraps, as allows you to also copy additional attributes such as the original name, module and annotations:
import functools
def decor_func(func):
#functools.wraps(func)
def wrap(*args, **kwargs):
return func(*args, **kwargs)
return wrap
#decor_func
def foo(*args, **kwargs):
"""
Foo documentation is here!
"""
return None
print(foo.__doc__)
# Foo documentation is here!

Note, as others have pointed out, you should use functools.wraps so that your wrapper "looks" like the function it is wrapping, and adds the wrapped fucntion to a __wrapped__ attribute. However, note, you can always introspect the wrapper's closure to retrieve a reference to the original function, since it is a free variable in the wrapper and thus will be stored in the closure:
>>> def decor_func(func):
... def wrap(*args, **kwargs):
... return func(*args, **kwargs)
... return wrap
...
>>> #decor_func
... def foo(*args, **kwargs):
... """
... Foo documentation is here!
... """
... return None
...
>>> foo.__closure__
(<cell at 0x10e69da90: function object at 0x10e83a700>,)
So,
>>> foo.__closure__[0].cell_contents.__doc__
'\n Foo documentation is here!\n '
But again, you should use functools.wraps to begin with. The above might help if you have no control over the decorator though.

Related

Check which decorator was applied to a function

Following this question I have an idea how to check whether my function was decorated or not.
Only that I need further information, namely the decorators that were actually applied onto the function (or called when the function was called if it suits better).
For being safe from the danger mentioned in this answer, I am using functools.wraps. This way I don't have to be careful for any naming redefinition of the wrapper used.
This is what I have so far:
from functools import wraps
def decorator_wraps(function):
#wraps(function)
def _wrapper(*a, **kw): ...
return _wrapper
def is_decorated(func):
return hasattr(func, '__wrapped__')
#decorator_wraps
def foo(x, y): ...
print(is_decorated(foo)) # True
But what I need:
from functools import wraps
def decorator_wraps_1(function):
#wraps(function)
def _wrapper(*a, **kw): ...
return _wrapper
def decorator_wraps_2(function):
#wraps(function)
def _wrapper(*a, **kw): ...
return _wrapper
def decorators(func):
# returns list of decorators on `func`
# OR
def is_decorated_by(func, decorator):
# returns True if `func` is decorated by `decorator`
#decorator_wraps_1
#decorator_wraps_2
def foo(x, y): ...
print(decorators(foo)) # [decorator_wraps_1, decorator_wraps_2]
print(is_decorated_by(foo, decorator_wraps_1)) # True
TLDR
I want to decide if my function was decorated and I need the names of these decorator functions as well.
Any idea how to achieve this?
TL;DR
Roll your own #wraps.
import functools
def update_wrapper(wrapper, wrapped, decorator, **kwargs):
wrapper = functools.update_wrapper(wrapper, wrapped, **kwargs)
if decorator is not None:
__decorators__ = getattr(wrapper, "__decorators__", [])
setattr(wrapper, "__decorators__", __decorators__ + [decorator])
return wrapper
def wraps(wrapped, decorator, **kwargs):
return functools.partial(
update_wrapper, wrapped=wrapped, decorator=decorator, **kwargs
)
def get_decorators(func):
return getattr(func, "__decorators__", [])
def is_decorated_by(func, decorator):
return decorator in get_decorators(func)
Usage:
def test_decorator_1(function):
#wraps(function, test_decorator_1)
def wrapper(*args, **kwargs):
return function(*args, **kwargs)
return wrapper
def test_decorator_2(function):
#wraps(function, test_decorator_2)
def wrapper(*args, **kwargs):
return function(*args, **kwargs)
return wrapper
#test_decorator_1
#test_decorator_2
def foo(x: str, y: int) -> None:
print(x, y)
assert get_decorators(foo) == [test_decorator_2, test_decorator_1]
assert is_decorated_by(foo, test_decorator_1)
Custom #wraps
Concept
There is no built-in way for this as far as I know. All it takes to create a (functional) decorator is to define a function that takes another function as argument and returns a function. No information about that "outer" function is magically imprinted onto the returned function by virtue of decoration.
However we can lean on the functools.wraps approach and simply roll our own variation of it. We can define it in such a way that it takes not just a reference to the wrapped function as argument, but also a reference to the outer decorator.
The same way that functools.update_wrapper defines the additional __wrapped__ attribute on the wrapper it outputs, we can define our own custom __decorators__ attribute, which will be simply a list of all the decorators in the order of application (the reverse order of notation).
Code
The proper type annotations are a bit tricky, but here is a full working example:
import functools
from collections.abc import Callable
from typing import Any, ParamSpec, TypeAlias, TypeVar
P = ParamSpec("P")
T = TypeVar("T")
AnyFunc: TypeAlias = Callable[..., Any]
def update_wrapper(
wrapper: Callable[P, T],
wrapped: AnyFunc,
decorator: AnyFunc | None = None,
assigned: tuple[str, ...] = functools.WRAPPER_ASSIGNMENTS,
updated: tuple[str, ...] = functools.WRAPPER_UPDATES,
) -> Callable[P, T]:
"""
Same as `functools.update_wrapper`, but can also add `__decorators__`.
If provided a `decorator` argument, it is appended to the the
`__decorators__` attribute of `wrapper` before returning it.
If `wrapper` has no `__decorators__` attribute, a list with just
`decorator` in it is created and set as that attribute on `wrapper`.
"""
wrapper = functools.update_wrapper(
wrapper,
wrapped,
assigned=assigned,
updated=updated,
)
if decorator is not None:
__decorators__ = getattr(wrapper, "__decorators__", [])
setattr(wrapper, "__decorators__", __decorators__ + [decorator])
return wrapper
def wraps(
wrapped: AnyFunc,
decorator: AnyFunc | None,
assigned: tuple[str, ...] = functools.WRAPPER_ASSIGNMENTS,
updated: tuple[str, ...] = functools.WRAPPER_UPDATES
) -> Callable[[Callable[P, T]], Callable[P, T]]:
"""Same as `functools.wraps`, but uses custom `update_wrapper` inside."""
return functools.partial(
update_wrapper, # type: ignore[arg-type]
wrapped=wrapped,
decorator=decorator,
assigned=assigned,
updated=updated,
)
def get_decorators(func: AnyFunc) -> list[AnyFunc]:
return getattr(func, "__decorators__", [])
def is_decorated_by(func: AnyFunc, decorator: AnyFunc) -> bool:
return decorator in get_decorators(func)
def test() -> None:
def test_decorator_1(function: Callable[P, T]) -> Callable[P, T]:
#wraps(function, test_decorator_1)
def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
print(f"Called wrapper from {test_decorator_1.__name__}")
return function(*args, **kwargs)
return wrapper
def test_decorator_2(function: Callable[P, T]) -> Callable[P, T]:
#wraps(function, test_decorator_2)
def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
print(f"Called wrapper from {test_decorator_2.__name__}")
return function(*args, **kwargs)
return wrapper
#test_decorator_1
#test_decorator_2
def foo(x: str, y: int) -> None:
print(x, y)
assert get_decorators(foo) == [test_decorator_2, test_decorator_1]
assert is_decorated_by(foo, test_decorator_1)
assert hasattr(foo, "__wrapped__")
foo("a", 1)
if __name__ == '__main__':
test()
The output is of course:
Called wrapper from test_decorator_1
Called wrapper from test_decorator_2
a 1
Details and caveats
With this approach, none of the original functionality of functools.wraps should be lost. Just like the original, this #wraps decorator obviously relies on you passing the correct arguments for the entire affair to make sense in the end. If you pass a nonsense argument to #wraps, it will add nonsense information to your wrapper.
The difference is you now have to provide two function references instead of one, namely the function being wrapped (as before) and the outer decorator (or None if you want to suppress that information for some reason). So you would typically use it as #wraps(function, decorator).
If you don't like that the decorator argument is mandatory, you could have it default to None. But I thought it was better this way, since the whole point is to have a consistent way of tracking who decorated whom, so omitting the decorator reference should be a conscious choice.
Note that I chose to implement __decorators__ in that order because while they are written in the reverse order, they are applied in that order. So in this example foo is decorated with #test_decorator_2 first and then the wrapper that comes out of that is decorated with #test_decorator_1. It made more sense to me for our list to reflect that order.
Static type checks
With the given type annotations mypy --strict is happy as well and any IDE should still provide the auto-suggestions as expected. The only thing that threw me off, was that mypy complained at my usage of update_wrapper as argument for functools.partial. I could not figure out, why that was, so I added a # type: ignore there.
NOTE: If you are on Python <3.10, you'll probably need to adjust the imports and take for example ParamSpec from typing_extensions instead. Also instead of T | None, you'll need to use typing.Optional[T] instead. Or upgrade your Python version. 🙂

Decorator on Property not called

Consider this decorator:
from functools import wraps
def non_null(func):
#wraps(func)
def wrapper(*args, **kwargs):
print("Test")
assert all(not a is None for a in args)
return func(*args, *kwargs)
return wrapper
Around a property setter of a class function:
#non_null
#foo.setter
def foo(self, argument):
print("Setting")
self._a = argument
If the non_null decorator is used, neither the function nor decorator gets called.
It looks like the #foo.setter have to be the outmost decorator:
#foo.setter
#non_null
def foo(self, argument):
print("Setting")
self._a = argument
I could find no reference for that in the documentation, but examining the content of a class makes clear that if the property decorator in not the outmost one, you only get a simple method instead of a property.

Member function decorator and self argument

The following minimal example of a decorator on a member function:
def wrap_function(func):
def wrapper(*args, **kwargs):
print(args)
print(kwargs)
return wrapper
class Foo:
#wrap_function
def mem_fun(self, msg):
pass
foo = Foo()
foo.mem_fun('hi')
outputs:
(<__main__.Foo object at 0x7fb294939898>, 'hi')
{}
So self is one of the args.
However when using a wrapper class:
class WrappedFunction:
def __init__(self, func):
self._func = func
def __call__(self, *args, **kwargs):
print(args)
print(kwargs)
def wrap_function(func):
return WrappedFunction(func)
class Foo:
#wrap_function
def mem_fun(self, msg):
pass
foo = Foo()
foo.mem_fun('hi')
the output is:
('hi',)
{}
So the self, that references the Foo object, is not accessible in the body of __call__ of the WrappedFunction object.
How can I make it accessible there?
You're losing the reference to your bounded instance by wrapping the function logic (but not the instance) and redirecting it to a class instance - at that point, the class instance's own self applies instead of the wrapped instance method as it gets lost in the intermediary decorator (wrap_function()).
You either have to wrap the call to the wrapped function and pass *args/**kwargs to it, or just make a proper wrapper class instead of adding an intermediary wrapper:
class WrappedFunction(object):
def __call__(self, func):
def wrapper(*args, **kwargs):
print(args)
print(kwargs)
# NOTE: `WrappedFunction` instance is available in `self`
return wrapper
class Foo:
#WrappedFunction() # wrap directly, without an intermediary
def mem_fun(self, msg):
pass
foo = Foo()
foo.mem_fun('hi')
# (<__main__.Foo object at 0x000001A2216CDBA8>, 'hi')
# {}
Sadly, but this might be the only solution as you need it in the __call__ function.
Would suggest checking this out: What is the difference between __init__ and __call__ in Python?
def wrap_function(func):
def wrapper(*args, **kwargs):
x = WrappedFunction(func)
x(*args, **kwargs)
return wrapper

A decorator-creating class

I am writing a bunch of code that has a possibility of mutable outputs, like an arithmetic function where I could have the output be a float or an int. Basically my problem is that if I were to create a decorator for each object type I need (probably seven or eight), I would go insane with the constant repetition of:
def Int(fn):
def wrapper():
return int(fn())
return wrapper
What I want to have is a class like below that would create a decorator based on the name it's instantiated with and it would be a copy of the function above but with the appropriate type modifications.
class Decorator(object):
def __init__(self):
...
...
Int = Decorator()
# Then I can use #Int
Any help would be really appreciated. Thanks.
You cannot have Decorator know what name it will be assigned to. Assignment occurs after instantiation, so the object will have already been created by the time it is assigned a name.
You could however make a decorator that creates decorators dynamically:
from functools import wraps
def set_return_type(typeobj):
def decorator(func):
#wraps(func)
def wrapper(*args, **kwargs):
return typeobj(func(*args, **kwargs))
return wrapper
return decorator
You would then use this decorator by giving a type object argument for the type you want:
#set_return_type(int) # Causes decorated function to return ints
#set_return_type(float) # Causes decorated function to return floats
Below is a demonstration:
>>> from functools import wraps
>>> def set_return_type(typeobj):
... def decorator(func):
... #wraps(func)
... def wrapper(*args, **kwargs):
... return typeobj(func(*args, **kwargs))
... return wrapper
... return decorator
...
>>> #set_return_type(float)
... def test():
... return 1
...
>>> test()
1.0
>>>

Get decorated function object by string name

def log(func):
def wraper(*a, **kw):
return func(*a, **kw)
return wraper
#log
def f():
print 'f'
print locals()['f'] # - prints <function wraper at 0x00CBF3F0>.
How do you get the real f object (not decorator wrap)?
The functools module also provides a wraps decorator which makes sure that the wrapped function looks more like the real function: correct name, module, and docstring, for example.
You don't.1 Store it if you need to access it later.
def log(func):
def wrapper(*a, **kw):
return func(*a, **kw)
wrapper.func = func
return wrapper
#log
def f():
print 'f'
print f.func
1 You could mess with the closure, but I can't recommend it.
If you're running python 3.2 or above, and you use functools.wraps then you will find the wrapped function on the __wrapped__ attribute:
from functools import wraps
def log(func):
#wraps(func)
def wrapper(*a, **kw):
return func(*a, **kw)
return wrapper
#log
def f():
print 'f'
print f.__wrapped__
functools.wrapsis a convenience function for decorating a decorated function with the function that does all the work, including adding this attribute functools.update_wrapper.

Categories