为什么pytest总是显示“ImportError: attempted relative import with no known parent package”?

31

我是pytest和Python的新手。我的项目结构如下:

projroot/
|-src/
  |-a.py
|-test/
  |-test_a.py

而 test_a.py 是:

from ..src import a
def test_a():
  pass

然后在 projroot 下运行pytest

projroot>pytest

并且总是出现错误信息:E ImportError: attempted relative import with no known parent package。 Python版本:3.8,操作系统为Windows 10 x64。 我已阅读了很多文章、博客以及官方建议,但仍然无法解决问题。请帮帮我!


5
你需要添加(可能为空的) __init__.py 文件以便让 Python 知道这些是包。至少在 src 中添加一个,在 test 中也可以,而且在 projroot 中可能也需要添加。这样应该可以解决你的问题。 - filbranden
谢谢,filbranden和napuzba!问题解决了!我发现在testprojroot中,__init__.py文件是必不可少的,但在src中不是。 - Jacques Yang
3个回答

24

我发现必须在testprojroot中添加__init__.py文件。src文件夹中的__init__.py文件不是必需的。 并且pytest可以在这3个文件夹中正确运行:

# parent folder of project root
C:\>pytest projroot

# project root
C:\>cd projroot
C:\projroot>pytest

# test folder
C:\projroot>cd test
C:\projroot\test>pytest

7

我将__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 Angletest_angles.py 内部进行绝对导入。
在发现问题是在项目根目录中有一个不必要的 __init__.py 之前,我也尝试了以下操作:
  1. 编辑 python.testing.cwd
  2. 设置 pytest 的 '--rootdir'
  3. 将 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。
  1. python -m (它添加的内容取决于命令所在的目录)
  2. 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__

5

在根目录中包含setup.py文件以及每个文件夹中的__init__.py文件对我来说是有效的,如果要在任何文件中导入一个包,只需从父目录中指定该包的位置即可。

setup.py

from setuptools import setup, find_packages

setup(name="PACKAGENAME", packages=find_packages())

projroot/
|-__init__.py
|-setup.py
|-src/
  |-a.py
  |-__init__.py
|-test/
  |-test_a.py
  |-__init__.py

from src import a
def test_a():
  pass

Reference https://docs.pytest.org/en/6.2.x/goodpractices.html


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接