具有多个 python 入口点和内部依赖项的存储库的最佳目录结构?

Ner*_*zle 6 python directory-structure entry-point

我正在开发一个具有以下目录结构的项目:

project/
    package1/
        module1.py
        module2.py
    package2/
        module1.py
        module2.py
    main1.py
    main2.py
    main3.py
    ...
    mainN.py
Run Code Online (Sandbox Code Playgroud)

其中每个mainX.py文件都是一个可执行的 Python 脚本,可从package1package2或两者导入模块。package1package2是子包,旨在与项目的其余部分一起分发(而不是独立地)。

标准的做法是将入口点放在顶级目录中。我有N个入口点,所以我把它们都放在顶级目录中。问题是 N 不断增长,所以我的顶级目录被入口点淹没。

我可以将mainX.py文件移动到子目录(例如,project/run),但是所有的package1package2导入都会中断。我可以将package1和提取package2到一个单独的存储库,并期望它安装在系统上(即,在系统/用户 python 路径中),但这会使安装复杂化。我可以作为前提条件或在运行时修改 Python 路径,但这很混乱,并且可能会带来意想不到的后果。我可以编写一个main.py入口点脚本,其中参数子解析器分别指向run/main1.py, ..., run/mainN.py,但这会引入main.py每个run/mainX.py文件之间的耦合。

这个问题的标准“Pythonic”解决方案是什么?

wim*_*wim 4

标准解决方案是为您的入口点使用打包 - 请在此处console_scripts阅读有关入口点规范的信息。此功能可用于在安装时生成脚本包装器,例如...。main1.pymainN.py

\n

由于这些脚本包装器是生成的代码,因此它们根本不存在于项目源目录中,因此混乱的问题(“顶级目录被入口点淹没”)消失了。

\n

脚本的实际代码将在包内的某个位置定义,并且main*.py脚本实际挂钩到包内的代码的位置在包元数据中定义。您可以将控制台脚本入口点挂钩到包内的任何可调用项,只要它可以在没有参数的情况下调用(可选参数,即具有默认值的参数就可以了)。

\n
project\n\xe2\x94\x9c\xe2\x94\x80\xe2\x94\x80 package1\n\xe2\x94\x82\xc2\xa0\xc2\xa0 \xe2\x94\x9c\xe2\x94\x80\xe2\x94\x80 __init__.py\n\xe2\x94\x82\xc2\xa0\xc2\xa0 \xe2\x94\x9c\xe2\x94\x80\xe2\x94\x80 module1.py\n\xe2\x94\x82\xc2\xa0\xc2\xa0 \xe2\x94\x94\xe2\x94\x80\xe2\x94\x80 module2.py\n\xe2\x94\x9c\xe2\x94\x80\xe2\x94\x80 package2\n\xe2\x94\x82\xc2\xa0\xc2\xa0 \xe2\x94\x9c\xe2\x94\x80\xe2\x94\x80 __init__.py\n\xe2\x94\x82\xc2\xa0\xc2\xa0 \xe2\x94\x9c\xe2\x94\x80\xe2\x94\x80 module1.py\n\xe2\x94\x82\xc2\xa0\xc2\xa0 \xe2\x94\x94\xe2\x94\x80\xe2\x94\x80 module2.py\n\xe2\x94\x9c\xe2\x94\x80\xe2\x94\x80 pyproject.toml\n\xe2\x94\x94\xe2\x94\x80\xe2\x94\x80 scripts\n    \xe2\x94\x94\xe2\x94\x80\xe2\x94\x80 __init__.py\n
Run Code Online (Sandbox Code Playgroud)\n

这是新的目录结构。请注意添加的__init__.py文件,这表明 和package1package2包而不仅仅是子目录。

\n

对于添加的新文件,这里是scripts/__init__.py

\n
# these imports should work\n#   from package1 import ...\n#   from package2.module1 import ...\n\ndef myscript1():\n    # put whatever main1.py did here\n    print("hello")\n\ndef myscript2():\n    # put whatever main2.py did here\n    print("world")\n
Run Code Online (Sandbox Code Playgroud)\n

这些不需要全部位于同一个文件中,您可以将它们实际放置在包中的任何位置,只要您更新[project.scripts]打包定义部分中的挂钩即可。

\n

这是包装定义:

\n
[build-system]\nrequires = ["setuptools"]\nbuild-backend = "setuptools.build_meta"\n\n[project]\nname = "mypackage"\nversion = "0.0.1"\n\n[project.scripts]\n"main1.py" = "scripts:myscript1"\n"main2.py" = "scripts:myscript2"\n\n[tool.setuptools]\npackages = ["package1", "package2", "scripts"]\n
Run Code Online (Sandbox Code Playgroud)\n

现在安装包后,会生成控制台脚本:

\n
$ pip install --editable .\n...\nSuccessfully installed mypackage-0.0.1\n$ main1.py\nhello\n$ main2.py\nworld\n
Run Code Online (Sandbox Code Playgroud)\n

如前所述,这些可执行文件并不位于项目目录中,而是位于站点的脚本目录中,该目录将出现在 $PATH 中。这些脚本是由 生成的pip,使用 distlib 的供应商代码ScriptMaker。如果您查看生成的脚本文件,您会发现它们是简单的包装器,它们只会从包中导入可调用对象,然后调用它。任何参数解析、日志配置等都必须在包代码中处理。

\n
$ ls\nmypackage.egg-info  package1  package2  pyproject.toml  scripts\n$ which main2.py\n/tmp/project/.venv/bin/main2.py\n
Run Code Online (Sandbox Code Playgroud)\n

脚本目录的确切位置取决于您的平台,但可以在 Python 中像这样检查:

\n
>>> import sysconfig\n>>> sysconfig.get_path("scripts")\n\'/tmp/project/.venv/bin\'\n
Run Code Online (Sandbox Code Playgroud)\n

  • @not2qubit 正如我所写,“myscript1”和“myscript2”是“scripts/__init__.py”中定义的函数。这些入口点的包装器(即实际的可执行脚本文件)在安装时由 [pip](https://github.com/pypa/pip/blob/22.3.1/src/pip/_internal/operations 动态创建/install/wheel.py#L422-L427)。 (2认同)