Always use, and improve the AlwaysDispatchPrettyPrinter

The normal default pretty printer is not great when objects are nested
and it can get hard to read the diff.

Instead, provide a pretty printer that behaves more like when json get
indented, which allows for smaller, more meaningful differences, at
the expense of a slightly longer diff
This commit is contained in:
Benjamin Schubert 2023-10-21 22:17:25 +01:00
parent c7e9b22f37
commit 2098854b20
5 changed files with 806 additions and 113 deletions

View File

@ -1,5 +1,9 @@
import collections
import dataclasses
import pprint import pprint
import reprlib import reprlib
import sys
import types
from typing import Any from typing import Any
from typing import Dict from typing import Dict
from typing import IO from typing import IO
@ -137,6 +141,9 @@ def saferepr_unlimited(obj: object, use_ascii: bool = True) -> str:
class AlwaysDispatchingPrettyPrinter(pprint.PrettyPrinter): class AlwaysDispatchingPrettyPrinter(pprint.PrettyPrinter):
"""PrettyPrinter that always dispatches (regardless of width).""" """PrettyPrinter that always dispatches (regardless of width)."""
# Type ignored because _dispatch is private.
_dispatch = pprint.PrettyPrinter._dispatch.copy() # type: ignore[attr-defined]
def _format( def _format(
self, self,
object: object, object: object,
@ -146,30 +153,227 @@ class AlwaysDispatchingPrettyPrinter(pprint.PrettyPrinter):
context: Dict[int, Any], context: Dict[int, Any],
level: int, level: int,
) -> None: ) -> None:
# Type ignored because _dispatch is private. p = self._dispatch.get(type(object).__repr__, None)
p = self._dispatch.get(type(object).__repr__, None) # type: ignore[attr-defined]
objid = id(object) objid = id(object)
if objid in context or p is None: if objid not in context:
# Type ignored because _format is private. # Force the dispatch is an object has a registered dispatched function
super()._format( # type: ignore[misc] if p is not None:
object, context[objid] = 1
p(self, object, stream, indent, allowance, context, level + 1)
del context[objid]
return
# Force the dispatch for dataclasses
elif (
sys.version_info[:2] >= (3, 10) # only supported upstream from 3.10
and dataclasses.is_dataclass(object)
and not isinstance(object, type)
and object.__dataclass_params__.repr # type: ignore[attr-defined]
and
# Check dataclass has generated repr method.
hasattr(object.__repr__, "__wrapped__")
and "__create_fn__" in object.__repr__.__wrapped__.__qualname__
):
context[objid] = 1
# Type ignored because _pprint_dataclass is private.
self._pprint_dataclass( # type: ignore[attr-defined]
object, stream, indent, allowance, context, level + 1
)
del context[objid]
return
# Fallback to the default pretty printer behavior
# Type ignored because _format is private.
super()._format( # type: ignore[misc]
object,
stream,
indent,
allowance,
context,
level,
)
def _format_items(self, items, stream, indent, allowance, context, level):
if not items:
return
# The upstream format_items will add indent_per_level -1 to the line, so
# we need to add the missing indent here
stream.write("\n" + " " * (indent + 1))
# Type ignored because _format_items is private.
super()._format_items( # type: ignore[misc]
items, stream, indent, allowance, context, level
)
stream.write(",\n" + " " * indent)
def _format_dict_items(self, items, stream, indent, allowance, context, level):
if not items:
return
write = stream.write
item_indent = indent + self._indent_per_level # type: ignore[attr-defined]
delimnl = "\n" + " " * item_indent
for key, ent in items:
write(delimnl)
write(self._repr(key, context, level)) # type: ignore[attr-defined]
write(": ")
self._format(ent, stream, item_indent, allowance + 1, context, level)
write(",")
write("\n" + " " * indent)
def _pprint_dataclass(self, object, stream, indent, allowance, context, level):
cls_name = object.__class__.__name__
items = [
(f.name, getattr(object, f.name))
for f in dataclasses.fields(object)
if f.repr
]
if not items:
# Type ignored because _repr is private.
stream.write(self._repr(object, context, level)) # type: ignore[attr-defined]
return
# Type ignored because _indent_per_level is private.
stream.write(cls_name + "(\n" + (" " * (indent + self._indent_per_level))) # type: ignore[attr-defined]
# Type ignored because _ is private.
self._format_namespace_items( # type: ignore[attr-defined]
items, stream, indent + self._indent_per_level, allowance, context, level # type: ignore[attr-defined]
)
stream.write(",\n" + " " * indent + ")")
def _pprint_chain_map(self, object, stream, indent, allowance, context, level):
if not len(object.maps) or (len(object.maps) == 1 and not len(object.maps[0])):
stream.write(repr(object))
return
cls = object.__class__
stream.write(cls.__name__ + "(")
# Type ignored because _indent_per_level is private.
item_indent = indent + self._indent_per_level # type: ignore[attr-defined]
for m in object.maps:
stream.write("\n" + " " * item_indent)
self._format(m, stream, item_indent, allowance + 1, context, level + 1)
stream.write(",")
stream.write("\n%s)" % (" " * indent))
_dispatch[collections.ChainMap.__repr__] = _pprint_chain_map
def _pprint_counter(self, object, stream, indent, allowance, context, level):
if not len(object):
stream.write(repr(object))
return
stream.write(object.__class__.__name__ + "({")
items = object.most_common()
self._format_dict_items(items, stream, indent, allowance + 1, context, level)
stream.write("})")
_dispatch[collections.Counter.__repr__] = _pprint_counter
def _pprint_deque(self, object, stream, indent, allowance, context, level):
if not len(object):
stream.write(repr(object))
return
cls = object.__class__
stream.write(cls.__name__ + "(")
if object.maxlen is not None:
stream.write("maxlen=%d, " % object.maxlen)
stream.write("[")
self._format_items(object, stream, indent, allowance + 1, context, level)
stream.write("])")
_dispatch[collections.deque.__repr__] = _pprint_deque
def _pprint_default_dict(self, object, stream, indent, allowance, context, level):
if not len(object):
stream.write(repr(object))
return
# Type ignored because _repr is private.
rdf = self._repr(object.default_factory, context, level) # type: ignore[attr-defined]
stream.write(object.__class__.__name__ + "(" + rdf + ", ")
self._pprint_dict(object, stream, indent, allowance + 1, context, level)
stream.write(")")
_dispatch[collections.defaultdict.__repr__] = _pprint_default_dict
def _pprint_dict(self, object, stream, indent, allowance, context, level):
stream.write("{")
length = len(object)
if length:
# Type ignored because _sort_dicts is private.
if self._sort_dicts: # type: ignore[attr-defined]
# Type ignored because _safe_tuple is private.
items = sorted(object.items(), key=pprint._safe_tuple) # type: ignore[attr-defined]
else:
items = object.items()
self._format_dict_items(
items, stream, indent, allowance + 1, context, level
)
stream.write("}")
_dispatch[dict.__repr__] = _pprint_dict
def _pprint_mappingproxy(self, object, stream, indent, allowance, context, level):
stream.write("mappingproxy(")
self._format(object.copy(), stream, indent, allowance + 1, context, level)
stream.write(")")
_dispatch[types.MappingProxyType.__repr__] = _pprint_mappingproxy
def _pprint_ordered_dict(self, object, stream, indent, allowance, context, level):
if not len(object):
stream.write(repr(object))
return
stream.write(object.__class__.__name__ + "(")
self._pprint_dict(object, stream, indent, allowance + 1, context, level) # type: ignore[attr-defined]
stream.write(")")
_dispatch[collections.OrderedDict.__repr__] = _pprint_ordered_dict
if sys.version_info[:2] > (3, 9):
def _pprint_simplenamespace(
self, object, stream, indent, allowance, context, level
):
if not len(object.__dict__):
stream.write(repr(object))
return
if type(object) is types.SimpleNamespace:
# The SimpleNamespace repr is "namespace" instead of the class
# name, so we do the same here. For subclasses; use the class name.
cls_name = "namespace"
else:
cls_name = object.__class__.__name__
items = object.__dict__.items()
# Type ignored because _indent_per_level is private.
stream.write(cls_name + "(\n" + " " * (indent + self._indent_per_level)) # type: ignore[attr-defined]
# Type ignored because _format_namespace_items is private.
self._format_namespace_items( # type: ignore[attr-defined]
items,
stream, stream,
indent, # Type ignored because _indent_per_level is private.
allowance, indent + self._indent_per_level, # type: ignore[attr-defined]
allowance + 1,
context, context,
level, level,
) )
return stream.write(",\n" + " " * indent + ")")
context[objid] = 1 _dispatch[types.SimpleNamespace.__repr__] = _pprint_simplenamespace
p(self, object, stream, indent, allowance, context, level + 1)
del context[objid] def _pprint_tuple(self, object, stream, indent, allowance, context, level):
stream.write("(")
self._format_items(object, stream, indent, allowance + 1, context, level)
stream.write(")")
_dispatch[tuple.__repr__] = _pprint_tuple
def _pformat_dispatch( def _pformat_dispatch(
object: object, object: object,
indent: int = 1, indent: int = 4,
width: int = 80, width: int = 80,
depth: Optional[int] = None, depth: Optional[int] = None,
*, *,

View File

@ -318,18 +318,6 @@ def _diff_text(left: str, right: str, verbose: int = 0) -> List[str]:
return explanation return explanation
def _surrounding_parens_on_own_lines(lines: List[str]) -> None:
"""Move opening/closing parenthesis/bracket to own lines."""
opening = lines[0][:1]
if opening in ["(", "[", "{"]:
lines[0] = " " + lines[0][1:]
lines[:] = [opening] + lines
closing = lines[-1][-1:]
if closing in [")", "]", "}"]:
lines[-1] = lines[-1][:-1] + ","
lines[:] = lines + [closing]
def _compare_eq_iterable( def _compare_eq_iterable(
left: Iterable[Any], left: Iterable[Any],
right: Iterable[Any], right: Iterable[Any],
@ -341,19 +329,8 @@ def _compare_eq_iterable(
# dynamic import to speedup pytest # dynamic import to speedup pytest
import difflib import difflib
left_formatting = pprint.pformat(left).splitlines() left_formatting = _pformat_dispatch(left).splitlines()
right_formatting = pprint.pformat(right).splitlines() right_formatting = _pformat_dispatch(right).splitlines()
# Re-format for different output lengths.
lines_left = len(left_formatting)
lines_right = len(right_formatting)
if lines_left != lines_right:
left_formatting = _pformat_dispatch(left).splitlines()
right_formatting = _pformat_dispatch(right).splitlines()
if lines_left > 1 or lines_right > 1:
_surrounding_parens_on_own_lines(left_formatting)
_surrounding_parens_on_own_lines(right_formatting)
explanation = ["Full diff:"] explanation = ["Full diff:"]
# "right" is the expected base against which we compare "left", # "right" is the expected base against which we compare "left",

View File

@ -1,3 +1,14 @@
import sys
import textwrap
from collections import ChainMap
from collections import Counter
from collections import defaultdict
from collections import deque
from collections import OrderedDict
from dataclasses import dataclass
from types import MappingProxyType
from types import SimpleNamespace
import pytest import pytest
from _pytest._io.saferepr import _pformat_dispatch from _pytest._io.saferepr import _pformat_dispatch
from _pytest._io.saferepr import DEFAULT_REPR_MAX_SIZE from _pytest._io.saferepr import DEFAULT_REPR_MAX_SIZE
@ -200,3 +211,438 @@ def test_saferepr_unlimited_exc():
assert saferepr_unlimited(A()).startswith( assert saferepr_unlimited(A()).startswith(
"<[ValueError(42) raised in repr()] A object at 0x" "<[ValueError(42) raised in repr()] A object at 0x"
) )
@dataclass
class EmptyDataclass:
pass
@dataclass
class DataclassWithOneItem:
foo: str
@dataclass
class DataclassWithTwoItems:
foo: str
bar: str
@pytest.mark.parametrize(
("data", "expected"),
(
pytest.param(
EmptyDataclass(),
"EmptyDataclass()",
id="dataclass-empty",
marks=[
pytest.mark.skipif(
sys.version_info[:2] < (3, 10),
reason="Not supported before python3.10",
)
],
),
pytest.param(
DataclassWithOneItem(foo="bar"),
"""
DataclassWithOneItem(
foo='bar',
)
""",
id="dataclass-one-item",
marks=[
pytest.mark.skipif(
sys.version_info[:2] < (3, 10),
reason="Not supported before python3.10",
)
],
),
pytest.param(
DataclassWithTwoItems(foo="foo", bar="bar"),
"""
DataclassWithTwoItems(
foo='foo',
bar='bar',
)
""",
id="dataclass-two-items",
marks=[
pytest.mark.skipif(
sys.version_info[:2] < (3, 10),
reason="Not supported before python3.10",
)
],
),
pytest.param(
{},
"{}",
id="dict-empty",
),
pytest.param(
{"one": 1},
"""
{
'one': 1,
}
""",
id="dict-one-item",
),
pytest.param(
{"one": 1, "two": 2},
"""
{
'one': 1,
'two': 2,
}
""",
id="dict-two-items",
),
pytest.param(OrderedDict(), "OrderedDict()", id="ordereddict-empty"),
pytest.param(
OrderedDict({"one": 1}),
"""
OrderedDict({
'one': 1,
})
""",
id="ordereddict-one-item",
),
pytest.param(
OrderedDict({"one": 1, "two": 2}),
"""
OrderedDict({
'one': 1,
'two': 2,
})
""",
id="ordereddict-two-items",
),
pytest.param(
[],
"[]",
id="list-empty",
),
pytest.param(
[1],
"""
[
1,
]
""",
id="list-one-item",
),
pytest.param(
[1, 2],
"""
[
1,
2,
]
""",
id="list-two-items",
),
pytest.param(
tuple(),
"()",
id="tuple-empty",
),
pytest.param(
(1,),
"""
(
1,
)
""",
id="tuple-one-item",
),
pytest.param(
(1, 2),
"""
(
1,
2,
)
""",
id="tuple-two-items",
),
pytest.param(
set(),
"set()",
id="set-empty",
),
pytest.param(
{1},
"""
{
1,
}
""",
id="set-one-item",
),
pytest.param(
{1, 2},
"""
{
1,
2,
}
""",
id="set-two-items",
),
pytest.param(
MappingProxyType({}),
"mappingproxy({})",
id="mappingproxy-empty",
),
pytest.param(
MappingProxyType({"one": 1}),
"""
mappingproxy({
'one': 1,
})
""",
id="mappingproxy-one-item",
),
pytest.param(
MappingProxyType({"one": 1, "two": 2}),
"""
mappingproxy({
'one': 1,
'two': 2,
})
""",
id="mappingproxy-two-items",
),
pytest.param(
SimpleNamespace(),
"namespace()",
id="simplenamespace-empty",
marks=[
pytest.mark.skipif(
sys.version_info[:2] < (3, 9),
reason="Not supported before python3.9",
)
],
),
pytest.param(
SimpleNamespace(one=1),
"""
namespace(
one=1,
)
""",
id="simplenamespace-one-item",
marks=[
pytest.mark.skipif(
sys.version_info[:2] < (3, 10),
reason="Different format before python3.10",
)
],
),
pytest.param(
SimpleNamespace(one=1, two=2),
"""
namespace(
one=1,
two=2,
)
""",
id="simplenamespace-two-items",
marks=[
pytest.mark.skipif(
sys.version_info[:2] < (3, 10),
reason="Different format before python3.10",
)
],
),
pytest.param(
defaultdict(str), "defaultdict(<class 'str'>, {})", id="defaultdict-empty"
),
pytest.param(
defaultdict(str, {"one": "1"}),
"""
defaultdict(<class 'str'>, {
'one': '1',
})
""",
id="defaultdict-one-item",
),
pytest.param(
defaultdict(str, {"one": "1", "two": "2"}),
"""
defaultdict(<class 'str'>, {
'one': '1',
'two': '2',
})
""",
id="defaultdict-two-items",
),
pytest.param(
Counter(),
"Counter()",
id="counter-empty",
),
pytest.param(
Counter("1"),
"""
Counter({
'1': 1,
})
""",
id="counter-one-item",
),
pytest.param(
Counter("121"),
"""
Counter({
'1': 2,
'2': 1,
})
""",
id="counter-two-items",
),
pytest.param(ChainMap(), "ChainMap({})", id="chainmap-empty"),
pytest.param(
ChainMap({"one": 1, "two": 2}),
"""
ChainMap(
{
'one': 1,
'two': 2,
},
)
""",
id="chainmap-one-item",
),
pytest.param(
ChainMap({"one": 1}, {"two": 2}),
"""
ChainMap(
{
'one': 1,
},
{
'two': 2,
},
)
""",
id="chainmap-two-items",
),
pytest.param(
deque(),
"deque([])",
id="deque-empty",
),
pytest.param(
deque([1]),
"""
deque([
1,
])
""",
id="deque-one-item",
),
pytest.param(
deque([1, 2]),
"""
deque([
1,
2,
])
""",
id="deque-two-items",
),
pytest.param(
deque([1, 2], maxlen=3),
"""
deque(maxlen=3, [
1,
2,
])
""",
id="deque-maxlen",
),
pytest.param(
{
"chainmap": ChainMap({"one": 1}, {"two": 2}),
"counter": Counter("122"),
"dataclass": DataclassWithTwoItems(foo="foo", bar="bar"),
"defaultdict": defaultdict(str, {"one": "1", "two": "2"}),
"deque": deque([1, 2], maxlen=3),
"dict": {"one": 1, "two": 2},
"list": [1, 2],
"mappingproxy": MappingProxyType({"one": 1, "two": 2}),
"ordereddict": OrderedDict({"one": 1, "two": 2}),
"set": {1, 2},
"simplenamespace": SimpleNamespace(one=1, two=2),
"tuple": (1, 2),
},
"""
{
'chainmap': ChainMap(
{
'one': 1,
},
{
'two': 2,
},
),
'counter': Counter({
'2': 2,
'1': 1,
}),
'dataclass': DataclassWithTwoItems(
foo='foo',
bar='bar',
),
'defaultdict': defaultdict(<class 'str'>, {
'one': '1',
'two': '2',
}),
'deque': deque(maxlen=3, [
1,
2,
]),
'dict': {
'one': 1,
'two': 2,
},
'list': [
1,
2,
],
'mappingproxy': mappingproxy({
'one': 1,
'two': 2,
}),
'ordereddict': OrderedDict({
'one': 1,
'two': 2,
}),
'set': {
1,
2,
},
'simplenamespace': namespace(
one=1,
two=2,
),
'tuple': (
1,
2,
),
}
""",
id="deep-example",
marks=[
pytest.mark.skipif(
sys.version_info[:2] < (3, 10),
reason="Not supported before python3.10",
)
],
),
),
)
def test_consistent_pretty_printer(data, expected):
assert _pformat_dispatch(data) == textwrap.dedent(expected).strip()

View File

@ -410,11 +410,14 @@ class TestAssert_reprcompare:
[0, 2], [0, 2],
""" """
Full diff: Full diff:
- [0, 2] [
0,
- 2,
? ^ ? ^
+ [0, 1] + 1,
? ^ ? ^
""", ]
""",
id="lists", id="lists",
), ),
pytest.param( pytest.param(
@ -422,10 +425,12 @@ class TestAssert_reprcompare:
{0: 2}, {0: 2},
""" """
Full diff: Full diff:
- {0: 2} {
? ^ - 0: 2,
+ {0: 1} ? ^
? ^ + 0: 1,
? ^
}
""", """,
id="dicts", id="dicts",
), ),
@ -434,10 +439,13 @@ class TestAssert_reprcompare:
{0, 2}, {0, 2},
""" """
Full diff: Full diff:
- {0, 2} {
0,
- 2,
? ^ ? ^
+ {0, 1} + 1,
? ^ ? ^
}
""", """,
id="sets", id="sets",
), ),
@ -454,6 +462,10 @@ class TestAssert_reprcompare:
assert expl[-1] == "Use -v to get more diff" assert expl[-1] == "Use -v to get more diff"
verbose_expl = callequal(left, right, verbose=1) verbose_expl = callequal(left, right, verbose=1)
assert verbose_expl is not None assert verbose_expl is not None
print("Verbose")
print(verbose_expl)
print("Expected:")
print(textwrap.dedent(expected))
assert "\n".join(verbose_expl).endswith(textwrap.dedent(expected).strip()) assert "\n".join(verbose_expl).endswith(textwrap.dedent(expected).strip())
def test_iterable_quiet(self) -> None: def test_iterable_quiet(self) -> None:
@ -501,10 +513,10 @@ class TestAssert_reprcompare:
"Right contains one more item: '" + long_d + "'", "Right contains one more item: '" + long_d + "'",
"Full diff:", "Full diff:",
" [", " [",
" 'a',", " 'a',",
" 'b',", " 'b',",
" 'c',", " 'c',",
"- '" + long_d + "',", "- '" + long_d + "',",
" ]", " ]",
] ]
@ -514,10 +526,10 @@ class TestAssert_reprcompare:
"Left contains one more item: '" + long_d + "'", "Left contains one more item: '" + long_d + "'",
"Full diff:", "Full diff:",
" [", " [",
" 'a',", " 'a',",
" 'b',", " 'b',",
" 'c',", " 'c',",
"+ '" + long_d + "',", "+ '" + long_d + "',",
" ]", " ]",
] ]
@ -533,10 +545,10 @@ class TestAssert_reprcompare:
"At index 0 diff: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' != 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbb'", "At index 0 diff: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' != 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbb'",
"Full diff:", "Full diff:",
" [", " [",
"+ 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',", "+ 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',",
" 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbb',", " 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbb',",
" 'cccccccccccccccccccccccccccccc',", " 'cccccccccccccccccccccccccccccc',",
"- 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',", "- 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',",
" ]", " ]",
] ]
@ -551,15 +563,15 @@ class TestAssert_reprcompare:
"Left contains 7 more items, first extra item: 'aaaaaaaaaa'", "Left contains 7 more items, first extra item: 'aaaaaaaaaa'",
"Full diff:", "Full diff:",
" [", " [",
"- 'should not get wrapped',", "- 'should not get wrapped',",
"+ 'a',", "+ 'a',",
"+ 'aaaaaaaaaa',", "+ 'aaaaaaaaaa',",
"+ 'aaaaaaaaaa',", "+ 'aaaaaaaaaa',",
"+ 'aaaaaaaaaa',", "+ 'aaaaaaaaaa',",
"+ 'aaaaaaaaaa',", "+ 'aaaaaaaaaa',",
"+ 'aaaaaaaaaa',", "+ 'aaaaaaaaaa',",
"+ 'aaaaaaaaaa',", "+ 'aaaaaaaaaa',",
"+ 'aaaaaaaaaa',", "+ 'aaaaaaaaaa',",
" ]", " ]",
] ]
@ -574,9 +586,13 @@ class TestAssert_reprcompare:
"Differing items:", "Differing items:",
"{'env': {'env1': 1, 'env2': 2}} != {'env': {'env1': 1}}", "{'env': {'env1': 1, 'env2': 2}} != {'env': {'env1': 1}}",
"Full diff:", "Full diff:",
"- {'common': 1, 'env': {'env1': 1}}", " {",
"+ {'common': 1, 'env': {'env1': 1, 'env2': 2}}", " 'common': 1,",
"? +++++++++++", " 'env': {",
" 'env1': 1,",
"+ 'env2': 2,",
" },",
" }",
] ]
long_a = "a" * 80 long_a = "a" * 80
@ -591,10 +607,16 @@ class TestAssert_reprcompare:
"{'new': 1}", "{'new': 1}",
"Full diff:", "Full diff:",
" {", " {",
" 'env': {'sub': {'long_a': '" + long_a + "',", " 'env': {",
" 'sub1': {'long_a': 'substring that gets wrapped substring '", " 'sub': {",
" 'that gets wrapped '}}},", f" 'long_a': '{long_a}',",
"- 'new': 1,", " 'sub1': {",
" 'long_a': 'substring that gets wrapped substring that gets '",
" 'wrapped ',",
" },",
" },",
" },",
"- 'new': 1,",
" }", " }",
] ]
@ -636,8 +658,13 @@ class TestAssert_reprcompare:
"Right contains 2 more items:", "Right contains 2 more items:",
"{'b': 1, 'c': 2}", "{'b': 1, 'c': 2}",
"Full diff:", "Full diff:",
"- {'b': 1, 'c': 2}", " {",
"+ {'a': 0}", "- 'b': 1,",
"? ^ ^",
"+ 'a': 0,",
"? ^ ^",
"- 'c': 2,",
" }",
] ]
lines = callequal({"b": 1, "c": 2}, {"a": 0}, verbose=2) lines = callequal({"b": 1, "c": 2}, {"a": 0}, verbose=2)
assert lines == [ assert lines == [
@ -647,8 +674,13 @@ class TestAssert_reprcompare:
"Right contains 1 more item:", "Right contains 1 more item:",
"{'a': 0}", "{'a': 0}",
"Full diff:", "Full diff:",
"- {'a': 0}", " {",
"+ {'b': 1, 'c': 2}", "- 'a': 0,",
"? ^ ^",
"+ 'b': 1,",
"? ^ ^",
"+ 'c': 2,",
" }",
] ]
def test_sequence_different_items(self) -> None: def test_sequence_different_items(self) -> None:
@ -658,8 +690,17 @@ class TestAssert_reprcompare:
"At index 0 diff: 1 != 3", "At index 0 diff: 1 != 3",
"Right contains one more item: 5", "Right contains one more item: 5",
"Full diff:", "Full diff:",
"- (3, 4, 5)", " (",
"+ (1, 2)", "- 3,",
"? ^",
"+ 1,",
"? ^",
"- 4,",
"? ^",
"+ 2,",
"? ^",
"- 5,",
" )",
] ]
lines = callequal((1, 2, 3), (4,), verbose=2) lines = callequal((1, 2, 3), (4,), verbose=2)
assert lines == [ assert lines == [
@ -667,8 +708,14 @@ class TestAssert_reprcompare:
"At index 0 diff: 1 != 4", "At index 0 diff: 1 != 4",
"Left contains 2 more items, first extra item: 2", "Left contains 2 more items, first extra item: 2",
"Full diff:", "Full diff:",
"- (4,)", " (",
"+ (1, 2, 3)", "- 4,",
"? ^",
"+ 1,",
"? ^",
"+ 2,",
"+ 3,",
" )",
] ]
def test_set(self) -> None: def test_set(self) -> None:
@ -1803,8 +1850,8 @@ def test_reprcompare_verbose_long() -> None:
assert [0, 1] == [0, 2] assert [0, 1] == [0, 2]
""", """,
[ [
"{bold}{red}E {light-red}- [0, 2]{hl-reset}{endline}{reset}", "{bold}{red}E {light-red}- 2,{hl-reset}{endline}{reset}",
"{bold}{red}E {light-green}+ [0, 1]{hl-reset}{endline}{reset}", "{bold}{red}E {light-green}+ 1,{hl-reset}{endline}{reset}",
], ],
), ),
( (
@ -1816,8 +1863,8 @@ def test_reprcompare_verbose_long() -> None:
""", """,
[ [
"{bold}{red}E {light-gray} {hl-reset} {{{endline}{reset}", "{bold}{red}E {light-gray} {hl-reset} {{{endline}{reset}",
"{bold}{red}E {light-gray} {hl-reset} 'number-is-1': 1,{endline}{reset}", "{bold}{red}E {light-gray} {hl-reset} 'number-is-1': 1,{endline}{reset}",
"{bold}{red}E {light-green}+ 'number-is-5': 5,{hl-reset}{endline}{reset}", "{bold}{red}E {light-green}+ 'number-is-5': 5,{hl-reset}{endline}{reset}",
], ],
), ),
), ),

View File

@ -21,10 +21,14 @@ TESTCASES = [
E assert [1, 4, 3] == [1, 2, 3] E assert [1, 4, 3] == [1, 2, 3]
E At index 1 diff: 4 != 2 E At index 1 diff: 4 != 2
E Full diff: E Full diff:
E - [1, 2, 3] E [
E 1,
E - 2,
E ? ^ E ? ^
E + [1, 4, 3] E + 4,
E ? ^ E ? ^
E 3,
E ]
""", """,
id="Compare lists, one item differs", id="Compare lists, one item differs",
), ),
@ -40,9 +44,11 @@ TESTCASES = [
E assert [1, 2, 3] == [1, 2] E assert [1, 2, 3] == [1, 2]
E Left contains one more item: 3 E Left contains one more item: 3
E Full diff: E Full diff:
E - [1, 2] E [
E + [1, 2, 3] E 1,
E ? +++ E 2,
E + 3,
E ]
""", """,
id="Compare lists, one extra item", id="Compare lists, one extra item",
), ),
@ -59,9 +65,11 @@ TESTCASES = [
E At index 1 diff: 3 != 2 E At index 1 diff: 3 != 2
E Right contains one more item: 3 E Right contains one more item: 3
E Full diff: E Full diff:
E - [1, 2, 3] E [
E ? --- E 1,
E + [1, 3] E - 2,
E 3,
E ]
""", """,
id="Compare lists, one item missing", id="Compare lists, one item missing",
), ),
@ -77,10 +85,14 @@ TESTCASES = [
E assert (1, 4, 3) == (1, 2, 3) E assert (1, 4, 3) == (1, 2, 3)
E At index 1 diff: 4 != 2 E At index 1 diff: 4 != 2
E Full diff: E Full diff:
E - (1, 2, 3) E (
E 1,
E - 2,
E ? ^ E ? ^
E + (1, 4, 3) E + 4,
E ? ^ E ? ^
E 3,
E )
""", """,
id="Compare tuples", id="Compare tuples",
), ),
@ -99,10 +111,12 @@ TESTCASES = [
E Extra items in the right set: E Extra items in the right set:
E 2 E 2
E Full diff: E Full diff:
E - {1, 2, 3} E {
E ? ^ ^ E 1,
E + {1, 3, 4} E - 2,
E ? ^ ^ E 3,
E + 4,
E }
""", """,
id="Compare sets", id="Compare sets",
), ),
@ -123,10 +137,13 @@ TESTCASES = [
E Right contains 1 more item: E Right contains 1 more item:
E {2: 'eggs'} E {2: 'eggs'}
E Full diff: E Full diff:
E - {1: 'spam', 2: 'eggs'} E {
E ? ^ E 1: 'spam',
E + {1: 'spam', 3: 'eggs'} E - 2: 'eggs',
E ? ^ E ? ^
E + 3: 'eggs',
E ? ^
E }
""", """,
id="Compare dicts with differing keys", id="Compare dicts with differing keys",
), ),
@ -145,10 +162,11 @@ TESTCASES = [
E Differing items: E Differing items:
E {2: 'eggs'} != {2: 'bacon'} E {2: 'eggs'} != {2: 'bacon'}
E Full diff: E Full diff:
E - {1: 'spam', 2: 'bacon'} E {
E ? ^^^^^ E 1: 'spam',
E + {1: 'spam', 2: 'eggs'} E - 2: 'bacon',
E ? ^^^^ E + 2: 'eggs',
E }
""", """,
id="Compare dicts with differing values", id="Compare dicts with differing values",
), ),
@ -169,10 +187,11 @@ TESTCASES = [
E Right contains 1 more item: E Right contains 1 more item:
E {3: 'bacon'} E {3: 'bacon'}
E Full diff: E Full diff:
E - {1: 'spam', 3: 'bacon'} E {
E ? ^ ^^^^^ E 1: 'spam',
E + {1: 'spam', 2: 'eggs'} E - 3: 'bacon',
E ? ^ ^^^^ E + 2: 'eggs',
E }
""", """,
id="Compare dicts with differing items", id="Compare dicts with differing items",
), ),