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:
parent
c7e9b22f37
commit
2098854b20
|
@ -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,11 +153,36 @@ 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:
|
||||
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,
|
||||
|
@ -160,16 +192,188 @@ class AlwaysDispatchingPrettyPrinter(pprint.PrettyPrinter):
|
|||
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
|
||||
|
||||
context[objid] = 1
|
||||
p(self, object, stream, indent, allowance, context, level + 1)
|
||||
del context[objid]
|
||||
# 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,
|
||||
# Type ignored because _indent_per_level is private.
|
||||
indent + self._indent_per_level, # type: ignore[attr-defined]
|
||||
allowance + 1,
|
||||
context,
|
||||
level,
|
||||
)
|
||||
stream.write(",\n" + " " * indent + ")")
|
||||
|
||||
_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,
|
||||
*,
|
||||
|
|
|
@ -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,9 @@ 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)
|
||||
|
||||
explanation = ["Full diff:"]
|
||||
# "right" is the expected base against which we compare "left",
|
||||
# see https://github.com/pytest-dev/pytest/issues/3333
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -410,10 +410,13 @@ class TestAssert_reprcompare:
|
|||
[0, 2],
|
||||
"""
|
||||
Full diff:
|
||||
- [0, 2]
|
||||
[
|
||||
0,
|
||||
- 2,
|
||||
? ^
|
||||
+ [0, 1]
|
||||
+ 1,
|
||||
? ^
|
||||
]
|
||||
""",
|
||||
id="lists",
|
||||
),
|
||||
|
@ -422,10 +425,12 @@ class TestAssert_reprcompare:
|
|||
{0: 2},
|
||||
"""
|
||||
Full diff:
|
||||
- {0: 2}
|
||||
{
|
||||
- 0: 2,
|
||||
? ^
|
||||
+ {0: 1}
|
||||
+ 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:
|
||||
|
@ -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,9 +607,15 @@ class TestAssert_reprcompare:
|
|||
"{'new': 1}",
|
||||
"Full diff:",
|
||||
" {",
|
||||
" 'env': {'sub': {'long_a': '" + long_a + "',",
|
||||
" 'sub1': {'long_a': 'substring that gets wrapped substring '",
|
||||
" 'that gets wrapped '}}},",
|
||||
" '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}",
|
||||
],
|
||||
),
|
||||
(
|
||||
|
|
|
@ -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',
|
||||
E - 2: 'eggs',
|
||||
E ? ^
|
||||
E + {1: 'spam', 3: 'eggs'}
|
||||
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",
|
||||
),
|
||||
|
|
Loading…
Reference in New Issue