Archived
为什么:_fetch_via_http 是全仓内置下载通道的落盘实现,也是本文件此前最大的
圈复杂度所在。94 行的单体里叠着三段互不相关的职责——Range 起点校验、流式写盘
与体积上限、HTTP 错误分类——还共享一个 try/except/finally。上一轮拆
download_music_file 时它是唯一没动的重函数。
改法:拆成 7 个具名成员,主函数只留四步编排(构造 Range 头 → 发请求 →
定追加/重写并校验分片起点 → 流式落盘)。
_range_headers 续传时请求「起点之后的全部字节」
_resolve_write_mode 续传且服务器确实给 206 才追加,否则从头重写
_verify_content_range 校验 206 分片起点;无法确认就清残片从头下
_stream_body 逐块写盘 + 取消检查 + 体积硬上限(500MB)
_safe_progress 上报进度,回调异常吞掉(原两处 try/except 收敛)
_http_status 从 HTTPError 取状态码
_is_deterministic_client_error 4xx 判定(408 例外,唯一保留可重试的 4xx)
复杂度(同口径 AST 实测,HEAD 与工作区用同一脚本):
_fetch_via_http 94 行 / 圈 21 / 嵌套 7 → 46 行 / 圈 5 / 嵌套 3
文件内最大圈复杂度 21(当时就是它) → 16(余下最大是 download_music_file)
文件行数 710 → 753(+43,全是新签名与 docstring)· 函数数 19 → 26
口径说明:上一轮口头报的「圈 35」用的是另一脚本(另计入布尔运算与比较),
本轮 HEAD 同口径实测为 21。以本轮为准——HEAD 与重构后由同一脚本测出,可比。
等价性证据(两道,互相独立):
1. 反向注入验证:把 HEAD 版 downloader 注入成 music_downloader.downloader,用同一套
tests/test_downloader.py 跑 → 30 例**全过**。证明这批测试忠实编码了旧行为,
因而对新实现构成等价性验证(含本次新增的 5xx 用例,它在旧实现上同样通过)。
2. AST 字面量多重集比对 + 逐条位置定位:数值/布尔 26 种、字符串 80 种,
差异全部归因,且**没有一条用户可见文案或阈值被改动**:
206 2→1:旧的两段独立 if(`!= 206` 归零 mode、`== 206` 校验起点)合并进
_resolve_write_mode 的一个判断
0 26→24:两处 progress 的 `or 0` 收敛成 expected_size(-1);
两处 206 判断各含一个 `> 0` 合并成一个 `<= 0`(-1)
'wb' 2→1:初始赋值与条件改写两处合并成一个 return
500MB 阈值、各条错误文案原样保留。
重写条件表达式必须补分支穷举(字面量审计不管这一层),已逐路核对:
resume_from <= 0 → 'wb'(旧:不进任何 if,保持初始 'wb')
resume_from > 0 且 status != 206 → 'wb'(旧:第一个 if 命中)
resume_from > 0 且 status == 206 → 校验起点 + 'ab'(旧:第二个 if 命中)
三路与旧实现一致。
刻意保留的语义(是决定,不是疏漏):
- 超限清理仍**延后到 with 退出后**——Windows 无法删除仍被打开的文件,原实现在
打开中 unlink 被静默吞掉,残片会留在磁盘上。
- 进度回调异常一律吞掉,绝不影响下载主流程。
- 4xx 里 408 是唯一例外(保留可重试);416 与「206 头被剥离解析不出起点」同口径
清残片从头下(盲追加拼出的是坏文件)。
- 非 416/4xx 的错误走**裸 raise**(不用 `raise e`),保持原 traceback 不被改动。
测试:新增 1 例 test_5xx_is_retried_and_keeps_part —— 5xx 是服务端瞬时故障而非
确定性失败,须可重试且保留 .part 供续传。这条路径此前无覆盖,且它正是
_is_deterministic_client_error 与 4xx 的分界所在。
顺带记录一个验证工具自身的坑(第一次跑反向注入时 30 例全红,看似「测试不忠实」,
实为注入没生效、测试静默走了真实网络,报错为 Cannot connect to host cdn.test):
只覆盖 sys.modules['music_downloader.downloader'] 不够——pytest 的 monkeypatch
解析目标字符串走 _pytest.monkeypatch.resolve(),是「逐级 getattr 父包」而非纯
sys.modules 查找,包的 downloader 属性仍指向真模块。必须同时 setattr 父包属性。
教训:等价性验证的**工具本身**也要能证伪,否则结论会 180 度反转。
验证:ruff 全过 · pyright 0/0 · pytest 275 passed(274→275)· 行尾 i/lf w/lf
与仓库一致 · 生产 data/ 未被触碰(全部用例走 tmp_path 沙箱)。