Как удалить декораторы из функции в 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.