夜雨聆风学习资料网

ARTICLE · 1048598

还在用压缩包分享你的源码?太麻烦了吧,赶紧上PyPI,别人一句 pip install 就能用

还在用压缩包分享你的源码?太麻烦了吧,赶紧上PyPI,别人一句 pip install 就能用

一名二三线城市物联网行业程序员,从硬核游戏已转为休闲玩家,非著名脑洞开发者。感谢您抽出时间来阅读我的文章,开心地多啃了三根草。

杂谈

这篇文章将教会你如何将辛辛苦苦撸的代码上传到 pypi 上,供全世界的人通过 pip install xxxx 的方式下载安装,你没听错,是全世界!我将会写一个小工具,然后通过该工具来演示上传过程。

写终端程序的人,肯定遇到过获取用户 input 内容时,一边需要校验,结束还得强转数据类型的情况,非常麻烦,为此我写了个小工具,叫 kkinput,干的事一句话能说完:用户输入不规范,就再问一遍,直到问出一个合法的值。

拿它取一个年龄,跑起来是这样:

请输入年龄:abc年龄必须是数字请输入年龄:200年龄必须在1~120之间请输入年龄:30

填错就重问,不崩、不抛异常,调用方拿到的永远是用户输入的,纯净的,你所需要的数据类型,例如: 30。这类小东西写起来不费劲,用起来很顺手。

麻烦出在「分享」上。

同事看见我这么用,说他也想要。那我能怎么给呢?

  • • 打成压缩包发过去,他得解压、翻目录、再把路径塞进 sys.path。我改一版,他重来一遍;
  • • 或者把两个文件的内容贴到聊天框,他复制粘贴存成 py 文件。改一次,黏一次;
  • • 再或者直接丢进群里。于是「kkinput 最终版」「kkinput 最终版2」「kkinput 真最终版」三份文件同时在线,谁都说不清自己用的是哪一版。

这三条路我都走过,没一条省心。而 python 世界里,这件事其实早就有标准答案了:把它传上 PyPI,然后告诉对方一句 pip install kkinput

PyPI 是 python 官方的包仓库。你平时敲 pip install requestspip install pandas,装的就是它上面的东西。你的包传上去,就和它们并排站在一起,全世界任何一台装了 python 的机器都能一行装走。

下面我拿 kkinput 从头到尾走一遍。你手上任何一个自己的库,步骤完全一样。

kkinput已经上传成功,赶紧通过 pip install kkinput 测试一下吧!

一、先看成品:一个能被 pip 安装的项目长什么样

上传有个前提:你的代码得先变成「一个包」

目录摆对了,后面一路顺;摆不对,你会在构建的时候撞上一堆看不懂的报错。能上传的最小结构就是这样,不多不少:

kkinput/├── pyproject.toml├── README.md├── LICENSE└── kkinput/    ├── __init__.py    └── core.py
  • • pyproject.toml 是唯一必需的文件。 它是这个项目的身份证——没有它,pip 不知道你叫什么、什么版本、依赖谁。第三节会专门讲它,这是全篇唯一需要动脑的地方。
  • • README.md 会被渲染成你 PyPI 项目页上的正文,全世界都看得见,所以要当「产品页」来写,而不是给自己记笔记。
  • • LICENSE 放许可证全文。MITApache-2.0 这类宽松许可证最省事,别人拿去用不必来问你,你也不必担心被告。
  • • kkinput/ 就是包目录,名字必须和项目名一致__init__.py 有它才算一个包。你原来的代码文件叫 core.py 还是 utils.py 都无所谓,放进这个目录里就行。

有个坑要提前说清楚:构建时,根目录里只能有一个包目录。

我在演练的时候故意往根目录丢了个 demo/ 目录,构建立刻翻脸:

error: Multiple top-level packages discovered in a flat-layout: ['demo', 'kkinput'].

打包器会自动去根目录里找「一级包」,一找到俩它就不敢替你选了,直接停工。

这个设计其实是好的——猜错的后果,是把你压根没想发的东西发到全世界去。真要放多个包,那就别让它猜,自己写明白:

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

至于 main.py.idea/ 这些开发时顺手留下的东西,不用特意清理,自动发现会跳过它们(我实测过:根目录那个 PyCharm 生成的 main.py,确实没被打进包里)。不过把它们写进 .gitignore,是个更好的习惯。

原理图1_能上传的项目长什么样.png

二、装工具:只有两个

打包上传这件事,全程只需要两个工具:

python -m pip install --upgrade build twine

build 负责把你的源码打包成别人能安装的文件,twine 负责把这个文件安全地传上 PyPI。分工就这么清楚,一个管做,一个管送。

装在一个专门的虚拟环境里最稳,原因在第四节会说到:

python -m venv .venv.venv\Scripts\activate

macOS 和 Linux 上,激活命令换成 source .venv/bin/activate

你如果翻到过老教程,会看到 python setup.py sdist upload 这种一条命令搞定的写法。现在官方不再推荐了——因为它把「构建」和「上传」揉在了一起,你没办法先检查一下做出来的东西长什么样,再决定发不发。现在都是先 build、再 upload 两步走。中间那一刀,才是安全感所在。

三、写 pyproject.toml:项目的身份证

这是全篇唯一需要你动脑的文件。kkinput 用的这份可以直接抄走,把名字、描述、依赖换成你自己的:

[build-system]requires = ["setuptools>=77"]build-backend = "setuptools.build_meta"[project]name = "kkinput"version = "0.1.0"description = "终端交互式输入校验:输入不规范就重复提示,直到拿到合法的值"readme = "README.md"requires-python = ">=3.9"license = "MIT"license-files = ["LICENSE"]authors = [    { name = "Python卡皮巴拉" },]keywords = ["input""terminal""cli""validation"]classifiers = ["Programming Language :: Python :: 3","Intended Audience :: Developers","Operating System :: OS Independent",]dependencies = ["python-dateutil>=2.8",][project.urls]Homepage = "https://github.com/xxxxx/kkinput"

逐项说清楚,不绕弯。

  • • [build-system] 告诉 pip 用什么工具来构建。这里要留意 setuptools>=77 这个版本号——它不是随便写的。下面 license 那一行的新写法,要 77 之后的 setuptools 才认,你把版本写低了会直接报错。
  • • name 是包名,只能用字母、数字和 ._-,而且必须全球唯一。还有个容易忽略的规则:名字跟已有项目太像也会被拒,官方原话是「too similar to an existing project and may be confusable」。所以起名之前,先去 pypi.org 搜一下,别等传的时候才发现。
  • • version 是版本号,第一次发布习惯上从 0.1.0 开始。关于这个数字的规矩在第八节,那里有一条不能反悔的规则。
  • • description 是一句话简介,会出现在搜索结果里,写清楚它是干什么的,别只写「a utility」。
  • • readme 指向 README.md,内容会渲染到项目页上。
  • • requires-python 声明支持的 python 版本。我写了 >=3.9,并且实测过:同一个 wheel,在 3.9.8 和 3.13 上装完都能正常跑。
  • • license 写 SPDX 标识符,就是 MITApache-2.0 这种短名字。以前那种 license = {text = "MIT"} 的写法已经过时了。license-files 指出许可证文件的位置。
  • • classifiers 是给索引和 pip 看的元数据,至少要写上支持的 python 版本和操作系统。完整可选值在 pypi.org/classifiers 那一页,照着挑就行。
  • • dependencies 是这里面的重点——真正的依赖要写在这里kkinput 内部用了 dateutil 来解析日期,所以这里写 python-dateutil>=2.8。别人 pip install kkinput 的时候,pip 会把这两个包一起装好。这比在 README 里加一句「使用前请先安装 dateutil」体面得多,也可靠得多。
  • • [project.urls] 是项目页上展示的链接,指向源码仓库、文档、issue 都行。

四、打包:两条命令,两个文件

在 pyproject.toml 所在的那个目录里,执行:

python -m build

你会看到它先建了一个隔离环境、装上 setuptools,然后开始干活。跑完的最后一行是:

Successfully built kkinput-0.1.0.tar.gz and kkinput-0.1.0-py3-none-any.whl

目录里随即多出一个 dist/,里面躺着两个文件:

kkinput-0.1.0-py3-none-any.whl      5.9 KBkkinput-0.1.0.tar.gz                5.8 KB

注意 python -m build 是在一个隔离环境里执行的,构建需要的工具它会自己装。所以哪怕你的环境里根本没装 setuptools,也一样能跑通。

这也正是我劝你用干净虚拟环境的原因:免得你本地某些奇怪的配置,悄悄漏进那个要发给全世界的包里。

这两个文件有什么区别,值得花三十秒搞清楚:

原理图3_源码包与预编译包.png
  • • .tar.gz 是源码包(sdist),压缩的就是你那些 py 文件。别人安装的时候得现场构建一遍,所以他机器上得有打包工具。
  • • .whl 是预编译包(wheel),装的时候直接解压就位,不用现场构建。pip 会优先选它。

关于 wheel 的文件名,多看两眼就懂了:

kkinput-0.1.0-py3-none-any.whl   |     |    |   |   |   |     |    |   |   └─ any  = 不挑操作系统   |     |    |   └───── none = 不挑解释器 ABI   |     |    └───────── py3  = 任何 py3 都能装   |     └────────────── 版本号   └──────────────────── 项目名

纯 python 的包,一个 wheel 就能通吃所有平台;带 C 扩展的包,就得每个平台、每个 python 版本各出一个,文件名会变得很长。

顺手把构建产物加进 .gitignore

dist/build/*.egg-info/.venv/

dist/ 千万别提交进仓库。 它是构建产物,随时能重新做出来,提交了只会让仓库里堆一串历史安装包,越来越肿。

插图1_把散落的文件装进纸箱.png

五、上传之前,先自己装一遍

这一步是整篇最省事、也最值钱的一步。别跳过。

先让 twine 检查一下元数据能不能正常渲染:

python -m twine check dist/*
Checking dist/kkinput-0.1.0-py3-none-any.whl: PASSEDChecking dist/kkinput-0.1.0.tar.gz: PASSED

两个 PASSED 就放心了。如果这里报错,通常是 README 的 markdown 有问题,或者某个元数据字段写得不合法。在本地发现,比传上去以后发现舒服一万倍——毕竟传上去就删不掉了(第八节细说)。

然后建一个全新的虚拟环境,把做好的 wheel 装进去:

python -m venv ../_test../_test/Scripts/python.exe -m pip install dist/kkinput-0.1.0-py3-none-any.whl

真实输出是这样的:

Collecting python-dateutil>=2.8 (from kkinput==0.1.0)Collecting six>=1.5 (from python-dateutil>=2.8->kkinput==0.1.0)Installing collected packages: six, python-dateutil, kkinputSuccessfully installed kkinput-0.1.0 python-dateutil-2.9.0.post0 six-1.17.0

看到没,我只是装了一个 kkinputpython-dateutil 和它自己的依赖 six 全自动跟了进来——这就是第三节那句 dependencies 的功劳。

装完别急着关,跑一段真实代码,确认它真的能用:

from kkinput import kk_input, IntValueage = kk_input(IntValue('年龄', min_value=1, max_value=120), loop=True)print('age ='repr(age))
请输入年龄:abc年龄必须是数字请输入年龄:200年龄必须在1~120之间请输入年龄:30age = 30

另外,开发阶段想边改边测,可以在自己的项目里装一份「可编辑」版本:

python -m pip install -e .

这样你改完 core.py 保存,不用重新安装,下次运行就是新代码。

为什么要花一整节来做自查?因为上传之后你几乎没有反悔的机会。而这两个动作能拦住绝大多数低级错误:

  • • README 里的示例代码根本跑不通;
  • • 依赖漏写了,别人装上一运行就 ImportError;
  • • 包目录里少放了文件,或者 __init__.py 忘了把函数导出;
  • • 元数据字段不合法,twine check 直接报错。

这些在本地全能提前发现。花三分钟,省一次尴尬。

六、注册账号,拿到 API token

到这一步才开始和 PyPI 打交道。一共三件事。

第一件:注册账号。 打开 pypi.org 注册,然后把邮箱验证掉。官方明确写了:注册新项目、上传新版本或文件,都需要一个已验证的邮箱。没验证,你是传不上去的。

第二件:开两步验证。 PyPI 上 2FA 是强制的,没开就没法登录上传。手机装个 Google Authenticator 或者 Microsoft Authenticator 就行。

可以查看官方说明:https://pypi.org/help/#twofa。卡卡用的是安卓的 Duo Mobile

这里有个细节建议照做:把恢复码存好。2FA 丢了又没有恢复码,账号有可能永久找不回来,官方也救不了你。

第三件:生成 API token。 这是用来代替密码的凭证。

登录后进 Account settings,找到 API tokens,点 Add API token:

  • • Token name:随便起,能认出来就行,比如 kkinput-upload。以后 token 多了才分得清;
  • • Scope:要么选 Entire account(整个账号通用),要么指定某个项目。如果只是给一个包用,选项目更安全——万一泄露了,也只影响那一个包。

点完 Add token,页面只显示一次,立刻复制走。 它长这样,前缀固定是 pypi-

pypi-AgEIcHlwaS5vcmc...(后面还有很长一串)

用的时候有一条铁律,务必记牢:

用户名写死的 __token__,密码填这串 token。

不是你的 PyPI 用户名,也不是你的登录密码。写错这两个,就会撞上第九节那个 403 报错。

至于这串东西怎么交给 twine,有三种存法,从省事到安全,你挑一个。

最省事的是上传时直接让 twine 问。 敲完上传命令,它会提示你(上传教学会在后面说明):

Enter your API token:

粘贴进去回车就行。注意它根本不回显,屏幕上什么都不显示是正常的,别以为没粘上。

顺便一个真实的坑:在 Windows 的 CMD 或 PowerShell 里,这个提示符下 Ctrl+V 和 Shift+Insert 都是没用的,得右键选粘贴。

适合写脚本的是环境变量。 设两个变量,twine 会自己读:

TWINE_USERNAME=__token__TWINE_PASSWORD=pypi-你的token

Windows CMD 用 set,PowerShell 用 $env:,macOS 和 Linux 用 export,值都一样。放在 CI 里跑自动发布时,这个方式最干净。

最舒服的是写进 ~/.pypirc 这个文件放在你的用户主目录下(Windows 上是 C:\Users\你的用户名\.pypirc),不在项目里:

[pypi]username = __token__password = pypi-你的token

设好之后,以后每次上传都不用再输入任何东西。

但这里必须多提醒一句:.pypirc 里存的是明文,官方文档自己也在警示这一点。所以两件事一定做到。

第一,别把它提交进 git。顺手在 .gitignore 里加一行 .pypirc

第二,能不用明文就优先用 keyring。twine 装的时候已经把 keyring 一起带上了,直接用它把 token 交给系统钥匙串保管:

keyring set https://upload.pypi.org/legacy/ __token__

之后 twine 会自动去钥匙串里取,配置文件里就不用留任何明文了。这比把密码写在纸上锁抽屉里还踏实。

插图2_举着发光的通行卡.png

七、先传 TestPyPI 练个手

正式发布之前,官方专门给你留了一块演练场,叫 TestPyPI。它和正式站是两套完全独立的数据库,传坏了不心疼,也不用担心把好名字占了。

它需要单独注册一个账号(和正式站不通用),然后上传命令只多一个参数:

python -m twine upload --repository testpypi dist/*

传完去 https://test.pypi.org/project/你的包名/ 看一眼。重点看项目页渲染得对不对:

  • • README 有没有正常显示成正文;
  • • 版本号、描述、分类是不是你要的样子;
  • • 项目页有没有缺图或者排版错乱。

然后再从 TestPyPI 把它装下来验一遍:

python -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ kkinput

后面那个 --extra-index-url 千万别省。TestPyPI 上经常没有你依赖的包(它的容量有限,还会定期清理),不额外从正式站补一份的话,pip 会因为找不到 python-dateutil 直接失败——而这个失败跟你的包一点关系都没有,纯属白紧张一场。

八、正式上传,以及那条不能反悔的规则

演练满意了,正式的命令就是把 --repository testpypi 去掉:

python -m twine upload dist/*

中间的交互过程大致是这样,在这里就需要填入你的API token了(下面是官方文档里的样式,你看到的包名和体积会不一样):

Uploading distributions to https://upload.pypi.org/legacy/Enter your API token:Uploading kkinput-0.1.0-py3-none-any.whlUploading kkinput-0.1.0.tar.gzView at:https://pypi.org/project/kkinput/0.1.0/

命令跑完不报错就算成功。等一两分钟,https://pypi.org/project/kkinput/ 就能打开了,全世界的 pip 也都能搜到它。

现在说整篇最需要记住的一条规则:

同一个文件名,PyPI 永远不允许重复上传。

文件名 = 项目名 + 版本号 + 分发类型。官方原话是:即使项目被删除又重新创建,文件名依然不能再次使用。

这条规则带来三个后果,都很硬:

  • • 传错了想覆盖?不行。把它删掉再重新传?也不行
  • • 删除项目、版本、文件都是永久且不可恢复的,官方管理员也救不回来;
  • • 唯一的出路是:改版本号,重新 build,重新上传。

这个设计不是刁难你,而是为了保护用你的人:它保证「某项目的某版本的某个包」永远对应同一个文件,谁也没法偷偷换掉里面的内容。

所以你以后的更新流程,是固定的三步:

python -m buildpython -m twine check dist/*python -m twine upload dist/*

当然,第三行之前记得先把 pyproject.toml 里的 version 从 0.1.0 改成 0.1.1

这个动作很容易忘,尤其是改完代码兴冲冲直接 build 的时候。好在忘了的后果只是上传被拒——不花钱也不出错,PyPI 在这一点上还是挺讲道理的。

原理图2_从代码到pip_install的六步.png

九、三个最常见的报错,对应三种病因

第一次发布,十有八九会撞上下面三个报错之一。提前认识一下,省得临场慌。

第一个:构建时报 Multiple top-level packages discovered

error: Multiple top-level packages discovered in a flat-layout: ['demo', 'kkinput'].

病因是根目录里出现了两个一级包。要么把多余的目录挪出去,要么在 pyproject.toml 里显式指定:

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

第二个:上传时报 Filename or contents already exists

病因是同一个「项目名 + 版本号 + 分发类型」你已经传过一次了。唯一解法就是改版本号,重新构建,重新上传。删掉旧文件也没用——文件名永久作废。

第三个:上传时报 Invalid or non-existent authentication information

这个 403 基本只有一个原因:用户名没写 __token__,或者 token 没复制全。

排查三处:.pypirc 或环境变量里的 username 是不是 __token__;token 有没有漏掉末尾、有没有混进换行或空格;scope 选项目时,token 是不是只授权给了另一个包。

顺便说一句,这三类报错里,只有第三个是「操作失误」,前两个都是「规则本身」。把规则记住,它们就不算报错了,只是流程的一部分。

总结

  1. 1. 最小结构就三样pyproject.toml、包目录、README.md,再加一个 LICENSE 更规范。
  2. 2. 工具只有两个build 负责做包,twine 负责送包。构建跑在隔离环境里,所以用干净的虚拟环境最稳。
  3. 3. 上传前必做两件事twine check 校验元数据,再建个新环境把 wheel 装一遍跑通。这一步能拦住绝大多数低级错误。
  4. 4. token 就等于密码:用户名固定 __token__,密码填 token,别提交进仓库,能用 keyring 就别用明文。
  5. 5. 版本号只能往前走:同一个「项目名+版本号+分发类型」只能上传一次,删了也不能重来。
  6. 6. 拿不准就先传 TestPyPI,练手不心疼,正式站的机会省着用。

走完这一整套,我最大的感受是:发布自己的包,难的从来不是技术,而是「原来还要准备这些东西」这层信息差。

一旦 pyproject.toml 写对了,后面每一步都只是复制命令而已。

下一次同事再问你「这工具能给我用吗」,你终于可以只回两个字:pip install。

往期回顾

python中的达摩克利斯之剑,用不好程序将完全崩溃

别再写 def f(a=[]) 了!python 默认参数竟然有"记忆"

小白学python经常会有这三个认知错误

好用的五个python表格自动化工具,谁都可以复制直接用

相关学习资料