Python 中的模拟 ImportError

发布于 2024-08-25 16:10:39 字数 4524 浏览 9 评论 0原文

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

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

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

有什么想法吗?

编辑:

亚历克斯的建议似乎不起作用:

>>> 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__.pyzope/component/__init__.py 文件在 tempdir 中,然后将 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 ...>

I'm trying this for almost two hours now, without any luck.

I have a module that looks like this:

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

Later in the code:

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

Any ideas?

EDIT:

Alex's suggestion doesn't seem to work:

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

When running the tests:

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

However, it does work when I run the same code from the python interactive console.

MORE EDIT:

I'm using zope.testing and a test file, shorturl.txt that has all the tests specific to this part of my module. First I'm importing the module with zope.component available, to demonstrate & test the usual usage. The absence of zope.* packages is considered an edge-case, so I'm testing it later. Thus, I have to reload() my module, after making zope.* unavailable, somehow.

So far I've even tried using tempfile.mktempdir() and empty zope/__init__.py and zope/component/__init__.py files in the tempdir, then inserting tempdir to sys.path[0], and removing the old zope.* packages from sys.modules.

Didn't work either.

EVEN MORE EDIT:

In the meantime, I've tried this:

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

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

And it works well for the namespace of the test suite (= for all imports in shorturl.txt), but it is not executed in my main module, ao.shorturl. Not even when I reload() it. Any idea why?

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

Importing zope.interfaces raises an ImportError, so it doesn't get to the part where I import zope.component, and it remains in the ao.shorturl namespace. Why?!

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

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

苍景流年 2024-09-01 16:10:39

只需将 Monkeypatch 放入 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__ 类似的函数;-)。

另请注意,如果您使用的不是 import zope.componentfrom zope.component import Something,而是 from zope import component,则name 将是 'zope''component' 将成为 fromlist 中的唯一项目。

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

Just monkeypatch into the builtins your own version of __import__ -- it can raise whatever you wish when it recognizes it's being called on the specific modules for which you want to mock up errors. See the docs for copious detail. Roughly:

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

In lieu of the ..., you can hardcode name == 'zope.component', or arrange things more flexibly with a callback of your own that can make imports raise on demand in different cases, depending on your specific testing needs, without requiring you to code multiple __import__-alike functions;-).

Note also that if what you use, instead of import zope.component or from zope.component import something, is from zope import component, the name will then be 'zope', and 'component' will then be the only item in the fromlist.

Edit: the docs for the __import__ function say that the name to import is builtin (like in Python 3), but in fact you need __builtins__ -- I've edited the code above so that it works either way.

悲念泪 2024-09-01 16:10:39

这就是我在单元测试中所说的。

它使用 PEP-302“新导入挂钩” 。 (警告:我链接的 PEP-302 文档和更简洁的发行说明并不完全准确。)

我使用 meta_path 因为它在导入序列中尽可能早。

如果模块已经导入(就像我的例子一样,因为早期的单元测试会模拟它),那么有必要在依赖模块上重新加载之前将其从 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

This is what I justed in my unittests.

It uses PEP-302 "New Import Hooks". (Warning: the PEP-302 document and the more concise release notes I linked aren't exactly accurate.)

I use meta_path because it's as early as possible in the import sequence.

If the module has already been imported (as in my case, because earlier unittests mock against it), then it's necessary to remove it from sys.modules before doing the reload on the dependent module.

 # 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)

Where the code inside pif.index looks like:

try:
    import xdg.BaseDirectory

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

To answer the question about why the newly reloaded module has properties of the old and new loads, here are two example files.

The first is a module y with an import failure case.

# y.py

try:
    import sys

    _loaded_with = 'sys'
except ImportError:
    import os

    _loaded_with = 'os'

The second is x which demonstrates how leaving handles about for a module can affect its properties when being reloaded.

# 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
懵少女 2024-09-01 16:10:39

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

If you don't mind changing your program itself, you could also put the import call in a function and patch that in your tests.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文