¿Puedo parchear un decorador de Python antes de que envuelva una función?

81

Tengo una función con un decorador que estoy intentando probar con la ayuda de la biblioteca Python Mock . Me gustaría usar mock.patchpara reemplazar el decorador real con un decorador "bypass" simulado que simplemente llama a la función.

Lo que no puedo entender es cómo aplicar el parche antes de que el decorador real envuelva la función. Probé algunas variaciones diferentes en el objetivo del parche y reordené el parche y las declaraciones de importación, pero sin éxito. ¿Algunas ideas?

Chris Sears
fuente

Respuestas:

59

Los decoradores se aplican en el momento de la definición de la función. Para la mayoría de las funciones, esto es cuando se carga el módulo. (Las funciones que están definidas en otras funciones tienen el decorador aplicado cada vez que se llama a la función adjunta).

Entonces, si desea parchear a un decorador, lo que debe hacer es:

  1. Importar el módulo que lo contiene
  2. Definir la función de decorador simulado
  3. Establecer por ejemplo module.decorator = mymockdecorator
  4. Importe los módulos que utilizan el decorador o utilícelo en su propio módulo

Si el módulo que contiene el decorador también contiene funciones que lo usan, esas ya están decoradas para cuando pueda verlas, y probablemente sea SOL

Edite para reflejar los cambios en Python desde que escribí esto originalmente: si el decorador usa functools.wraps()y la versión de Python es lo suficientemente nueva, es posible que pueda desenterrar la función original usando el __wrapped__atributo y volver a decorarla, pero esto de ninguna manera garantizado, y el decorador al que desea reemplazar tampoco puede ser el único decorador aplicado.

un poco
fuente
17
Lo siguiente fue una pérdida de mi tiempo: tenga en cuenta que Python solo importa módulos una vez. Si está ejecutando un conjunto de pruebas, tratando de burlarse de un decorador en una de sus pruebas, y la función decorada se importa en otro lugar, burlarse del decorador no tendrá ningún efecto.
Paragon
2
use la reloadfunción incorporada para regenerar el código binario de Python docs.python.org/2/library/functions.html#reload y monkeypatch su decorador
IxDay
2
Me encontré con el problema informado por @Paragon y lo solucioné parcheando mi decorador en el directorio de prueba __init__. Eso aseguró que el parche se cargara antes que cualquier archivo de prueba. Tenemos una carpeta de pruebas aislada, por lo que la estrategia funciona para nosotros, pero es posible que esto no funcione para todos los diseños de carpeta.
Claytond
4
Después de leer esto varias veces, todavía estoy confundido. ¡Esto necesita un ejemplo de código!
ritratt
@claytond ¡Gracias, su solución funcionó para mí ya que tenía una carpeta de pruebas aislada!
Srivathsa
55

Cabe señalar que varias de las respuestas aquí parchearán el decorador para toda la sesión de prueba en lugar de una sola instancia de prueba; que puede ser indeseable. A continuación, se explica cómo parchear un decorador que solo persiste a través de una única prueba.

Nuestra unidad para ser probada con el decorador no deseado:

# app/uut.py

from app.decorators import func_decor

@func_decor
def unit_to_be_tested():
    # Do stuff
    pass

Desde el módulo de decoradores:

# app/decorators.py

def func_decor(func):
    def inner(*args, **kwargs):
        print "Do stuff we don't want in our test"
        return func(*args, **kwargs)
    return inner

Para cuando nuestra prueba se recopila durante una ejecución de prueba, el decorador no deseado ya se ha aplicado a nuestra unidad bajo prueba (porque eso ocurre en el momento de la importación). Para deshacernos de eso, necesitaremos reemplazar manualmente el decorador en el módulo del decorador y luego volver a importar el módulo que contiene nuestra UUT.

Nuestro módulo de prueba:

#  test_uut.py

from unittest import TestCase
from app import uut  # Module with our thing to test
from app import decorators  # Module with the decorator we need to replace
import imp  # Library to help us reload our UUT module
from mock import patch


class TestUUT(TestCase):
    def setUp(self):
        # Do cleanup first so it is ready if an exception is raised
        def kill_patches():  # Create a cleanup callback that undoes our patches
            patch.stopall()  # Stops all patches started with start()
            imp.reload(uut)  # Reload our UUT module which restores the original decorator
        self.addCleanup(kill_patches)  # We want to make sure this is run so we do this in addCleanup instead of tearDown

        # Now patch the decorator where the decorator is being imported from
        patch('app.decorators.func_decor', lambda x: x).start()  # The lambda makes our decorator into a pass-thru. Also, don't forget to call start()          
        # HINT: if you're patching a decor with params use something like:
        # lambda *x, **y: lambda f: f
        imp.reload(uut)  # Reloads the uut.py module which applies our patched decorator

La devolución de llamada de limpieza, kill_patches, restaura el decorador original y lo vuelve a aplicar a la unidad que estábamos probando. De esta manera, nuestro parche solo persiste a través de una única prueba en lugar de toda la sesión, que es exactamente como debería comportarse cualquier otro parche. Además, dado que la limpieza llama a patch.stopall (), podemos iniciar cualquier otro parche en el setUp () que necesitemos y se limpiarán todo en un solo lugar.

Lo importante que hay que entender sobre este método es cómo afectará la recarga a las cosas. Si un módulo tarda demasiado o tiene una lógica que se ejecuta en la importación, es posible que solo deba encogerse de hombros y probar el decorador como parte de la unidad. :( Esperemos que tu código esté mejor escrito que eso. ¿Verdad?

Si a uno no le importa si el parche se aplica a toda la sesión de prueba , la forma más sencilla de hacerlo es en la parte superior del archivo de prueba:

# test_uut.py

from mock import patch
patch('app.decorators.func_decor', lambda x: x).start()  # MUST BE BEFORE THE UUT GETS IMPORTED ANYWHERE!

from app import uut

Asegúrese de parchear el archivo con el decorador en lugar del alcance local de la UUT y de iniciar el parche antes de importar la unidad con el decorador.

Curiosamente, incluso si se detiene el parche, todos los archivos que ya se importaron tendrán el parche aplicado al decorador, que es lo contrario de la situación con la que comenzamos. Tenga en cuenta que este método parcheará cualquier otro archivo en la ejecución de prueba que se importe posteriormente, incluso si ellos mismos no declaran un parche.

usuario2859458
fuente
1
user2859458, esto me ayudó significativamente. La respuesta aceptada es buena, pero esto me explicó las cosas de manera significativa e incluyó múltiples casos de uso en los que es posible que desee algo ligeramente diferente.
Malcolm Jones
1
¡Gracias por esta respuesta! En caso de que esto sea útil para otros, hice una extensión del parche que aún funcionará como administrador de contexto y hará la recarga por usted: gist.github.com/Geekfish/aa43368ceade131b8ed9c822d2163373
Geekfish
12

Cuando me encontré con este problema por primera vez, solía devanar mi cerebro durante horas. Encontré una forma mucho más fácil de manejar esto.

Esto evitará por completo al decorador, como si el objetivo ni siquiera estuviera decorado en primer lugar.

Esto se divide en dos partes. Sugiero leer el siguiente artículo.

http://alexmarandon.com/articles/python_mock_gotchas/

Dos problemas con los que seguí encontrándome:

1.) Burlarse del Decorador antes de importar su función / módulo.

Los decoradores y funciones se definen en el momento en que se carga el módulo. Si no realiza la simulación antes de la importación, se ignorará la simulación. Después de la carga, tienes que hacer un mock.patch.object extraño, que se vuelve aún más frustrante.

2.) Asegúrate de burlarte del camino correcto hacia el decorador.

Recuerde que el parche del decorador del que se está burlando se basa en cómo su módulo carga al decorador, no en cómo su prueba carga al decorador. Es por eso que sugiero usar siempre rutas completas para las importaciones. Esto facilita mucho las pruebas.

Pasos:

1.) La función Mock:

from functools import wraps

def mock_decorator(*args, **kwargs):
    def decorator(f):
        @wraps(f)
        def decorated_function(*args, **kwargs):
            return f(*args, **kwargs)
        return decorated_function
    return decorator

2.) Burlarse del decorador:

2a.) Camino interior con.

with mock.patch('path.to.my.decorator', mock_decorator):
     from mymodule import myfunction

2b.) Parche en la parte superior del archivo o en TestCase.setUp

mock.patch('path.to.my.decorator', mock_decorator).start()

Cualquiera de estas formas le permitirá importar su función en cualquier momento dentro de TestCase o su método / casos de prueba.

from mymodule import myfunction

2.) Utilice una función separada como efecto secundario del mock.patch.

Ahora puedes usar mock_decorator para cada decorador del que quieras burlarte. Tendrás que burlarte de cada decorador por separado, así que ten cuidado con los que echas de menos.

usuario7815681
fuente
1
¡La publicación del blog que citaste me ayudó a entender esto mucho mejor!
ritratt
2

Lo siguiente funcionó para mí:

  1. Elimina la declaración de importación que carga el objetivo de prueba.
  2. Parche el decorador en el inicio de la prueba como se aplicó anteriormente.
  3. Invoque importlib.import_module () inmediatamente después de aplicar el parche para cargar el objetivo de prueba.
  4. Ejecute las pruebas normalmente.

Funcionó a las mil maravillas.

Eric Mintz
fuente
1

Intentamos burlarnos de un decorador que a veces obtiene otro parámetro como una cadena, y otras veces no, por ejemplo:

@myDecorator('my-str')
def function()

OR

@myDecorator
def function()

Gracias a una de las respuestas anteriores, escribimos una función simulada y parcheamos el decorador con esta función simulada:

from mock import patch

def mock_decorator(f):

    def decorated_function(g):
        return g

    if callable(f): # if no other parameter, just return the decorated function
        return decorated_function(f)
    return decorated_function # if there is a parametr (eg. string), ignore it and return the decorated function

patch('path.to.myDecorator', mock_decorator).start()

from mymodule import myfunction

Tenga en cuenta que este ejemplo es bueno para un decorador que no ejecuta la función decorada, solo hace algunas cosas antes de la ejecución real. En caso de que el decorador también ejecute la función decorada y, por tanto, necesite transferir los parámetros de la función, la función mock_decorator tiene que ser un poco diferente.

Espero que esto ayude a otros ...

InbalZelig
fuente
0

Tal vez pueda aplicar otro decorador a las definiciones de todos sus decoradores que básicamente verifique alguna variable de configuración para ver si el modo de prueba está destinado a ser utilizado.
Si es así, reemplaza al decorador que está decorando con un decorador ficticio que no hace nada.
De lo contrario, deja pasar a este decorador.

Aditya Mukherji
fuente
0

Concepto

Esto puede sonar un poco extraño, pero se puede parchear sys.path, con una copia de sí mismo, y realizar una importación dentro del alcance de la función de prueba. El siguiente código muestra el concepto.

from unittest.mock import patch
import sys

@patch('sys.modules', sys.modules.copy())
def testImport():
 oldkeys = set(sys.modules.keys())
 import MODULE
 newkeys = set(sys.modules.keys())
 print((newkeys)-(oldkeys))

oldkeys = set(sys.modules.keys())
testImport()                       -> ("MODULE") # Set contains MODULE
newkeys = set(sys.modules.keys())
print((newkeys)-(oldkeys))         -> set()      # An empty set

MODULEluego se puede sustituir por el módulo que está probando. (Esto funciona en Python 3.6 MODULEsustituido por, xmlpor ejemplo)

OP

Para su caso, digamos que la función decoradora reside en el módulo prettyy la función decorada reside en present, luego parchearía pretty.decoratorusando la maquinaria simulada y sustituiría MODULEpor present. Algo como lo siguiente debería funcionar (no probado).

clase TestDecorator (unittest.TestCase): ...

  @patch(`pretty.decorator`, decorator)
  @patch(`sys.path`, sys.path.copy())
  def testFunction(self, decorator) :
   import present
   ...

Explicación

Esto funciona proporcionando una "limpieza" sys.pathpara cada función de prueba, utilizando una copia de la corriente sys.pathdel módulo de prueba. Esta copia se realiza cuando el módulo se analiza por primera vez, lo que garantiza una coherencia sys.pathpara todas las pruebas.

Matices

Sin embargo, hay algunas implicaciones. Si el marco de prueba ejecuta varios módulos de prueba en la misma sesión de Python, cualquier módulo de prueba que importe MODULEglobalmente rompe cualquier módulo de prueba que lo importe localmente. Esto obliga a realizar la importación localmente en todas partes. Si el marco ejecuta cada módulo de prueba en una sesión de Python separada, esto debería funcionar. De manera similar, no puede importar MODULEglobalmente dentro de un módulo de prueba donde está importando MODULElocalmente.

Las importaciones locales deben realizarse para cada función de prueba dentro de una subclase de unittest.TestCase. Quizás sea posible aplicar esto a la unittest.TestCasesubclase directamente haciendo que una importación particular del módulo esté disponible para todas las funciones de prueba dentro de la clase.

Incorporado Ins

Aquellos que estén jugando con las builtinimportaciones encontrarán que el reemplazo MODULEcon sys, osetc. fallará, ya que estos ya están activados sys.pathcuando intente copiarlos. El truco aquí es invocar Python con las importaciones integradas deshabilitadas, creo que python -X test.pylo haré, pero olvido la bandera apropiada (Ver python --help). Posteriormente, estos pueden importarse localmente utilizando import builtins, IIRC.

Carel
fuente
0

Para parchear un decorador, necesitas importar o recargar el módulo que usa ese decorador después de parchearlo O redefinir la referencia del módulo a ese decorador por completo.

Los decoradores se aplican en el momento en que se importa un módulo. Esta es la razón por la que si importó un módulo que usa un decorador que desea parchear en la parte superior de su archivo e intenta parchearlo más tarde sin volver a cargarlo, el parche no tendrá ningún efecto.

Aquí hay un ejemplo de la primera forma mencionada de hacer esto: recargar un módulo después de parchear un decorador que usa:

import moduleA
...

  # 1. patch the decorator
  @patch('decoratorWhichIsUsedInModuleA', examplePatchValue)
  def setUp(self)
    # 2. reload the module which uses the decorator
    reload(moduleA)

  def testFunctionA(self):
    # 3. tests...
    assert(moduleA.functionA()...

Referencias útiles:

Arthur S
fuente
-2

para @lru_cache (max_size = 1000)


class MockedLruCache(object):

def __init__(self, maxsize=0, timeout=0):
    pass

def __call__(self, func):
    return func

cache.LruCache = MockedLruCache

si usa un decorador que no tiene parámetros, debe:

def MockAuthenticated(func):
    return func

from tornado import web web.authenticated = MockAuthenticated

guochunyang
fuente
1
Veo muchos problemas en esta respuesta. El primero (y el más grande) es que no puede tener acceso a la función original si aún está decorada (ese es el problema OP). Además, no quita el parche una vez finalizada la prueba y eso puede causar problemas cuando lo ejecuta en un conjunto de pruebas.
Michele d'Amico