From de4de5afc165fc8ab94d658f6a5eb6128c4cff4c Mon Sep 17 00:00:00 2001 From: Ariel Even Date: Thu, 22 Oct 2026 20:54:33 +0300 Subject: [PATCH 1/3] Fix mapping assertion diff when keys() returns a list Since the mapping comparison started using the key views' set operations, two mappings whose keys() returns a plain list (for example starlette.datastructures.Headers) made the assertion explanation fail with "TypeError: unsupported operand type(s) for &: 'list' and 'list'". Wrap such keys in a collections.abc.KeysView, which keeps resolving membership through the mapping, so case-insensitive mappings still match up their keys. --- src/_pytest/assertion/_compare_mapping.py | 17 ++++++++-- testing/test_assertion.py | 38 +++++++++++++++++++++++ 2 files changed, 53 insertions(+), 2 deletions(-) diff --git a/src/_pytest/assertion/_compare_mapping.py b/src/_pytest/assertion/_compare_mapping.py index 9b7f392d9a9..31e1b71302e 100644 --- a/src/_pytest/assertion/_compare_mapping.py +++ b/src/_pytest/assertion/_compare_mapping.py @@ -2,7 +2,9 @@ from collections.abc import Collection from collections.abc import Iterator +from collections.abc import KeysView from collections.abc import Mapping +from collections.abc import Set as AbstractSet import heapq import pprint @@ -13,6 +15,17 @@ from _pytest.assertion._typing import TruncationBudget +def _keys_view(mapping: Mapping[object, object]) -> AbstractSet[object]: + """Return a set-like view of ``mapping``'s keys. + + Some mappings override ``keys()`` to return a plain list (for example + ``starlette.datastructures.Headers``); wrap those in a ``KeysView``, which + still resolves membership through the mapping. + """ + keys = mapping.keys() + return keys if isinstance(keys, AbstractSet) else KeysView(mapping) + + def _compare_eq_mapping( left: Mapping[object, object], right: Mapping[object, object], @@ -24,8 +37,8 @@ def _compare_eq_mapping( # operations resolve membership through the mappings themselves, so a # mapping with its own key equivalence (case-insensitive keys, for # example) is compared the way it compares itself. - keys_left = left.keys() - keys_right = right.keys() + keys_left = _keys_view(left) + keys_right = _keys_view(right) common = keys_left & keys_right same = {k: left[k] for k in common if left[k] == right[k]} if same and verbose < 2: diff --git a/testing/test_assertion.py b/testing/test_assertion.py index 035aad31c54..4635d5cf28d 100644 --- a/testing/test_assertion.py +++ b/testing/test_assertion.py @@ -1116,6 +1116,44 @@ def __len__(self) -> int: # pragma: no cover "Use -v to get more diff", ] + def test_mapping_with_list_keys(self) -> None: + """A mapping whose ``keys()`` returns a list still gets the mapping diff. + + ``starlette.datastructures.Headers`` is such a mapping: it overrides + ``keys()`` to return a list, which has no set operations. + """ + + class ListKeysMapping(Mapping[str, int]): + def __init__(self, values: dict[str, int]) -> None: + self._values = values + + def __getitem__(self, key: str) -> int: + return self._values[key] + + def __iter__(self) -> Iterator[str]: + return iter(self._values) + + def __len__(self) -> int: # pragma: no cover + return len(self._values) + + def keys(self) -> list[str]: # type: ignore[override] + return list(self._values) + + lines = callequal( + ListKeysMapping({"a": 0, "b": 1}), + ListKeysMapping({"a": 1, "b": 1, "c": 2}), + ) + + assert lines is not None + assert lines[2:] == [ + "Omitting 1 identical items, use -vv to show", + "Differing items:", + "{'a': 0} != {'a': 1}", + "Right contains 1 more item:", + "{'c': 2}", + "Use -v to get more diff", + ] + def test_sequence_different_items(self) -> None: lines = callequal((1, 2), (3, 4, 5), verbose=2) assert lines == [ From 5fd6377dbacb02305c133002ff2b55f4fb524ebc Mon Sep 17 00:00:00 2001 From: taltal-beep Date: Sat, 31 Oct 2026 11:35:28 +0200 Subject: [PATCH 2/3] Add Ariel Even to AUTHORS --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index 136ffd6c5ee..c20b4898fce 100644 --- a/AUTHORS +++ b/AUTHORS @@ -54,6 +54,7 @@ Anton Zhilin Antony Lee Arel Cordero Arias Emmanuel +Ariel Even Ariel Pillemer Armin Rigo Aron Coyle From 1558698c2f9468b9f5eccff82eeae98e42c20563 Mon Sep 17 00:00:00 2001 From: taltal-beep Date: Sat, 31 Oct 2026 12:31:30 +0200 Subject: [PATCH 3/3] Add changelog entry for the mapping diff fix --- changelog/15310.bugfix.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog/15310.bugfix.rst diff --git a/changelog/15310.bugfix.rst b/changelog/15310.bugfix.rst new file mode 100644 index 00000000000..1ef38763231 --- /dev/null +++ b/changelog/15310.bugfix.rst @@ -0,0 +1 @@ +Fixed the assertion diff for mappings whose ``keys()`` returns a list, such as ``starlette.datastructures.Headers``, which failed with a ``TypeError`` since :pr:`14463`.