Подтвердить что ты не робот

Получение сфинкса для распознавания правильной подписи

Я пытаюсь получить свою документацию для проекта с открытым исходным кодом, над которым я работаю, что связано с зеркальным API-интерфейсом клиента и сервера. С этой целью я создал декоратор, который большую часть времени может использоваться для документирования метода, который просто выполняет проверку на его входе. Вы можете найти класс, полный этих методов здесь и реализация декоратора здесь.

Декоратор, как вы можете видеть, использует functools.wraps, чтобы сохранить docstring, и я подумал также о сигнатуре, однако исходный код и сгенерированная документация выглядят следующим образом:

Источник: source code

против

Документы: sphinx docs

Кто-нибудь знает какой-либо способ иметь setH сгенерированную документацию, показывающую правильную сигнатуру вызова? (без наличия нового декоратора для каждой подписи - есть hudreds методов, которые мне нужно отразить)

Я нашел обходное решение, связанное с тем, что декоратор не менял несвязанный метод, но, если класс мутировал метод во время привязки (объект-экземпляр объекта) - это похоже на взломать, поэтому любые комментарии к этому или альтернативные способы сделать это, будут оценены.

4b9b3361

Ответ 1

Чтобы развернуть мой короткий комментарий к Ответ этана, вот мой оригинальный код с помощью пакета 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)

Мы хотели переместить код для определения правильного регистратора и где-строка из фактической оболочки функций по соображениям производительности. Следовательно, подход с вложенной функцией обертки в обеих версиях.

Обе версии кода работают на Python 2 и Python 3, но вторая версия создает правильные прототипы для украшенных функций при использовании Sphinx и autodoc (без необходимости повторять прототип в операторах autodoc, как это предлагается в этот ответ).

Это с cPython, я не пробовал Jython и т.д.

Ответ 2

Я бы хотел не полагаться на слишком гадость за пределами стандартной библиотеки, поэтому, пока я смотрел на модуль Decorator, я в основном пытался воспроизвести его функциональность.... Неудачно...

Итак, я взглянул на проблему под другим углом, и теперь у меня есть частично работающее решение, которое можно в основном описать, просто глядя на эту фиксацию. Он не идеален, поскольку он полагается на использование частичного, которое сжимает помощь в 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.

Таким образом, проблемы с этим несколько:

  • Использование частичного при инициализации приводит к потере информации связанного метода.
  • Я беспокоюсь о атрибутах clobbering, которые просто имеют атрибут run...

Поэтому, когда у меня есть ответ на мой собственный вопрос, я не совсем удовлетворен этим.


Update

Итак, после немного дополнительной работы я закончил с этим:

 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 доступен, хотя он не используется, так что, если он не существует, атрибут объекта не будет затронут.

Мне было бы интересно, есть ли у кого-нибудь замечания по этому поводу?

Ответ 3

В PRAW я справился с этой проблемой, получив условные декораторы, которые возвращают исходную функцию (а не украшенную функцию), когда сфинкс происходит сборка.

В 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 подбирать правильную подпись.

Соответствующий источник

Ответ 4

functools.wraps сохраняет только __name__, __doc__ и __module__. Чтобы сохранить подпись, посмотрите на Michele Simionato Decorator module.