2010-03-20 10 views
6

Intento esto por casi dos horas, sin suerte.Mocking ImportError en Python

que tienen un módulo que tiene este aspecto:

try: 
    from zope.component import queryUtility # and things like this 
except ImportError: 
    # do some fallback operations <-- how to test this? 

Más adelante en el código:

try: 
    queryUtility(foo) 
except NameError: 
    # do some fallback actions <-- this one is easy with mocking 
    # zope.component.queryUtility to raise a NameError 

¿Alguna idea?

EDIT:

no parece la sugerencia de Alex a trabajar:

>>> import __builtin__ 
>>> realimport = __builtin__.__import__ 
>>> def fakeimport(name, *args, **kw): 
...  if name == 'zope.component': 
...   raise ImportError 
...  realimport(name, *args, **kw) 
... 
>>> __builtin__.__import__ = fakeimport 

al ejecutar las pruebas:

[email protected] ~/work/ao.shorturl $ ./bin/test --coverage . 
Running zope.testing.testrunner.layer.UnitTests tests: 
    Set up zope.testing.testrunner.layer.UnitTests in 0.000 seconds. 


Error in test /home/aatiis/work/ao.shorturl/src/ao/shorturl/shorturl.txt 
Traceback (most recent call last): 
    File "/usr/lib64/python2.5/unittest.py", line 260, in run 
    testMethod() 
    File "/usr/lib64/python2.5/doctest.py", line 2123, in runTest 
    test, out=new.write, clear_globs=False) 
    File "/usr/lib64/python2.5/doctest.py", line 1361, in run 
    return self.__run(test, compileflags, out) 
    File "/usr/lib64/python2.5/doctest.py", line 1282, in __run 
    exc_info) 
    File "/usr/lib64/python2.5/doctest.py", line 1148, in report_unexpected_exception 
    'Exception raised:\n' + _indent(_exception_traceback(exc_info))) 
    File "/usr/lib64/python2.5/doctest.py", line 1163, in _failure_header 
    out.append(_indent(source)) 
    File "/usr/lib64/python2.5/doctest.py", line 224, in _indent 
    return re.sub('(?m)^(?!$)', indent*' ', s) 
    File "/usr/lib64/python2.5/re.py", line 150, in sub 
    return _compile(pattern, 0).sub(repl, string, count) 
    File "/usr/lib64/python2.5/re.py", line 239, in _compile 
    p = sre_compile.compile(pattern, flags) 
    File "/usr/lib64/python2.5/sre_compile.py", line 507, in compile 
    p = sre_parse.parse(p, flags) 
AttributeError: 'NoneType' object has no attribute 'parse' 



Error in test BaseShortUrlHandler (ao.shorturl) 
Traceback (most recent call last): 
    File "/usr/lib64/python2.5/unittest.py", line 260, in run 
    testMethod() 
    File "/usr/lib64/python2.5/doctest.py", line 2123, in runTest 
    test, out=new.write, clear_globs=False) 
    File "/usr/lib64/python2.5/doctest.py", line 1351, in run 
    self.debugger = _OutputRedirectingPdb(save_stdout) 
    File "/usr/lib64/python2.5/doctest.py", line 324, in __init__ 
    pdb.Pdb.__init__(self, stdout=out) 
    File "/usr/lib64/python2.5/pdb.py", line 57, in __init__ 
    cmd.Cmd.__init__(self, completekey, stdin, stdout) 
    File "/usr/lib64/python2.5/cmd.py", line 90, in __init__ 
    import sys 
    File "<doctest shorturl.txt[10]>", line 4, in fakeimport 
NameError: global name 'realimport' is not defined 

Sin embargo, hace trabajo cuando corro el mismo código de la consola interactiva de python.

más Editar:

estoy usando zope.testing y un archivo de prueba, shorturl.txt que tiene todas las pruebas específicas para esta parte de mi módulo. Primero estoy importando el módulo con zope.component disponible, para demostrar & probar el uso habitual. La ausencia de paquetes zope.* se considera un caso extremo, así que lo estoy probando más tarde. Por lo tanto, tengo que reload() mi módulo, después de hacer zope.* no está disponible, de alguna manera.

Hasta ahora incluso he intentado usar tempfile.mktempdir() y vacío zope/__init__.py y zope/component/__init__.py archivos en el tempdir, a continuación, insertar tempdir a sys.path[0], y la eliminación de los viejos zope.* paquetes de sys.modules.

No funcionó tampoco.

AÚN MÁS EDIT:

Mientras tanto, he intentado esto:

>>> class NoZope(object): 
...  def find_module(self, fullname, path): 
...   if fullname.startswith('zope'): 
...    raise ImportError 
... 

>>> import sys 
>>> sys.path.insert(0, NoZope()) 

Y funciona bien para el espacio de nombres del conjunto de pruebas (= para todas las importaciones en shorturl.txt) , pero no se ejecuta en mi módulo principal, ao.shorturl. Ni siquiera cuando yo reload() él. ¿Alguna idea de por qué?

>>> import zope # ok, this raises an ImportError 
>>> reload(ao.shorturl) <module ...> 

Importación zope.interfaces plantea una ImportError, por lo que no llega a la parte donde puedo importar zope.component y que permanece en el espacio de nombres ao.shorturl. ¡¿Por qué?!

>>> ao.shorturl.zope.component # why?! 
<module ...> 

Respuesta

9

Sólo monkeypatch en el builtins su propia versión de __import__ - puede aumentar lo que quieran cuando reconoce que está siendo llamado a los módulos específicos para los que desea burlarse de errores. Vea the docs para los detalles copiosos.Aproximadamente:

try: 
    import builtins 
except ImportError: 
    import __builtin__ as builtins 
realimport = builtins.__import__ 

def myimport(name, globals, locals, fromlist, level): 
    if ...: 
     raise ImportError 
    return realimport(name, globals, locals, fromlist, level) 

builtins.__import__ = myimport 

En lugar de la ..., puede codificar name == 'zope.component', o disponer las cosas de manera más flexible con una devolución de llamada de su propia que puede hacer que las importaciones aumentan la demanda en los distintos casos, en función de sus necesidades de pruebas específicas, sin requiriendo que codifiques múltiples __import__ -al igual que funciones ;-).

Tenga en cuenta también que si lo que se utiliza, en lugar de import zope.component o from zope.component import something, es from zope import component, la name será entonces 'zope' y 'component' A continuación tendrá lugar el único elemento de la fromlist.

Editar: la documentación para la función __import__ decir que el nombre de importación es builtin (como en Python 3), pero en realidad lo que necesita __builtins__ - He editado el código anterior para que funcione en ambos sentidos .

+0

¡Ah, gracias! Por alguna razón, traté de hacer 'def __import __()', pero no lo asigné a 'builtin .__ import__'; tonto de mí. Interesante, estaba leyendo tu respuesta aquí: http://stackoverflow.com/questions/2216828/mock-y-of-from-x-import-y-in-doctest-python/2217512#2217512 - ¿Crees? ¿Esta situación sería más fácil si no importara queryUtility al alcance de mi módulo? –

+2

@Attila, si hiciste 'from zope import component' y luego usaste' component.queryUtility', sería más fácil, por ejemplo, usar lo real algunas veces, y una versión falsa/falsa en otros tiempos.Como escribí en esa respuesta, lo recomiendo como algo general, y es parte de la forma en que codificamos Python en Google (a veces una cláusula 'as' para acortar el nombre que uno importa está garantizada, por supuesto, pero eso no t cambiar la semántica). –

+1

Si lo hace 'desde el componente de importación zope', BTW, su función' __import__' -like verá '' zope'' como el argumento 'name', y' 'component'' como un elemento en el argumento 'fromlist' (el único, a menos que lo haga 'desde zope import this, that, component' o similar ;-); así que asegúrese de disparar en consecuencia. –

3

Esto es lo que acerté en mis pruebas de unidad.

Utiliza PEP-302 "New Import Hooks". (Advertencia: el documento PEP-302 y las notas de la versión más concisa he vinculado no son exactamente precisa.)

utilizo meta_path porque es lo más temprano posible en la secuencia de importación.

Si el módulo ya se ha importado (como en mi caso, debido a que los unittest anteriores se burlan de él), entonces es necesario eliminarlo de sys.modules antes de hacer el reload en el módulo dependiente.

Ensure we fallback to using ~/.pif if XDG doesn't exist. 

>>> import sys 

>>> class _(): 
... def __init__(self, modules): 
... self.modules = modules 
... 
... def find_module(self, fullname, path=None): 
... if fullname in self.modules: 
... raise ImportError('Debug import failure for %s' % fullname) 

>>> fail_loader = _(['xdg.BaseDirectory']) 
>>> sys.meta_path.append(fail_loader) 

>>> del sys.modules['xdg.BaseDirectory'] 

>>> reload(pif.index) #doctest: +ELLIPSIS 
<module 'pif.index' from '...'> 

>>> pif.index.CONFIG_DIR == os.path.expanduser('~/.pif') 
True 

>>> sys.meta_path.remove(fail_loader) 

caso de que el código dentro de pif.index parece:

try: 
    import xdg.BaseDirectory 

    CONFIG_DIR = os.path.join(xdg.BaseDirectory.xdg_data_home, 'pif') 
except ImportError: 
    CONFIG_DIR = os.path.expanduser('~/.pif') 

Para responder a la pregunta de por qué el módulo recién recargada tiene propiedades de las viejas y nuevas cargas, aquí hay dos ejemplos archivos.

El primero es un módulo y con un caso de error de importación.

# y.py 

try: 
    import sys 

    _loaded_with = 'sys' 
except ImportError: 
    import os 

    _loaded_with = 'os' 

La segunda es x que demuestra cómo salir de asas para tener un módulo puede afectar a sus propiedades cuando se vuelve a cargar.

# x.py 

import sys 

import y 

assert y._loaded_with == 'sys' 
assert y.sys 

class _(): 
    def __init__(self, modules): 
     self.modules = modules 

    def find_module(self, fullname, path=None): 
     if fullname in self.modules: 
      raise ImportError('Debug import failure for %s' % fullname) 

# Importing sys will not raise an ImportError. 
fail_loader = _(['sys']) 
sys.meta_path.append(fail_loader) 

# Demonstrate that reloading doesn't work if the module is already in the 
# cache. 

reload(y) 

assert y._loaded_with == 'sys' 
assert y.sys 

# Now we remove sys from the modules cache, and try again. 
del sys.modules['sys'] 

reload(y) 

assert y._loaded_with == 'os' 
assert y.sys 
assert y.os 

# Now we remove the handles to the old y so it can get garbage-collected. 
del sys.modules['y'] 
del y 

import y 

assert y._loaded_with == 'os' 
try: 
    assert y.sys 
except AttributeError: 
    pass 
assert y.os 
+0

Genial, ahora logré subir un 'ImportError', que es todo lo que necesito. Lo interesante es esto: si recargo 'ao.shorturl', y en él tengo' try: import zope.component, zope.interface; excepto ImportError: fallback() ', y obtengo el primer' ImportError' para 'zope.component', ** zope.interface seguirá estando disponible en ao.shorturl (ao.shorturl.zope.interface) **. ¿Porqué es eso? –

+0

Acabo de agregar una sección adicional que describe por qué sucede eso. tl; dr, necesitas 'del ao.shorturl' antes de tu' reload'. –

0

Si no le importa cambiar su programa en sí, también se puede poner la llamada de importación en una función y el parche que en sus pruebas.

Cuestiones relacionadas