Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ Anton Zhilin
Antony Lee
Arel Cordero
Arias Emmanuel
Ariel Even
Ariel Pillemer
Armin Rigo
Aron Coyle
Expand Down
1 change: 1 addition & 0 deletions changelog/15310.bugfix.rst
Original file line number Diff line number Diff line change
@@ -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`.
17 changes: 15 additions & 2 deletions src/_pytest/assertion/_compare_mapping.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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],
Expand All @@ -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:
Expand Down
38 changes: 38 additions & 0 deletions testing/test_assertion.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 == [
Expand Down
Loading