Warm tip: This article is reproduced from serverfault.com, please click

其他-测试发现会从相对导入中删除Python名称空间吗?

(其他 - Test discovery drops Python namespaces from relative imports?)

发布于 2020-12-08 14:18:41

我在命名空间包中的单元测试中遇到了一个奇怪的问题。这是我在GitHub上构建的示例基本结构如下:

$ tree -P '*.py' src 
src
└── namespace
    └── testcase
        ├── __init__.py
        ├── a.py
        ├── sub
        │   ├── __init__.py
        │   └── b.py
        └── tests
            ├── __init__.py
            └── test_imports.py

4 directories, 6 files

我希望命名空间包中的相对导入将维护命名空间。通常,这似乎是正确的:

$ cat src/namespace/testcase/a.py 
print(__name__)
$ cat src/namespace/testcase/sub/b.py 
print(__name__)

from ..a import *
$ python -c 'from namespace.testcase.sub import b'
namespace.testcase.sub.b
namespace.testcase.a

但是,如果涉及测试,我会感到惊讶:

$ cat src/namespace/testcase/tests/test_imports.py 
from namespace.testcase import a
from ..sub import b
$ python -m unittest discover src/namespace/
namespace.testcase.a
testcase.sub.b
testcase.a

----------------------------------------------------------------------
Ran 0 tests in 0.000s

OK

中的代码src/namespace/testcase/a.py将运行两次!以我为例,这导致我打断的单例被重新初始化为真实对象,随后导致测试失败。

这是预期的行为吗?这里的正确用法是什么?我是否应该始终避免相对进口(如果我的公司决定重命名某些东西,并且必须进行全局搜索和替换?)

Questioner
kojiro
Viewed
0
Pi Delport 2020-12-17 17:43:04

问题:重叠的sys.path条目

sys.path条目重叠时,即具有相同的模块名称的重复导入发生:也就是说,当同时sys.path包含父目录和子目录作为单独的条目时。这种情况几乎总是一个错误:它将使Python将子目录视为一个单独的,不相关的导入根目录,这会导致令人惊讶的行为。

在你的示例中:

$ python -m unittest discover src/namespace/
namespace.testcase.a
testcase.sub.b
testcase.a

这意味着src和都src/namespace以结束sys.path,因此:

  • namespace.testcase.a是相对于导入的src
  • testcase.sub.btestcase.a相对于导入src/namespace

为什么?

在这种情况下,sys.path发生重叠的条目是因为unittest discover想提供帮助:默认情况下,假设测试发现的起始目录也是你导入所相对的顶层目录,并将该顶层目录插入到目录中。sys.path(如果还没有的话),为方便起见。(…事实证明不太方便。😔️)

解决方案:明确指定正确的顶层目录

你可以使用-t--top-level-directory显式指定正确的顶层目录

python -m unittest discover -t src -s src/namespace/

这将像以前一样工作,但不会src/namespace作为要插入的顶级目录sys.path

旁注:-s选项前缀src/namespace/在上一个示例中是隐式的:上面的示例使它明确。unittest discover具有怪异位置参数处理:它把它的前三个位置参数作为数值-s-p以及-t,以该顺序)。

细节

负责此工作的代码位于unittest / loader.py中

class TestLoader(object):

    def discover(self, start_dir, pattern='test*.py', top_level_dir=None):

        ...

        if top_level_dir is None:
            set_implicit_top = True
            top_level_dir = start_dir

        top_level_dir = os.path.abspath(top_level_dir)

        if not top_level_dir in sys.path:
            # all test modules must be importable from the top level directory
            # should we *unconditionally* put the start directory in first
            # in sys.path to minimise likelihood of conflicts between installed
            # modules and development versions?
            sys.path.insert(0, top_level_dir)

        ...