代码质量工程:Ruff + mypy + pytest 三位一体
代码质量工程:Ruff + mypy + pytest 三位一体
学习目标
本章要解决什么问题:一个 10,000+ 行的大型 Python 框架如何在快速迭代中保持代码质量?
读者将学到:
- Ruff 全量规则集配置与选择性忽略
- mypy 严格类型检查的启用方式
- pytest 分层测试体系(单元/集成/基准)
前置知识
本章是 LangChain 特有的工程实践,不涉及通用知识原理。
项目实践
Ruff:全量规则集
LangChain 采用 Ruff 的激进策略——启用 ALL 规则,再选择性关闭不适用的部分:
[tool.ruff.lint]select = ["ALL"]ignore = [ "C90", # McCabe 复杂度 "COM812", # 与 formatter 冲突 "CPY", # 不要求版权声明 "FIX002", # 允许代码中包含 TODO "PERF203", # 性能建议,通常不适用 "PLR09", # 参数/语句数量限制 "TD002", # TODO 不需要标注作者 "TD003", # TODO 不需要关联 issue "ANN401", # 允许使用 Any 类型 "BLE", # 允许捕获盲异常]关键配置:
| 配置项 | 值 | 说明 |
|---|---|---|
line-length | 100 | 行宽限制 |
docstring-code-format | true | 格式化 docstring 中的代码 |
convention | Google 风格 docstring | |
ban-relative-imports | all | 禁止相对导入,强制绝对导入 |
分区忽略:测试文件和脚本使用 per-file ignores:
[tool.ruff.lint.extend-per-file-ignores]"tests/*" = ["D1", "S101", "S311", "SLF001", "PLR2004"]"scripts/*" = ["INP", "T201"]mypy:严格类型检查
[tool.mypy]strict = trueenable_error_code = "deprecated"warn_unreachable = true关键配置:
strict = true:启用所有 mypy 严格模式enable_error_code = "deprecated":将使用已弃用 API 视为错误warn_unreachable = true:报告不可达代码(如return后的代码)
warn_return_any 暂时关闭(标记为 TODO),等待后续激活完整严格检查。
pytest:分层测试体系
测试配置使用完整的 pytest 插件生态:
[tool.pytest.ini_options]addopts = "--strict-markers --strict-config --durations=5 --snapshot-warn-unused -vv"markers = [ "requires: 标记需要特定依赖的测试", "scheduled: 标记定时运行的测试", "compile: 标记仅用于编译集成的测试", "benchmark: 标记基准测试",]asyncio_mode = "auto"测试依赖:
| 工具 | 用途 |
|---|---|
pytest-asyncio | 异步测试支持 |
pytest-xdist | 并行执行测试 |
pytest-cov | 覆盖率报告 |
pytest-socket | 网络访问控制 |
pytest-benchmark | 基准性能测试 |
syrupy | 快照测试 |
pytest-mock | Mock 对象 |
blockbuster | IO 阻塞检测 |
vcrpy | HTTP 录制/回放(集成测试) |
PR 标题 Lint
PR 标题必须遵循 Conventional Commits 格式:
<type>[optional scope]: <description>
feat(core): add multi-tenant supportfix(langchain): resolve errordocs: update API usage examples允许的 type 包括:feat(MINOR)、fix(PATCH)、docs、style、refactor、perf、test、build、ci、chore、revert、release、hotfix。
允许的 scope 预定义:core、langchain、langchain-classic、model-profiles、standard-tests、text-splitters、docs、各 partner 名称、infra、deps、partners。
传统 Commits
CI 中的 pr_lint.yml 检查 PR 标题格式,确保:
- type 以小写字母开头
- Breaking changes 使用
!后缀(如feat!: drop support) - Release commit格式为
release(scope): x.y.z,不允许 body 或 footer
问题与规避
Ruff ALL 规则的维护成本
问题:启用 ALL 规则后,Ruff 版本升级可能引入新规则,导致 CI 突然失败。
对策:ignore 列表中明确排除不适用的规则。新版本引入的规则默认启用,如果不需要则加入 ignore。
mypy 严格模式与 Any 类型
问题:strict = true 要求所有类型注解完整,但 LLM 交互中返回类型经常是动态的。
对策:ignore ANN401 允许在必要时使用 Any。对于确实需要 Any 的场景(如动态模型加载),这是可接受的妥协。
测试速度
问题:全量测试在 Monorepo 中可能非常耗时。
对策:CI 通过 check_diffs.yml 变更检测,只运行被修改包的测试。本地开发使用 pytest-xdist 并行执行。集成测试使用 VCR 录制/回放,避免每次运行都调用外部 API。
设计取舍
ALL 规则 vs 精选规则集
| 方案 | 优势 | 代价 |
|---|---|---|
| ALL + 选择性忽略 | 不错过任何潜在问题、新规则自动生效 | 需要理解每个忽略的规则为何不适用 |
| 精选规则集 | 只启用已知需要的规则 | 可能遗漏有价值的检查 |
LangChain 选择 ALL + 选择性忽略,因为大型项目需要尽可能全面的代码质量检查,忽略特定规则比遗漏更有可控性。
严格类型检查 vs 渐进式
| 方案 | 优势 | 代价 |
|---|---|---|
| 严格模式 | 最大程度减少运行时类型错误 | 初期改造成本高、需要处理 Any 类型 |
| 渐进式 | 可以逐步启用 | 部分文件类型错误被忽略 |
LangChain 选择 strict = true,因为类型系统的价值在大型项目中尤为明显。通过 enable_error_code = "deprecated" 还能检测已弃用 API 的使用。