Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions changelog/1531.improvement.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Improved the very verbose diff for every standard library container types. Previously,
this would use the default python pretty printer, which puts opening and closing
markers on the same line as the first/last entry, in addition to not having
consistent indentation.

The indentation is now consistent and the markers on their own separate lines
which should reduce the diffs shown to users.
206 changes: 191 additions & 15 deletions src/_pytest/_io/saferepr.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -146,30 +153,199 @@ def _format(
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,
stream,
indent,
allowance,
context,
level,
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

# Type ignored because _indent_per_level is private.
item_indent = indent + self._indent_per_level # type: ignore[attr-defined]
delimnl = "\n" + " " * item_indent
for key, ent in items:
stream.write(delimnl)
# Type ignored because _repr is private.
stream.write(self._repr(key, context, level)) # type: ignore[attr-defined]
stream.write(": ")
self._format(ent, stream, item_indent, allowance + 1, context, level)
stream.write(",")

stream.write("\n" + " " * indent)

def _format_namespace_items(self, items, stream, indent, allowance, context, level):
if not items:
return

# Force a recomputation of the indent to be at a consistent level
# Type ignored because _indent_per_level is private.
indent = self._indent_per_level * level # type: ignore[attr-defined]

stream.write("\n" + " " * indent)
# Type ignored because _format_items is private.
super()._format_namespace_items( # type: ignore[misc]
items, stream, indent, allowance, context, level
)
# Type ignored because _indent_per_level is private.
stream.write(",\n" + " " * (indent - self._indent_per_level)) # type: ignore[attr-defined]

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

stream.write(object.__class__.__name__ + "(")
self._format_items(object.maps, stream, indent, allowance + 1, context, level)
stream.write(")")

_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

stream.write(object.__class__.__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,
context,
# Force the mappingproxy to go back one level up.
# This is necessary as we are doing two subsequent calls
# to `_format`, which leads to the level being increased
# twice
level - 1,
)
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

context[objid] = 1
p(self, object, stream, indent, allowance, context, level + 1)
del context[objid]
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

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,
*,
Expand Down
27 changes: 2 additions & 25 deletions src/_pytest/assertion/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -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],
Expand All @@ -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",
Expand Down
Loading