Python warnings 没显示,不等于没有触发:过滤顺序与重复抑制怎样测
测试里没有出现弃用告警,不一定说明旧接口没有被调用。warnings会经过过滤和重复抑制;同一条调用路径第二次执行,可能不再显示,而某些告警类别在普通运行环境里本来就默认隐藏。
如果测试只是在运行结束后检查日志有没有一行warning,结论就同时依赖过滤配置、调用位置与此前执行历史。更可靠的做法是为测试建立受控过滤作用域,明确要观察、忽略还是把某类告警升级为异常。
下面按Python 3.12编写,只在单线程测试作用域里修改告警过滤。它没有把这套全局配置流程当成可以随意并发进入的请求隔离工具。
同一处调用,四种动作
保存warnings_contract.py执行。DemoDeprecation是一类专门用于实验的弃用告警,避免修改所有第三方类别。
import warnings
class DemoDeprecation(DeprecationWarning):
pass
def old_api():
warnings.warn('old_api will be removed', DemoDeprecation, stacklevel=2)
return 7
def emit_twice():
for _ in range(2):
old_api()
def recorded(action):
with warnings.catch_warnings(record=True) as captured:
warnings.simplefilter(action, DemoDeprecation)
emit_twice()
return [(str(item.message), item.category.__name__) for item in captured]
def main():
default = recorded('default')
always = recorded('always')
ignored = recorded('ignore')
print('default count:', len(default))
print('always count:', len(always))
print('ignore count:', len(ignored))
assert len(default) == 1 and len(always) == 2 and ignored == []
with warnings.catch_warnings():
warnings.simplefilter('error', DemoDeprecation)
try:
old_api()
raise AssertionError('warning should become an exception')
except DemoDeprecation as error:
print('error mode:', str(error))
with warnings.catch_warnings(record=True) as captured:
warnings.simplefilter('ignore', DemoDeprecation)
warnings.simplefilter('always', DemoDeprecation, append=True)
old_api()
assert captured == []
warnings.simplefilter('always', DemoDeprecation)
old_api()
assert len(captured) == 1
print('first matching filter wins:', len(captured))
if __name__ == '__main__':
main()
结果应是default记录一次、always两次、ignore零次,error动作则把告警作为异常抛出。最后一组先把always追加到ignore后面,没有改变结果;再把always放到前面,它才优先匹配。
emit_twice中的调用写在同一行,stacklevel=2把位置指向调用方,因此两次落在同一观察位置。这个结构是为了固定重复抑制条件,不是在证明任何地方只要消息文本相同就一定合并。
default 的“只显示一次”包含位置维度
重复判断不是一个简单的全局消息集合。不同动作有各自规则,default考虑告警消息、类别、模块与相应位置;换一个调用行,可能重新显示同样文本。module或once等动作又有不同抑制范围。
因此把一段调用挪到公共辅助函数、调整stacklevel,可能改变告警显示次数,即使业务调用总量没有变化。测试若真的关心旧接口被调用多少次,告警条数不是可靠计数器;应使用调用跟踪或明确测试替身。
stacklevel的目的在于让告警指向使用者可修改的调用处,而不是库内部固定那一行。库函数包装层级变化后,也应核对位置是否仍然准确。告警文本描述了将发生的变化,位置则应帮助读者找到需要迁移的代码。
有些测试只验证类别和关键信息即可,不必把完整路径与行号写死。行号会随正常重构变化,过度精确的快照会制造无关失败,反而让维护者倾向于关闭告警检查。
过滤顺序是一份优先级,不是所有规则共同投票
warnings按相应顺序寻找第一条匹配过滤项。simplefilter默认把规则放到前面,append=True则追加,所以程序最后一组的两次设置有不同结果。
项目启动、测试框架和某个依赖都可能增加过滤项。只看自己写的那一行simplefilter,无法解释最终行为;排查应在受控环境打印必要配置,确认实际顺序,而不是不断添加更多ignore试图让测试变绿。
类别过滤也应尽量具体。为了暂时处理一个已知依赖告警,把所有Warning都忽略,会把未来新出现的资源告警或兼容问题一起隐藏。专门的实验子类还能避免其他弃用类别混入计数,让每条断言知道自己观察的对象。迁移完成以后,应移除临时过滤,而不是让ignore永久成为项目初始化的一部分。
error动作有助于让需要迁移的调用在测试中立即失败,但适用范围应当明确。把整个依赖树所有告警一律升级,可能让无关路径阻塞当前工作;针对自己承诺支持的接口和版本建立策略更容易维护。
捕获用于观察,不代表生产行为已经改变
catch_warnings(record=True)让测试收集WarningMessage对象,包含类别、文本和位置等信息。它不只是把标准错误输出换一个颜色,也不要求测试从日志字符串反向解析告警。
离开作用域以后会恢复相关过滤配置,但不能据此把此前告警历史当成完全没有发生。具体重复状态与运行实现有关,测试最好在每个受控作用域明确设置所需动作,不依赖另一条测试是否先执行。
按本文的Python 3.12模型,告警配置会涉及全局状态,不应让多个线程同时进入不同catch_warnings配置,期待每条请求互不影响。这里把所有实验顺序放在main里,正是为了排除这份并发变量。换Python版本时应重新核对其并发安全说明,而不是沿用一个跨所有版本的结论。
弃用告警默认策略还与类别和运行入口有关。DeprecationWarning通常面向开发者,普通库模块中的显示策略与__main__中的情况可能不同;测试又可能启用更严格规则。命令行看不到,不代表测试也一定看不到,反过来也一样。
如果需要临时核查整个程序,可以用解释器告警选项或环境配置建立一次可记录的运行条件,再确认哪些类别实际出现。不要把个人终端一直设置的过滤变量当成项目默认,其他开发者和CI未必继承它。
回到最初那个没有warning的测试,先在局部把目标类别设为always并记录对象;确认调用存在后,再按项目策略决定应保留、忽略还是升级。把“有没有触发”与“应该怎样显示”分别验证,才能避免告警系统自己成为观测盲区。