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 reprlib
import sys
import types
from typing import Any
from typing import Dict
from typing import IO
@ -137,6 +141,9 @@ def saferepr_unlimited(obj: object, use_ascii: bool = True) -> str:
class AlwaysDispatchingPrettyPrinter(pprint.PrettyPrinter):
"""PrettyPrinter that always dispatches (regardless of width)."""
# Type ignored because _dispatch is private.
_dispatch = pprint.PrettyPrinter._dispatch.copy() # type: ignore[attr-defined]
def _format(
self,
object: object,
@ -146,30 +153,227 @@ class AlwaysDispatchingPrettyPrinter(pprint.PrettyPrinter):
context: Dict[int, Any],
level: int,
) -> None:
# Type ignored because _dispatch is private.
p = self._dispatch.get(type(object).__repr__, None) # type: ignore[attr-defined]
p = self._dispatch.get(type(object).__repr__, None)
objid = id(object)
if objid in context or p is None:
# Type ignored because _format is private.
super()._format( # type: ignore[misc]
object,
if objid not in context:
# Force the dispatch is an object has a registered dispatched function
if p is not None:
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,
indent,
allowance,
# Type ignored because _indent_per_level is private.
indent + self._indent_per_level, # type: ignore[attr-defined]
allowance + 1,
context,
level,
)
return
stream.write(",\n" + " " * indent + ")")
context[objid] = 1
p(self, object, stream, indent, allowance, context, level + 1)
del context[objid]
_dispatch[types.SimpleNamespace.__repr__] = _pprint_simplenamespace
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(
object: object,
indent: int = 1,
indent: int = 4,
width: int = 80,
depth: Optional[int] = None,
*,

View File

@ -318,18 +318,6 @@ def _diff_text(left: str, right: str, verbose: int = 0) -> List[str]:
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(
left: Iterable[Any],
right: Iterable[Any],
@ -341,19 +329,8 @@ def _compare_eq_iterable(
# dynamic import to speedup pytest
import difflib
left_formatting = pprint.pformat(left).splitlines()
right_formatting = pprint.pformat(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)
left_formatting = _pformat_dispatch(left).splitlines()
right_formatting = _pformat_dispatch(right).splitlines()
explanation = ["Full diff:"]
# "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
from _pytest._io.saferepr import _pformat_dispatch
from _pytest._io.saferepr import DEFAULT_REPR_MAX_SIZE
@ -200,3 +211,438 @@ def test_saferepr_unlimited_exc():
assert saferepr_unlimited(A()).startswith(
"<[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],
"""
Full diff:
- [0, 2]
[
0,
- 2,
? ^
+ [0, 1]
+ 1,
? ^
""",
]
""",
id="lists",
),
pytest.param(
@ -422,10 +425,12 @@ class TestAssert_reprcompare:
{0: 2},
"""
Full diff:
- {0: 2}
? ^
+ {0: 1}
? ^
{
- 0: 2,
? ^
+ 0: 1,
? ^
}
""",
id="dicts",
),
@ -434,10 +439,13 @@ class TestAssert_reprcompare:
{0, 2},
"""
Full diff:
- {0, 2}
{
0,
- 2,
? ^
+ {0, 1}
+ 1,
? ^
}
""",
id="sets",
),
@ -454,6 +462,10 @@ class TestAssert_reprcompare:
assert expl[-1] == "Use -v to get more diff"
verbose_expl = callequal(left, right, verbose=1)
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())
def test_iterable_quiet(self) -> None:
@ -501,10 +513,10 @@ class TestAssert_reprcompare:
"Right contains one more item: '" + long_d + "'",
"Full diff:",
" [",
" 'a',",
" 'b',",
" 'c',",
"- '" + long_d + "',",
" 'a',",
" 'b',",
" 'c',",
"- '" + long_d + "',",
" ]",
]
@ -514,10 +526,10 @@ class TestAssert_reprcompare:
"Left contains one more item: '" + long_d + "'",
"Full diff:",
" [",
" 'a',",
" 'b',",
" 'c',",
"+ '" + long_d + "',",
" 'a',",
" 'b',",
" 'c',",
"+ '" + long_d + "',",
" ]",
]
@ -533,10 +545,10 @@ class TestAssert_reprcompare:
"At index 0 diff: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' != 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbb'",
"Full diff:",
" [",
"+ 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',",
" 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbb',",
" 'cccccccccccccccccccccccccccccc',",
"- 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',",
"+ 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',",
" 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbb',",
" 'cccccccccccccccccccccccccccccc',",
"- 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',",
" ]",
]
@ -551,15 +563,15 @@ class TestAssert_reprcompare:
"Left contains 7 more items, first extra item: 'aaaaaaaaaa'",
"Full diff:",
" [",
"- 'should not get wrapped',",
"+ 'a',",
"+ 'aaaaaaaaaa',",
"+ 'aaaaaaaaaa',",
"+ 'aaaaaaaaaa',",
"+ 'aaaaaaaaaa',",
"+ 'aaaaaaaaaa',",
"+ 'aaaaaaaaaa',",
"+ 'aaaaaaaaaa',",
"- 'should not get wrapped',",
"+ 'a',",
"+ 'aaaaaaaaaa',",
"+ 'aaaaaaaaaa',",
"+ 'aaaaaaaaaa',",
"+ 'aaaaaaaaaa',",
"+ 'aaaaaaaaaa',",
"+ 'aaaaaaaaaa',",
"+ 'aaaaaaaaaa',",
" ]",
]
@ -574,9 +586,13 @@ class TestAssert_reprcompare:
"Differing items:",
"{'env': {'env1': 1, 'env2': 2}} != {'env': {'env1': 1}}",
"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
@ -591,10 +607,16 @@ class TestAssert_reprcompare:
"{'new': 1}",
"Full diff:",
" {",
" 'env': {'sub': {'long_a': '" + long_a + "',",
" 'sub1': {'long_a': 'substring that gets wrapped substring '",
" 'that gets wrapped '}}},",
"- 'new': 1,",
" 'env': {",
" 'sub': {",
f" 'long_a': '{long_a}',",
" '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:",
"{'b': 1, 'c': 2}",
"Full diff:",
"- {'b': 1, 'c': 2}",
"+ {'a': 0}",
" {",
"- 'b': 1,",
"? ^ ^",
"+ 'a': 0,",
"? ^ ^",
"- 'c': 2,",
" }",
]
lines = callequal({"b": 1, "c": 2}, {"a": 0}, verbose=2)
assert lines == [
@ -647,8 +674,13 @@ class TestAssert_reprcompare:
"Right contains 1 more item:",
"{'a': 0}",
"Full diff:",
"- {'a': 0}",
"+ {'b': 1, 'c': 2}",
" {",
"- 'a': 0,",
"? ^ ^",
"+ 'b': 1,",
"? ^ ^",
"+ 'c': 2,",
" }",
]
def test_sequence_different_items(self) -> None:
@ -658,8 +690,17 @@ class TestAssert_reprcompare:
"At index 0 diff: 1 != 3",
"Right contains one more item: 5",
"Full diff:",
"- (3, 4, 5)",
"+ (1, 2)",
" (",
"- 3,",
"? ^",
"+ 1,",
"? ^",
"- 4,",
"? ^",
"+ 2,",
"? ^",
"- 5,",
" )",
]
lines = callequal((1, 2, 3), (4,), verbose=2)
assert lines == [
@ -667,8 +708,14 @@ class TestAssert_reprcompare:
"At index 0 diff: 1 != 4",
"Left contains 2 more items, first extra item: 2",
"Full diff:",
"- (4,)",
"+ (1, 2, 3)",
" (",
"- 4,",
"? ^",
"+ 1,",
"? ^",
"+ 2,",
"+ 3,",
" )",
]
def test_set(self) -> None:
@ -1803,8 +1850,8 @@ def test_reprcompare_verbose_long() -> None:
assert [0, 1] == [0, 2]
""",
[
"{bold}{red}E {light-red}- [0, 2]{hl-reset}{endline}{reset}",
"{bold}{red}E {light-green}+ [0, 1]{hl-reset}{endline}{reset}",
"{bold}{red}E {light-red}- 2,{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} 'number-is-1': 1,{endline}{reset}",
"{bold}{red}E {light-green}+ 'number-is-5': 5,{hl-reset}{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}",
],
),
),

View File

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