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

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

View File

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