una todas que otra modulos llamar lista librerias libreria las importar descargar dentro clases clase python class unit-testing monkeypatching

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()