33

I am attempting to use Sphinx to document my Python class. I do so using autodoc:

.. autoclass:: Bus
   :members:

While it correctly fetches the docstrings for my methods, those that are decorated:

    @checkStale
    def open(self):
        """
        Some docs.
        """
        # Code

with @checkStale being

def checkStale(f):
    @wraps(f)
    def newf(self, *args, **kwargs):
        if self._stale:
            raise Exception
        return f(self, *args, **kwargs)
    return newf

have an incorrect prototype, such as open(*args, **kwargs).

How can I fix this? I was under the impression that using @wraps would fix up this kind of thing.

bad_coder
  • 11,289
  • 20
  • 44
  • 72
Freddie Witherden
  • 2,369
  • 1
  • 26
  • 41
  • 1
    The documentation in the stdlib and in Sphinx both seem to imply that you are doing everything right. :( – Ned Batchelder Sep 10 '10 at 18:23
  • Have you tried using the [decorator package](http://pypi.python.org/pypi/decorator) and putting `@decorator` on `checkStale`? I had a similar issue using `epydoc` with a decorated function. – bstpierre Sep 12 '10 at 02:38
  • @bstpierre I take it that the decorator package is not part of a normal Python distribution? I wonder if it is possible to use it where available, and otherwise fallback to what I've got? – Freddie Witherden Sep 12 '10 at 15:58
  • I monkey-patched ``functools.wraps`` to *undo* wrapping, see: http://stackoverflow.com/questions/28366818/preserve-default-arguments-of-wrapped-decorated-python-function-in-sphinx-docume – SmCaterpillar Feb 06 '15 at 17:54

9 Answers9

15

I had the same problem with the celery @task decorator.

You can also fix this in your case by adding the correct function signature to your rst file, like this:

.. autoclass:: Bus
    :members:

    .. automethod:: open(self)
    .. automethod:: some_other_method(self, param1, param2)

It will still document the non-decorator members automatically.

This is mentioned in the sphinx documentation at http://www.sphinx-doc.org/en/master/ext/autodoc.html#directive-automodule -- search for "This is useful if the signature from the method is hidden by a decorator."

In my case, I had to use autofunction to specify the signature of my celery tasks in the tasks.py module of a django app:

.. automodule:: django_app.tasks
    :members:
    :undoc-members:
    :show-inheritance:

    .. autofunction:: funct1(user_id)
    .. autofunction:: func2(iterations)
Josh
  • 2,767
  • 1
  • 27
  • 31
Charl Botha
  • 4,373
  • 34
  • 53
  • 1
    In addition to this answer (which is great, thanks!) I also had to exclude the decorated function using `:exclude-members: funcname` in order to prevent it from appearing twice. –  Sep 07 '14 at 11:42
  • Celery now includes a Sphinx extension: [`celery.contrib.sphinx`](http://docs.celeryproject.org/en/latest/reference/celery.contrib.sphinx.html), which can automatically document the tasks. – 153957 Sep 07 '16 at 09:04
14

To expand on my comment:

Have you tried using the decorator package and putting @decorator on checkStale? I had a similar issue using epydoc with a decorated function.

As you asked in your comment, the decorator package is not part of the standard library.

You can fall back using code something like the following (untested):

try:
    from decorator import decorator
except ImportError:
    # No decorator package available. Create a no-op "decorator".
    def decorator(f):
        return f
bstpierre
  • 30,042
  • 15
  • 70
  • 103
  • This isn't exactly a fallback. Unfortunately decorator and functools.wraps have different signatures, otherwise the preferred method would be `try: from decorator import decorator as wraps;except ImportError: from functools import wraps`. – kitsu.eb Sep 08 '12 at 17:21
  • 1
    if i add that it works for sphinx but sometimes (e.g., when i run the tests) i get this error `user_required() takes exactly 1 argument (2 given)` . basically i should import the devorator only when sphinx compiles the docs otherwise the other "fake" function.. any idea? – EsseTi Mar 03 '15 at 21:41
  • I don't believe this works for class decorator (functions.) I get an error of "TypeError: You are decorating a non function" – Urchin Feb 22 '19 at 20:41
5

I just found an easy solution which works for me, but don't ask me why. If you know why add it in the comments.

from functools import wraps 

def a_decorator(f):
    """A decorator 

    Args:
        f (function): the function to wrap
    """
    @wraps(f) # use this annotation on the wrapper works like a charm
    def wrapper(*args, **kwargs):
        some code
        return ret

return wrapper

The doc of the decorated function and of the decorator are both kept

Gwenael C
  • 96
  • 1
  • 6
3

Added in version 1.1 you can now override the method signature by providing a custom value in the first line of your docstring.

http://sphinx-doc.org/ext/autodoc.html#confval-autodoc_docstring_signature

@checkStale
def open(self):
    """
    open()
    Some docs.
    """
    # Code
adam
  • 6,582
  • 4
  • 29
  • 28
3

Add '.__ doc __':

def checkStale(f):
    @wraps(f)
    def newf(self, *args, **kwargs):
       if self._stale:
          raise Exception
       return f(self, *args, **kwargs)
    newf.__doc__ = f.__doc__
    return newf

And on decorated function add:

@checkStale
def open(self):
    """
    open()
    Some docs.
    """
    # Code
  • Please explain ratione here - doesn't `funcotols.wraps()` already copy the docstring? In any case, if `__doc__` is correctly set, why do you need to duplicate the function signature in the docstring? – Tuukka Mustonen Oct 05 '21 at 14:21
  • The above answer is (somewhat) explained here (search forward for the Note): https://www.sphinx-doc.org/en/master/usage/extensions/autodoc.html#directive-autodecorator – rlandster Jul 14 '23 at 20:20
1

If you're particularly adamant about not adding another dependency here's a code snippet that works with the regular inspector by injecting into the docstring. It's quite hackey and not really recommended unless there are good reasons to not add another module, but here it is.

# inject the wrapped functions signature at the top of a docstring
args, varargs, varkw, defaults = inspect.getargspec(method)
defaults = () if defaults is None else defaults
defaults = ["\"{}\"".format(a) if type(a) == str else a for a in defaults]
l = ["{}={}".format(arg, defaults[(idx+1)*-1]) if len(defaults)-1 >= idx else arg for idx, arg in enumerate(reversed(list(args)))]
if varargs: allargs.append('*' + varargs)
if varkw: allargs.append('**' + varkw)
doc = "{}({})\n{}".format(method.__name__, ', '.join(reversed(l)), method.__doc__)
wrapper.__doc__ = doc
whoknows
  • 78
  • 5
0

the answer to this is quite simple, but none of the threads I've seen have mentioned it. Have a look at functools.update_wrapper()

import functools

def schema_in(orig_func):
    schema = Schema() 
    def validate_args(*args, **kwargs):
        clean_kwargs = schema.load(**kwargs)
        return orig_func(**clean_kwargs)

    functools.update_wrapper(validate_args, orig_func)
    return validate_args
    

I'm not sure this will run, but it illustrates the concept. If your wrapper is injecting validated_args between the caller and the callee, the example shows how to update the wrapper (validated_args) method with the metadata of orig_method. Ultimately, this will allow Sphinx and other type analysis tools such as mypy (I'm assuming!) to see the data needed to behave as expected. I have just finished testing this and can confirm it works as described, Sphinx autodoc is behaving as desired.

OnNIX
  • 422
  • 4
  • 10
0

In recent versions of python, you can update the signature of the decorated function from the decorator itself. For example:

import inspect

def my_decorator(f):
    def new_f(*args, **kwargs):
        # Decorate function
        pass

    new_f.__doc__ = f.__doc__
    new_f.__module__ = f.__module__
    new_f.__signature__ = inspect.signature(f)
    return new_f
Peter Macgregor
  • 143
  • 1
  • 8
0

UPDATE: this may be "impossible" to do cleanly because sphinx uses the function's code object to generate its function signature. But, since you're using sphinx, there is a hacky workaround that does works.

It's hacky because it effectively disables the decorator while sphinx is running, but it does work, so it's a practical solution.

At first I went down the route of constructing a new types.CodeType object, to replace the wrapper's func_code code object member, which is what sphinx uses when generating the signatures.

I was able to segfault python by going down the route or trying to swap in the co_varnames, co_nlocals, etc. members of the code object from the original function, and while appealing, it was too complicated.

The following solution, while it is a hacky heavy hammer, is also very simple =)

The approach is as follows: when running inside sphinx, set an environment variable that the decorator can check. inside the decorator, when sphinx is detected, don't do any decorating at all, and instead return the original function.

Inside your sphinx conf.py:

import os
os.environ['SPHINX_BUILD'] = '1'

And then here is an example module with a test case that shows what it might look like:

import functools
import os
import types
import unittest


SPHINX_BUILD = bool(os.environ.get('SPHINX_BUILD', ''))


class StaleError(StandardError):
    """Custom exception for staleness"""
    pass


def check_stale(f):
    """Raise StaleError when the object has gone stale"""

    if SPHINX_BUILD:
        # sphinx hack: use the original function when sphinx is running so that the
        # documentation ends up with the correct function signatures.
        # See 'SPHINX_BUILD' in conf.py.
        return f

    @functools.wraps(f)
    def wrapper(self, *args, **kwargs):
        if self.stale:
            raise StaleError('stale')

        return f(self, *args, **kwargs)
    return wrapper


class Example(object):

    def __init__(self):
        self.stale = False
        self.value = 0

    @check_stale
    def get(self):
        """docstring"""
        return self.value

    @check_stale
    def calculate(self, a, b, c):
        """docstring"""
        return self.value + a + b + c


class TestCase(unittest.TestCase):

    def test_example(self):

        example = Example()
        self.assertEqual(example.get(), 0)

        example.value = 1
        example.stale = True
        self.assertRaises(StaleError, example.get)

        example.stale = False
        self.assertEqual(example.calculate(1, 1, 1), 4)


if __name__ == '__main__':
    unittest.main()
davvid
  • 339
  • 3
  • 4
  • 3
    I believe you are being down voted because the question was regarding the method signature, not the docs. Additionally, functionality you describe is exactly what is provided by the Python standard lib with http://docs.python.org/2/library/functools.html#functools.wraps – adam Nov 06 '12 at 19:59
  • It's insufficient anyway. – Asclepius Jul 16 '13 at 15:27
  • Thanks for the note, I must've misread the question and didn't test it. The answer has been replaced with a solution that addresses the method signatures. I verified it with a local sphinx project as well (omitted for brevity). cheers! – davvid Jul 22 '17 at 05:29