todas - que es una libreria python
Mono parcheando una clase en otro módulo en Python (6)
Aquí hay un ejemplo que surgió con monkeypatch Popen
usando pytest
.
importar el módulo:
# must be at module level in order to affect the test function context
from some_module import helpers
Un objeto MockBytes
:
class MockBytes(object):
all_read = []
all_write = []
all_close = []
def read(self, *args, **kwargs):
# print(''read'', args, kwargs, dir(self))
self.all_read.append((self, args, kwargs))
def write(self, *args, **kwargs):
# print(''wrote'', args, kwargs)
self.all_write.append((self, args, kwargs))
def close(self, *args, **kwargs):
# print(''closed'', self, args, kwargs)
self.all_close.append((self, args, kwargs))
def get_all_mock_bytes(self):
return self.all_read, self.all_write, self.all_close
Una fábrica de MockPopen
para recolectar el simulacro de popens:
def mock_popen_factory():
all_popens = []
class MockPopen(object):
def __init__(self, args, stdout=None, stdin=None, stderr=None):
all_popens.append(self)
self.args = args
self.byte_collection = MockBytes()
self.stdin = self.byte_collection
self.stdout = self.byte_collection
self.stderr = self.byte_collection
pass
return MockPopen, all_popens
Y una prueba de ejemplo:
def test_copy_file_to_docker():
MockPopen, all_opens = mock_popen_factory()
helpers.Popen = MockPopen # replace builtin Popen with the MockPopen
result = copy_file_to_docker(''asdf'', ''asdf'')
collected_popen = all_popens.pop()
mock_read, mock_write, mock_close = collected_popen.byte_collection.get_all_mock_bytes()
assert mock_read
assert result.args == [''docker'', ''cp'', ''asdf'', ''some_container:asdf'']
Este es el mismo ejemplo, pero usando pytest.fixture
reemplaza la importación de la clase Popen
incorporada dentro de los helpers
:
@pytest.fixture
def all_popens(monkeypatch): # monkeypatch is magically injected
all_popens = []
class MockPopen(object):
def __init__(self, args, stdout=None, stdin=None, stderr=None):
all_popens.append(self)
self.args = args
self.byte_collection = MockBytes()
self.stdin = self.byte_collection
self.stdout = self.byte_collection
self.stderr = self.byte_collection
pass
monkeypatch.setattr(helpers, ''Popen'', MockPopen)
return all_popens
def test_copy_file_to_docker(all_popens):
result = copy_file_to_docker(''asdf'', ''asdf'')
collected_popen = all_popens.pop()
mock_read, mock_write, mock_close = collected_popen.byte_collection.get_all_mock_bytes()
assert mock_read
assert result.args == [''docker'', ''cp'', ''asdf'', ''fastload_cont:asdf'']
Estoy trabajando con un módulo escrito por otra persona. Me gustaría parchear el método __init__
de una clase definida en el módulo. Los ejemplos que he encontrado que muestran cómo hacerlo han supuesto que llamaría yo mismo a la clase (por ejemplo , la clase Python de parche de mono ). Sin embargo, éste no es el caso. En mi caso, la clase se initaliza dentro de una función en otro módulo. Vea el ejemplo (simplificado en gran medida) a continuación:
thirdpartymodule_a.py
class SomeClass(object):
def __init__(self):
self.a = 42
def show(self):
print self.a
thirdpartymodule_b.py
import thirdpartymodule_a
def dosomething():
sc = thirdpartymodule_a.SomeClass()
sc.show()
mymodule.py
import thirdpartymodule_b
thirdpartymodule.dosomething()
¿Hay alguna manera de modificar el método __init__
de SomeClass
para que cuando se llama dosomething desde mymodule.py, por ejemplo, imprima 43 en lugar de 42? Lo ideal sería que pudiera envolver el método existente.
No puedo cambiar los archivos thirdpartymodule * .py, ya que otros scripts dependen de la funcionalidad existente. Prefiero no tener que crear mi propia copia del módulo, ya que el cambio que necesito hacer es muy simple.
Editar 2013-10-24
Pasé por alto un detalle pequeño pero importante en el ejemplo anterior. SomeClass
es importado por thirdpartymodule_b
siguiente manera: from thirdpartymodule_a import SomeClass
.
Para hacer el parche sugerido por FJ, necesito reemplazar la copia en thirdpartymodule_b
, en lugar de thirdpartymodule_a
. por ejemplo, thirdpartymodule_b.SomeClass.__init__ = new_init
.
Lo siguiente debería funcionar:
import thirdpartymodule_a
import thirdpartymodule_b
def new_init(self):
self.a = 43
thirdpartymodule_a.SomeClass.__init__ = new_init
thirdpartymodule_b.dosomething()
Si desea que el nuevo init llame al antiguo init, reemplace la definición new_init()
por lo siguiente:
old_init = thirdpartymodule_a.SomeClass.__init__
def new_init(self, *k, **kw):
old_init(self, *k, **kw)
self.a = 43
Otro posible enfoque, muy similar al de Andrew Clark , es usar la librería wrapt . Entre otras cosas útiles, esta biblioteca proporciona wrap_function_wrapper
y patch_function_wrapper
helpers. Se pueden usar así:
import wrapt
import thirdpartymodule_a
import thirdpartymodule_b
@wrapt.patch_function_wrapper(thirdpartymodule_a.SomeClass, ''__init__'')
def new_init(wrapped, instance, args, kwargs):
# here, wrapped is the original __init__,
# instance is `self` instance (it is not true for classmethods though),
# args and kwargs are tuple and dict respectively.
# first call original init
wrapped(*args, **kwargs) # note it is already bound to the instance
# and now do our changes
instance.a = 43
thirdpartymodule_b.do_something()
O a veces querrá usar wrap_function_wrapper
que no es un decorador, sino que funciona de la misma manera:
def new_init(wrapped, instance, args, kwargs):
pass # ...
wrapt.wrap_function_wrapper(thirdpartymodule_a.SomeClass, ''__init__'', new_init)
Sucio, pero funciona:
class SomeClass2(object):
def __init__(self):
self.a = 43
def show(self):
print self.a
import thirdpartymodule_b
# Monkey patch the class
thirdpartymodule_b.thirdpartymodule_a.SomeClass = SomeClass2
thirdpartymodule_b.dosomething()
# output 43
Una versión solo ligeramente menos hacky usa variables globales como parámetros:
sentinel = False
class SomeClass(object):
def __init__(self):
global sentinel
if sentinel:
<do my custom code>
else:
# Original code
self.a = 42
def show(self):
print self.a
cuando el centinela es falso, actúa exactamente como antes. Cuando es verdad, entonces obtienes tu nuevo comportamiento. En tu código, harías:
import thirdpartymodule_b
thirdpartymodule_b.sentinel = True
thirdpartymodule.dosomething()
thirdpartymodule_b.sentinel = False
Por supuesto, es bastante trivial hacer de esto una solución adecuada sin afectar el código existente. Pero tienes que cambiar el otro módulo ligeramente:
import thirdpartymodule_a
def dosomething(sentinel = False):
sc = thirdpartymodule_a.SomeClass(sentinel)
sc.show()
y pasar a init:
class SomeClass(object):
def __init__(self, sentinel=False):
if sentinel:
<do my custom code>
else:
# Original code
self.a = 42
def show(self):
print self.a
El código existente continuará funcionando; lo llamarán sin argumentos, lo que mantendrá el valor falso predeterminado, que mantendrá el comportamiento anterior. Pero su código ahora tiene una manera de decirle a toda la pila que hay un nuevo comportamiento disponible.
Utilice la biblioteca mock
.
import thirdpartymodule_a
import thirdpartymodule_b
import mock
def new_init(self):
self.a = 43
with mock.patch.object(thirdpartymodule_a.SomeClass, ''__init__'', new_init):
thirdpartymodule_b.dosomething() # -> print 43
thirdpartymodule_b.dosomething() # -> print 42
o
import thirdpartymodule_b
import mock
def new_init(self):
self.a = 43
with mock.patch(''thirdpartymodule_a.SomeClass.__init__'', new_init):
thirdpartymodule_b.dosomething()
thirdpartymodule_b.dosomething()