Патч __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=***):
...
Как это сделать элегантно?
Ответы
Ответ 1
Когда вы запускаете __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
TL;DR;
a()
не вызывает a.__call__
. Он вызывает type(a).__call__(a)
.
Ссылки
Есть хорошее объяснение, почему это происходит в ответе на "Почему type(x).__enter__(x)
вместо x.__enter__()
в стандартном контекстном списке Python?" .
Это поведение документировано в документации Python по Специальный поиск метода.
Ответ 2
[EDIT]
Возможно, самая интересная часть этого вопроса - Почему я не могу заплатить 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
code:
>>> 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
).
Ответ 3
Как прокомментировал вопрос, поскольку datetime.datetime написано на C, Mock не может заменить атрибуты в классе (см. Mocking 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
, и исправлять их. Это, по сути, то, что делает фризегун. Он принимает объект, издевающийся datetime
, итерации через sys.modules
, чтобы найти, где datetime
был импортирован и заменяет каждый экземпляр. Я предполагаю, что это спорно, можно ли сделать это элегантно в одной функции.