pytest 断言 diff 改进解析:dict.items() 与 dict.keys() 视图比较为何能显示缺失项
2026/9/14 6:51:41 网站建设 项目流程

pytest 断言 diff 改进解析:dict.items() 与 dict.keys() 视图比较为何能显示缺失项

【免费下载链接】pytestThe pytest framework makes it easy to write small tests, yet scales to support complex functional testing项目地址: https://gitcode.com/GitHub_Trending/py/pytest

本篇基于 pytest 的 changelog 条目 12860.improvement.rst 展开:当在断言中直接比较dict.items()/dict.keys()返回的视图对象(>=<=><==)时,断言失败输出现在会像 set 比较那样明确指出缺失(多余)的具体项。读完后你将掌握 pytest 断言重写(assertion rewriting)如何按"操作符 + 操作数类型"分发到专用比较器、_compare_set各比较函数的语义与输出格式,以及如何用 testing/test_assertion.py 中的集成测试验证这一行为。

改进内容是什么

changelog 原文如下:

Assertion diffs fordict.items()anddict.keys()comparisons (>=,<=,>,<,==) now show which items are missing, the same way set comparisons do.

翻译成开发场景:Python 的dict视图天然支持集合代数运算——

d1 = {"a": 1, "b": 2} d2 = {"a": 1, "b": 2, "c": 3} d1.items() <= d2.items() # True:d1 的键值对是 d2 的子集 d1.keys() < d2.keys() # True:d1 的键是 d2 键的真子集

这类"子集/超集"断言常用于校验"期望的最小字段集"是否全部出现、"实际字段"是否未超纲。改动之前,当这类断言失败时,pytest 无法给出专用解释,只能回退到通用的逐元素迭代对比,开发者必须自己肉眼比对两侧数据才能定位缺失项;改动之后,失败输出会直接列出"哪一侧多出了哪些项",与 set 比较的输出风格保持一致。

原理一:视图对象被识别为集合

改动生效的前提是:d.items()d.keys()的返回值是collections.abc.Set的注册子类型(ItemsViewKeysView),而非Mapping。这一点决定了 pytest 分发器会把它们路由进"集合比较"分支,而不是"映射比较"分支。

分发逻辑位于 src/_pytest/assertion/util.py 的assertrepr_compare中,它用match语句按(左操作数, 操作符, 右操作数)三元组精确匹配:

try: match (left, op, right): case (_, "==", _): source = _compare_eq_any( left, right, highlighter, verbose, assertion_text_diff_style, truncation_budget, ) case (str(), "not in", str()): source = _notin_text(left, right, verbose, truncation_budget) case (AbstractSet(), "!=" | ">=" | "<=" | ">" | "<", AbstractSet()): source = SET_COMPARISON_FUNCTIONSop case _: source = iter(())

三个关键分支值得注意:

  • (_, "==", _):等值比较走 src/_pytest/assertion/_compare_any.py 的_compare_eq_any,其中isset(left) and isset(right)才进入_compare_eq_set,而ismapping(left) and ismapping(right)进入_compare_eq_mapping(实现于 src/_pytest/assertion/_compare_mapping.py);
  • (AbstractSet(), "!=" | ">=" | "<=" | ">" | "<", AbstractSet()):两侧都是集合类型(含 dict 视图)且操作符是子集/超集系时,查表SET_COMPARISON_FUNCTIONS[op]取对应比较函数;
  • case _:没有任何专用解释可用时返回空迭代器——assertrepr_compare的文档字符串也说明"Yields nothing when no specialised explanation applies",此时测试输出只显示原始的assert x >= y摘要行。

原理二:_compare_set 中五个操作符的语义

所有子集系比较函数集中在 src/_pytest/assertion/_compare_set.py。核心是单向差集工具:

def _set_one_sided_diff(posn, set1, set2, highlighter): diff = set1 - set2 if diff: yield f"Extra items in the {posn} set:" for item in diff: yield highlighter(saferepr(item))

它只计算一侧相对另一侧"多出来"的项并逐行输出。五个操作符的映射与语义如下(见该文件 L35-L101):

操作符对应函数失败时输出
>=_compare_gte_setExtra items in the right set:+ 右侧(右减左)缺失项列表
<=_compare_lte_setExtra items in the left set:+ 左侧(左减右)缺失项列表
>_compare_gt_setleft == right输出Both sets are equal;否则同>=,报右侧多出项
<_compare_lt_setleft == right输出Both sets are equal;否则同<=,报左侧多出项
!=_both_sets_are_equal仅当两侧实际相等(断言不应失败却失败)时输出Both sets are equal

注意>=<=的"报谁"方向与直觉一致:x >= y失败说明 y 里有 x 缺的项,所以报 right;x <= y失败说明 x 里有 y 缺的项,所以报 left。而><额外处理了"两侧相等"这一容易困惑的失败形态(集合相等时严格包含关系不成立),直接告诉开发者Both sets are equal,避免面对两个看似相同的集合输出无从下手。

从源码结构看:==为何不产生专用 diff

changelog 条目把==与四个子集操作符并列列出,但当前代码对等号的处理路径不同:

  • SET_COMPARISON_FUNCTIONS字典(src/_pytest/assertion/_compare_set.py)里刻意注释掉了"=="条目,注释说明原因:"== can't be done here without a prior refactor because there's an additional explanation for iterable in _compare_eq_any";
  • 从源码结构看,视图的==比较走(_, "==", _)分支进入_compare_eq_any,再按isset/ismapping判定落到集合或迭代器比较,而不是SET_COMPARISON_FUNCTIONS查表路径。

换言之,四个子集操作符走的是"按操作符查函数表"的专门路径,==的视图比较在等值路径中处理;两者共同保证了视图比较失败时都有可读的解释输出,而非裸的assert a == b

实操演示:用测试套件中的用例复现

testing/test_assertion.py 中的test_dict_items_view_subset参数化覆盖了>=<=两种方向,是用 Pytester 直接验证该行为的官方用例:

@pytest.mark.parametrize("op", [">=", "<="]) def test_dict_items_view_subset(self, op, pytester: Pytester) -> None: """dict.items() supports set-like comparisons; assert diff should show the missing items.""" if op == ">=": pytester.makepyfile( """ def test_hello(): x = {"a": 1, "b": 2} y = {"a": 1, "b": 2, "c": 3} assert x.items() >= y.items() """ ) else: pytester.makepyfile( """ def test_hello(): x = {"a": 1, "b": 2, "c": 3} y = {"a": 1, "b": 2} assert x.items() <= y.items() """ ) result = pytester.runpytest() side = "right" if op == ">=" else "left" result.stdout.fnmatch_lines( [ "*def test_hello():*", f"*assert x.items() {op} y.items()*", f"*E*Extra items in the {side} set:*", "*E*('c', 3)*", ] )

该用例断言了失败输出必须同时包含原始断言行、Extra items in the {left|right} set:提示行,以及具体缺失项('c', 3)(由_set_one_sided_diffsaferepr渲染)。

在本地跑一个等价的最小示例,>=方向的输出大致为:

def test_hello(): x = {"a": 1, "b": 2} y = {"a": 1, "b": 2, "c": 3} > assert x.items() >= y.items() E AssertionError: assert dict_items([(('a', 1), ('b', 2))]) >= dict_items([(('a', 1), ('b', 2), ('c', 3))]) E E Extra items in the right set: E ('c', 3)

<=方向的两侧交换后,提示行变为Extra items in the left set:——这与测试中side = "right" if op == ">=" else "left"的行完全对应。

与"直接比较两个 dict"的输出差异

同一份数据,比较对象不同,pytest 的解释器也不同:

  • assert d1 == d2(两侧是Mapping):走_compare_eq_mapping(src/_pytest/assertion/_compare_mapping.py),按Omitting N identical items, use -vv to showDiffering items:Left/Right contains N more items:分组展示键值差异,并支持truncation_budget截断控制;
  • assert d1.items() == d2.items()d1.keys() < d2.keys()(两侧是 Set 视图):走上文所述的集合比较路径,按Extra items in the X set:逐行列出项。

因此编写断言时,"比较两个 dict 的内容"与"比较两个 dict 的键/键值对子集关系"是两类意图,pytest 会分别给出最贴切的 diff 形态。若需要为自定义类型补充断言解释,可参考pytest_assertrepr_compare钩子(规范见 src/_pytest/hookspec.py),以及 testing/test_assertion.py 中test_assertrepr_loaded_per_dir展示的 conftest 注册方式。

小结

本次改进(changelog 12860.improvement.rst)让dict.items()/dict.keys()的子集系比较断言与 set 比较共享同一套解释器:

  1. 视图对象满足AbstractSet模式匹配,被路由进SET_COMPARISON_FUNCTIONS查表(src/_pytest/assertion/util.py);
  2. _compare_gte_set/_compare_lte_set/_compare_gt_set/_compare_lt_set分别针对>=<=><输出缺失项列表,><额外给出Both sets are equal提示(src/_pytest/assertion/_compare_set.py);
  3. test_dict_items_view_subset(testing/test_assertion.py)作为集成级回归用例,锁定了Extra items in the left/right set:与具体项('c', 3)的输出形态。

排错子集断言失败时,只需盯住Extra items in the ... set:之后的行,即可直接读出缺失的键或键值对,无需再展开完整 repr 逐条比对。

【免费下载链接】pytestThe pytest framework makes it easy to write small tests, yet scales to support complex functional testing项目地址: https://gitcode.com/GitHub_Trending/py/pytest

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询