我可以使用 python 模块的 main 进行测试吗?

Can I use a python module's main for testing?

我正在 Python 3.8.2 上开发一个 Python 库,我想 运行 一个模块作为主要模块用于测试目的。当我尝试时,出现 ModuleNotFound 错误。

这是我的库结构:

.
├── foo
│   ├── __init__.py
│   ├── bar.py
│   └── quux
│       ├── __init__.py
│       ├── corge.py
│       └── garply.py
├── main.py

bary.py:

def baz():
    print("baz")

corge.py

from foo.quux.garply import *


def grault():
    waldo()
    print("grault")


if __name__ == '__main__':
    grault()

garply.py

def waldo():
    print("waldo")

main.py

from foo.bar import *
from foo.quux.corge import *

if __name__ == '__main__':
    baz()
    grault()

(所有 __init__.py 个文件为空)

当我 运行 main.py 时,它起作用了。

$ python main.py
baz
waldo
grault

如果我尝试 运行 corge.py,我会收到以下错误:

$ python foo/quux/corge.py 
Traceback (most recent call last):
  File "foo/quux/corge.py", line 1, in <module>
    from foo.quux.garply import *
ModuleNotFoundError: No module named 'foo'

不管我当前的工作目录是什么,它总是给出这个错误。

$ cd foo/quux/
$ python corge.py 
Traceback (most recent call last):
  File "corge.py", line 1, in <module>
    from foo.quux.garply import *
ModuleNotFoundError: No module named 'foo'

在测试时,我使用 PyCharm 2020.1 创建了一个新的 PyCharm 项目并实现了我描述的结构。令我惊讶的是,它适用于默认检测到的 运行 配置。

我试过使用PyCharm自动创建的venv,但还是不行。如果我直接 copy/paste 命令并使用他们的 CWD,它不起作用。它不适用于 PyCharm 内置的终端。它 与 PyCharm 运行 按钮一起使用。

我的模块结构有问题吗?如果是这样,PyCharm 可以做什么来使这项工作成功?如果不是,为什么它在 PyCharm 之外不起作用?

您对 main.py 的调用有效,因为 Python 将在直接子目录中查找模块。任何其他位置的任何模块想要导入 foo.yadda.whatever 都必须通过搜索您的 PYTHONPATH 找到 foo。因此,您需要将 foo 的父目录添加到您的 PYTHONPATH.

Python 需要知道 foo 所在的目录才能导入它。 sys.path 列出 python 将搜索的目录。

当你安装一个包时,安装程​​序会担心这样做 - 通常是将模块放在一个众所周知的目录中或将安装包路径添加到 sys.path

当您 运行 一个脚本时,python 会自动将该脚本的路径添加到 sys.path,因此当您 运行 main.py 时,您会发现 foo

如何运行作为模块作为__main__

一个选项是使软件包可安装(setup.py、wheels 等....)并使用开发模式( 进行一些讨论)。这就是我在开发计划提供给其他人的东西时所做的事情。

另一种是将你的目录添加到 PYTHONPATH 中,甚至可以作为你的 运行 程序。在 linux 上会是

PYTHONPATH=path/to/fooproject:$PYTHONPATH python foo/quux/corge.py

还有一个,我也在做这个,是破解模块本身的路径。 __file__ 给出相对于当前工作目录的文件名,并且您知道您在包层次结构中的深度。所以你可以让 __file__ 绝对化并剥离几个目录名

corge.py

import sys
import os

if __name__ == "__main__":
    # I'm two levels deep in the package so package directory is
    packagedir = os.path.abspath(os.path.join(os.path.dirname(__file__),
        "..", ".."))
    sys.path.insert(0, packagedir)
    import foo

最后,一开始就不要这样做。当您 运行 corge.py 作为脚本时,它获得的命名空间 __main__ 与作为模块导入的 foo.bar.corge 不同。它的全局变量 / 类 / 函数被加载两次,根据你是通过 __main__ 命名空间还是 foo.bar.corge 调用它们,你会得到不同的变量。

最好将任何您想放入 corge.py 中的主脚本,让它们成为独立的脚本。例如,您可以将 def main() 添加到您的模块中。在 main.py 中,您可以添加一个选项 --run foo.bar.corge 告诉 main 导入 corge.py 和 运行 它的 main()argparse subcommands 可用于此。