三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

Python项目打包上传PyPI全攻略:从项目结构到自动化发布

Python项目打包上传PyPI全攻略:从项目结构到自动化发布

1. 项目概述:为什么要把自己的代码“上架”到PyPI?

如果你写过Python代码,尤其是写过一些自认为有点用的小工具、小库,那么你很可能遇到过这样的场景:同事或朋友想用你的代码,你得把整个项目文件夹打个压缩包发过去,对方解压后还得手动安装依赖、处理路径,麻烦不说,还容易出错。或者,你自己换了台电脑,想重新安装自己的工具,也得翻出那个压缩包。这个过程,既不优雅,也不高效。

这时候,PyPI(Python Package Index)就该登场了。你可以把它想象成Python世界的“应用商店”或“软件仓库”。当你通过pip install requestspip install numpy时,pip这个包管理器就是从PyPI上查找、下载并安装这些包的。把自己写的项目打包上传到PyPI,意味着你的代码获得了“官方”分发渠道。从此以后,任何人,在任何地方,只需要一行简单的pip install your-package-name,就能轻松安装和使用你的项目。这不仅仅是方便了他人,更是对你项目专业性的一种认可,是开源协作的基石。

我最初上传自己的第一个小工具到PyPI时,纯粹是为了解决团队内部重复安装的麻烦。但后来发现,这个过程本身就是一个极佳的工程实践:它强迫你思考项目的结构、依赖管理、版本控制和文档。今天,我就以一个过来人的身份,手把手带你走一遍从零开始,将一个本地Python项目打包并上传到PyPI的全过程,并分享那些官方文档里不会写的“坑”和技巧。

2. 项目打包前的核心准备工作

在上传之前,我们不能把一个乱七八糟的文件夹直接扔上去。PyPI要求你的项目必须是一个结构清晰、包含必要元数据的“包”。这就像你要开一家店,得先准备好营业执照(项目信息)、商品清单(代码文件)和说明书(文档)一样。

2.1 规划一个标准的项目结构

一个典型的、适合上传的Python项目目录结构应该如下所示。这不仅是PyPI的要求,也是良好项目管理的习惯。

your_awesome_project/ # 项目根目录 ├── your_awesome_project/ # 包的源代码目录(与项目同名) │ ├── __init__.py # 使Python将其视为一个包 │ ├── core.py # 你的核心模块 │ └── utils.py # 工具函数模块 ├── tests/ # 测试目录(非必须,但强烈推荐) │ └── test_core.py ├── docs/ # 文档目录(可选) ├── README.md # 项目说明,非常重要! ├── LICENSE # 开源许可证,必须要有! ├── pyproject.toml # 现代构建配置(核心!) ├── setup.cfg # 传统配置,可与pyproject.toml配合 └── MANIFEST.in # 指定包含的非代码文件

关键点解析:

  • 双层目录结构:注意,源代码放在一个与项目同名的子目录里。这被称为“src-layout”或直接布局。这样做可以避免很多导入路径的混乱,尤其是在开发模式下。__init__.py文件(可以是空的)是标志这个目录为Python包的关键。
  • README.md:这是你的门面。PyPI会将其渲染成项目主页的详细描述。务必认真编写,包括项目简介、安装方法、快速入门示例等。支持Markdown格式。
  • LICENSE:没有许可证的文件,在法律上默认是保留所有权利的,别人无法安全地使用、修改或分发。选择一个合适的开源许可证(如MIT、Apache 2.0)并放入该文件,是开源的第一步。

2.2 选择并配置现代构建工具:告别 setup.py

过去,我们依赖一个名为setup.py的Python脚本来定义项目元数据。但这种方式有很多问题:它是一段可执行代码,可能导致构建过程不确定;且配置分散。现在,社区主推并已被pipbuild工具原生支持的是pyproject.toml文件。

pyproject.toml是一个配置文件,它声明了构建项目所需的前置依赖和工具。我们将在其中使用setuptools作为构建后端。

在你的项目根目录创建pyproject.toml文件,内容如下:

[build-system] requires = ["setuptools>=61.0", "wheel"] build-backend = "setuptools.build_meta" [project] name = "your-awesome-project" version = "0.1.0" authors = [ {name = "Your Name", email = "your.email@example.com"}, ] description = "A brief description of your awesome project." readme = "README.md" license = {file = "LICENSE"} classifiers = [ "Programming Language :: Python :: 3", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", ] keywords = ["utility", "tool", "automation"] dependencies = [ "requests>=2.25.0", "click>=8.0.0", ] [project.urls] Homepage = "https://github.com/yourusername/your_awesome_project" Repository = "https://github.com/yourusername/your_awesome_project.git"

配置详解与避坑指南:

  1. name:这是你的包在PyPI上的唯一标识,也是pip install时使用的名字。必须全小写,可以使用连字符-。它不需要和你的代码目录名完全一致,但建议有关联。
  2. version:遵循语义化版本规范主版本号.次版本号.修订号。每次上传新版本到PyPI,版本号必须递增。
  3. readmelicense:这里直接指向文件。确保文件路径和名称正确。
  4. classifiers:分类器,帮助PyPI对项目进行分类。可以从 PyPI分类器列表 选择。写上Python版本和许可证是最基本的。
  5. dependencies:你的项目运行时必须依赖的其他PyPI包。pip在安装你的包时会自动安装它们。务必仔细核对,不要遗漏,也不要将仅用于开发的依赖(如测试框架pytest、代码格式化工具black)写在这里。
  6. [project.urls]:提供项目主页和代码仓库链接,方便用户查看源码和报告问题。

注意:如果你有仅用于开发、测试或构建的依赖(如pytest,black,twine),应该将它们放在另一个名为[project.optional-dependencies]的章节,或者更常见的做法是使用requirements-dev.txt文件来管理,而不是放在dependencies里。

2.3 管理非代码文件:MANIFEST.in

默认情况下,构建工具只会包含它识别出的Python代码文件(.py)。如果你的项目需要包含数据文件、模板、静态资源(如图片、配置文件)等,你需要一个MANIFEST.in文件来明确指示。

在项目根目录创建MANIFEST.in

include LICENSE include README.md include pyproject.toml recursive-include your_awesome_project/data *.json *.csv recursive-include docs *.md
  • include:包含指定的单个文件。
  • recursive-include:递归包含某个目录下符合模式的所有文件。

实操心得:一个常见的坑是,更新了README.md但打包后发现PyPI页面没变。这通常是因为MANIFEST.in没有正确包含该文件,或者构建时没有清理旧构建产物。每次打包前,最好删除distbuild目录以及*.egg-info文件夹,进行全新构建。

3. 本地构建与测试:确保包“能打”

在真正上传之前,我们必须先在本地把包构建出来,并测试安装是否正常。这是避免上传一个“残次品”到公共仓库的关键步骤。

3.1 安装构建工具并生成分发文件

首先,确保你安装了最新的构建工具build和打包工具wheel

pip install --upgrade build wheel

然后,在项目根目录执行构建命令:

python -m build

这个命令会做两件事:

  1. 读取pyproject.toml配置。
  2. 在项目根目录下生成一个dist文件夹,里面包含两种分发格式的文件:
    • .tar.gz源码归档:这是传统的分发格式。
    • .whl轮子文件:这是一种预构建的分发格式,安装速度极快,是现代Python包分发的首选。pip会优先安装.whl文件。

执行成功后,你的dist目录应该类似这样:

dist/ ├── your_awesome_project-0.1.0-py3-none-any.whl └── your_awesome_project-0.1.0.tar.gz

3.2 在虚拟环境中进行安装测试

千万不要直接在系统Python或你的开发环境中用pip install刚生成的.whl文件!这可能会污染环境。正确的做法是使用虚拟环境。

# 1. 创建一个新的临时虚拟环境(例如在/tmp下) python -m venv /tmp/test_env # 2. 激活虚拟环境 # Linux/macOS: source /tmp/test_env/bin/activate # Windows: # .\tmp\test_env\Scripts\activate # 3. 从本地dist目录安装你的包 pip install /path/to/your/project/dist/your_awesome_project-0.1.0-py3-none-any.whl # 4. 启动Python解释器,尝试导入你的包并运行基本功能 python -c “import your_awesome_project; print(your_awesome_project.__version__)”

关键检查点:

  • 导入是否成功?没有ModuleNotFoundError
  • 核心功能能否运行?写一个小脚本调用你包里的主要函数。
  • 依赖包是否被正确安装?检查虚拟环境的pip list,确认requests,click等依赖已存在。
  • 非代码文件是否可访问?如果你的包需要读取data/下的文件,测试在安装后能否正确找到这些文件的路径。这里通常需要使用importlib.resourcespkg_resources来访问包内数据,而不是简单的文件路径。

3.3 验证元数据

使用twine工具检查你的分发文件是否有明显的元数据错误。

# 安装twine pip install twine # 检查dist目录下的所有分发文件 twine check dist/*

如果输出显示PASSED,说明基本元数据格式无误。这一步能提前发现很多pyproject.toml中的书写错误。

4. 注册并上传到PyPI

本地测试通过后,就可以准备上传了。PyPI分为两个环境:测试环境(TestPyPI)生产环境(PyPI)务必先在测试环境演练!

4.1 注册账号与配置认证

  1. 注册账号

    • 访问 https://test.pypi.org/account/register/ 注册TestPyPI账号。
    • 访问 https://pypi.org/account/register/ 注册PyPI账号。
    • 建议使用不同的密码。务必开启两步验证(2FA),这是保护你账户安全的重要措施。
  2. 配置API Token(推荐): 现在不推荐直接使用用户名和密码上传。PyPI提供了更安全的API Token。

    • 登录PyPI -> 点击用户名 ->Account settings->API tokens->Add API token
    • 作用域(Scope):对于整个项目,选择Entire account (all projects)。为了安全,你也可以为单个项目创建Token。
    • 创建后,立即复制并保存Token,因为它只显示一次。
    • 为TestPyPI也创建一个Token(流程相同)。
  3. 本地配置Token: 在你的用户主目录(~)下,创建或编辑文件~/.pypirc,将Token配置进去:

    [distutils] index-servers = testpypi pypi [testpypi] repository = https://test.pypi.org/legacy/ username = __token__ password = pypi-你的TestPyPI-API-Token-字符串 [pypi] repository = https://upload.pypi.org/legacy/ username = __token__ password = pypi-你的PyPI-API-Token-字符串

    重要安全提示~/.pypirc文件包含敏感信息,务必设置其文件权限为仅当前用户可读:chmod 600 ~/.pypirc。切勿将此文件提交到Git仓库!

4.2 上传到TestPyPI进行演练

首先,清理旧的构建产物并重新构建,确保上传的是最新版本。

# 清理旧构建 rm -rf dist build *.egg-info # 重新构建 python -m build # 使用twine上传到TestPyPI twine upload --repository testpypi dist/*

上传过程中,twine会显示上传进度。成功后,它会给出你包在TestPyPI上的URL。

立刻进行测试安装

# 创建一个新的干净虚拟环境 python -m venv /tmp/test_pypi_env source /tmp/test_pypi_env/bin/activate # 从TestPyPI安装你的包,注意指定额外的索引URL pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ your-awesome-project
  • --index-url:指定主要从TestPyPI查找包。
  • --extra-index-url:因为你的包可能依赖其他不在TestPyPI上的正式包(如requests),所以需要同时指定正式的PyPI作为备用源。

在测试环境中完整地走一遍安装、导入、功能测试的流程,确保一切完美。

4.3 正式上传到PyPI

TestPyPI验证无误后,就可以信心满满地上传到正式的PyPI了。

# 确保dist目录下是最新的构建文件 twine upload dist/*

这个命令会读取~/.pypirc[pypi]的配置进行上传。

上传后的操作:

  1. 访问项目主页:上传成功后,twine会输出类似https://pypi.org/project/your-awesome-project/0.1.0/的链接。打开它,检查你的README.md是否被正确渲染,所有元信息是否准确。
  2. 进行最终安装测试:在另一个干净的虚拟环境中,执行pip install your-awesome-project,进行最终验证。
  3. 庆祝一下:你的项目现在对全球的Python开发者可用了!

5. 常见问题、排查技巧与进阶维护

即使按照步骤操作,你也可能会遇到一些棘手的问题。下面是我在多次上传中积累的“排坑”实录。

5.1 上传失败与错误码解析

错误现象可能原因解决方案
HTTPError: 400 Client Error: File already exists.你尝试上传的版本号(如0.1.0)在PyPI上已存在。PyPI不允许覆盖已发布的版本。永远不要试图重新上传同一版本。修复问题后,在pyproject.toml中增加版本号(如改为0.1.1),重新构建并上传。
HTTPError: 403 Client Error: Invalid or non-existent authentication information.认证失败。.pypirc文件中的Token错误、过期,或格式不对。检查~/.pypirc文件:
1. 确认username__token__(双下划线)。
2. 确认password是完整的Token,以pypi-开头。
3. 在PyPI网站上重新生成Token并更新配置文件。
ImportErrorModuleNotFoundError在安装后1. 包名name与代码中导入的包名不一致。
2.pyproject.tomlpackages配置有误,未包含你的源码目录。
1. 检查pyproject.tomlname和源码目录名、import语句使用的名字之间的关系。
2. 如果使用setuptools,确保在[tool.setuptools]setup.cfg中正确配置了packages或使用find:指令。现代pyproject.toml[project]通常能自动发现。
README.md在PyPI上显示为纯文本或乱码1.MANIFEST.in未包含README.md
2.README.md包含不兼容的复杂Markdown或HTML。
1. 确认MANIFEST.ininclude README.md
2. 简化README.md,避免使用可能不被渲染的复杂语法。先使用最基本的Markdown。
依赖包未自动安装pyproject.tomldependencies列表未正确填写或格式错误。仔细检查dependencies的TOML语法,确保每个依赖项是字符串,并且版本说明符正确(如>=2.25.0)。在干净的虚拟环境中测试安装以验证。

5.2 版本管理与发布策略

  • 语义化版本:严格遵守主版本号.次版本号.修订号。修复Bug升修订号,向后兼容的新功能升次版本号,不兼容的改动升主版本号。
  • 发布流程
    1. 在本地完成开发和测试。
    2. 更新pyproject.toml中的version
    3. 更新CHANGELOG.md(如果维护了的话)。
    4. 提交代码并打上Git标签:git tag -a v0.1.0 -m “Release version 0.1.0”
    5. 将标签推送到远程仓库:git push origin v0.1.0
    6. 执行构建和上传PyPI的流程。
  • .gitignore:确保将构建产物目录加入.gitignore
    dist/ build/ *.egg-info/

5.3 进阶:自动化与持续集成

手动上传毕竟麻烦。你可以利用GitHub Actions或GitLab CI等工具,实现“打标签即发布”的自动化流程。

核心思路是:当你在GitHub上创建一个新的发布(Release)或推送一个版本标签(如v1.0.0)时,CI流水线自动执行以下步骤:

  1. 检出代码。
  2. 安装Python和构建工具。
  3. 运行测试(确保质量)。
  4. 构建分发包。
  5. 使用存储在仓库Secret中的PyPI Token,将包上传到PyPI。

这需要编写一个CI配置文件(如.github/workflows/publish.yml),其中最关键的一步是安全地使用Token进行上传。这能极大提升发布效率和规范性。

整个过程走下来,你会发现,将一个项目上传到PyPI,远不止是执行几条命令那么简单。它是对你项目结构、依赖管理、文档和发布流程的一次全面体检。第一次可能会遇到不少小麻烦,但一旦流程跑通,后续的版本更新就会变得非常顺畅。当看到别人通过pip轻松安装并使用你写的工具时,那种成就感和为开源社区贡献了一分力量的满足感,会让你觉得这一切都是值得的。

← 返回列表