我将__init__.py
添加到项目根目录后,pytest的发现和pylint都出了问题(导致无法导入'angles' pylint(import-error)
)。我不得不将其从项目根目录中删除,并仅将其添加到我的测试文件夹中,这解决了pytest的发现和pylint警告。
如果在项目根目录中有__init__.py
,则会出现ModuleNotFoundError: No module named 'angles'
下面是我的文件夹结构,同时pytest的发现和pylint(没有警告)都能正常工作:
powerfulpython
├── angles.py
└── tests
├── __init__.py
└── test_angles.py
在 angles.py 中,我有一个名为
Class Angle
的类,我尝试使用
from angles import Angle
在
test_angles.py
内部进行绝对导入。
在发现问题是在项目根目录中有一个不必要的
__init__.py
之前,我也尝试了以下操作:
- 编辑
python.testing.cwd
- 设置 pytest 的 '--rootdir'
- 将 Python 扩展程序降级为2020.9.114305(遵循 https://github.com/microsoft/vscode-python/issues/14579)
但以上方法都没有奏效。
在删除项目根目录下的
__init__.py
之前,当测试pytest时,执行
python -m pytest
是有效的,因为项目根目录会自动添加到sys.path中,因此绝对导入可以从根目录正确工作,导入语句可以找到根目录下的
angles
。执行
pytest
(不带python -m)失败,因为项目根目录没有被添加到sys.path中。后者可以通过手动将根目录添加到$PYTHONPATH来暂时解决。然而,这不是一种可持续的解决方案,因为我不想将每个要测试的新项目都添加为PYTHONPATH编辑到我的~/.zshrc中。
我尝试的另一个hack可以解决
pytest
和测试发现的问题,即在代码中插入
sys.path.insert(0,'/Users/hanqi/Documents/Python/powerfulpython')
,但这太丑陋了。我不想为了测试而向代码中添加行,然后再将它们删除。
在所有这些之后,我删除了根目录下的
__init__.py
,一切都很好。不需要上述3项中的任何一项。
许多解决方案处理查找测试文件夹,但这不是我的问题。我的测试在其文件夹中被正确找到,只是
from angles import Angle
导致测试发现失败。
调查项目根目录中的
__init__.py
如何破坏事物:
我打印了sys.path并发现,其中~/Documents/Python
被添加到了sys.path的最前面,而没有它,则是~/Documents/Python/powerfulpython
被添加到了最前面。后者的路径恰好是我的项目根目录。
我猜这与https://docs.pytest.org/en/6.2.x/pythonpath.html有关(默认使用--import-mode=prepend
)。
我本以为在tests
中看到__init__.py
会导致~/Documents/Python/powerfulpython
被添加到sys.path的最前面,并且在项目根目录中的__init__.py
会导致~/Documents/Python
被添加到sys.path的最前面,但奇怪的是,当我两个__init__.py
都存在时,我只看到了后者。
我还尝试删除
tests
中的
__init__.py
(但保留根目录中的
__init__.py
),然后看到
~Documents/Python/powerfulpython/tests
添加到了sys.path。如果我同时删除根目录和tests中的
__init__.py
,那么同样会添加这一行,不确定为什么。
编辑:上述行为在
https://docs.pytest.org/en/6.2.x/pythonpath.html中有解释。它会向上搜索,直到找到最后一个包含
__init__.py
文件的文件夹,以找到包的根目录。通过使用
python -m pytest -s
(-s使成功的测试运行不会吞没我的
print(sys.path)
)而不是仅使用
pytest
来重复我的上述实验后,我确认有两个actor添加到sys.path。
python -m
(它添加的内容取决于命令所在的目录)
pytest
(它添加的内容取决于__init__.py
插入的位置(在上面链接的pytest文档中有解释))
两者都会编辑sys.path,帮助实现失败的导入
Jacques Yang需要2个__init__.py
将包含projroot的文件夹放入sys.path中,以便projroot.test
出现在他的test_a.py
中的__package__
值中,而__name__
将出现为projroot.test.test_a
,并且这个模块名字(__name__
)中的2个点允许使用相对导入from ..src import a
。他也可以使用绝对导入from projroot.src import a
。
如果他没有在projroot中添加__init__.py,那么__package__将变成test,而__name__将是test.test_a,这只允许向上相对步骤最多1步。(这个步骤的概念也由BrenBarn在
Relative imports for the billionth time中解释)。
当test中有__init__.py但是projroot中没有时,会出现ValueError: attempted relative import beyond top-level package,因为__package__是test,而__name__是test.test_a,只允许向上1步,但编码了2步向上。
当test中甚至没有__init__.py时,标题中的问题就会出现,因此__package__完全为空,而__name__仅是test_a文件本身。
我在思考为什么OP需要2个
__init__.py
而我只需要1个,而且2个实际上会破坏我的尝试。然后我意识到OP正在使用相对导入,而我正在使用绝对导入
from angles import Angle
。如果我按照OP的模式并进行相对导入
from ..angles import Angle
,那么我也需要在
powerfulpython
下添加
__init__.py
,否则我将得到相同的错误。
那么为什么在
~/Documents/Python/powerfulpython
中有额外的
__init__.py
会破坏绝对导入呢?因为
__init__.py
会导致pytest在层次结构中向上跳一级,并在sys.path中添加
~/Documents/Python
,但我的绝对导入需要
~/Documents/Python/powerfulpython
(可以通过使用如上所述的
python -m pytest
来解决,但与简单的
pytest
相比,感觉有些不自然)。
我有一个30分钟的演示视频,介绍了
https://towardsdatascience.com/taming-the-python-import-system-fbee2bf0a1e4中的导入概念,这可以帮助人们理解
__package__
和
__name__
。
__init__.py
文件以便让 Python 知道这些是包。至少在src
中添加一个,在test
中也可以,而且在projroot
中可能也需要添加。这样应该可以解决你的问题。 - filbrandentest
和projroot
中,__init__.py
文件是必不可少的,但在src
中不是。 - Jacques Yang