2

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?

neves
  • 33,186
  • 27
  • 159
  • 192
  • Does this answer your question? [Python decorator handling docstrings](https://stackoverflow.com/questions/1782843/python-decorator-handling-docstrings) – Edward Ji Jun 03 '22 at 16:14
  • 1
    @SpearAndShield thanks for the tip, but I think it doesn't apply to my case. I've updated the question – neves Jun 03 '22 at 18:16
  • 1
    The function *is the result of the decorator now*. If the people who created the function didn't add `wraps` (or manually do the equivalent) then I don't think there is anything you can do, other than maybe writing your own version of the decorator that does (maybe deferring to the one that doesn't do what you want) – juanpa.arrivillaga Jun 03 '22 at 18:22
  • 1
    but I think this is actually an issue with `help` – juanpa.arrivillaga Jun 03 '22 at 18:24

1 Answers1

2

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

Why the Difference

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.

Reference

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.

Edward Ji
  • 745
  • 8
  • 19