Preserve default arguments of wrapped/decorated Py

2019-02-12 22:36发布

问题:

How can I replace *args and **kwargs with the real signature in the documentation of decorated functions?

Let's say I have the following decorator and decorated function:

import functools

def mywrapper(func):
    @functools.wraps(func)
    def new_func(*args, **kwargs):
        print('Wrapping Ho!')
        return func(*args, **kwargs)
    return new_func

@mywrapper
def myfunc(foo=42, bar=43):
    """Obscure Addition

    :param foo: bar!
    :param bar: bla bla
    :return: foo + bar

    """
    return foo + bar

Accordingly, calling print(myfunc(3, 4)) gives us:

Wrapping Ho!
7

So far so good. I also want my library containing myfunc properly documented with Sphinx. However, if I include my function in my sphinx html page via:

.. automodule:: mymodule
    :members: myfunc

It will actually show up as:

myfunc(*args, **kwargs)

Obscure Addition

  • Parameters:
    • foo: bar!
    • bar: bla bla
  • Returns: foo + bar

How can I get rid of the generic myfunc(*args, **kwargs) in the title? This should be replaced by myfunc(foo=42, bar=43). How can I change sphinx or my decorator mywrapper such that the default keyword arguments are preserved in the documentation?

EDIT:

As pointed out this question has been asked before, but the answers are not so helpful.

However, I had an idea and wonder if this is possible. Does Sphinx set some environment variable that tells my module that it is actually imported by Sphinx? If so, I could simply monkey-patch my own wrappers. If my module is imported by Sphinx my wrappers return the original functions instead of wrapping them. Thus, the signature is preserved.

回答1:

I came up with a monkey-patch for functools.wraps. Accordingly, I simply added this to the conf.py script in my project documentation's sphinx source folder:

# Monkey-patch functools.wraps
import functools

def no_op_wraps(func):
    """Replaces functools.wraps in order to undo wrapping.

    Can be used to preserve the decorated function's signature
    in the documentation generated by Sphinx.

    """
    def wrapper(decorator):
        return func
    return wrapper

functools.wraps = no_op_wraps

Hence, when building the html page via make html, functools.wraps is replaced with this decorator no_op_wraps that does absolutely nothing but simply return the original function.



回答2:

You ordinarily can't. That is because the variable names used as parameters in the wrapped function are not even present on the wrapped function - so Sphinx do not know about them.

That is a known complicated issue in Python - so much that recent versions - including not only Python 3, but also Python 2.7 included a __wrapped__ attribute on class decorated that make the proper use from functools.wraps - that way, upon inspecting the decorated function one is able to know about the actual wrrapped function by looking at __wrapped__. Unfortunatelly, Sphinxs ignores the __wrapped__, and show the info on the wrapper function instead.

SO, one thing to do is certainly to report this as a bug to the Sphinx project itself - it should take __wrapped__ in account.

A meantime workaround for that would be to change the wrapper function to actually include more information about the wrapped - like its signature - so you could write another function to be called in place of "functools.wraps" for your project, which does just that: pre-pend the function signature to its docstring, if any. Unfortunatelly, retrieving the function signatures in Python older than 3.3 is tricky - (for 3.3 and newer, check https://docs.python.org/3/library/inspect.html#inspect-signature-object ) - but anyway, for a naive form, you could write another version of "wraps" along:

def wraps(original_func):
   wrap_decorator = functools.wraps(original_func)
   def re_wrapper(func):
       wrapper = wrap_decorator(func)
       poorman_sig = original_func.__code__.co_varnames[
                         :original_func.__code__.co_argcount]
       wrapper.__doc__ = "{} ({})\n\n{}".format (
            original_func.__name__, ", ".join(poorman_sig),
            wrapper.__doc__) 
       return wrapper
   return re_wrapper

And use that instead of "functools.wraps". It would at least add a line with the parameter names, (but not th e defalt values) as first line in the docs.

---Hmm..maybe it would be easier just to patch Sphinx to use __wrapped__ before getting this done right.