我在命名空间包中的单元测试中遇到了一个奇怪的问题。这是我在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
将运行两次!以我为例,这导致我打断的单例被重新初始化为真实对象,随后导致测试失败。
这是预期的行为吗?这里的正确用法是什么?我是否应该始终避免相对进口(如果我的公司决定重命名某些东西,并且必须进行全局搜索和替换?)
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
,因此:
src
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)
...
是的!感谢您链接到loader.py。现在,我也明白了为什么我第一次尝试使用点缀的模块名称失败了。在我看来,loader.py中的名称空间模块处理不完整,因为
_get_directory_containing_module
如果给定的模块是一个名称空间,则无法处理。但是,如果我进行了修补,则测试发现将完美运行!