Reputation: 39383
Python has the nice help()
built-in that displays the doc string of an object. When I use it in the REPL passing a function in my module it nicely displays:
>>> help(mymodule.myfunction)
Help on function myfunction in module mymodule.main:
myfunction(parameter=False) -> Dict[str, str]
Doc string of myfunction
But if I use a decorator like functools.@lru_cache
the help function is somewhat confusing:
>>> help(mymodule.myfunction)
Help on _lru_cache_wrapper in module mymodule.main:
myfunction(parameter=False) -> Dict[str, str]
Doc string of myfunction
The doc string is displayed, but the first line of the message is confusing for my users who aren't experienced Python programmers.
Note that I didn't create the decorator, it is from the functools module in stdlib. It looks like the solution of using functools.wraps won't work for me.
Can I do something to force the display of the first message even if the function has a decorator?
Upvotes: 3
Views: 588
Reputation: 799
The following wrapper makes the cached function look more like the original.
from functools import wraps
def restore(func):
@wraps(func.__wrapped__)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
It creates yet another wrapper around your decorated function that restores its type as a function while preserving the docstring.
For example, if you have a function like this:
@restore
@lru_cache
def func_dup(x: int):
"""my doc"""
return x
Then, run help(func_dup)
Help on function func_dup in module __main__:
func_dup(x: int)
my doc
I will be using CPython 3.10, which is the latest version as of the time I wrote this answer.
The help
callable is actually implemented in pydoc
as a Helper
object. The magic method Helper.__call__
is defined to call Helper.help
. It then calls doc
, which calls render_doc
. The render_doc
function makes up the string that gets printed. Inside this function, it calls pydoc.describe
for a descriptive name for your function.
Your original mymodule.myfunction
is a function, so describe
returns in this branch.
if inspect.isfunction(thing):
return 'function ' + thing.__name__
This gives "function myfunction"
.
However, after you decorate your function with @lru_cache
, it becomes an instance of the built-in/extension type functools._lru_cache_wrapper
. I am not sure why it is implemented this way, but the decorated function is not of type types.FunctionType
anymore. So the describe(mymodule.myfunction)
function returns on the last line after being decorated.
return type(thing).__name__
This returns "_lru_cache_wrapper"
.
The functools.update_wrapper
function attempts to
Update a wrapper function to look like the wrapped function
It doesn't restore the wrapper as an instance of types.FunctionType
. It does, however, reference the original function in the __wrapped__
attribute. Hence, we can use that to wrap your original function yet again.
There is a Python Issue bpo-46761 that may or may not relate to this issue.
when using functools.partial() to pre-supply arguments to a function, if you then call functools.update_wrapper() to update that partial object, inspect.signature() returns the original function's signature, not the wrapped function's signature.
It is primarily on functools.partial
, which doesn't even preserve the wrapped function's signature.
Upvotes: 3