我正在尝试使用Pytest来测试一个较大的(~100k LOC,1k文件)项目,并且还有其他几个类似的项目,我最终也想做同样的事情。这不是standard Python package;它是一个高度定制的系统的一部分,我几乎没有能力改变,至少在短期内。测试模块与代码集成在一起,而不是在一个单独的目录中,这对我们很重要。配置与this question和my answer非常相似,也可能提供有用的背景。
我遇到的问题是项目几乎只使用PEP 420 implicit namespace packages;也就是说,任何包目录中几乎没有__init__.py
个文件。我还没有看到任何情况下包 成为命名空间包,但鉴于这个项目与其他也有Python代码的项目相结合,这可能会发生(或者已经发生)我只是没注意到它。)
考虑一个如下所示的存储库。 (对于它的可运行副本,包括下面描述的测试,从GitHub克隆0cjs/pytest-impl-ns-pkg
。)以下所有测试都假定为project/thing/thing_test.py
。
repo/
project/
util/
thing.py
thing_test.py
我对测试配置有足够的控制权,我可以确保sys.path
已正确设置,以便导入正在测试的代码以使其正常工作。也就是说,以下测试将通过:
def test_good_import():
import project.util.thing
但是,Pytest正在使用its usual system从文件中确定软件包名称,为我的配置提供不是标准软件包的软件包名称,并将项目的子目录添加到sys.path
。所以以下两个测试失败了:
def test_modulename():
assert 'project.util.thing_test' == __name__
# Result: AssertionError: assert 'project.util.thing_test' == 'thing_test'
def test_bad_import():
''' While we have a `project.util.thing` deep in our hierarchy, we do
not have a top-level `thing` module, so this import should fail.
'''
with raises(ImportError):
import thing
# Result: Failed: DID NOT RAISE <class 'ImportError'>
正如您所看到的,虽然thing.py
始终可以导入为project.util.thing
,但thing_test.py
在Pytest之外是project.util.thing_test
,但在Pytest运行中project/util
已添加到sys.path
,模块名为thing_test
。
这引入了许多问题:
project/util/thing_test.py
和project/otherstuff/thing_test.py
之间)。sys.path
中,因为我认为在此过程中会出现很多错误。但是,让我们称之为第一个(现在,我猜,默认)选项。我认为我希望能够告诉Pytest它应该确定相对于我提供的特定文件系统路径的模块名称,而不是根据{{的存在和不存在来决定使用哪些路径。 1}}文件。但是,我认为没有办法用Pytest做到这一点。 (对于我来说,将这个添加到Pytest并不是不可能的,但是在不久的将来也不会发生这种情况,因为我认为在提议之前我想要更深入地理解Pytest究竟该怎么做。)
第三个选项(仅仅与当前情况一起生活并且如上所述更改pytest)只是向项目中添加了数十个__init__.py
文件。然而,虽然它们中的using extend_path
(我认为)处理普通Python世界中的命名空间与常规包问题,但我认为它会破坏我们在多个项目中声明的包的不寻常的发布系统。 (也就是说,如果另一个项目有一个__init__.py
模块并且与我们的项目合并发布,那么他们project.util.other
和我们project/util/__init__.py
之间的冲突将是一个主要问题。)修复此问题这是一项重大挑战,因为我们必须添加一些方法来声明包含project/util/__init__.py
的某些目录实际上是命名空间包。
有没有办法改善上述选项?我还缺少其他选择吗?
答案 0 :(得分:2)
您面临的问题是您将测试放在命名空间包中的生产代码旁边。如上所述here,pytest
将您的设置识别为独立测试模块:
独立测试模块/ conftest.py文件
...
pytest会找到
foo/bar/tests/test_foo.py
并意识到它不是一部分 一个包,假设在同一个文件夹中没有__init__.py
个文件。然后,它会将root/foo/bar/tests
添加到sys.path
,以便导入test_foo.py
作为模块test_foo
。通过将conftest.py
添加到root/foo
以将其导入为sys.path
,conftest
文件也是如此。
因此,解决(至少部分)这个问题的正确方法是调整sys.path
并将测试与生产代码分开,例如:将测试模块thing_test.py
移动到单独的目录project/util/tests
中。由于你无法做到这一点,你别无选择,只能弄乱pytest
的内部(因为你不能通过钩子覆盖模块导入行为)。以下是提案:使用已修补的repo/conftest.py
类创建LocalPath
:
# repo/conftest.py
import pathlib
import py._path.local
# the original pypkgpath method can't deal with namespace packages,
# considering only dirs with __init__.py as packages
pypkgpath_orig = py._path.local.LocalPath.pypkgpath
# we consider all dirs in repo/ to be namespace packages
rootdir = pathlib.Path(__file__).parent.resolve()
namespace_pkg_dirs = [str(d) for d in rootdir.iterdir() if d.is_dir()]
# patched method
def pypkgpath(self):
# call original lookup
pkgpath = pypkgpath_orig(self)
if pkgpath is not None:
return pkgpath
# original lookup failed, check if we are subdir of a namespace package
# if yes, return the namespace package we belong to
for parent in self.parts(reverse=True):
if str(parent) in namespace_pkg_dirs:
return parent
return None
# apply patch
py._path.local.LocalPath.pypkgpath = pypkgpath