Skip to content

Commit 86545ed

Browse files
Rework the PrettyPrinter implementation for full diffs
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.
1 parent fdb8bbf commit 86545ed

File tree

6 files changed

+363
-307
lines changed

6 files changed

+363
-307
lines changed

changelog/1531.improvement.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
Improved the very verbose diff for every standard library container types. Previously,
2+
this would use the default python pretty printer, which puts opening and closing
3+
markers on the same line as the first/last entry, in addition to not having
4+
consistent indentation.
5+
6+
The indentation is now consistent and the markers on their own separate lines
7+
which should reduce the diffs shown to users.

src/_pytest/_io/pprint.py

Lines changed: 66 additions & 131 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ def _safe_tuple(t):
5858
class PrettyPrinter:
5959
def __init__(
6060
self,
61-
indent=1,
61+
indent=4,
6262
width=80,
6363
depth=None,
6464
stream=None,
@@ -146,7 +146,6 @@ def _format(self, object, stream, indent, allowance, context, level):
146146

147147
def _pprint_dataclass(self, object, stream, indent, allowance, context, level):
148148
cls_name = object.__class__.__name__
149-
indent += len(cls_name) + 1
150149
items = [
151150
(f.name, getattr(object, f.name))
152151
for f in _dataclasses.fields(object)
@@ -164,17 +163,11 @@ def _pprint_dataclass(self, object, stream, indent, allowance, context, level):
164163
def _pprint_dict(self, object, stream, indent, allowance, context, level):
165164
write = stream.write
166165
write("{")
167-
if self._indent_per_level > 1:
168-
write((self._indent_per_level - 1) * " ")
169-
length = len(object)
170-
if length:
171-
if self._sort_dicts:
172-
items = sorted(object.items(), key=_safe_tuple)
173-
else:
174-
items = object.items()
175-
self._format_dict_items(
176-
items, stream, indent, allowance + 1, context, level
177-
)
166+
if self._sort_dicts:
167+
items = sorted(object.items(), key=_safe_tuple)
168+
else:
169+
items = object.items()
170+
self._format_dict_items(items, stream, indent, allowance, context, level)
178171
write("}")
179172

180173
_dispatch[dict.__repr__] = _pprint_dict
@@ -185,32 +178,22 @@ def _pprint_ordered_dict(self, object, stream, indent, allowance, context, level
185178
return
186179
cls = object.__class__
187180
stream.write(cls.__name__ + "(")
188-
self._format(
189-
list(object.items()),
190-
stream,
191-
indent + len(cls.__name__) + 1,
192-
allowance + 1,
193-
context,
194-
level,
195-
)
181+
self._pprint_dict(object, stream, indent, allowance, context, level)
196182
stream.write(")")
197183

198184
_dispatch[_collections.OrderedDict.__repr__] = _pprint_ordered_dict
199185

200186
def _pprint_list(self, object, stream, indent, allowance, context, level):
201187
stream.write("[")
202-
self._format_items(object, stream, indent, allowance + 1, context, level)
188+
self._format_items(object, stream, indent, allowance, context, level)
203189
stream.write("]")
204190

205191
_dispatch[list.__repr__] = _pprint_list
206192

207193
def _pprint_tuple(self, object, stream, indent, allowance, context, level):
208194
stream.write("(")
209-
endchar = ",)" if len(object) == 1 else ")"
210-
self._format_items(
211-
object, stream, indent, allowance + len(endchar), context, level
212-
)
213-
stream.write(endchar)
195+
self._format_items(object, stream, indent, allowance, context, level)
196+
stream.write(")")
214197

215198
_dispatch[tuple.__repr__] = _pprint_tuple
216199

@@ -225,11 +208,8 @@ def _pprint_set(self, object, stream, indent, allowance, context, level):
225208
else:
226209
stream.write(typ.__name__ + "({")
227210
endchar = "})"
228-
indent += len(typ.__name__) + 1
229211
object = sorted(object, key=_safe_key)
230-
self._format_items(
231-
object, stream, indent, allowance + len(endchar), context, level
232-
)
212+
self._format_items(object, stream, indent, allowance, context, level)
233213
stream.write(endchar)
234214

235215
_dispatch[set.__repr__] = _pprint_set
@@ -319,7 +299,7 @@ def _pprint_bytearray(self, object, stream, indent, allowance, context, level):
319299

320300
def _pprint_mappingproxy(self, object, stream, indent, allowance, context, level):
321301
stream.write("mappingproxy(")
322-
self._format(object.copy(), stream, indent + 13, allowance + 1, context, level)
302+
self._format(object.copy(), stream, indent, allowance, context, level)
323303
stream.write(")")
324304

325305
_dispatch[_types.MappingProxyType.__repr__] = _pprint_mappingproxy
@@ -333,7 +313,6 @@ def _pprint_simplenamespace(
333313
cls_name = "namespace"
334314
else:
335315
cls_name = object.__class__.__name__
336-
indent += len(cls_name) + 1
337316
items = object.__dict__.items()
338317
stream.write(cls_name + "(")
339318
self._format_namespace_items(items, stream, indent, allowance, context, level)
@@ -342,32 +321,30 @@ def _pprint_simplenamespace(
342321
_dispatch[_types.SimpleNamespace.__repr__] = _pprint_simplenamespace
343322

344323
def _format_dict_items(self, items, stream, indent, allowance, context, level):
324+
if not items:
325+
return
326+
345327
write = stream.write
346-
indent += self._indent_per_level
347-
delimnl = ",\n" + " " * indent
348-
last_index = len(items) - 1
349-
for i, (key, ent) in enumerate(items):
350-
last = i == last_index
351-
rep = self._repr(key, context, level)
352-
write(rep)
328+
item_indent = indent + self._indent_per_level
329+
delimnl = "\n" + " " * item_indent
330+
for key, ent in items:
331+
write(delimnl)
332+
write(self._repr(key, context, level))
353333
write(": ")
354-
self._format(
355-
ent,
356-
stream,
357-
indent + len(rep) + 2,
358-
allowance if last else 1,
359-
context,
360-
level,
361-
)
362-
if not last:
363-
write(delimnl)
334+
self._format(ent, stream, item_indent, 1, context, level)
335+
write(",")
336+
337+
write("\n" + " " * indent)
364338

365339
def _format_namespace_items(self, items, stream, indent, allowance, context, level):
340+
if not items:
341+
return
342+
366343
write = stream.write
367-
delimnl = ",\n" + " " * indent
368-
last_index = len(items) - 1
369-
for i, (key, ent) in enumerate(items):
370-
last = i == last_index
344+
item_indent = indent + self._indent_per_level
345+
delimnl = "\n" + " " * item_indent
346+
for key, ent in items:
347+
write(delimnl)
371348
write(key)
372349
write("=")
373350
if id(ent) in context:
@@ -378,52 +355,36 @@ def _format_namespace_items(self, items, stream, indent, allowance, context, lev
378355
self._format(
379356
ent,
380357
stream,
381-
indent + len(key) + 1,
382-
allowance if last else 1,
358+
item_indent + len(key) + 1,
359+
1,
383360
context,
384361
level,
385362
)
386-
if not last:
387-
write(delimnl)
363+
364+
write(",")
365+
366+
write("\n" + " " * indent)
388367

389368
def _format_items(self, items, stream, indent, allowance, context, level):
369+
if not items:
370+
return
371+
390372
write = stream.write
391-
indent += self._indent_per_level
392-
if self._indent_per_level > 1:
393-
write((self._indent_per_level - 1) * " ")
394-
delimnl = ",\n" + " " * indent
395-
delim = ""
396-
width = max_width = self._width - indent + 1
373+
item_indent = indent + self._indent_per_level
374+
delimnl = "\n" + " " * item_indent
375+
397376
it = iter(items)
398-
try:
399-
next_ent = next(it)
400-
except StopIteration:
401-
return
402-
last = False
403-
while not last:
404-
ent = next_ent
377+
while True:
405378
try:
406379
next_ent = next(it)
407380
except StopIteration:
408-
last = True
409-
max_width -= allowance
410-
width -= allowance
411-
if self._compact:
412-
rep = self._repr(ent, context, level)
413-
w = len(rep) + 2
414-
if width < w:
415-
width = max_width
416-
if delim:
417-
delim = delimnl
418-
if width >= w:
419-
width -= w
420-
write(delim)
421-
delim = ", "
422-
write(rep)
423-
continue
424-
write(delim)
425-
delim = delimnl
426-
self._format(ent, stream, indent, allowance if last else 1, context, level)
381+
break
382+
383+
write(delimnl)
384+
self._format(next_ent, stream, item_indent, 1, context, level)
385+
write(",")
386+
387+
write("\n" + " " * indent)
427388

428389
def _repr(self, object, context, level):
429390
repr, readable, recursive = self.format(
@@ -443,66 +404,40 @@ def format(self, object, context, maxlevels, level):
443404
return self._safe_repr(object, context, maxlevels, level)
444405

445406
def _pprint_default_dict(self, object, stream, indent, allowance, context, level):
446-
if not len(object):
447-
stream.write(repr(object))
448-
return
449407
rdf = self._repr(object.default_factory, context, level)
450-
cls = object.__class__
451-
indent += len(cls.__name__) + 1
452-
stream.write(f"{cls.__name__}({rdf},\n{' ' * indent}")
453-
self._pprint_dict(object, stream, indent, allowance + 1, context, level)
408+
stream.write(f"{object.__class__.__name__}({rdf}, ")
409+
self._pprint_dict(object, stream, indent, allowance, context, level)
454410
stream.write(")")
455411

456412
_dispatch[_collections.defaultdict.__repr__] = _pprint_default_dict
457413

458414
def _pprint_counter(self, object, stream, indent, allowance, context, level):
459-
if not len(object):
460-
stream.write(repr(object))
461-
return
462-
cls = object.__class__
463-
stream.write(cls.__name__ + "({")
464-
if self._indent_per_level > 1:
465-
stream.write((self._indent_per_level - 1) * " ")
415+
stream.write(object.__class__.__name__ + "({")
466416
items = object.most_common()
467-
self._format_dict_items(
468-
items, stream, indent + len(cls.__name__) + 1, allowance + 2, context, level
469-
)
417+
self._format_dict_items(items, stream, indent, allowance, context, level)
470418
stream.write("})")
471419

472420
_dispatch[_collections.Counter.__repr__] = _pprint_counter
473421

474422
def _pprint_chain_map(self, object, stream, indent, allowance, context, level):
475-
if not len(object.maps):
423+
if not len(object.maps) or (len(object.maps) == 1 and not len(object.maps[0])):
476424
stream.write(repr(object))
477425
return
478-
cls = object.__class__
479-
stream.write(cls.__name__ + "(")
480-
indent += len(cls.__name__) + 1
481-
for i, m in enumerate(object.maps):
482-
if i == len(object.maps) - 1:
483-
self._format(m, stream, indent, allowance + 1, context, level)
484-
stream.write(")")
485-
else:
486-
self._format(m, stream, indent, 1, context, level)
487-
stream.write(",\n" + " " * indent)
426+
427+
stream.write(object.__class__.__name__ + "(")
428+
self._format_items(object.maps, stream, indent, allowance, context, level)
429+
stream.write(")")
488430

489431
_dispatch[_collections.ChainMap.__repr__] = _pprint_chain_map
490432

491433
def _pprint_deque(self, object, stream, indent, allowance, context, level):
492-
if not len(object):
493-
stream.write(repr(object))
494-
return
495-
cls = object.__class__
496-
stream.write(cls.__name__ + "(")
497-
indent += len(cls.__name__) + 1
434+
stream.write(object.__class__.__name__ + "(")
435+
if object.maxlen is not None:
436+
stream.write("maxlen=%d, " % object.maxlen)
498437
stream.write("[")
499-
if object.maxlen is None:
500-
self._format_items(object, stream, indent, allowance + 2, context, level)
501-
stream.write("])")
502-
else:
503-
self._format_items(object, stream, indent, 2, context, level)
504-
rml = self._repr(object.maxlen, context, level)
505-
stream.write(f"],\n{' ' * indent}maxlen={rml})")
438+
439+
self._format_items(object, stream, indent, allowance + 1, context, level)
440+
stream.write("])")
506441

507442
_dispatch[_collections.deque.__repr__] = _pprint_deque
508443

src/_pytest/assertion/util.py

Lines changed: 2 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -318,18 +318,6 @@ def _diff_text(left: str, right: str, verbose: int = 0) -> List[str]:
318318
return explanation
319319

320320

321-
def _surrounding_parens_on_own_lines(lines: List[str]) -> None:
322-
"""Move opening/closing parenthesis/bracket to own lines."""
323-
opening = lines[0][:1]
324-
if opening in ["(", "[", "{"]:
325-
lines[0] = " " + lines[0][1:]
326-
lines[:] = [opening] + lines
327-
closing = lines[-1][-1:]
328-
if closing in [")", "]", "}"]:
329-
lines[-1] = lines[-1][:-1] + ","
330-
lines[:] = lines + [closing]
331-
332-
333321
def _compare_eq_iterable(
334322
left: Iterable[Any],
335323
right: Iterable[Any],
@@ -341,20 +329,8 @@ def _compare_eq_iterable(
341329
# dynamic import to speedup pytest
342330
import difflib
343331

344-
left_formatting = pprint.pformat(left).splitlines()
345-
right_formatting = pprint.pformat(right).splitlines()
346-
347-
# Re-format for different output lengths.
348-
lines_left = len(left_formatting)
349-
lines_right = len(right_formatting)
350-
if lines_left != lines_right:
351-
printer = PrettyPrinter()
352-
left_formatting = printer.pformat(left).splitlines()
353-
right_formatting = printer.pformat(right).splitlines()
354-
355-
if lines_left > 1 or lines_right > 1:
356-
_surrounding_parens_on_own_lines(left_formatting)
357-
_surrounding_parens_on_own_lines(right_formatting)
332+
left_formatting = PrettyPrinter().pformat(left).splitlines()
333+
right_formatting = PrettyPrinter().pformat(right).splitlines()
358334

359335
explanation = ["Full diff:"]
360336
# "right" is the expected base against which we compare "left",

0 commit comments

Comments
 (0)