Как удалить декораторы из функции в python
Скажем, у меня есть следующее:
def with_connection(f):
def decorated(*args, **kwargs):
f(get_connection(...), *args, **kwargs)
return decorated
@with_connection
def spam(connection):
# Do something
Я хочу протестировать функцию spam
, не пропуская проблемы с настройкой соединения (или независимо от того, что делает декоратор).
Учитывая spam
, как мне удалить из него декоратор и получить базовую функцию "undecorated"?
Ответы
Ответ 1
В общем случае вы не можете, потому что
@with_connection
def spam(connection):
# Do something
эквивалентно
def spam(connection):
# Do something
spam = with_connection(spam)
что означает, что "оригинальный" спам может даже не существовать. A (не слишком красивый) взлом будет следующим:
def with_connection(f):
def decorated(*args, **kwargs):
f(get_connection(...), *args, **kwargs)
decorated._original = f
return decorated
@with_connection
def spam(connection):
# Do something
spam._original(testcon) # calls the undecorated function
Ответ 2
Решение balpha можно сделать более обобщенным с помощью этого мета-декоратора:
def include_original(dec):
def meta_decorator(f):
decorated = dec(f)
decorated._original = f
return decorated
return meta_decorator
Затем вы можете украсить ваши декораторы с помощью @include_original, и каждый из них будет иметь тестируемую (не декорированную) версию, спрятанную внутри него.
@include_original
def shout(f):
def _():
string = f()
return string.upper()
return _
@shout
def function():
return "hello world"
>>> print function()
HELLO_WORLD
>>> print function._original()
hello world
Ответ 3
Для этого вопроса было немного обновлений. Если вы используете Python 3, вы можете использовать свойство __wrapped__
, которое возвращает завернутую функцию.
Вот пример из Python Cookbook, 3-е издание
>>> @somedecorator
>>> def add(x, y):
... return x + y
...
>>> orig_add = add.__wrapped__
>>> orig_add(3, 4)
7
>>>
См. обсуждение для более подробного использования этого атрибута.
Ответ 4
Вот, FuglyHackThatWillWorkForYourExampleButICantPromiseAnythingElse:
orig_spam = spam.func_closure[0].cell_contents
Изменить. Для функций/методов, украшенных более одного раза и более сложных декораторов, вы можете попробовать использовать следующий код. Он полагается на тот факт, что декорированные функции __name__d отличаются от оригинальной.
def search_for_orig(decorated, orig_name):
for obj in (c.cell_contents for c in decorated.__closure__):
if hasattr(obj, "__name__") and obj.__name__ == orig_name:
return obj
if hasattr(obj, "__closure__") and obj.__closure__:
found = search_for_orig(obj, orig_name)
if found:
return found
return None
>>> search_for_orig(spam, "spam")
<function spam at 0x027ACD70>
Это не дурацкое доказательство. Это не удастся, если имя функции, возвращаемой из декоратора, будет таким же, как и украшенное. Порядок проверок hasattr() также является эвристическим, существуют цепочки отделки, которые в любом случае возвращают неверные результаты.
Ответ 5
Вместо того, чтобы делать..
def with_connection(f):
def decorated(*args, **kwargs):
f(get_connection(...), *args, **kwargs)
return decorated
@with_connection
def spam(connection):
# Do something
orig_spam = magic_hack_of_a_function(spam)
Вы могли бы просто сделать.
def with_connection(f):
....
def spam_f(connection):
...
spam = with_connection(spam_f)
.., который является синтаксисом @decorator
- вы можете, очевидно, получить доступ к исходному spam_f
обычно
Ответ 6
Теперь вы можете использовать пакет undecorated:
>>> from undecorated import undecorated
>>> undecorated(spam)
Он переживает стычки во всех слоях разных декораторов, пока не достигнет нижней функции и не требует изменения оригинальных декораторов. Работает как на python2, так и на python3.
Ответ 7
Обычным подходом к тестированию таких функций является создание любых зависимостей, таких как get_connection, настраиваемых. Затем вы можете переопределить его с макетом во время тестирования. В основном это то же самое, что и инъекция зависимостей в мире Java, но намного проще благодаря динамической природе Pythons.
Код для него может выглядеть примерно так:
# decorator definition
def with_connection(f):
def decorated(*args, **kwargs):
f(with_connection.connection_getter(), *args, **kwargs)
return decorated
# normal configuration
with_connection.connection_getter = lambda: get_connection(...)
# inside testsuite setup override it
with_connection.connection_getter = lambda: "a mock connection"
В зависимости от вашего кода вы можете найти лучший объект, чем декоратор, чтобы включить функцию factory. Проблема с наличием на декораторе заключается в том, что вам придется не забывать восстановить его до старого значения в методе teardown.
Ответ 8
Добавьте декоратор do-nothing:
def do_nothing(f):
return f
После определения или импорта with_connection, но прежде чем вы перейдете к методам, использующим его в качестве декоратора, добавьте:
if TESTING:
with_connection = do_nothing
Затем, если вы установите глобальное ИСПЫТАНИЕ на Истину, вы замените на_соединение декоратором do-nothing.