DEBUG.WIKI
PanstarCloud:部署应用,接入 AI,云服务器与 AI 网关
京东云;让 AI 进入真实业务;零售 · 物流 · 客服 · 硬件;查看方案 →
搬瓦工;先看线路,再选 VPS;CN2 GIA · 对应线路套餐;查看线路 →

Click CliRunner 分页输出后报 I/O operation on closed file

Click 8.4 的分页回归会在借用标准输出后关闭它,CliRunner 收尾 flush 才报错。8.4.2 完成相关修补,升级后同时检查分页后普通输出和捕获结果。

命令里用了 click.echo_via_pager(),CliRunner.invoke() 收尾时却在 sys.stdout.flush() 报 ValueError: I/O operation on closed file。输出内容可能已经写出,错误直到退出捕获环境才暴露,容易让人误以为测试框架关早了流。

Click 8.4.0 的分页重构中,没有启动外部分页器的路径会借用标准输出,但后续包装处理可能把借来的流关闭。测试捕获、非终端输出等环境容易走到这条路径。错误的关键是输出流的所有权,而不是分页内容中含有某一种字符。

使用完成借用流修补的版本

Click 8.4.1 曾加入相关修补,但那次调整并不完整。8.4.2 的正式发行记录明确补齐 echo_via_pager() 和 get_pager_file() 在没有外部分页器时关闭借用标准输出的问题,8.5.0 也保留不关闭所包装流的实现。

更新时选择项目依赖允许且包含完整修补的正式版本,并让锁文件与实际运行 CLI 的环境一起更新。不要只看到 8.4.1 的“closed file”修复说明,就排除所有剩余的借用流问题;也不要把版本号已更新的另一个解释器,当成测试实际使用的环境。

可以保留一个分页后继续普通输出的最小测试,下面是测试结构示例:

import click
from click.testing import CliRunner

@click.command()
def cli():
    click.echo_via_pager("first")
    click.echo("after pager")

result = CliRunner().invoke(cli)
assert result.exit_code == 0, result.exception
assert "after pager" in result.output

它检查分页调用返回以后,标准输出是否仍可写,以及捕获收尾能否完成。仅检查结果文本中出现第一行不够,因为提前输出成功与后续流被关闭可以同时发生。

不要在测试里重新打开标准输出

把 sys.stdout 换成新的文件对象,会改变 CliRunner 的捕获边界;捕获结果看起来为空或转写到真实终端,又可能掩盖原来的故障。吞掉收尾的 ValueError 也不能恢复已经被关闭的流,后续测试仍可能失败。

短期无法更新时,可让应用提供不分页的输出分支,并在这类非交互调用中直接使用普通 echo()。这是明确关闭分页功能的取舍,只能说明不分页路径正常,不能用该测试替代原来的分页验收。真正需要检查交互式分页器时,还要在具备终端条件的环境覆盖对应路径。

这条问题与 Windows 临时文件后端把文本写入二进制文件、或清理时出现 WinError 32 的故障不同。它们都可能经过分页函数,但失败的资源、症状和修补范围各不相同,不能看到 echo_via_pager 就沿用同一种根因。

完成更新后,使用原来的捕获模式核对分页输出、分页后输出和测试收尾,并检查退出码及 result.exception。涉及颜色或生成器输出的命令,还要保留原输入形式,确认更新没有让输出丢失、额外改写或只剩最后一段。