13

我一直在尝试为我正在处理的开源项目获取我的文档,该项目涉及镜像客户端和服务器 API。为此,我创建了一个装饰器,它可以在大多数情况下用于记录一个简单地对其输入执行验证的方法。您可以在此处找到一个包含这些方法的类,并在此处找到装饰器的实现。

如您所见,装饰器functools.wraps用于保存文档字符串,我还认为是签名,但是源代码与生成的文档如下所示:

资源:源代码

对比

文件:狮身人面像文档

有谁知道有什么方法可以让setH生成的文档显示正确的调用签名?(没有每个签名的新装饰器 - 我需要镜像数以百计的方法)

我找到了一种解决方法,其中涉及让装饰器不更改未绑定的方法,但让类在绑定时改变方法(对象实例化) - 这似乎是一个 hack,所以对此有任何评论,或者其他方法这将不胜感激。

4

4 回答 4

4

我想避免依赖标准库之外的太多垃圾,所以当我查看装饰器模块时,我主要尝试重现它的功能......不成功......

于是换个角度看问题,现在有了部分可行的解决方案,主要可以看这个commit来描述。它并不完美,因为它依赖于使用部分,这破坏了 REPL 中的帮助。这个想法是,不是替换应用装饰器的函数,而是增加属性。

+def s_repr(obj):
+    """ :param obj: object """
+    return (repr(obj) if not isinstance(obj, SikuliClass)
+            else "self._get_jython_object(%r)" % obj._str_get)
+
+
 def run_on_remote(func):
     ...
-    func.s_repr = lambda obj: (repr(obj)
-                               if not isinstance(obj, SikuliClass) else
-                               "self._get_jython_object(%r)" % obj._str_get)
-
-    def _inner(self, *args):
-        return self.remote._eval("self._get_jython_object(%r).%s(%s)" % (
-            self._id,
-            func.__name__,
-            ', '.join([func.s_repr(x) for x in args])))
-
-    func.func = _inner
+    gjo = "self._get_jython_object"
+    func._augment = {
+        'inner': lambda self, *args: (self.remote._eval("%s(%r).%s(%s)"
+                                      % (gjo, self._id, func.__name__,
+                                         ', '.join([s_repr(x)for x in args]))))
+    }

     @wraps(func)
     def _outer(self, *args, **kwargs):
         func(self, *args, **kwargs)
-        if hasattr(func, "arg"):
-            args, kwargs = func.arg(*args, **kwargs), {}
-        result = func.func(*args, **kwargs)
-        if hasattr(func, "post"):
+        if "arg" in func._augment:
+            args, kwargs = func._augment["arg"](self, *args, **kwargs), {}
+        result = func._augment['inner'](self, *args, **kwargs)
+        if "post" in func._augment:
             return func.post(result)
         else:
             return result

     def _arg(arg_func):
-        func.arg = arg_func
-        return _outer
+        func._augment['arg'] = arg_func
+        return func

     def _post(post_func):
-        func.post = post_func
-        return _outer
+        func._augment['post'] = post_func
+        return func

     def _func(func_func):
-        func.func = func_func
-        return _outer
-    _outer.arg = _arg
-    _outer.post = _post
-    _outer.func = _func
-    return _outer
+        func._augment['inner'] = func_func
+        return func
+
+    func.arg  = _outer.arg = _arg
+    func.post = _outer.post = _post
+    func.func = _outer.func = _func
+    func.run  = _outer.run = _outer
+    return func

所以这实际上并没有改变未绑定的方法,因此生成的文档保持不变。诡计的第二部分发生在类初始化时。

 class ClientSikuliClass(ServerSikuliClass):
     """ Base class for types based on the Sikuli native types """
     ...
     def __init__(self, remote, server_id, *args, **kwargs):
         """
         :type server_id: int
         :type remote: SikuliClient
         """
         super(ClientSikuliClass, self).__init__(None)
+        for key in dir(self):
+            try:
+                func = getattr(self, key)
+            except AttributeError:
+                pass
+            else:
+                try:
+                    from functools import partial, wraps
+                    run = wraps(func.run)(partial(func.run, self))
+                    setattr(self, key, run)
+                except AttributeError:
+                    pass
         self.remote = remote
         self.server_id = server_id

因此,在实例化任何继承类ClientSikuliClass的实例时,会尝试获取该实例的每个属性的 run 属性,并使其在尝试获取该属性时返回,因此绑定方法现在是部分应用_outer函数。

所以这个问题是多方面的:

  1. 在初始化时使用部分会导致丢失绑定的方法信息。
  2. 我担心破坏恰好具有属性的run属性...

因此,虽然我对自己的问题有答案,但我对此并不十分满意。


更新

好的,经过更多的工作,我最终得到了这个:

 class ClientSikuliClass(ServerSikuliClass):
     """ Base class for types based on the Sikuli native types """
     ...
     def __init__(self, remote, server_id, *args, **kwargs):
         """
         :type server_id: int
         :type remote: SikuliClient
         """
         super(ClientSikuliClass, self).__init__(None)
-        for key in dir(self):
+
+        def _apply_key(key):
             try:
                 func = getattr(self, key)
+                aug = func._augment
+                runner = func.run
             except AttributeError:
-                pass
-            else:
-                try:
-                    from functools import partial, wraps
-                    run = wraps(func.run)(partial(func.run, self))
-                    setattr(self, key, run)
-                except AttributeError:
-                    pass
+                return
+
+            @wraps(func)
+            def _outer(*args, **kwargs):
+                return runner(self, *args, **kwargs)
+
+            setattr(self, key, _outer)
+
+        for key in dir(self):
+            _apply_key(key)
+
         self.remote = remote
         self.server_id = server_id

这可以防止对象上的文档丢失。您还将看到 func._augment 属性被访问,即使它没有被使用,所以如果它不存在,对象属性将不会被触及。

如果有人对此有任何意见,我会很感兴趣?

于 2013-01-22T11:20:18.790 回答
4

PRAW中,我通过在 sphinx 构建发生时返回原始函数(而不是修饰函数)的条件装饰器来处理这个问题。

在 PRAW 的 sphinx conf.py 中,我添加了以下内容来确定 SPHINX 当前是否正在构建:

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

然后在 PRAW 中,它的装饰器看起来像:

import os

# Don't decorate functions when building the documentation
IS_SPHINX_BUILD = bool(os.getenv('SPHINX_BUILD'))

def limit_chars(function):
    """Truncate the string returned from a function and return the result."""
    @wraps(function)
    def wrapped(self, *args, **kwargs):
        output_string = function(self, *args, **kwargs)
        if len(output_string) > MAX_CHARS:
            output_string = output_string[:MAX_CHARS - 3] + '...'
        return output_string
    return function if IS_SPHINX_BUILD else wrapped

return function if IS_SPHINX_BUILD else wrapped行允许 SPHINX 获取正确的签名。

相关来源

于 2014-02-25T19:07:31.527 回答
3

functools.wraps只保留__name__,__doc____module__. 要保留签名,请查看 Michele Simionato 的Decorator 模块

于 2013-01-21T22:34:52.737 回答
2

为了扩展我对Ethan 的回答的简短评论,这是我使用该functools包的原始代码:


import functools

def trace(f):
    """The trace decorator."""
    logger = ... # some code to determine the right logger
    where = ... # some code to create a string describing where we are

    @functools.wraps(f)
    def _trace(*args, **kwargs):
        logger.debug("Entering %s", where)
        result = f(*args, **kwargs)
        logger.debug("Leaving %s", where)
        return result

    return _trace

这里是使用decorator包的代码:


import decorator

def trace(f):
    """The trace decorator."""
    logger = ... # some code to determine the right logger
    where = ... # some code to create a string describing where we are

    def _trace(f, *args, **kwargs):
        logger.debug("Entering %s", where)
        result = f(*args, **kwargs)
        logger.debug("Leaving %s", where)
        return result

    return decorator.decorate(f, _trace)

出于性能原因,我们希望将代码移出实际的函数包装器,以确定正确的记录器和 where-string。因此,在两个版本中都使用了嵌套包装函数的方法。

两个版本的代码都适用于 Python 2 和 Python 3,但第二个版本在使用 Sphinx 和 autodoc 时为修饰函数创建了正确的原型(无需在 autodoc 语句中重复原型,如this answer中所建议)。

这是使用 cPython,我没有尝试过 Jython 等。

于 2016-08-29T15:20:51.443 回答