Post

我也想用 pip下载安装我的代码

pip install的包是如何创建的呢

我也想用 pip下载安装我的代码

前言

本文基于现代 Python 打包生态(PEP 621 / PEP 517/518、pyproject.toml 为中心),并同时给出 setup.py 的最小示例以兼容老项目或需要程序化构建的场景。有关 pyproject.toml 的规范与 build-system 的行为,请参阅官方规范与 PEP 文档。(packaging.python.org)


最小项目结构(示例)

1
2
3
4
5
6
7
8
9
10
your_project/
├── src/
│   └── your_package/
│       └── __init__.py
├── tests/
├── README.md
├── LICENSE
├── pyproject.toml
├── setup.py         # 可选:仅当需要程序化步骤时保留
├── MANIFEST.in      # 可选:包含额外资源

说明:推荐使用 src/ 布局(能减少同名包调试问题),把可读文档放在 README.md,并在 pyproject.toml 中声明元数据。有关如何书写 pyproject.toml 的指南见官方 Packaging 文档。(packaging.python.org)


pyproject.toml — 完整示例与逐行解释

示例文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
[build-system]
requires = ["setuptools>=61", "wheel"]
build-backend = "setuptools.build_meta"

[project]
name = "your-package-name"
version = "0.1.0"
description = "A short description of the package."
readme = { file = "README.md", content-type = "text/markdown" }
requires-python = ">=3.8"
authors = [{ name = "Your Name", email = "you@example.com" }]
license = { text = "MIT" }
dependencies = [
  "requests>=2.25",
  "numpy>=1.21"
]
classifiers = [
  "Programming Language :: Python :: 3",
  "License :: OSI Approved :: MIT License",
  "Operating System :: OS Independent"
]

[project.optional-dependencies]
dev = ["pytest", "black", "sphinx"]

[project.scripts]
your-cmd = "your_package.cli:main"

下面逐段解释该文件中每一项的含义与常见写法。


【build-system】

1
2
3
[build-system]
requires = ["setuptools>=61", "wheel"]
build-backend = "setuptools.build_meta"
  • requires:构建时需要安装的 package 列表(pip 在构建时会先安装这些依赖)。常见值是 setuptools + wheel(也可能包含 buildflit 等,取决于你选的后端)。这个字段由 PEP 517/518 定义,用来告诉工具在构建前需要哪些依赖。(Python Enhancement Proposals (PEPs))
  • build-backend:指定构建后端(back-end),例如 setuptools.build_meta,或者 flit_core.buildapipoetry.core.masonry.api 等。后端负责把源码打包成 sdist/wheel。(Python Enhancement Proposals (PEPs))

通常来说我都闭眼直接复制了。


【project】

该表遵循 PEP 621 的 metadata 规范(即把包的“元数据”声明在 pyproject.toml[project] 中)。(Python Enhancement Proposals (PEPs))

  • name:包的分发名(在 PyPI 上显示并用于 pip install name)。注意命名规范(小写、短横线通常被接受),并在发布前确认是否被占用。
  • version:遵循 PEP 440 的版本号(语义化或你自己的策略)。也可以用 dynamic = ["version"] 并由后端在构建时填充(例如基于 git tag)。(packaging.python.org)
  • description:一句话描述(短摘要)。
  • readme:可以是简单的字符串路径或带 file/content-type 的表,现代工具将把 README 内容作为 long description 展示。示例里用了 { file = "README.md", content-type = "text/markdown" }。(packaging.python.org)
  • requires-python:指定 Python 版本兼容性(例如 >=3.8)。pip 和安装索引会据此拒绝不满足的环境。
  • authors / maintainers:作者/维护者列表,使用数组表形式(包含 name/email)。
  • license:可以用 { text = "MIT" }{ file = "LICENSE" }(把许可证文本或文件声明出来)。不同后端对这两种形式的处理细节略有差别。(packaging.python.org)
  • dependencies:主依赖,写法为 PEP 508 风格的字符串(例如 "requests>=2.25")。构建后的元数据会把这些信息写入 METADATA。(packaging.python.org)
  • classifiers:PyPI 上的分类(帮助他人检索),例如 Python 版本 / License / OS。
  • urls:可选的 homepage / repository / documentation 链接(用于 PyPI 展示)。

entry points / console scripts

1
2
[project.scripts]
your-cmd = "your_package.cli:main"
  • 以上表示:安装包时会创建一个可执行命令 your-cmd,其调用目标为 your_package.cli 模块中的 main 函数。

闭眼复制…

optional-dependencies / dynamic 等说明

  • [project.optional-dependencies]:声明可选依赖组(例如 devdocstests),便于用户 pip install your-package[dev]
  • dynamic:如果某些字段在打包前由脚本/后端动态生成(例如 version),需要在 [project] 中列出 dynamic = ["version"] 并确保后端支持。(packaging.python.org)

setup.py — 示例与说明

为什么现代项目倾向于 pyproject.toml

PEP 517/621 推动 metadata 与构建配置向 pyproject.toml 集中,减少对 setup.py 的依赖。大多数情况下,声明式配置(pyproject.tomlsetup.cfg)已足够。若你需要程序化步骤(比如运行 C 语言扩展的自定义编译逻辑、在打包时运行自定义 Python 代码来生成文件等),才保留 setup.py。官方 setuptools 文档也建议新项目尽量使用声明式配置,仅在必要时保留 setup.py。(setuptools.pypa.io)

minimal setup.py 示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# setup.py (最小示例)
from setuptools import setup, find_packages
from pathlib import Path

README = Path(__file__).parent.joinpath("README.md").read_text(encoding="utf-8")

setup(
    name="your-package-name",
    version="0.1.0",
    description="A short description of the package.",
    long_description=README,
    long_description_content_type="text/markdown",
    author="Your Name",
    author_email="you@example.com",
    packages=find_packages(where="src"),
    package_dir={"": "src"},
    include_package_data=True,
    python_requires=">=3.8",
    install_requires=[
        "requests>=2.25",
    ],
    entry_points={
        "console_scripts": [
            "your-cmd=your_package.cli:main",
        ]
    },
    classifiers=[
        "Programming Language :: Python :: 3",
        "License :: OSI Approved :: MIT License",
        "Operating System :: OS Independent",
    ],
)
  • find_packages(where="src") + package_dir 配合 src/ 布局使用。
  • include_package_data=True 会把 MANIFEST.in 指定的资源包含进 sdist。
  • 如果只是声明性元数据,优先把这些字段放到 pyproject.toml / setup.cfgsetup.py 保留最小 stub 或程序化逻辑。(setuptools.pypa.io)

MANIFEST.in 示例

如果你的包需要包含配置文件、模板、数据文件等非 Python 资源,可以创建 MANIFEST.in 文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# MANIFEST.in 示例
include README.md
include LICENSE
include CHANGELOG.md
include requirements.txt

# 包含所有配置文件
include *.cfg
include *.ini
include *.yaml
include *.yml

# 包含特定目录下的文件
recursive-include your_package/templates *
recursive-include your_package/static *
recursive-include your_package/data *.json *.csv *.txt

# 排除不需要的文件
global-exclude *.pyc
global-exclude *.pyo
global-exclude __pycache__
global-exclude .DS_Store
global-exclude *.so
prune tests/
prune docs/

pyproject.toml 中启用:

1
2
[tool.setuptools]
include-package-data = true

或在 setup.py 中:

1
2
3
4
setup(
    # ... 其他参数
    include_package_data=True,
)

打包与上传(命令汇总)

  1. 安装构建工具:

    1
    
    pip install --upgrade build
    
  2. 在项目根目录生成分发包(sdist + wheel):

    1
    
    python -m build
    

    生成结果放到 dist/ 文件夹。 (工具会读取 pyproject.toml[build-system] 指令来安装构建依赖并调用后端进行构建。)(Python Enhancement Proposals (PEPs))

  3. 安装上传工具(Twine):

    1
    
    pip install --upgrade twine
    
  4. 推荐先上传到 TestPyPI 测试(验证元数据/README 渲染/包名等):

    1
    
    twine upload --repository testpypi dist/*
    

    在 TestPyPI 上确认包的显示与安装行为正确后,再上传正式 PyPI。(packaging.python.org)

  5. 上传到正式 PyPI:

    1
    
    twine upload dist/*
    
  6. 关于 PyPI API token:在 PyPI 网站生成 token 后,上传时将用户名设为 __token__,密码设为 token(包含 pypi- 前缀),或在 CI 中把它作为 TWINE_USERNAME/TWINE_PASSWORD 或在 ~/.pypirc 中配置。推荐在 CI 使用加密 secrets(不把 token 明文放到仓库)。(PyPI)


常见错误与排查

  • 包名已被占用 / 403 或 name collision:在发布前先在 TestPyPI 或 PyPI 搜索包名,确认未被占用。若被占用只能换名或联系包名拥有者。
  • long_description 在 PyPI 上渲染失败:确保 readme/long_description_content_type 正确(text/markdown)并在本地用 twine check dist/* 检查元数据。
  • twine 上传认证失败 (403 / 401):通常是 token/凭据问题。建议在 PyPI 控制台生成 API token,并按官方说明把用户名设为 __token__。(PyPI)
  • 构建失败找不到后端/依赖:确认 [build-system].requires 中包含了构建所需的包(例如 setuptoolswheel),并在本地或 CI 里升级到兼容的版本。(Python Enhancement Proposals (PEPs))
  • 包含非 Python 文件(模板/数据)缺失:使用 MANIFEST.in + include_package_data=True 把额外资源包含进 sdist。参考 setuptools 文档。(setuptools.pypa.io)

This post is licensed under CC BY 4.0 by the author.