Подтвердить что ты не робот

Патч __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=***):
        ...

Как это сделать элегантно?

4b9b3361

Ответ 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 был импортирован и заменяет каждый экземпляр. Я предполагаю, что это спорно, можно ли сделать это элегантно в одной функции.