引入依赖是长期承诺:评审、来源、许可、退出方式四项。成本接近零,评审的人在一千行里注意不到。
如果是准备 pip install 一个新库,先答引入前回答的五个问题。
这次改动新增的依赖,它的名字、许可、替代方案、移除方式,能不能在合并请求说明里找到?
说明里找不到这四项,说明它是在没人注意的时候进来的。补法:把下面的五问模板补进说明;第 1 问和第 5 问答不出,评审人直接拒。后果是可以量化的:一个装进 site-packages 的包出了问题,要么连带它的一整棵依赖树一起换掉,要么继续用一个没人维护的库。
五个问题不用写成长文档,但每问都要留一条命令或一段可查的记录,答案要有人看过。「助手说这个库很常用」不是答案。下面这份模板直接贴进合并请求的「新增依赖」一栏。
它是「填完才能提交」的检查表,不是问卷。空着的一栏等于没查。
## 新增依赖:fastcache-lite==0.4.2 1. 仓库里有没有同类库? - 搜索:grep -rn "cache" pyproject.toml requirements*.txt app/ - 结论:无同类(仓库内仅用 functools.lru_cache,无持久化缓存) 2. 许可是什么? - 许可类型:MIT(包内 LICENSE 文件) - 与项目许可(MIT)是否冲突:否 3. 维护状态如何? - 最近一次发布:近 6 个月内 - 是否单人维护:否 - 停更是否超过 12 个月:否 4. 体积与传递依赖可接受吗? - 新增传递依赖:0 个(pipdeptree 查看) - 结论:可接受 5. 出问题怎么移除? - 引用范围:只在 app/clients/cache.py - 替换成本:改动 1 个文件,接口只有 get/set/delete
判据只有两处:答不出第 1 问的搜索命令,说明没查;答不出第 5 问的引用范围,说明这个依赖会渗进整个项目,移除成本高,要更谨慎。边界:只用标准库就能完成的改动,不必填这份模板,它只针对第三方包。
## 新增依赖:fastcache-lite==0.4.2 1. 仓库里有没有同类库? 无(助手说这个库很常用) 2. 许可是什么? MIT(助手说) 3. 维护状态如何? 近期有发布(助手说) 4. 传递依赖可接受吗? 0 个(助手说) 5. 出问题怎么移除? 改动 1 个文件(助手说)
认它的办法:装之前去注册表亲自查一次这个名字,看首次发布时间和下载量。五问全由助手代填、每问都是「助手说」,等于一问没答——页面上那句「『助手说这个库很常用』不是答案」说的就是它。差别在于查一次只要十秒,而这个包会一直留在依赖树里。
值得单独说的是:助手推荐的包名,可能是它编出来的。Spracklen 等人在 2025 年那篇测量里(USENIX Security 2025,arXiv:2406.10279)跑了 16 个模型、57.6 万次生成,19.7% 的包引用在现实中不存在——开源模型平均 21.7%,商业模型 5.2%。更难办的是其中 43% 的假名字在重复生成里稳定复现,攻击者抢注这些名字就能批量收割,这个手法叫 slopsquatting。查一次注册表同时挡住两件事:不存在的包,和刚被抢注的包。
把它分成两样:人写的直接依赖,和工具生成的锁文件。前者评审时看,后者保证同一个版本在每台机器上装出同样的东西。锁文件必须进版本库,每次变更要在合并请求里看得见。
不写版本上限,只在出现不兼容时加约束。
flask httpx pytest
由 pip-compile 生成,含所有传递依赖与哈希。不要手工编辑它;要改就重新生成整份。
# 由 pip-compile --generate-hashes 生成,不要手工编辑 flask==3.0.3 --hash=sha256:... httpx==0.27.0 --hash=sha256:... pytest==8.3.2 --hash=sha256:... # 传递依赖(节选) click==8.1.7 --hash=sha256:... certifi==2024.8.30 --hash=sha256:...
它一次性回答「我们用了哪些许可」。由工具生成,不用手写。
# 依赖许可清单(由 pip-licenses 生成) | 包 | 版本 | 许可 | 用途 | |---|---|---|---| | flask | 3.0.3 | BSD-3-Clause | Web 框架 | | httpx | 0.27.0 | BSD-3-Clause | 调用外部模型服务 | | pytest | 8.3.2 | MIT | 测试,不进入运行环境 |
边界:带哈希的锁文件保证「同一个版本装出同样的东西」,代价是升级一个包就要重新生成整份,冲突时还要再跑一遍。课程项目规模下这个代价很小。本组决定不要哈希,至少要把精确版本(==)锁住,不要用 >=——后者会让不同机器装出不同的依赖。
下面六种情况里前三种最常见,出现任一条就不要引。
| 情况 | 判据 | 改用 |
|---|---|---|
| 要的功能标准库里有 | 日期时间、JSON、压缩、HTTP 基本请求 | 标准库,先查 python -c "import json" 一类能否满足 |
| 只用它一两个函数 | 全项目对该库的调用点 ≤ 2 处 | 自己写十几行,写进 app/utils/ |
| 会带进一棵依赖树 | 传递依赖数量 > 10 个,或新增包体积明显变大 | 标准库或自写;换更小的替代库 |
| 停更超过 12 个月 | 最近一次发布距今 > 12 个月 | 换维护中的库,或自写 |
| 许可与项目冲突 | 许可类型与项目不一致,以 SPDX 标识为准 | 不引入,换许可相容的替代 |
| 只是让代码看起来短 | 说不出它在解决哪个具体问题 | 不引入,保留现有实现 |
pipdeptree。pipdeptree 看它会带进来多少包;最后把引用收在一个模块里,只暴露必要的方法。
import 同一个库;把「这个库很常用」「能跑通」当成引入理由。
五条命令:装工具、生成锁文件、安装、查漏洞、查许可。把它们全部写进 Makefile 或工作流,本地和持续集成跑同一条。
第一次要装两个工具。之后每次改依赖,只跑第二到第五条。
# 1. 装工具 pip install pip-tools pip-audit pipdeptree # 2. 生成带哈希的锁文件(改动它等于改动依赖,要单独评审) pip-compile --generate-hashes requirements.in -o requirements.lock # 3. 按锁文件安装 pip install -r requirements.lock # 4. 查已知漏洞(退出码非零即失败) pip-audit -r requirements.lock # 5. 查传递依赖与许可 pipdeptree --warn silence pip-licenses --format=markdown --with-urls > docs/licenses.md # 6. 看这次改动动了哪些依赖(评审用) git diff origin/main -- requirements.lock docs/licenses.md
| 严重度 | 处置 | 时限 |
|---|---|---|
| 高危 | 阻断合并,升级或替换该依赖 | 立即 |
| 中危 | 限期修复,超过时限自动升为阻断 | 7 天内 |
| 低危 | 记录,下次依赖变更时一并处理 | 无单独时限 |
一条容易忽略的边界:扫描工具本身也是依赖。装一个扫描器不会让事情变好,让它跑起来才会——仓库里没有可复制命令的扫描项,不写进清单。密钥扫描要覆盖历史提交(gitleaks detect --redact):已经进过仓库的密钥一律轮换,只删掉那一行,它在历史里仍然可查,等于没处理。这一条与质量门禁共用同一套阈值。