Sphinx: Preserve signature and default arguments of wrapped/decorated Python function

Created on 6 Feb 2015  Â·  8Comments  Â·  Source: sphinx-doc/sphinx

Can you enhance Sphinx to 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

It would be nice if the generic myfunc(*args, **kwargs) in the tile would be replaced by myfunc(foo=42, bar=43).

autodoc bug

All 8 comments

I cam up with a work-around by simply monkey-patching functools.wraps.
I simply added this to the conf.py script in the sphinx documentation 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 that does absolutely nothing but simply return the original function.

Also discussed on Stackoverflow.

It seems that importing matplotlib in a module will give errors when functools.wraps is replaced by a dummy. To work around this you can test if the function to be wrapped is inside the package you are documenting. Put the following in the conf.py file:

# Monkey-patch functools.wraps
# See: https://github.com/sphinx-doc/sphinx/issues/1711
import functools
def no_op_wraps(func):
    """
    Replaces functools.wraps in order to undo wrapping when generating Sphinx documentation
    """
    if func.__module__ is None or 'MY_PACKAGE_NAME' not in func.__module__:
        return functools.orig_wraps(func)
    def wrapper(decorator):
        return func
    return wrapper
functools.orig_wraps = functools.wraps 
functools.wraps = no_op_wraps

This works for me at least

Following the given monkey patch with:

import contextlib
contextlib.wraps = no_op_wraps

works for functions and methods decorated by @contextlib.contextmanager

@SmokinCaterpillar — Is there a special place in conf.py where the patch goes? Simply placing it in the file has no effect for me.

Does anyone know whether it would be possible to just patch Sphinx to use __wrapped__? I have no idea how one might do that!

@orome you can see a version of my conf.py workaround here if it helps, https://github.com/jquast/blessed/blob/master/docs/conf.py#L38-L55

Take note of the conditional expression, or 'blessed' not in func.__module__ -- this ensures patching is contained to the blessed module

Here is an example of a custom decorator to solve the same issue:

my_project/somefile.py
def required(*req_args):
    def wrapper(f):
        def wrap(self, *args, **kwargs):
            return f(self, *args, **kwargs)
        return wrap
    return wrapper

@required('something')
def my_thing(self, *args, **kwargs):
    pass

source/conf.py

import my_project
def no_wrap(*args)
    def wrap(f):
        return f
    return wrap
my_project.required = no_wrap

It seems the latest Sphinx (v3.1.2) uses the signature of original function. On my local, the reported code is rendered as example.myfunc(foo=42, bar=43). I don't know which release fixes this.
Anyway, this issue was already resolved. Now I'm closing this.
Please file a new issue if you still have the same error.

Was this page helpful?
0 / 5 - 0 ratings