在Python中模拟ImportError
我已经试了快两个小时了,还是没成功。
我有一个模块,长这样:
try:
from zope.component import queryUtility # and things like this
except ImportError:
# do some fallback operations <-- how to test this?
在代码的后面部分:
try:
queryUtility(foo)
except NameError:
# do some fallback actions <-- this one is easy with mocking
# zope.component.queryUtility to raise a NameError
有没有什么想法?
补充:
Alex的建议似乎不管用:
>>> import __builtin__
>>> realimport = __builtin__.__import__
>>> def fakeimport(name, *args, **kw):
... if name == 'zope.component':
... raise ImportError
... realimport(name, *args, **kw)
...
>>> __builtin__.__import__ = fakeimport
运行测试时:
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
不过,当我在Python交互式控制台中运行同样的代码时,它是能工作的。
更多补充:
我在使用zope.testing
和一个测试文件shorturl.txt
,这个文件里有我模块特定的所有测试。首先,我导入了可用的zope.component
模块,以展示和测试通常的用法。缺少zope.*
包被认为是一个边缘情况,所以我稍后会测试这个。因此,我必须在某种情况下让zope.*
不可用后,再reload()
我的模块。
到目前为止,我甚至尝试过使用tempfile.mktempdir()
和空的zope/__init__.py
以及zope/component/__init__.py
文件放在临时目录里,然后把临时目录插入到sys.path[0]
,并从sys.modules
中移除旧的zope.*
包。
这也没成功。
更进一步的补充:
与此同时,我尝试了这个:
>>> class NoZope(object):
... def find_module(self, fullname, path):
... if fullname.startswith('zope'):
... raise ImportError
...
>>> import sys
>>> sys.path.insert(0, NoZope())
这在测试套件的命名空间中(也就是在shorturl.txt
中的所有导入)效果很好,但在我的主模块ao.shorturl
中却没有执行。即使我reload()
它也不行。你知道为什么吗?
>>> import zope # ok, this raises an ImportError
>>> reload(ao.shorturl) <module ...>
导入zope.interfaces
时出现了ImportError
,所以它没有执行到我导入zope.component
的部分,并且它仍然在ao.shorturl的命名空间中。为什么?!
>>> ao.shorturl.zope.component # why?!
<module ...>
3 个回答
如果你不介意修改你的程序,可以把导入的代码放在一个函数里,然后在测试的时候调用这个函数。
这是我在单元测试中使用的内容。
它使用了PEP-302 "新的导入钩子"。(警告:PEP-302文档和我链接的简明发布说明并不完全准确。)
我使用meta_path
是因为它在导入顺序中尽可能早。
如果模块已经被导入(就像我这种情况,因为之前的单元测试对它进行了模拟),那么在对依赖模块进行reload
之前,有必要先将其从sys.modules中移除。
# 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)
在pif.index中的代码看起来像这样:
try:
import xdg.BaseDirectory
CONFIG_DIR = os.path.join(xdg.BaseDirectory.xdg_data_home, 'pif')
except ImportError:
CONFIG_DIR = os.path.expanduser('~/.pif')
为了回答关于为什么重新加载的模块同时具有旧的和新的属性的问题,这里有两个示例文件。
第一个是一个模块y
,它有一个导入失败的情况。
# y.py
try:
import sys
_loaded_with = 'sys'
except ImportError:
import os
_loaded_with = 'os'
第二个是x
,它演示了在模块被重新加载时,留下的句柄如何影响其属性。
# 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
你可以在 builtins
里自己写一个版本的 __import__
,这样当它识别到你想要模拟错误的特定模块时,就可以抛出你想要的错误。想了解更多细节,可以查看 官方文档。大致上是这样的:
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
在 ...
的地方,你可以直接写死 name == 'zope.component'
,或者用你自己的回调函数来灵活处理,这样可以根据你的具体测试需求,让导入在不同情况下按需抛出错误,而不需要你写多个类似 __import__
的函数;-)。
另外要注意,如果你用的是 from zope import component
而不是 import zope.component
或 from zope.component import something
,那么 name
就会是 'zope'
,而 'component'
将是 fromlist
中唯一的项目。
编辑:关于 __import__
函数的文档说要导入的名称是 builtin
(就像在 Python 3 中那样),但实际上你需要用 __builtins__
-- 我已经修改了上面的代码,使其两种方式都能工作。