使用 pytest 在 __init__.py 中测试可选依赖项的导入:Python 3.5 /3.6 行为不同
Test for import of optional dependencies in __init__.py with pytest: Python 3.5 /3.6 differs in behaviour
我有一个适用于 python 3.5 和 3.6 的包,它具有可选的依赖项,我希望在任一版本上进行 运行 测试 (pytest)。
我在下面做了一个由两个文件组成的简化示例,一个简单的 __init__.py
,其中导入了可选包 "requests"(只是一个示例),并设置了一个标志以指示请求的可用性.
mypackage/
├── mypackage
│ └── __init__.py
└── test_init.py
__init__.py
文件内容:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
requests_available = True
try:
import requests
except ImportError:
requests_available = False
test_init.py
文件内容:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import pytest, sys
def test_requests_missing(monkeypatch):
import mypackage
import copy
fakesysmodules = copy.copy(sys.modules)
fakesysmodules["requests"] = None
monkeypatch.delitem(sys.modules,"requests")
monkeypatch.setattr("sys.modules", fakesysmodules)
from importlib import reload
reload(mypackage)
assert mypackage.requests_available == False
if __name__ == '__main__':
pytest.main([__file__, "-vv", "-s"])
test_requests_missing
测试适用于 Python 3.6.5:
runfile('/home/bjorn/python_packages/mypackage/test_init.py', wdir='/home/bjorn/python_packages/mypackage')
============================= test session starts ==============================
platform linux -- Python 3.6.5, pytest-3.6.1, py-1.5.2, pluggy-0.6.0 -- /home/bjorn/anaconda3/envs/bjorn36/bin/python
cachedir: .pytest_cache
rootdir: /home/bjorn/python_packages/mypackage, inifile:
plugins: requests-mock-1.5.0, mock-1.10.0, cov-2.5.1, nbval-0.9.0, hypothesis-3.38.5
collecting ... collected 1 item
test_init.py::test_requests_missing PASSED
=========================== 1 passed in 0.02 seconds ===========================
但不在 Python 3.5.4:
runfile('/home/bjorn/python_packages/mypackage/test_init.py', wdir='/home/bjorn/python_packages/mypackage')
========================================================= test session starts ==========================================================
platform linux -- Python 3.5.4, pytest-3.6.1, py-1.5.2, pluggy-0.6.0 -- /home/bjorn/anaconda3/envs/bjorn35/bin/python
cachedir: .pytest_cache
rootdir: /home/bjorn/python_packages/mypackage, inifile:
plugins: requests-mock-1.5.0, mock-1.10.0, cov-2.5.1, nbval-0.9.1, hypothesis-3.38.5
collecting ... collected 1 item
test_init.py::test_requests_missing FAILED
=============================================================== FAILURES ===============================================================
________________________________________________________ test_requests_missing _________________________________________________________
monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x7f9a2953acc0>
def test_requests_missing(monkeypatch):
import mypackage
import copy
fakesysmodules = copy.copy(sys.modules)
fakesysmodules["requests"] = None
monkeypatch.delitem(sys.modules,"requests")
monkeypatch.setattr("sys.modules", fakesysmodules)
from importlib import reload
> reload(mypackage)
test_init.py:13:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
../../anaconda3/envs/bjorn35/lib/python3.5/importlib/__init__.py:166: in reload
_bootstrap._exec(spec, module)
<frozen importlib._bootstrap>:626: in _exec
???
<frozen importlib._bootstrap_external>:697: in exec_module
???
<frozen importlib._bootstrap>:222: in _call_with_frames_removed
???
mypackage/__init__.py:8: in <module>
import requests
../../anaconda3/envs/bjorn35/lib/python3.5/site-packages/requests/__init__.py:97: in <module>
from . import utils
.... VERY LONG OUTPUT ....
from . import utils
../../anaconda3/envs/bjorn35/lib/python3.5/site-packages/requests/__init__.py:97: in <module>
from . import utils
<frozen importlib._bootstrap>:968: in _find_and_load
???
<frozen importlib._bootstrap>:953: in _find_and_load_unlocked
???
<frozen importlib._bootstrap>:896: in _find_spec
???
<frozen importlib._bootstrap_external>:1171: in find_spec
???
<frozen importlib._bootstrap_external>:1145: in _get_spec
???
<frozen importlib._bootstrap_external>:1273: in find_spec
???
<frozen importlib._bootstrap_external>:1245: in _get_spec
???
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
name = 'requests', location = '/home/bjorn/anaconda3/envs/bjorn35/lib/python3.5/site-packages/requests/__init__.py'
> ???
E RecursionError: maximum recursion depth exceeded
<frozen importlib._bootstrap_external>:575: RecursionError
======================================================= 1 failed in 2.01 seconds =======================================================
我有两个问题:
为什么我会看到这种差异?相关包在 3.5 和 3.6 上似乎是相同的版本。
有没有更好的方法来做我想做的事?我现在拥有的代码是从网上找到的示例拼接在一起的。我试图修补导入机制以试图避免"reload",但我没有成功。
如果测试测试可选功能,如果缺少该功能,则应跳过而不是通过。
test.support.import_module()
是 Python 自动测试套件中使用的函数,用于在缺少模块时跳过测试或测试文件:
import test.support
import unittest
nonexistent = test.support.import_module("nonexistent")
class TestDummy(unittest.testCase):
def test_dummy():
self.assertTrue(nonexistent.vaporware())
然后,当运行:
> python -m py.test -rs t.py
<...>
collected 0 items / 1 skipped
=========================== short test summary info ===========================
SKIP [1] C:\Python27\lib\test\support\__init__.py:90: SkipTest: No module named
nonexistent
========================== 1 skipped in 0.05 seconds ==========================
我要么模拟 __import__
function(在 import modname
语句后面调用的那个),要么通过添加自定义元路径查找器来自定义导入机制。示例:
改变sys.meta_path
添加自定义 MetaPathFinder
实现,在尝试导入 pkgnames
中的任何包时引发 ImportError
:
class PackageDiscarder:
def __init__(self):
self.pkgnames = []
def find_spec(self, fullname, path, target=None):
if fullname in self.pkgnames:
raise ImportError()
@pytest.fixture
def no_requests():
sys.modules.pop('requests', None)
d = PackageDiscarder()
d.pkgnames.append('requests')
sys.meta_path.insert(0, d)
yield
sys.meta_path.remove(d)
@pytest.fixture(autouse=True)
def cleanup_imports():
yield
sys.modules.pop('mypackage', None)
def test_requests_available():
import mypackage
assert mypackage.requests_available
@pytest.mark.usefixtures('no_requests2')
def test_requests_missing():
import mypackage
assert not mypackage.requests_available
fixture no_requests
将在调用时更改 sys.meta_path
,因此自定义元路径查找器从可以导入的包名称中过滤掉 requests
包名称(我们不能raise 任何导入或 pytest
本身都会中断)。 cleanup_imports
只是为了确保每次测试都会重新导入mypackage
。
嘲笑__import__
import builtins
import sys
import pytest
@pytest.fixture
def no_requests(monkeypatch):
import_orig = builtins.__import__
def mocked_import(name, globals, locals, fromlist, level):
if name == 'requests':
raise ImportError()
return import_orig(name, locals, fromlist, level)
monkeypatch.setattr(builtins, '__import__', mocked_import)
@pytest.fixture(autouse=True)
def cleanup_imports():
yield
sys.modules.pop('mypackage', None)
def test_requests_available():
import mypackage
assert mypackage.requests_available
@pytest.mark.usefixtures('no_requests')
def test_requests_missing():
import mypackage
assert not mypackage.requests_available
在这里,fixture no_requests
负责将 __import__
函数替换为将在 import requests
尝试时引发的函数,在其余导入中表现良好。
import sys
from unittest.mock import patch
def test_without_dependency(self):
with patch.dict(sys.modules, {'optional_dependency': None}):
# do whatever you want
上面的代码所做的是,它模拟未安装包 optional_dependency
并在 context-manager(with
).[=15= 中的隔离环境中运行测试。 ]
请记住,根据您的用例,您可能需要重新加载 module
正在测试的
import sys
from unittest.mock import patch
from importlib import reload
def test_without_dependency(self):
with patch.dict(sys.modules, {'optional_dependency': None}):
reload(sys.modules['my_module_under_test'])
# do whatever you want
我有一个适用于 python 3.5 和 3.6 的包,它具有可选的依赖项,我希望在任一版本上进行 运行 测试 (pytest)。
我在下面做了一个由两个文件组成的简化示例,一个简单的 __init__.py
,其中导入了可选包 "requests"(只是一个示例),并设置了一个标志以指示请求的可用性.
mypackage/
├── mypackage
│ └── __init__.py
└── test_init.py
__init__.py
文件内容:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
requests_available = True
try:
import requests
except ImportError:
requests_available = False
test_init.py
文件内容:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import pytest, sys
def test_requests_missing(monkeypatch):
import mypackage
import copy
fakesysmodules = copy.copy(sys.modules)
fakesysmodules["requests"] = None
monkeypatch.delitem(sys.modules,"requests")
monkeypatch.setattr("sys.modules", fakesysmodules)
from importlib import reload
reload(mypackage)
assert mypackage.requests_available == False
if __name__ == '__main__':
pytest.main([__file__, "-vv", "-s"])
test_requests_missing
测试适用于 Python 3.6.5:
runfile('/home/bjorn/python_packages/mypackage/test_init.py', wdir='/home/bjorn/python_packages/mypackage')
============================= test session starts ==============================
platform linux -- Python 3.6.5, pytest-3.6.1, py-1.5.2, pluggy-0.6.0 -- /home/bjorn/anaconda3/envs/bjorn36/bin/python
cachedir: .pytest_cache
rootdir: /home/bjorn/python_packages/mypackage, inifile:
plugins: requests-mock-1.5.0, mock-1.10.0, cov-2.5.1, nbval-0.9.0, hypothesis-3.38.5
collecting ... collected 1 item
test_init.py::test_requests_missing PASSED
=========================== 1 passed in 0.02 seconds ===========================
但不在 Python 3.5.4:
runfile('/home/bjorn/python_packages/mypackage/test_init.py', wdir='/home/bjorn/python_packages/mypackage')
========================================================= test session starts ==========================================================
platform linux -- Python 3.5.4, pytest-3.6.1, py-1.5.2, pluggy-0.6.0 -- /home/bjorn/anaconda3/envs/bjorn35/bin/python
cachedir: .pytest_cache
rootdir: /home/bjorn/python_packages/mypackage, inifile:
plugins: requests-mock-1.5.0, mock-1.10.0, cov-2.5.1, nbval-0.9.1, hypothesis-3.38.5
collecting ... collected 1 item
test_init.py::test_requests_missing FAILED
=============================================================== FAILURES ===============================================================
________________________________________________________ test_requests_missing _________________________________________________________
monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x7f9a2953acc0>
def test_requests_missing(monkeypatch):
import mypackage
import copy
fakesysmodules = copy.copy(sys.modules)
fakesysmodules["requests"] = None
monkeypatch.delitem(sys.modules,"requests")
monkeypatch.setattr("sys.modules", fakesysmodules)
from importlib import reload
> reload(mypackage)
test_init.py:13:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
../../anaconda3/envs/bjorn35/lib/python3.5/importlib/__init__.py:166: in reload
_bootstrap._exec(spec, module)
<frozen importlib._bootstrap>:626: in _exec
???
<frozen importlib._bootstrap_external>:697: in exec_module
???
<frozen importlib._bootstrap>:222: in _call_with_frames_removed
???
mypackage/__init__.py:8: in <module>
import requests
../../anaconda3/envs/bjorn35/lib/python3.5/site-packages/requests/__init__.py:97: in <module>
from . import utils
.... VERY LONG OUTPUT ....
from . import utils
../../anaconda3/envs/bjorn35/lib/python3.5/site-packages/requests/__init__.py:97: in <module>
from . import utils
<frozen importlib._bootstrap>:968: in _find_and_load
???
<frozen importlib._bootstrap>:953: in _find_and_load_unlocked
???
<frozen importlib._bootstrap>:896: in _find_spec
???
<frozen importlib._bootstrap_external>:1171: in find_spec
???
<frozen importlib._bootstrap_external>:1145: in _get_spec
???
<frozen importlib._bootstrap_external>:1273: in find_spec
???
<frozen importlib._bootstrap_external>:1245: in _get_spec
???
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
name = 'requests', location = '/home/bjorn/anaconda3/envs/bjorn35/lib/python3.5/site-packages/requests/__init__.py'
> ???
E RecursionError: maximum recursion depth exceeded
<frozen importlib._bootstrap_external>:575: RecursionError
======================================================= 1 failed in 2.01 seconds =======================================================
我有两个问题:
为什么我会看到这种差异?相关包在 3.5 和 3.6 上似乎是相同的版本。
有没有更好的方法来做我想做的事?我现在拥有的代码是从网上找到的示例拼接在一起的。我试图修补导入机制以试图避免"reload",但我没有成功。
如果测试测试可选功能,如果缺少该功能,则应跳过而不是通过。
test.support.import_module()
是 Python 自动测试套件中使用的函数,用于在缺少模块时跳过测试或测试文件:
import test.support
import unittest
nonexistent = test.support.import_module("nonexistent")
class TestDummy(unittest.testCase):
def test_dummy():
self.assertTrue(nonexistent.vaporware())
然后,当运行:
> python -m py.test -rs t.py
<...>
collected 0 items / 1 skipped
=========================== short test summary info ===========================
SKIP [1] C:\Python27\lib\test\support\__init__.py:90: SkipTest: No module named
nonexistent
========================== 1 skipped in 0.05 seconds ==========================
我要么模拟 __import__
function(在 import modname
语句后面调用的那个),要么通过添加自定义元路径查找器来自定义导入机制。示例:
改变sys.meta_path
添加自定义 MetaPathFinder
实现,在尝试导入 pkgnames
中的任何包时引发 ImportError
:
class PackageDiscarder:
def __init__(self):
self.pkgnames = []
def find_spec(self, fullname, path, target=None):
if fullname in self.pkgnames:
raise ImportError()
@pytest.fixture
def no_requests():
sys.modules.pop('requests', None)
d = PackageDiscarder()
d.pkgnames.append('requests')
sys.meta_path.insert(0, d)
yield
sys.meta_path.remove(d)
@pytest.fixture(autouse=True)
def cleanup_imports():
yield
sys.modules.pop('mypackage', None)
def test_requests_available():
import mypackage
assert mypackage.requests_available
@pytest.mark.usefixtures('no_requests2')
def test_requests_missing():
import mypackage
assert not mypackage.requests_available
fixture no_requests
将在调用时更改 sys.meta_path
,因此自定义元路径查找器从可以导入的包名称中过滤掉 requests
包名称(我们不能raise 任何导入或 pytest
本身都会中断)。 cleanup_imports
只是为了确保每次测试都会重新导入mypackage
。
嘲笑__import__
import builtins
import sys
import pytest
@pytest.fixture
def no_requests(monkeypatch):
import_orig = builtins.__import__
def mocked_import(name, globals, locals, fromlist, level):
if name == 'requests':
raise ImportError()
return import_orig(name, locals, fromlist, level)
monkeypatch.setattr(builtins, '__import__', mocked_import)
@pytest.fixture(autouse=True)
def cleanup_imports():
yield
sys.modules.pop('mypackage', None)
def test_requests_available():
import mypackage
assert mypackage.requests_available
@pytest.mark.usefixtures('no_requests')
def test_requests_missing():
import mypackage
assert not mypackage.requests_available
在这里,fixture no_requests
负责将 __import__
函数替换为将在 import requests
尝试时引发的函数,在其余导入中表现良好。
import sys
from unittest.mock import patch
def test_without_dependency(self):
with patch.dict(sys.modules, {'optional_dependency': None}):
# do whatever you want
上面的代码所做的是,它模拟未安装包 optional_dependency
并在 context-manager(with
).[=15= 中的隔离环境中运行测试。 ]
请记住,根据您的用例,您可能需要重新加载 module
正在测试的
import sys
from unittest.mock import patch
from importlib import reload
def test_without_dependency(self):
with patch.dict(sys.modules, {'optional_dependency': None}):
reload(sys.modules['my_module_under_test'])
# do whatever you want