Патч __call__ функции

Мне нужно исправить текущую дату и время в тестах. Я использую это решение:

def _utcnow():
    return datetime.datetime.utcnow()


def utcnow():
    """A proxy which can be patched in tests.
    """
    # another level of indirection, because some modules import utcnow
    return _utcnow()

Затем в своих тестах я делаю что-то вроде:

    with mock.patch('***.utils._utcnow', return_value=***):
        ...

Но сегодня пришла идея, что можно упростить реализацию, пропатчив __call__ функции utcnow вместо того, чтобы иметь дополнительную _utcnow.

Это не работает для меня:

    from ***.utils import utcnow
    with mock.patch.object(utcnow, '__call__', return_value=***):
        ...

Как это сделать изящно?


person warvariuc    schedule 14.12.2015    source источник
comment
Что не так с with mock.patch('***.utils.utcnow', return_value=***): do_something()?   -  person Łukasz Rogalski    schedule 14.12.2015
comment
@Rogalski это не будет работать в случаях, когда пользовательский код from ***.utils import utcnow содержит ссылку на исходную реализацию.   -  person warvariuc    schedule 14.12.2015
comment
В этом случае вы должны использовать mock.patch('module.which.imported.utcnow'). Это то, что вы хотите? Ваша проблема не в исправлении __call__, а в пространствах имен Python, механизмах импорта и разрешения имен.   -  person Łukasz Rogalski    schedule 14.12.2015
comment
Да, я мог бы сделать так, но тогда я должен отслеживать каждый такой модуль и, возможно, патчить несколько модулей в одном тесте. Я бы лучше пропатчил одно-единственное место.   -  person warvariuc    schedule 14.12.2015
comment
А как насчет патча datetime.datetime.utcnow напрямую? почему ты этого не сделал? Я ненавижу исправлять внутренний или защищенный метод.   -  person Michele d'Amico    schedule 18.12.2015
comment
@Micheled'Amico › Во-первых, datetime.datetime написан на C, поэтому Mock не может заменить атрибуты в классе, поэтому вы не можете просто смоделировать только функцию today(). nedbatchelder.com/blog/201209/mocking_datetimetoday.html   -  person warvariuc    schedule 18.12.2015
comment
@warvariuc или исправьте все datetime.datetime   -  person Michele d'Amico    schedule 18.12.2015
comment
Я знаю обо всех других решениях. Я даже уже использую один, как указано в вопросе. Я просто хочу знать, может ли текущее решение быть проще.   -  person warvariuc    schedule 18.12.2015
comment
@warvariuc, хорошо, я объяснил, почему исправление __call__ не работает.   -  person Michele d'Amico    schedule 18.12.2015
comment
@warvariuc Подумайте о том, чтобы изменить принятый ответ на ответ zvone. Его правильный, мой был просто попыткой объяснить то, что я не понял.   -  person Michele d'Amico    schedule 24.12.2015


Ответы (3)


Когда вы исправляете __call__ функции, вы устанавливаете атрибут __call__ этого экземпляра. Python фактически вызывает метод __call__, определенный в классе.

Например:

>>> class A(object):
...     def __call__(self):
...         print 'a'
...
>>> a = A()
>>> a()
a
>>> def b(): print 'b'
...
>>> b()
b
>>> a.__call__ = b
>>> a()
a
>>> a.__call__ = b.__call__
>>> a()
a

Назначать что-либо a.__call__ бессмысленно.

Однако:

>>> A.__call__ = b.__call__
>>> a()
b

TLDR;

a() не звонит a.__call__. Он вызывает type(a).__call__(a).

Ссылки

Есть хорошее объяснение того, почему это происходит, в ответе на "Почему type(x).__enter__(x) вместо x.__enter__() в стандартной контекстной библиотеке Python?".

Это поведение описано в документации по Python в разделе поиск специального метода.

person zvone    schedule 23.12.2015

[ИЗМЕНИТЬ]

Возможно, наиболее интересной частью этого вопроса является Почему я не могу исправить somefunction.__call__?

Потому что функция не использует код __call__, а __call__ (объект-оболочка метода) использует код функции.

Я не нашел никакой документации по этому поводу, но я могу доказать это (Python2.7):

>>> def f():
...     return "f"
... 
>>> def g():
...     return "g"
... 
>>> f
<function f at 0x7f1576381848>
>>> f.__call__
<method-wrapper '__call__' of function object at 0x7f1576381848>
>>> g
<function g at 0x7f15763817d0>
>>> g.__call__
<method-wrapper '__call__' of function object at 0x7f15763817d0>

Замените код f кодом g:

>>> f.func_code = g.func_code
>>> f()
'g'
>>> f.__call__()
'g'

Конечно, ссылки f и f.__call__ не изменяются:

>>> f
<function f at 0x7f1576381848>
>>> f.__call__
<method-wrapper '__call__' of function object at 0x7f1576381848>

Восстановите исходную реализацию и вместо этого скопируйте ссылки __call__:

>>> def f():
...     return "f"
... 
>>> f()
'f'
>>> f.__call__ = g.__call__
>>> f()
'f'
>>> f.__call__()
'g'

Это не влияет на функцию f. Примечание. В Python 3 следует использовать __code__ вместо func_code.

Я надеюсь, что кто-нибудь может указать мне на документацию, объясняющую такое поведение.

У вас есть способ обойти это: в utils вы можете определить

class Utcnow(object):
    def __call__(self):
        return datetime.datetime.utcnow()


utcnow = Utcnow()

И теперь ваш патч может работать как шарм.


Следуйте исходному ответу, который я считаю даже лучшим способом реализации ваших тестов.

У меня есть собственное золотое правило: никогда не исправлять защищенные методы. В этом случае все немного проще, потому что защищенный метод был введен только для тестирования, но я не понимаю, почему.

Настоящая проблема здесь в том, что вы не можете напрямую исправить datetime.datetime.utcnow (это расширение C, как вы написали в комментарии выше). Что вы можете сделать, так это исправить datetime, обернув стандартное поведение и переопределив функцию utcnow:

>>> with mock.patch("datetime.datetime", mock.Mock(wraps=datetime.datetime, utcnow=mock.Mock(return_value=3))):
...  print(datetime.datetime.utcnow())
... 
3

Хорошо, это не очень понятно и аккуратно, но вы можете ввести свою собственную функцию, например

def mock_utcnow(return_value):
    return mock.Mock(wraps=datetime.datetime, 
                     utcnow=mock.Mock(return_value=return_value)):

и сейчас

mock.patch("datetime.datetime", mock_utcnow(***))

делайте именно то, что вам нужно, без какого-либо другого слоя и для каждого вида импорта.

Другим решением может быть импорт datetime в utils и исправление ***.utils.datetime; это может дать вам некоторую свободу для изменения эталонной реализации datetime без изменения ваших тестов (в этом случае также позаботьтесь об изменении аргумента mock_utcnow() wraps).

person Michele d'Amico    schedule 18.12.2015

Как прокомментировал вопрос, поскольку datetime.datetime написан на C, Mock не может заменить атрибуты в классе (см. Насмешка над datetime.today Неда Батчелдера). Вместо этого вы можете использовать freezegun.

$ pip install freezegun

Вот пример:

import datetime

from freezegun import freeze_time

def my_now():
    return datetime.datetime.utcnow()


@freeze_time('2000-01-01 12:00:01')
def test_freezegun():
    assert my_now() == datetime.datetime(2000, 1, 1, 12, 00, 1)

Как вы упомянули, альтернативой является отслеживание каждого модуля, импортирующего datetime, и их исправление. По сути, это то, что делает freezegun. Он берет объект, имитирующий datetime, перебирает sys.modules, чтобы найти, где был импортирован datetime, и заменяет каждый экземпляр. Я думаю, спорно, можно ли сделать это элегантно в одной функции.

person Dag Høidahl    schedule 17.12.2015
comment
Да, я знаю об этом инструменте. Но я не хочу ее использовать - для меня более явно и проще использовать промежуточную функцию. - person warvariuc; 18.12.2015
comment
Если вы добавите информацию, почему исправление __call__ невозможно, я приму ваш ответ. - person warvariuc; 18.12.2015
comment
Я имел в виду, почему я не могу исправить __call__ с помощью mock -- я думаю, это потому, что FunctionType также имеет реализацию C. - person warvariuc; 18.12.2015
comment
@J.F.Sebastian, спасибо за информацию. Даг, я принял другой ответ, поскольку он дает ответ на мой вопрос. - person warvariuc; 19.12.2015
comment
Мой ответ был ответом на вашу проблему, а не на ваш вопрос, так что это достаточно справедливо. - person Dag Høidahl; 19.12.2015