diff --git a/src/_pytest/_io/saferepr.py b/src/_pytest/_io/saferepr.py index c70187223..28799a7ba 100644 --- a/src/_pytest/_io/saferepr.py +++ b/src/_pytest/_io/saferepr.py @@ -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, *, diff --git a/src/_pytest/assertion/util.py b/src/_pytest/assertion/util.py index b9123c97d..f403cf82b 100644 --- a/src/_pytest/assertion/util.py +++ b/src/_pytest/assertion/util.py @@ -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", diff --git a/testing/io/test_saferepr.py b/testing/io/test_saferepr.py index 24746bc22..ffb30f694 100644 --- a/testing/io/test_saferepr.py +++ b/testing/io/test_saferepr.py @@ -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(, {})", id="defaultdict-empty" + ), + pytest.param( + defaultdict(str, {"one": "1"}), + """ + defaultdict(, { + 'one': '1', + }) + """, + id="defaultdict-one-item", + ), + pytest.param( + defaultdict(str, {"one": "1", "two": "2"}), + """ + defaultdict(, { + '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(, { + '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() diff --git a/testing/test_assertion.py b/testing/test_assertion.py index 62c465d8a..7e2147b0b 100644 --- a/testing/test_assertion.py +++ b/testing/test_assertion.py @@ -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}", ], ), ), diff --git a/testing/test_error_diffs.py b/testing/test_error_diffs.py index eb7812108..cad7a17c0 100644 --- a/testing/test_error_diffs.py +++ b/testing/test_error_diffs.py @@ -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", ),