Description
Preface
Python has a way of checking that one dict is a subset of another one, as highlighed in #2376 (comment). This is quite a nifty feature that allows for partial checks when you don't want to check all the fields e.g., some of them are not stable, so you want to split the checks:
def test_unstable():
dict = {"a": 42, "b": random.random()}
assert dict.items() >= {"a": 42}.items()
assert 0 < dict["b"] <= 1
or the dict
has too many irrelevant items and you don't want to list them all just to check a couple of interesting ones:
def test_irrelevant():
dict = requests.get("https://example.com/big-json-with-lots-of-fields")
assert dict.items() >= {"a": 42, "b": "a113"}.items()
What's the problem this feature will solve?
Described approach works perfectly fine from the functional standpoint however since there is no dedicated handling of this case, reporting fallbacks to repr
of the ItemsView
:
def test_big_left():
left = {"a": 1, "b": 2, "c": 3, "d": 4}
right = left.copy() | {"e": 5}
> assert left.items() >= right.items()
E AssertionError: assert dict_items([('a', 1), ('b', 2), ('c', 3), ('d', 4)]) >= dict_items([('a', 1), ('b', 2), ('c', 3), ('d', 4), ('e', 5)])
E + where dict_items([('a', 1), ('b', 2), ('c', 3), ('d', 4)]) = <built-in method items of dict object at 0x103a65bc0>()
E + where <built-in method items of dict object at 0x103a65bc0> = {'a': 1, 'b': 2, 'c': 3, 'd': 4}.items
E + and dict_items([('a', 1), ('b', 2), ('c', 3), ('d', 4), ('e', 5)]) = <built-in method items of dict object at 0x103a65ec0>()
E + where <built-in method items of dict object at 0x103a65ec0> = {'a': 1, 'b': 2, 'c': 3, 'd': 4, ...}.items
It would be nice to have a dedicated comparison for this that shows only the difference, similar to dict
comparison:
def test_repr():
left = {"a": 1, "b": 2, "c": 3, "d": 4}
right = left.copy() | {"e": 5}
> assert left == right
E AssertionError: assert {'a': 1, 'b':...c': 3, 'd': 4} == {'a': 1, 'b':..., 'd': 4, ...}
E
E Omitting 4 identical items, use -vv to show
E Right contains 1 more item:
E {'e': 5}
E Use -v to get more diff
Describe the solution you'd like
Currently, I have a naive implementation for such case that utilizes pytest_assertrepr_compare
hook:
def pytest_assertrepr_compare(
config: Config,
op: str,
left: object,
right: object,
) -> list[str] | None:
if (
isinstance(left, ItemsView)
and isinstance(right, ItemsView)
and
# Naive implementation for superset comparison only. XXX: Support other
# comparisons?
op == ">="
):
missing: list[str] = []
differing: list[str] = []
left_dict = dict(left)
for k, v in right:
# XXX: Shouldn't `k` and `v` go through `pytest_assertrepr_compare` as well?
if k not in left_dict:
missing.append(f" {k!r}: {v!r}")
elif left_dict[k] != v:
differing.append(f" {k!r}: {left_dict[k]!r} != {v!r}")
assert missing or differing # Otherwise, why are we even here?
# XXX: Better header?
output = ["left dict_items(...) is not a superset of right dict_items(...)"]
if missing:
output.append("Missing:")
output.extend(missing)
if differing:
output.append("Differing:")
output.extend(differing)
return output
return None
With this hook in place, the output looks like this:
def test_repr():
left = {"a": 1, "b": 2, "c": 3, "d": 4}
right = left.copy() | {"e": 5}
> assert left.items() >= right.items()
E AssertionError: assert left dict_items(...) is not a superset of right dict_items(...)
E Missing:
E 'e': 5
and for a mismatch:
def test_repr():
left = {"a": 1, "b": 2, "c": 3, "d": 4}
right = left.copy() | {"a": 12}
> assert left.items() >= right.items()
E AssertionError: assert left dict_items(...) is not a superset of right dict_items(...)
E Differing:
E 'a': 1 != 12
Alternative Solutions
I think it's possible to introduce rich comparison with third-party plugins that implement the hook similar to aforementioned or we can introduce some unittest-style assertDictIsSubset
that handles reporting, but to be honest I'm leaning towards thinking that first-party support via assertrepr
for this would be best.
Additional context
—