如何使用PEP 420命名空间包Pytest项目?

时间:2018-05-04 11:43:25

标签: pytest

我正在尝试使用Pytest来测试一个较大的(~100k LOC,1k文件)项目,并且还有其他几个类似的项目,我最终也想做同样的事情。这不是standard Python package;它是一个高度定制的系统的一部分,我几乎没有能力改变,至少在短期内。测试模块与代码集成在一起,而不是在一个单独的目录中,这对我们很重要。配置与this questionmy 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

这引入了许多问题:

  1. 模块命名空间冲突(例如,在project/util/thing_test.pyproject/otherstuff/thing_test.py之间)。
  2. 错误的导入语句未被捕获,因为测试中的代码也在使用这些非生产导入路径。
  3. 相对导入可能无法在测试代码中运行,因为模块已被移动&#34;在层次结构中。
  4. 一般来说,我非常担心在测试中会有大量额外路径添加到sys.path中,因为我认为在此过程中会出现很多错误。但是,让我们称之为第一个(现在,我猜,默认)选项。
  5. 我认为我希望能够告诉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的某些目录实际上是命名空间包。

    有没有办法改善上述选项?我还缺少其他选择吗?

1 个答案:

答案 0 :(得分:2)

您面临的问题是您将测试放在命名空间包中的生产代码旁边。如上所述herepytest将您的设置识别为独立测试模块:

  

独立测试模块/ 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.pathconftest文件也是如此。

因此,解决(至少部分)这个问题的正确方法是调整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