如何打包一个 Python 项目

/ dousha99

In Python, "There should be one-- and preferably only one --obvious way to do it." When the Way does not align with yours, welp.

从其他开发框架迁移过来,使用如下的项目目录结构应该还是蛮常见的:

project/
|_ src/   - 源码
|_ test/  - 测试
|_ ...    - 项目的其他配置文件

但这样的一个代码组织会带来一些反直觉的问题。当我们建立了两个文件,需要相互引用的时候,这个代码应该怎么写呢?

project/
|_ src/
   |_ __init__.py
   |_ module.py
   |_ main.py
# in src/module.py

def hello():
    print('Hello')

直觉上,可以这么写:

# in src/main.py
from module import hello

if __name__ == '__main__':
    hello()

然后如果真的运行的话,它也能用:

python src/main.py

Hello

但是如果你恰好装了一个名字叫 module 的包的话,这个玩意就崩掉了:

python src/main.py

Traceback (most recent call last):
  File "/.../src/main.py", line 1, in <module>
    from module import hello
ImportError: cannot import name 'hello' from 'module' (/.../site-packages/module/__init__.py)

好在 Python 可以指定从相对位置引入:

# in src/main.py
from .module import hello

但真这么写的话,你就会发现直接调脚本调不动了:

python src/main.py

Traceback (most recent call last):
  File "/.../src/main.py", line 1, in <module>
    from .module import hello
ImportError: attempted relative import with no known parent package

需要按照模块的方式来调用才行:

python -m src.main

Hello

简单研究了一下,为了让这两种调用方式都可行,需要写成这样的一个模式:

# in src/main.py
from src.module import hello

在你完成脚本调试之后,可能会希望把它打包成一个命令行工具。简单用 setuptools 起一个打包过程即可:

# in pyproject.toml
[project]
name = "my-tool"
version = "0.0.1"
dependencies = [
    # ...
]
requires-python = ">= 3.11"

[project.scripts]
my-tool = 'src.main:main'

[build-system]
requires = ["setuptools>=61.0"]
build-backend = "setuptools.build_meta"

不过,真这么打包的话,会发现 setuptools 打包的根是在 src/ 下的。pip install . 之后,它反而又报错了:

my-tool

Traceback (most recent call last):
  File "/.../.venv/bin/my-tool", line 3, in <module>
    from src.main import main
ModuleNotFoundError: No module named 'src'

如果在打包工具里去掉 src. 的前缀呢?

# in pyproject.toml
# ...
[project.scripts]
my-tool = 'main:main'

这样做虽然 my-tool 脚本可以正常运行了,但是会炸掉剩下东西的引用:

my-tool

Traceback (most recent call last):
  File "/.../.venv/bin/my-tool", line 3, in <module>
    from main import main
  File "/.../site-packages/main.py", line 1, in <module>
    from src.module import hello
ModuleNotFoundError: No module named 'src'

要让程序可以正常运行,需要告诉 setuptoolssrc/ 作为打包目标:

# in pyproject.toml
# ...
[tool.setuptools]
packages = ["src"]

这样最终打出来的包才是正常可用的。

不过你可能会立刻意识到:这个包在 site-packages 里的名字是 src. 如果这个包要发布的话,叫这个名字肯定是不行的。所以最好的方法是采用这样的一个结构:

project
|_ my_tool <- 你的包名,取代原有的 src
|  |_ __init__.py
|  |_ ...
|_ test

然后对应地指定 setuptools 使用同样的包名:

# in pyproject.toml
# ...
[project.scripts]
my-tool = "my_tool.main:main"

[tool.setuptools]
packages = ["my_tool"]

正在加载评论……

发表评论

您的评论将由管理员审核后方可公开显示。

Your comments will be submitted to a human moderator and will only be shown publicly after approval.