【问题标题】:Mocking ImportError in Python在 Python 中模拟 ImportError
【发布时间】:2010-03-20 01:21:51
【问题描述】:

我现在尝试了将近两个小时,但没有任何运气。

我有一个如下所示的模块:

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() 我的模块。

到目前为止,我什至尝试在 tempdir 中使用 tempfile.mktempdir() 并清空 zope/__init__.pyzope/component/__init__.py 文件,然后将 tempdir 插入 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 ...>

【问题讨论】:

    标签: python unit-testing mocking doctest zope.component


    【解决方案1】:

    只需将猴子补丁添加到您自己的__import__ 版本的builtins 中——当它识别出在您要模拟错误的特定模块上调用它时,它可以引发您希望的任何事情。有关详细信息,请参阅the docs。大致:

    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__-alike 函数;-)。

    另请注意,如果您使用的不是import zope.componentfrom zope.component import something,而是from zope import component,那么name 将是'zope',而'component' 将是@ 中的唯一项目987654335@.

    编辑__import__ 函数的文档说要导入的名称是 builtin(就像在 Python 3 中一样),但实际上你需要 __builtins__——我已经编辑了上面的代码,使其能够以任何方式工作。

    【讨论】:

    • 啊,谢谢!出于某种原因,我尝试做def __import__(),但没有将其分配给builtin.__import__;傻我。有趣的是,我只是在这里阅读您的答案:stackoverflow.com/questions/2216828/… - 如果我不将 queryUtility 导入模块的范围,您认为这会使这种情况变得更容易吗?
    • @Attila,如果您使用了from zope import component,然后使用了component.queryUtility,它会更容易,例如,在某些时候使用真实的东西,以及模拟/伪造其他时候的版本。正如我在那个答案中所写的那样,我确实推荐它作为一个通用的东西,它是我们在 Google 编写 Python 的方式的一部分(当然,有必要使用 as 子句来缩短一个导入的名称,但这并不t 改变语义)。
    • 如果您执行from zope import component,顺便说一句,您的__import__-alike 函数会将'zope' 视为name 参数,并将'component' 视为fromlist 参数中的一个项目(唯一的一个,除非你这样做from zope import this, that, component 或类似的;-);所以一定要相应地触发。
    • 感谢您的推荐,我会采用这种编码风格,因为它在这种情况下似乎很有帮助。但是,我刚刚意识到您的回答对我不起作用(如果我将 builtin 替换为 __builtin__,它确实适用于交互式控制台。)
    • import __builtin__ 在 2.5 中有效——没有 s,“魔术名称”(与文档相矛盾)。再次编辑以修复。
    【解决方案2】:

    这就是我在单元测试中刚刚提到的。

    它使用PEP-302 "New Import Hooks"。 (警告: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
    

    【讨论】:

    • 太好了,现在我设法提出了一个ImportError,这就是我所需要的。有趣的是:如果我重新加载ao.shorturl,其中我有try: import zope.component, zope.interface; except ImportError: fallback(),并且我为zope.component 获得第一个ImportErrorzope.interface 仍将在ao.shorturl 中可用( ao.shorturl.zope.interface)。这是为什么呢?
    • 我刚刚添加了另一部分来描述为什么会发生这种情况。 tl;博士,您需要在 reload 之前 del ao.shorturl
    【解决方案3】:

    如果您不介意更改程序本身,您也可以将导入调用放在一个函数中并在您的测试中对其进行修补。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2017-12-21
      • 2012-10-04
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多