Mocking ImportError en Python
unit-testing doctest (3)
Intento esto por casi dos horas, sin suerte.
Tengo un módulo que se ve así:
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
¿Algunas ideas?
EDITAR:
La sugerencia de Alex no parece funcionar:
>>> 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:
aatiis@aiur ~/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, funciona cuando ejecuto el mismo código desde 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 de esta parte de mi módulo. Primero estoy importando el módulo con zope.component
disponible, para demostrar y 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()
a reload()
mi módulo, después de hacer zope.*
disponible, de alguna manera.
Hasta ahora, incluso he intentado usar tempfile.mktempdir()
y vaciar los zope/__init__.py
y zope/component/__init__.py
en el tempdir, luego inserto tempdir en sys.path[0]
y sys.path[0]
el viejo zope.*
paquetes de sys.modules
.
Tampoco funcionó.
AÚN MÁS EDITAR:
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 lo reload()
. ¿Alguna idea de por qué?
>>> import zope # ok, this raises an ImportError
>>> reload(ao.shorturl) <module ...>
La importación de zope.interfaces
genera un ImportError
, por lo que no llega a la parte donde importo zope.component
, y permanece en el espacio de nombres ao.shorturl . ¡¿Por qué?!
>>> ao.shorturl.zope.component # why?!
<module ...>
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 concisas que he vinculado no son exactamente exactas ).
Utilizo meta_path
porque es lo más temprano posible en la secuencia de importación.
Si el módulo ya ha sido importado (como en mi caso, debido a que los unittest anteriores se burlan de él), entonces es necesario eliminarlo de sys.modules antes de reload
a 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)
Donde el código dentro de pif.index se ve así:
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 la pregunta sobre por qué el módulo recién recargado tiene propiedades de cargas antiguas y nuevas, aquí hay dos archivos de ejemplo.
El primero es un módulo y
con un caso de falla de importación.
# y.py
try:
import sys
_loaded_with = ''sys''
except ImportError:
import os
_loaded_with = ''os''
El segundo es x
que demuestra cómo dejar identificadores para un módulo puede afectar sus propiedades cuando se recarga.
# 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
Si no le importa cambiar su programa, también puede colocar la llamada de importación en una función y aplicarle un parche en sus pruebas.
Solo revise en el builtins
su propia versión de __import__
- puede generar lo que desee cuando reconozca que se está solicitando en los módulos específicos para los que desea simular errores. Consulte los documentos para obtener información detallada. 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 ...
, puede hardcode name == ''zope.component''
, o arregle las cosas de manera más flexible con una devolución de llamada propia que puede aumentar las importaciones bajo demanda en diferentes casos, dependiendo de sus necesidades específicas de prueba, sin requiriendo que codifiques múltiples funciones __import__
-alike ;-).
Tenga en cuenta también que si lo que usa, en lugar de import zope.component
o from zope.component import something
, es from zope import component
, el name
será ''zope''
, y ''component''
será entonces el único elemento en la lista fromlist
.
Editar : los documentos para la función __import__
dicen que el nombre para importar está builtin
(como en Python 3), pero de hecho necesita __builtins__
- He editado el código anterior para que funcione de cualquier manera.