Skip to content

Commit 2d1710e

Browse files
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.
1 parent fe8cda0 commit 2d1710e

File tree

6 files changed

+402
-319
lines changed

6 files changed

+402
-319
lines changed

changelog/1531.improvement.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
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.
2+
3+
Previously, the default python pretty printer was used to generate the output, which puts opening and closing
4+
markers on the same line as the first/last entry, in addition to not having consistent indentation.

src/_pytest/_io/pprint.py

Lines changed: 71 additions & 137 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,30 @@ 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):
390-
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
397-
it = iter(items)
398-
try:
399-
next_ent = next(it)
400-
except StopIteration:
369+
if not items:
401370
return
402-
last = False
403-
while not last:
404-
ent = next_ent
405-
try:
406-
next_ent = next(it)
407-
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)
371+
372+
write = stream.write
373+
item_indent = indent + self._indent_per_level
374+
delimnl = "\n" + " " * item_indent
375+
376+
for item in items:
377+
write(delimnl)
378+
self._format(item, stream, item_indent, 1, context, level)
379+
write(",")
380+
381+
write("\n" + " " * indent)
427382

428383
def _repr(self, object, context, level):
429384
repr, readable, recursive = self.format(
@@ -443,66 +398,45 @@ def format(self, object, context, maxlevels, level):
443398
return self._safe_repr(object, context, maxlevels, level)
444399

445400
def _pprint_default_dict(self, object, stream, indent, allowance, context, level):
446-
if not len(object):
447-
stream.write(repr(object))
448-
return
449401
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)
402+
stream.write(f"{object.__class__.__name__}({rdf}, ")
403+
self._pprint_dict(object, stream, indent, allowance, context, level)
454404
stream.write(")")
455405

456406
_dispatch[_collections.defaultdict.__repr__] = _pprint_default_dict
457407

458408
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) * " ")
466-
items = object.most_common()
467-
self._format_dict_items(
468-
items, stream, indent + len(cls.__name__) + 1, allowance + 2, context, level
469-
)
470-
stream.write("})")
409+
stream.write(object.__class__.__name__ + "(")
410+
411+
if object:
412+
stream.write("{")
413+
items = object.most_common()
414+
self._format_dict_items(items, stream, indent, allowance, context, level)
415+
stream.write("}")
416+
417+
stream.write(")")
471418

472419
_dispatch[_collections.Counter.__repr__] = _pprint_counter
473420

474421
def _pprint_chain_map(self, object, stream, indent, allowance, context, level):
475-
if not len(object.maps):
422+
if not len(object.maps) or (len(object.maps) == 1 and not len(object.maps[0])):
476423
stream.write(repr(object))
477424
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)
425+
426+
stream.write(object.__class__.__name__ + "(")
427+
self._format_items(object.maps, stream, indent, allowance, context, level)
428+
stream.write(")")
488429

489430
_dispatch[_collections.ChainMap.__repr__] = _pprint_chain_map
490431

491432
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
433+
stream.write(object.__class__.__name__ + "(")
434+
if object.maxlen is not None:
435+
stream.write("maxlen=%d, " % object.maxlen)
498436
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})")
437+
438+
self._format_items(object, stream, indent, allowance + 1, context, level)
439+
stream.write("])")
506440

507441
_dispatch[_collections.deque.__repr__] = _pprint_deque
508442

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)