sphinx-doc / sphinx

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

Decorator factory all documented as "alias of" #7761

Open ZaxR opened 4 years ago

ZaxR commented 4 years ago

Describe the bug I have a library module where functions have decorator versions generated for them via a factory function. The newly generated decorators have docstrings inherited from the base function. Factory code below, for context, see https://github.com/ZaxR/bulwark/blob/master/bulwark/decorators.py:

def decorator_factory(decorator_name, func):
    """Takes in a function and outputs a class that can be used as a decorator."""
    class decorator_name(BaseDecorator):
        __doc__ = func.__doc__
        check_func = staticmethod(func)

    return decorator_name

When I check the docstrings via the code, everything looks fine, but my Sphinx docs show each decorator's docstring as:

alias of bulwark.decorators.decorator_factory.<locals>.decorator_name

See https://bulwark.readthedocs.io/en/latest/_source/bulwark.decorators.html#bulwark.decorators.HasColumns .

To Reproduce Steps to reproduce the behavior:

<Paste your command-line here which cause the problem>

$ git clone https://github.com/ZaxR/bulwark.git
$ cd bulwark
$ pip install -e ".[dev]"
$ cd docs
$ make html
$ # open _build/html/bulwark.decorators.html and see the descriptions

Expected behavior I would like the factory-generated decorators to each show their docstring in the Sphinx docs.

Your project https://github.com/ZaxR/bulwark

Screenshots Example on readthedocs: https://bulwark.readthedocs.io/en/latest/_source/bulwark.decorators.html#bulwark.decorators.

Environment info

Additional context Link above is to the readthedocs, but the same behavior is observed locally as well.

tk0miya commented 4 years ago

I suppose your case is a well-known problem of decorators. functools.wraps() will resolve your case. Please try it. https://docs.python.org/3/library/functools.html#functools.wraps

ZaxR commented 4 years ago

Can you elaborate on what you mean? I'm already using wraps for the function, but this is a class factory. Where would I put the functools.wraps in this?

class BaseDecorator(object):
    def __init__(self, *args, **kwargs):
        ...
    def __call__(self, f):
        @functools.wraps(f)
        def decorated(*args, **kwargs):
            df = f(*args, **kwargs)
            if self.enabled:
                self.check_func(df, **self.check_func_params)
            return df
        return decorated

def decorator_factory(decorator_name, func):
    """Takes in a function and outputs a class that can be used as a decorator."""
    class decorator_name(BaseDecorator):
        __doc__ = func.__doc__
        check_func = staticmethod(func)

    return decorator_name

# Automatically creates decorators for each function in bulwark.checks
this_module = sys.modules[__name__]
check_functions = [func[1]
                   for func in getmembers(ck, isfunction)
                   if func[1].__module__ == 'bulwark.checks']

for func in check_functions:
    decorator_name = snake_to_camel(func.__name__)
    setattr(this_module, decorator_name, decorator_factory(decorator_name, func))

To clarify, the setting of doc is working in python:

>>> import bulwark.decorators as dc
>>> dc.HasColumns.__doc__
'Asserts that `df` has ``columns``\n\n    Args:\n        df (pd.DataFrame): Any pd.DataFrame.\n        columns (list or tuple): Columns that are expected to be in ``df``.\n        exact_cols (bool): Whether or not ``columns`` need to be the only columns in ``df``.\n        exact_order (bool): Whether or not ``columns`` need to be in\n                            the same order as the columns in ``df``.\n\n    Returns:\n        Original `df`.\n\n    '
tk0miya commented 3 years ago

Oh, I'm very sorry. I overlooked your last comment. It seems decorator_name class in the decorator_factory decorator has its original __name__. Please check dc.HasColumns.__name__ (or __qualname__). Indeed, you overrides __doc__ attributes manually. But some other attributes are not.

Please check the document of functools.update_wrapper() (a function called inside functools.wraps()). It describes some attributes are copied. https://docs.python.org/3/library/functools.html#functools.update_wrapper