Improve the full diff by having more consistent indentation in the PrettyPrinter (#11571)

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 does not touch the other places where the pretty printer is used,
and only updated the full diff one.
This commit is contained in:
Benjamin Schubert 2023-11-27 14:47:18 +00:00 committed by GitHub
parent fe8cda051b
commit 2d1710e0e9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 402 additions and 319 deletions

View File

@ -0,0 +1,4 @@
Improved the very verbose diff for every standard library container types: the indentation is now consistent and the markers are on their own separate lines, which should reduce the diffs shown to users.
Previously, the default python pretty printer was used to generate the output, which puts opening and closing
markers on the same line as the first/last entry, in addition to not having consistent indentation.

View File

@ -58,7 +58,7 @@ def _safe_tuple(t):
class PrettyPrinter:
def __init__(
self,
indent=1,
indent=4,
width=80,
depth=None,
stream=None,
@ -146,7 +146,6 @@ class PrettyPrinter:
def _pprint_dataclass(self, object, stream, indent, allowance, context, level):
cls_name = object.__class__.__name__
indent += len(cls_name) + 1
items = [
(f.name, getattr(object, f.name))
for f in _dataclasses.fields(object)
@ -164,17 +163,11 @@ class PrettyPrinter:
def _pprint_dict(self, object, stream, indent, allowance, context, level):
write = stream.write
write("{")
if self._indent_per_level > 1:
write((self._indent_per_level - 1) * " ")
length = len(object)
if length:
if self._sort_dicts:
items = sorted(object.items(), key=_safe_tuple)
else:
items = object.items()
self._format_dict_items(
items, stream, indent, allowance + 1, context, level
)
if self._sort_dicts:
items = sorted(object.items(), key=_safe_tuple)
else:
items = object.items()
self._format_dict_items(items, stream, indent, allowance, context, level)
write("}")
_dispatch[dict.__repr__] = _pprint_dict
@ -185,32 +178,22 @@ class PrettyPrinter:
return
cls = object.__class__
stream.write(cls.__name__ + "(")
self._format(
list(object.items()),
stream,
indent + len(cls.__name__) + 1,
allowance + 1,
context,
level,
)
self._pprint_dict(object, stream, indent, allowance, context, level)
stream.write(")")
_dispatch[_collections.OrderedDict.__repr__] = _pprint_ordered_dict
def _pprint_list(self, object, stream, indent, allowance, context, level):
stream.write("[")
self._format_items(object, stream, indent, allowance + 1, context, level)
self._format_items(object, stream, indent, allowance, context, level)
stream.write("]")
_dispatch[list.__repr__] = _pprint_list
def _pprint_tuple(self, object, stream, indent, allowance, context, level):
stream.write("(")
endchar = ",)" if len(object) == 1 else ")"
self._format_items(
object, stream, indent, allowance + len(endchar), context, level
)
stream.write(endchar)
self._format_items(object, stream, indent, allowance, context, level)
stream.write(")")
_dispatch[tuple.__repr__] = _pprint_tuple
@ -225,11 +208,8 @@ class PrettyPrinter:
else:
stream.write(typ.__name__ + "({")
endchar = "})"
indent += len(typ.__name__) + 1
object = sorted(object, key=_safe_key)
self._format_items(
object, stream, indent, allowance + len(endchar), context, level
)
self._format_items(object, stream, indent, allowance, context, level)
stream.write(endchar)
_dispatch[set.__repr__] = _pprint_set
@ -319,7 +299,7 @@ class PrettyPrinter:
def _pprint_mappingproxy(self, object, stream, indent, allowance, context, level):
stream.write("mappingproxy(")
self._format(object.copy(), stream, indent + 13, allowance + 1, context, level)
self._format(object.copy(), stream, indent, allowance, context, level)
stream.write(")")
_dispatch[_types.MappingProxyType.__repr__] = _pprint_mappingproxy
@ -333,7 +313,6 @@ class PrettyPrinter:
cls_name = "namespace"
else:
cls_name = object.__class__.__name__
indent += len(cls_name) + 1
items = object.__dict__.items()
stream.write(cls_name + "(")
self._format_namespace_items(items, stream, indent, allowance, context, level)
@ -342,32 +321,30 @@ class PrettyPrinter:
_dispatch[_types.SimpleNamespace.__repr__] = _pprint_simplenamespace
def _format_dict_items(self, items, stream, indent, allowance, context, level):
if not items:
return
write = stream.write
indent += self._indent_per_level
delimnl = ",\n" + " " * indent
last_index = len(items) - 1
for i, (key, ent) in enumerate(items):
last = i == last_index
rep = self._repr(key, context, level)
write(rep)
item_indent = indent + self._indent_per_level
delimnl = "\n" + " " * item_indent
for key, ent in items:
write(delimnl)
write(self._repr(key, context, level))
write(": ")
self._format(
ent,
stream,
indent + len(rep) + 2,
allowance if last else 1,
context,
level,
)
if not last:
write(delimnl)
self._format(ent, stream, item_indent, 1, context, level)
write(",")
write("\n" + " " * indent)
def _format_namespace_items(self, items, stream, indent, allowance, context, level):
if not items:
return
write = stream.write
delimnl = ",\n" + " " * indent
last_index = len(items) - 1
for i, (key, ent) in enumerate(items):
last = i == last_index
item_indent = indent + self._indent_per_level
delimnl = "\n" + " " * item_indent
for key, ent in items:
write(delimnl)
write(key)
write("=")
if id(ent) in context:
@ -378,52 +355,30 @@ class PrettyPrinter:
self._format(
ent,
stream,
indent + len(key) + 1,
allowance if last else 1,
item_indent + len(key) + 1,
1,
context,
level,
)
if not last:
write(delimnl)
write(",")
write("\n" + " " * indent)
def _format_items(self, items, stream, indent, allowance, context, level):
write = stream.write
indent += self._indent_per_level
if self._indent_per_level > 1:
write((self._indent_per_level - 1) * " ")
delimnl = ",\n" + " " * indent
delim = ""
width = max_width = self._width - indent + 1
it = iter(items)
try:
next_ent = next(it)
except StopIteration:
if not items:
return
last = False
while not last:
ent = next_ent
try:
next_ent = next(it)
except StopIteration:
last = True
max_width -= allowance
width -= allowance
if self._compact:
rep = self._repr(ent, context, level)
w = len(rep) + 2
if width < w:
width = max_width
if delim:
delim = delimnl
if width >= w:
width -= w
write(delim)
delim = ", "
write(rep)
continue
write(delim)
delim = delimnl
self._format(ent, stream, indent, allowance if last else 1, context, level)
write = stream.write
item_indent = indent + self._indent_per_level
delimnl = "\n" + " " * item_indent
for item in items:
write(delimnl)
self._format(item, stream, item_indent, 1, context, level)
write(",")
write("\n" + " " * indent)
def _repr(self, object, context, level):
repr, readable, recursive = self.format(
@ -443,66 +398,45 @@ class PrettyPrinter:
return self._safe_repr(object, context, maxlevels, level)
def _pprint_default_dict(self, object, stream, indent, allowance, context, level):
if not len(object):
stream.write(repr(object))
return
rdf = self._repr(object.default_factory, context, level)
cls = object.__class__
indent += len(cls.__name__) + 1
stream.write(f"{cls.__name__}({rdf},\n{' ' * indent}")
self._pprint_dict(object, stream, indent, allowance + 1, context, level)
stream.write(f"{object.__class__.__name__}({rdf}, ")
self._pprint_dict(object, stream, indent, allowance, context, level)
stream.write(")")
_dispatch[_collections.defaultdict.__repr__] = _pprint_default_dict
def _pprint_counter(self, object, stream, indent, allowance, context, level):
if not len(object):
stream.write(repr(object))
return
cls = object.__class__
stream.write(cls.__name__ + "({")
if self._indent_per_level > 1:
stream.write((self._indent_per_level - 1) * " ")
items = object.most_common()
self._format_dict_items(
items, stream, indent + len(cls.__name__) + 1, allowance + 2, context, level
)
stream.write("})")
stream.write(object.__class__.__name__ + "(")
if object:
stream.write("{")
items = object.most_common()
self._format_dict_items(items, stream, indent, allowance, context, level)
stream.write("}")
stream.write(")")
_dispatch[_collections.Counter.__repr__] = _pprint_counter
def _pprint_chain_map(self, object, stream, indent, allowance, context, level):
if not len(object.maps):
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__ + "(")
indent += len(cls.__name__) + 1
for i, m in enumerate(object.maps):
if i == len(object.maps) - 1:
self._format(m, stream, indent, allowance + 1, context, level)
stream.write(")")
else:
self._format(m, stream, indent, 1, context, level)
stream.write(",\n" + " " * indent)
stream.write(object.__class__.__name__ + "(")
self._format_items(object.maps, stream, indent, allowance, context, level)
stream.write(")")
_dispatch[_collections.ChainMap.__repr__] = _pprint_chain_map
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__ + "(")
indent += len(cls.__name__) + 1
stream.write(object.__class__.__name__ + "(")
if object.maxlen is not None:
stream.write("maxlen=%d, " % object.maxlen)
stream.write("[")
if object.maxlen is None:
self._format_items(object, stream, indent, allowance + 2, context, level)
stream.write("])")
else:
self._format_items(object, stream, indent, 2, context, level)
rml = self._repr(object.maxlen, context, level)
stream.write(f"],\n{' ' * indent}maxlen={rml})")
self._format_items(object, stream, indent, allowance + 1, context, level)
stream.write("])")
_dispatch[_collections.deque.__repr__] = _pprint_deque

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,20 +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:
printer = PrettyPrinter()
left_formatting = printer.pformat(left).splitlines()
right_formatting = printer.pformat(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 = PrettyPrinter().pformat(left).splitlines()
right_formatting = PrettyPrinter().pformat(right).splitlines()
explanation = ["Full diff:"]
# "right" is the expected base against which we compare "left",

View File

@ -40,15 +40,19 @@ class DataclassWithTwoItems:
pytest.param(
DataclassWithOneItem(foo="bar"),
"""
DataclassWithOneItem(foo='bar')
DataclassWithOneItem(
foo='bar',
)
""",
id="dataclass-one-item",
),
pytest.param(
DataclassWithTwoItems(foo="foo", bar="bar"),
"""
DataclassWithTwoItems(foo='foo',
bar='bar')
DataclassWithTwoItems(
foo='foo',
bar='bar',
)
""",
id="dataclass-two-items",
),
@ -60,15 +64,19 @@ class DataclassWithTwoItems:
pytest.param(
{"one": 1},
"""
{'one': 1}
{
'one': 1,
}
""",
id="dict-one-item",
),
pytest.param(
{"one": 1, "two": 2},
"""
{'one': 1,
'two': 2}
{
'one': 1,
'two': 2,
}
""",
id="dict-two-items",
),
@ -76,18 +84,19 @@ class DataclassWithTwoItems:
pytest.param(
OrderedDict({"one": 1}),
"""
OrderedDict([('one',
1)])
OrderedDict({
'one': 1,
})
""",
id="ordereddict-one-item",
),
pytest.param(
OrderedDict({"one": 1, "two": 2}),
"""
OrderedDict([('one',
1),
('two',
2)])
OrderedDict({
'one': 1,
'two': 2,
})
""",
id="ordereddict-two-items",
),
@ -99,15 +108,19 @@ class DataclassWithTwoItems:
pytest.param(
[1],
"""
[1]
[
1,
]
""",
id="list-one-item",
),
pytest.param(
[1, 2],
"""
[1,
2]
[
1,
2,
]
""",
id="list-two-items",
),
@ -119,15 +132,19 @@ class DataclassWithTwoItems:
pytest.param(
(1,),
"""
(1,)
(
1,
)
""",
id="tuple-one-item",
),
pytest.param(
(1, 2),
"""
(1,
2)
(
1,
2,
)
""",
id="tuple-two-items",
),
@ -139,15 +156,19 @@ class DataclassWithTwoItems:
pytest.param(
{1},
"""
{1}
{
1,
}
""",
id="set-one-item",
),
pytest.param(
{1, 2},
"""
{1,
2}
{
1,
2,
}
""",
id="set-two-items",
),
@ -159,15 +180,19 @@ class DataclassWithTwoItems:
pytest.param(
MappingProxyType({"one": 1}),
"""
mappingproxy({'one': 1})
mappingproxy({
'one': 1,
})
""",
id="mappingproxy-one-item",
),
pytest.param(
MappingProxyType({"one": 1, "two": 2}),
"""
mappingproxy({'one': 1,
'two': 2})
mappingproxy({
'one': 1,
'two': 2,
})
""",
id="mappingproxy-two-items",
),
@ -179,15 +204,19 @@ class DataclassWithTwoItems:
pytest.param(
SimpleNamespace(one=1),
"""
namespace(one=1)
namespace(
one=1,
)
""",
id="simplenamespace-one-item",
),
pytest.param(
SimpleNamespace(one=1, two=2),
"""
namespace(one=1,
two=2)
namespace(
one=1,
two=2,
)
""",
id="simplenamespace-two-items",
),
@ -197,17 +226,19 @@ class DataclassWithTwoItems:
pytest.param(
defaultdict(str, {"one": "1"}),
"""
defaultdict(<class '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'})
defaultdict(<class 'str'>, {
'one': '1',
'two': '2',
})
""",
id="defaultdict-two-items",
),
@ -219,15 +250,19 @@ class DataclassWithTwoItems:
pytest.param(
Counter("1"),
"""
Counter({'1': 1})
Counter({
'1': 1,
})
""",
id="counter-one-item",
),
pytest.param(
Counter("121"),
"""
Counter({'1': 2,
'2': 1})
Counter({
'1': 2,
'2': 1,
})
""",
id="counter-two-items",
),
@ -235,16 +270,26 @@ class DataclassWithTwoItems:
pytest.param(
ChainMap({"one": 1, "two": 2}),
"""
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})
ChainMap(
{
'one': 1,
},
{
'two': 2,
},
)
""",
id="chainmap-two-items",
),
@ -256,24 +301,29 @@ class DataclassWithTwoItems:
pytest.param(
deque([1]),
"""
deque([1])
deque([
1,
])
""",
id="deque-one-item",
),
pytest.param(
deque([1, 2]),
"""
deque([1,
2])
deque([
1,
2,
])
""",
id="deque-two-items",
),
pytest.param(
deque([1, 2], maxlen=3),
"""
deque([1,
2],
maxlen=3)
deque(maxlen=3, [
1,
2,
])
""",
id="deque-maxlen",
),
@ -293,34 +343,60 @@ class DataclassWithTwoItems:
"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([1,
2],
maxlen=3),
'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)}
{
'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",
),

View File

@ -451,11 +451,14 @@ class TestAssert_reprcompare:
[0, 2],
"""
Full diff:
- [0, 2]
[
0,
- 2,
? ^
+ [0, 1]
+ 1,
? ^
""",
]
""",
id="lists",
),
pytest.param(
@ -463,10 +466,12 @@ class TestAssert_reprcompare:
{0: 2},
"""
Full diff:
- {0: 2}
? ^
+ {0: 1}
? ^
{
- 0: 2,
? ^
+ 0: 1,
? ^
}
""",
id="dicts",
),
@ -475,10 +480,13 @@ class TestAssert_reprcompare:
{0, 2},
"""
Full diff:
- {0, 2}
{
0,
- 2,
? ^
+ {0, 1}
+ 1,
? ^
}
""",
id="sets",
),
@ -542,10 +550,10 @@ class TestAssert_reprcompare:
"Right contains one more item: '" + long_d + "'",
"Full diff:",
" [",
" 'a',",
" 'b',",
" 'c',",
"- '" + long_d + "',",
" 'a',",
" 'b',",
" 'c',",
"- '" + long_d + "',",
" ]",
]
@ -555,10 +563,10 @@ class TestAssert_reprcompare:
"Left contains one more item: '" + long_d + "'",
"Full diff:",
" [",
" 'a',",
" 'b',",
" 'c',",
"+ '" + long_d + "',",
" 'a',",
" 'b',",
" 'c',",
"+ '" + long_d + "',",
" ]",
]
@ -574,10 +582,10 @@ class TestAssert_reprcompare:
"At index 0 diff: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' != 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbb'",
"Full diff:",
" [",
"+ 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',",
" 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbb',",
" 'cccccccccccccccccccccccccccccc',",
"- 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',",
"+ 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',",
" 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbb',",
" 'cccccccccccccccccccccccccccccc',",
"- 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',",
" ]",
]
@ -592,15 +600,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',",
" ]",
]
@ -615,13 +623,17 @@ 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
sub = {"long_a": long_a, "sub1": {"long_a": "substring that gets wrapped " * 2}}
sub = {"long_a": long_a, "sub1": {"long_a": "substring that gets wrapped " * 3}}
d1 = {"env": {"sub": sub}}
d2 = {"env": {"sub": sub}, "new": 1}
diff = callequal(d1, d2, verbose=True)
@ -632,10 +644,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 '",
" 'substring that gets wrapped ',",
" },",
" },",
" },",
"- 'new': 1,",
" }",
]
@ -677,8 +695,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 == [
@ -688,8 +711,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:
@ -699,8 +727,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 == [
@ -708,8 +745,27 @@ 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,",
" )",
]
lines = callequal((1, 2, 3), (1, 20, 3), verbose=2)
assert lines == [
"(1, 2, 3) == (1, 20, 3)",
"At index 1 diff: 2 != 20",
"Full diff:",
" (",
" 1,",
"- 20,",
"? -",
"+ 2,",
" 3,",
" )",
]
def test_set(self) -> None:
@ -1844,8 +1900,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}",
],
),
(
@ -1857,8 +1913,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}",
],
),
),
@ -1917,14 +1973,32 @@ def test_fine_grained_assertion_verbosity(pytester: Pytester):
f"{p.name} .FFF [100%]",
"E At index 2 diff: 'grapes' != 'orange'",
"E Full diff:",
"E - ['banana', 'apple', 'orange', 'melon', 'kiwi']",
"E ? ^ ^^",
"E + ['banana', 'apple', 'grapes', 'melon', 'kiwi']",
"E ? ^ ^ +",
"E [",
"E 'banana',",
"E 'apple',",
"E - 'orange',",
"E ? ^ ^^",
"E + 'grapes',",
"E ? ^ ^ +",
"E 'melon',",
"E 'kiwi',",
"E ]",
"E Full diff:",
"E - {'0': 0, '10': 10, '20': 20, '30': 30, '40': 40}",
"E ? - - - - - - - -",
"E + {'0': 0, '1': 1, '2': 2, '3': 3, '4': 4}",
"E {",
"E '0': 0,",
"E - '10': 10,",
"E ? - -",
"E + '1': 1,",
"E - '20': 20,",
"E ? - -",
"E + '2': 2,",
"E - '30': 30,",
"E ? - -",
"E + '3': 3,",
"E - '40': 40,",
"E ? - -",
"E + '4': 4,",
"E }",
f"E AssertionError: assert 'hello world' in '{long_text}'",
]
)

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",
),