用 deptry 检查 Python 依赖腐化并接入 CI 门禁
问题场景:依赖表会慢慢腐化
Python 项目里的 pyproject.toml 或 requirements.txt 是一份人工维护的清单,而代码里的 import 是另一份事实清单。两份清单长期不同步,就会出现三类典型问题:
- 代码导入了,但依赖没声明:本地因为装了别的包顺带带上,或者开发机上装过,测试能过,换到干净环境就
ModuleNotFoundError。 - 依赖声明了,但代码没用:重构删了代码没删依赖,安装变慢、镜像变大,还多背一份升级与漏洞治理成本。
- 直接使用传递依赖:自己没声明,却
import了上游包带进来的模块。上游哪天换实现或去掉该依赖,构建就断。
人工 review 很难稳定发现这些问题。deptry 通过对源码做 AST 静态分析,把「导入的模块」与「声明的依赖」对齐,用固定规则输出问题清单。
deptry 的规则含义
常见规则编号(以所用版本文档为准):
| 规则 | 含义 |
|---|---|
| DEP001 | 代码导入了但依赖未声明 |
| DEP002 | 依赖声明了但代码未使用 |
| DEP003 | 导入了传递依赖(非直接声明) |
| DEP004 | 在非开发代码中导入了开发依赖 |
最小可运行示例
目录结构:
demo-svc/
├── pyproject.toml
└── src/demo/
├── main.py
└── loader.py
pyproject.toml:
[project]
name = "demo-svc"
version = "0.1.0"
requires-python = ">=3.10"
dependencies = [
"requests>=2.31",
"python-dateutil>=2.8",
"pyyaml>=6.0",
]
[project.optional-dependencies]
dev = ["pytest>=8.0", "deptry>=0.20"]
[tool.deptry]
exclude = ["build", "tests"]
per_rule_ignores = { DEP002 = ["pyyaml"] }
package_module_name_map = { "python-dateutil" = "dateutil" }
src/demo/main.py(dateutil 是包名与模块名不一致的例子):
import requests
from dateutil import parser
def parse_time(raw: str) -> str:
return parser.isoparse(raw).isoformat()
def fetch(url: str) -> dict:
resp = requests.get(url, timeout=5) # 占位地址
resp.raise_for_status()
return resp.json()
src/demo/loader.py(故意制造两个问题):
import urllib3 # 传递依赖,未被直接声明
def version() -> str:
return urllib3.__version__
这里的 urllib3 只通过 requests 间接进入环境,属于 DEP003;pyyaml 已声明但没有任何模块导入它,会报 DEP002,示例中用 per_rule_ignores 演示如何按需豁免(真实项目里更推荐直接删依赖)。
运行方法
python -m venv .venv
source .venv/bin/activate
pip install -e ".[dev]"
deptry .
要点:先安装依赖再运行。deptry 需要已安装包的元数据来完成「发行包名 → 模块名」的映射,纯空环境容易产生 DEP001 误报。输出非零退出码即代表发现问题,可直接作为门禁条件。需要更细的定位时加 --verbose,需要机器可读结果时用 --json-output(具体参数名以 deptry --help 为准)。
接入 CI 门禁
以常见的流水线为例,核心只有「装依赖 + 跑 deptry」两步:
name: deps-check
on: [push, pull_request]
jobs:
deptry:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install
run: pip install -e ".[dev]"
- name: Check dependency hygiene
run: deptry .
迁移到其他 CI 平台时,把最后一行放进构建脚本即可。建议先从「只检查、只告警」开始,把存量问题用忽略清单收敛,再逐步改为阻断,避免一次开闸就让所有分支红灯。忽略清单要写进版本库并附带原因注释,否则豁免本身也会腐化。
边界与限制
- 只做静态分析。
importlib.import_module、__import__、字符串拼出来的导入不会被识别,这类插件式加载需要在配置里按包名忽略。 - 可选依赖与 extras。只在特定环境启用的依赖,常会被判成 DEP002;不同版本对可选依赖组的支持方式不同,必要时用
per_rule_ignores精确豁免。 - 命名空间包与 C 扩展包。元数据缺失或模块布局特殊时可能误报,
package_module_name_map是最直接的兜底手段。 - 动态声明的依赖。通过变量或文件动态生成的依赖列表,deptry 无法推断,需要显式指定依赖文件。
- 多包仓库。monorepo 中应针对每个子包分别运行并设置
exclude,不要对整个仓库一次性扫描。 - 能力边界。deptry 不检查版本范围是否合理,也不检查已知漏洞,那属于其他工具的职责。
- 开箱即红的存量项目。首次运行大概率会列出大量问题,建议先统计、分类,再决定删除依赖还是补充声明。
小结
依赖腐化是渐进式的,单次代码评审很难拦住。把 deptry 放进流水线,配合包名映射和少量有理由的忽略规则,可以把「导入与声明一致」变成每次提交都要通过的硬约束,让依赖清单重新成为可信的构建输入。
- 点赞
- 收藏
- 关注作者
评论(0)