python - update - Patch__call__ de una función
mongoengine upsert (3)
Necesito parchar la fecha y hora actual en las pruebas. Estoy usando esta solución:
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()
Luego, en mis pruebas, hago algo como:
with mock.patch(''***.utils._utcnow'', return_value=***):
...
Pero hoy se me ocurrió una idea, que podría simplificar la implementación __call__
de la función utcnow
lugar de tener un _utcnow
adicional.
Esto no funciona para mi:
from ***.utils import utcnow
with mock.patch.object(utcnow, ''__call__'', return_value=***):
...
¿Cómo hacer esto elegantemente?
Como se comentó sobre la pregunta, dado que datetime.datetime está escrito en C, Mock no puede reemplazar los atributos de la clase (ver Mocking datetime.today por Ned Batchelder). En cambio, puedes usar Freezegun .
$ pip install freezegun
Aquí hay un ejemplo:
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)
Como mencionas, una alternativa es rastrear cada módulo importando datetime
y aplicarles parches. Esto es en esencia lo que hace Freezegun . Se necesita un objeto que se burla de la datetime
, itera a través de sys.modules
para encontrar dónde se ha importado la fecha y reemplaza cada instancia. Supongo que es discutible si puedes hacer esto elegantemente en una función.
[EDITAR]
Tal vez la parte más interesante de esta pregunta es ¿Por qué no puedo parchar somefunction.__call__
?
Como la función no utiliza el código de __call__
sino __call__
(un objeto de envoltura de método) usa el código de la función.
No encuentro ninguna documentación bien fundada sobre eso, pero puedo probarlo (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>
Reemplace el código de g
por el código de g
:
>>> f.func_code = g.func_code
>>> f()
''g''
>>> f.__call__()
''g''
Por supuesto, las referencias f
y f.__call__
no se modifican:
>>> f
<function f at 0x7f1576381848>
>>> f.__call__
<method-wrapper ''__call__'' of function object at 0x7f1576381848>
Recupere la implementación original y copie __call__
referencias __call__
lugar:
>>> def f():
... return "f"
...
>>> f()
''f''
>>> f.__call__ = g.__call__
>>> f()
''f''
>>> f.__call__()
''g''
Esto no tiene ningún efecto en la función f
. Nota: en Python 3 debe usar __code__
lugar de func_code
.
Espero que alguien pueda señalarme la documentación que explica este comportamiento.
Tienes una forma de utils
: en utils
puedes definir
class Utcnow(object):
def __call__(self):
return datetime.datetime.utcnow()
utcnow = Utcnow()
Y ahora tu parche puede funcionar como un amuleto.
Sigue la respuesta original que considero incluso la mejor manera de implementar tus pruebas.
Tengo mi propia regla de oro : nunca aplique parches a los métodos protegidos . En este caso, las cosas son un poco más suaves porque el método protegido se introdujo solo para probar, pero no puedo ver por qué.
El verdadero problema aquí es que no puede parchear datetime.datetime.utcnow
directamente (es la extensión C como escribió en el comentario anterior). Lo que puede hacer es aplicar un parche a la datetime
y la utcnow
, utcnow
comportamiento estándar y anulando la función utcnow
:
>>> with mock.patch("datetime.datetime", mock.Mock(wraps=datetime.datetime, utcnow=mock.Mock(return_value=3))):
... print(datetime.datetime.utcnow())
...
3
Ok eso no es muy claro y ordenado, pero puedes presentar tu propia función como
def mock_utcnow(return_value):
return mock.Mock(wraps=datetime.datetime,
utcnow=mock.Mock(return_value=return_value)):
y ahora
mock.patch("datetime.datetime", mock_utcnow(***))
haga exactamente lo que necesita sin ninguna otra capa y para cada tipo de importación.
Otra solución puede ser import datetime
in utils
y parchear ***.utils.datetime
; eso puede darle cierta libertad para cambiar la implementación de referencia de datetime
sin cambiar sus pruebas (en este caso, tenga cuidado de cambiar el mock_utcnow()
también).
Cuando rastrea __call__
de una función, está configurando el atributo __call__
de esa instancia . Python realmente llama al método __call__
definido en la clase.
Por ejemplo:
>>> 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
Asignar algo a a.__call__
tiene sentido.
Sin embargo:
>>> A.__call__ = b.__call__
>>> a()
b
TLDR;
a()
no llama a.__call__
. Llama a type(a).__call__(a)
.
Campo de golf
Hay una buena explicación de por qué sucede eso en respuesta a "¿Por qué type(x).__enter__(x)
lugar de x.__enter__()
en Python estándar contextlib?" .
Este comportamiento está documentado en la documentación de Python en la búsqueda de métodos especiales .