sphinx-doc / sphinx

The Sphinx documentation generator
https://www.sphinx-doc.org/
Other
6.59k stars 2.12k forks source link

Issue documenting decorators that return classes. #10221

Open trevorflahardy opened 2 years ago

trevorflahardy commented 2 years ago

Describe the bug

The issue

Let's propose this simple example, and then I'll walk through its use case.

from typing import Callable, ParamSpec, TypeVar, Generic

T = TypeVar('T')
P = ParamSpec('P')

class Command(Generic[Callable[P, T]]):
    def __init__(self, function: Callable[P, T], **kwargs) -> None:
        self.function = function
        self.kwargs = kwargs
        self.__doc__ = function.__doc__

    def __call__(self, *args: P.args, **kwargs: P.kwargs) -> T:
        return self.function(**self.kwargs)

def to_command(**kwargs):
    def wrapped(func: Callable) -> Command:
        new = Command(**kwargs)
        return new

    return wrapped

# In the class that sphinx should be documenting:
@to_command(name='foo', description='bar')
async def foo_bar(self, *, name: str, description: str) -> None:
    """This is a doc string that isnt showing up"""
    pass

In this example, we use a decorator to return a new class called a Command. If I tried to print(foo_bar.function), I would get the actual callback, good.

What I'm actually using it for

Using my discord.py library, I want to document a command addon I've started updating. Each extension in this addon has a main class with a bunch of these Command objects in it, and each has their own unique doc string. When sphinx documents a class like this, the docs do not show correctly. You can see this here. Looking in sphinx autodoc documentation, I found this:

If you document decorated functions or methods, keep in mind that autodoc retrieves its docstrings by importing the module and inspecting the doc attribute of the given function or method. That means that if a decorator replaces the decorated function with another, it must copy the original doc to the new function.

Although this does help a bit, I'm replacing the decorated function with a new class instance, not a new function. Has anyone had an issue like this before or know how to fix it?

How the example shown above was documented

View how this example was documented:

PythonFeature
~~~~~~~~~~~~~

.. attributetable:: PythonFeature

.. autoclass:: PythonFeature()
    :members:

How to Reproduce

Have a decorator replace the function it's decorating with a new class instance. Make sure the class instance has a __doc__ attribute. Then try to use .. autoclass to document the class.

Expected behavior

Hopefully speaking, the function to be documented as normal regardless of it being decorated and replaced by a class.

Your project

https://github.com/NextChai/chai-jishaku/tree/master/docs

Screenshots

No response

OS

Win

Python version

3.8

Sphinx version

Recent

Sphinx extensions

sphinx.ext.autodoc

Extra tools

No response

Additional context

No response

tk0miya commented 2 years ago

It seems your example depends on the newer typing module since 3.11. I guess your example does not require it. Could you update it not to use it?

And I also can't see a real case in https://chai-jishaku.readthedocs.io/en/latest/commands.html#jishaku.features.PythonFeature.jsk_disassemble. Where can I see it?

ggydush commented 1 year ago

I ran into a similar issue using autodoc when a function is decorated, but the decorator returns a class. If I document the function with autoclass it is picked up, but it is not picked up with automodule. Taking a similar approach that was done in Celery where a custom FunctionDocumenter resolved, but am wondering if there's a cleaner way to handle this.