Получение сфинкса для распознавания правильной подписи
Я пытаюсь получить свою документацию для проекта с открытым исходным кодом, над которым я работаю, что связано с зеркальным API-интерфейсом клиента и сервера. С этой целью я создал декоратор, который большую часть времени может использоваться для документирования метода, который просто выполняет проверку на его входе. Вы можете найти класс, полный этих методов здесь и реализация декоратора здесь.
Декоратор, как вы можете видеть, использует functools.wraps
, чтобы сохранить docstring, и я подумал также о сигнатуре, однако исходный код и сгенерированная документация выглядят следующим образом:
Источник: ![source code]()
против
Документы: ![sphinx docs]()
Кто-нибудь знает какой-либо способ иметь setH
сгенерированную документацию, показывающую правильную сигнатуру вызова? (без наличия нового декоратора для каждой подписи - есть hudreds методов, которые мне нужно отразить)
Я нашел обходное решение, связанное с тем, что декоратор не менял несвязанный метод, но, если класс мутировал метод во время привязки (объект-экземпляр объекта) - это похоже на взломать, поэтому любые комментарии к этому или альтернативные способы сделать это, будут оценены.
Ответы
Ответ 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.