Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Preserve default arguments of wrapped/decorated Python function in Sphinx documentation

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.

like image 438
SmCaterpillar Avatar asked Feb 06 '15 13:02

SmCaterpillar


2 Answers

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.

like image 166
SmCaterpillar Avatar answered Nov 15 '22 18:11

SmCaterpillar


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.

like image 42
jsbueno Avatar answered Nov 15 '22 18:11

jsbueno