Python Sphinx autodoc 和装饰成员

发布于 2024-09-19 12:05:09 字数 631 浏览 6 评论 0原文

我正在尝试使用 Sphinx 来记录我的 Python 类。我使用 autodoc 来执行此操作:

.. autoclass:: Bus
   :members:

虽然它正确地获取了我的方法的文档字符串,但那些修饰过的文档字符串:

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

with @checkStale

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

的原型不正确,例如 open(*args, **kwargs )

我该如何解决这个问题?我的印象是使用 @wraps 可以解决这种问题。

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.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(9

歌枕肩 2024-09-26 12:05:09

我对 celery @task 装饰器也有同样的问题。

您还可以通过在第一个文件中添加正确的函数签名来修复此问题,如下所示:

.. autoclass:: Bus
    :members:

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

它仍然会自动记录非装饰器成员。

sphinx 文档中提到了这一点 http://www .sphinx-doc.org/en/master/ext/autodoc.html#directive-automodule - 搜索“如果方法的签名被装饰器隐藏,这很有用。”

就我而言,我必须使用 autofunction 在 django 应用程序的tasks.py 模块中指定 celery 任务的签名:

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

    .. autofunction:: funct1(user_id)
    .. autofunction:: func2(iterations)

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)
源来凯始玺欢你 2024-09-26 12:05:09

扩展我的评论:

您是否尝试过使用装饰器包并将@decorator放在checkStale上?我有
使用带有修饰函数的 epydoc 会出现类似的问题。

正如您在评论中所问的那样,装饰器包不是标准库的一部分。

您可以使用如下代码(未经测试)进行回退:

try:
    from decorator import decorator
except ImportError:
    # No decorator package available. Create a no-op "decorator".
    def decorator(f):
        return f

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
再可℃爱ぅ一点好了 2024-09-26 12:05:09

我刚刚找到了一个适合我的简单解决方案,但不要问我为什么。如果你知道为什么在评论中添加它。

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

装饰函数和装饰器的文档都被保留

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

你列表最软的妹 2024-09-26 12:05:09

添加 '.__ 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

并在修饰函数上添加:

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

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
沧笙踏歌 2024-09-26 12:05:09

在版本 1.1 中添加,您现在可以通过在文档字符串的第一行中提供自定义值来覆盖方法签名。

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

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

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
蒲公英的约定 2024-09-26 12:05:09

如果您特别坚决不添加其他依赖项,这里有一个代码片段,可以通过注入文档字符串来与常规检查器一起使用。这是相当hackey的,并不真正推荐,除非有充分的理由不添加另一个模块,但它就是这样。

# 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

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
扎心 2024-09-26 12:05:09

更新:这可能“不可能”干净地完成,因为 sphinx 使用函数的代码对象来生成其函数签名。但是,由于您使用的是 sphinx,因此有一个可行的解决方法。

它很hacky,因为它在sphinx运行时有效地禁用了装饰器,但它确实有效,所以它是一个实用的解决方案。

首先,我沿着构造新的 types.CodeType 对象的路线,以替换包装器的 func_code 代码对象成员,这是 sphinx 在生成签名时使用的。

我能够通过沿着路线走下去或尝试交换原始函数中代码对象的 co_varnames 、 co_nlocals 等成员来对 python 进行段错误,同时吸引,太复杂了。

下面的解决方案虽然是一个hacky重锤,但也很简单 =)

方法如下:在sphinx内部运行时,设置一个装饰器可以检查的环境变量。在装饰器内部,当检测到 sphinx 时,根本不进行任何装饰,而是返回原始函数。

在 sphinx conf.py 中:

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

然后这是一个示例模块,其中包含一个测试用例,显示了它的外观:

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()

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()
荆棘i 2024-09-26 12:05:09

答案很简单,但我见过的线程都没有提到它。看看 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
    

我不确定这是否会运行,但它说明了这个概念。如果您的包装器在调用者和被调用者之间注入 validated_args,则该示例演示如何使用 orig_method 的元数据更新包装器 (validated_args) 方法>。最终,这将允许 Sphinx 和其他类型分析工具(例如 mypy)(我假设!)查看按预期运行所需的数据。我刚刚完成测试并可以确认它按描述工作,Sphinx autodoc 的行为符合预期。

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.

冰火雁神 2024-09-26 12:05:09

在最新版本的 python 中,您可以从装饰器本身更新装饰函数的签名。例如:

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

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
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文