1 # Author: Fred L. Drake, Jr.
2 # fdrake@acm.org
3 #
4 # This is a simple little module I wrote to make life easier. I didn't
5 # see anything quite like it in the library, though I may have overlooked
6 # something. I wrote this when I was trying to read some heavily nested
7 # tuples with fairly non-descriptive content. This is modeled very much
8 # after Lisp/Scheme - style pretty-printing of lists. If you find it
9 # useful, thank small children who sleep at night.
10
11 """Support to pretty-print lists, tuples, & dictionaries recursively.
12
13 Very simple, but useful, especially in debugging data structures.
14
15 Classes
16 -------
17
18 PrettyPrinter()
19 Handle pretty-printing operations onto a stream using a configured
20 set of formatting parameters.
21
22 Functions
23 ---------
24
25 pformat()
26 Format a Python object into a pretty-printed representation.
27
28 pprint()
29 Pretty-print a Python object to a stream [default is sys.stdout].
30
31 saferepr()
32 Generate a 'standard' repr()-like value, but protect against recursive
33 data structures.
34
35 """
36
37 import collections as _collections
38 import dataclasses as _dataclasses
39 import re
40 import sys as _sys
41 import types as _types
42 from io import StringIO as _StringIO
43
44 __all__ = ["pprint","pformat","isreadable","isrecursive","saferepr",
45 "PrettyPrinter", "pp"]
46
47
48 def pprint(object, stream=None, indent=1, width=80, depth=None, *,
49 compact=False, sort_dicts=True, underscore_numbers=False):
50 """Pretty-print a Python object to a stream [default is sys.stdout]."""
51 printer = PrettyPrinter(
52 stream=stream, indent=indent, width=width, depth=depth,
53 compact=compact, sort_dicts=sort_dicts,
54 underscore_numbers=underscore_numbers)
55 printer.pprint(object)
56
57 def pformat(object, indent=1, width=80, depth=None, *,
58 compact=False, sort_dicts=True, underscore_numbers=False):
59 """Format a Python object into a pretty-printed representation."""
60 return PrettyPrinter(indent=indent, width=width, depth=depth,
61 compact=compact, sort_dicts=sort_dicts,
62 underscore_numbers=underscore_numbers).pformat(object)
63
64 def pp(object, *args, sort_dicts=False, **kwargs):
65 """Pretty-print a Python object"""
66 pprint(object, *args, sort_dicts=sort_dicts, **kwargs)
67
68 def saferepr(object):
69 """Version of repr() which can handle recursive data structures."""
70 return PrettyPrinter()._safe_repr(object, {}, None, 0)[0]
71
72 def isreadable(object):
73 """Determine if saferepr(object) is readable by eval()."""
74 return PrettyPrinter()._safe_repr(object, {}, None, 0)[1]
75
76 def isrecursive(object):
77 """Determine if object requires a recursive representation."""
78 return PrettyPrinter()._safe_repr(object, {}, None, 0)[2]
79
80 class ESC[4;38;5;81m_safe_key:
81 """Helper function for key functions when sorting unorderable objects.
82
83 The wrapped-object will fallback to a Py2.x style comparison for
84 unorderable types (sorting first comparing the type name and then by
85 the obj ids). Does not work recursively, so dict.items() must have
86 _safe_key applied to both the key and the value.
87
88 """
89
90 __slots__ = ['obj']
91
92 def __init__(self, obj):
93 self.obj = obj
94
95 def __lt__(self, other):
96 try:
97 return self.obj < other.obj
98 except TypeError:
99 return ((str(type(self.obj)), id(self.obj)) < \
100 (str(type(other.obj)), id(other.obj)))
101
102 def _safe_tuple(t):
103 "Helper function for comparing 2-tuples"
104 return _safe_key(t[0]), _safe_key(t[1])
105
106 class ESC[4;38;5;81mPrettyPrinter:
107 def __init__(self, indent=1, width=80, depth=None, stream=None, *,
108 compact=False, sort_dicts=True, underscore_numbers=False):
109 """Handle pretty printing operations onto a stream using a set of
110 configured parameters.
111
112 indent
113 Number of spaces to indent for each level of nesting.
114
115 width
116 Attempted maximum number of columns in the output.
117
118 depth
119 The maximum depth to print out nested structures.
120
121 stream
122 The desired output stream. If omitted (or false), the standard
123 output stream available at construction will be used.
124
125 compact
126 If true, several items will be combined in one line.
127
128 sort_dicts
129 If true, dict keys are sorted.
130
131 """
132 indent = int(indent)
133 width = int(width)
134 if indent < 0:
135 raise ValueError('indent must be >= 0')
136 if depth is not None and depth <= 0:
137 raise ValueError('depth must be > 0')
138 if not width:
139 raise ValueError('width must be != 0')
140 self._depth = depth
141 self._indent_per_level = indent
142 self._width = width
143 if stream is not None:
144 self._stream = stream
145 else:
146 self._stream = _sys.stdout
147 self._compact = bool(compact)
148 self._sort_dicts = sort_dicts
149 self._underscore_numbers = underscore_numbers
150
151 def pprint(self, object):
152 if self._stream is not None:
153 self._format(object, self._stream, 0, 0, {}, 0)
154 self._stream.write("\n")
155
156 def pformat(self, object):
157 sio = _StringIO()
158 self._format(object, sio, 0, 0, {}, 0)
159 return sio.getvalue()
160
161 def isrecursive(self, object):
162 return self.format(object, {}, 0, 0)[2]
163
164 def isreadable(self, object):
165 s, readable, recursive = self.format(object, {}, 0, 0)
166 return readable and not recursive
167
168 def _format(self, object, stream, indent, allowance, context, level):
169 objid = id(object)
170 if objid in context:
171 stream.write(_recursion(object))
172 self._recursive = True
173 self._readable = False
174 return
175 rep = self._repr(object, context, level)
176 max_width = self._width - indent - allowance
177 if len(rep) > max_width:
178 p = self._dispatch.get(type(object).__repr__, None)
179 if p is not None:
180 context[objid] = 1
181 p(self, object, stream, indent, allowance, context, level + 1)
182 del context[objid]
183 return
184 elif (_dataclasses.is_dataclass(object) and
185 not isinstance(object, type) and
186 object.__dataclass_params__.repr and
187 # Check dataclass has generated repr method.
188 hasattr(object.__repr__, "__wrapped__") and
189 "__create_fn__" in object.__repr__.__wrapped__.__qualname__):
190 context[objid] = 1
191 self._pprint_dataclass(object, stream, indent, allowance, context, level + 1)
192 del context[objid]
193 return
194 stream.write(rep)
195
196 def _pprint_dataclass(self, object, stream, indent, allowance, context, level):
197 cls_name = object.__class__.__name__
198 indent += len(cls_name) + 1
199 items = [(f.name, getattr(object, f.name)) for f in _dataclasses.fields(object) if f.repr]
200 stream.write(cls_name + '(')
201 self._format_namespace_items(items, stream, indent, allowance, context, level)
202 stream.write(')')
203
204 _dispatch = {}
205
206 def _pprint_dict(self, object, stream, indent, allowance, context, level):
207 write = stream.write
208 write('{')
209 if self._indent_per_level > 1:
210 write((self._indent_per_level - 1) * ' ')
211 length = len(object)
212 if length:
213 if self._sort_dicts:
214 items = sorted(object.items(), key=_safe_tuple)
215 else:
216 items = object.items()
217 self._format_dict_items(items, stream, indent, allowance + 1,
218 context, level)
219 write('}')
220
221 _dispatch[dict.__repr__] = _pprint_dict
222
223 def _pprint_ordered_dict(self, object, stream, indent, allowance, context, level):
224 if not len(object):
225 stream.write(repr(object))
226 return
227 cls = object.__class__
228 stream.write(cls.__name__ + '(')
229 self._format(list(object.items()), stream,
230 indent + len(cls.__name__) + 1, allowance + 1,
231 context, level)
232 stream.write(')')
233
234 _dispatch[_collections.OrderedDict.__repr__] = _pprint_ordered_dict
235
236 def _pprint_list(self, object, stream, indent, allowance, context, level):
237 stream.write('[')
238 self._format_items(object, stream, indent, allowance + 1,
239 context, level)
240 stream.write(']')
241
242 _dispatch[list.__repr__] = _pprint_list
243
244 def _pprint_tuple(self, object, stream, indent, allowance, context, level):
245 stream.write('(')
246 endchar = ',)' if len(object) == 1 else ')'
247 self._format_items(object, stream, indent, allowance + len(endchar),
248 context, level)
249 stream.write(endchar)
250
251 _dispatch[tuple.__repr__] = _pprint_tuple
252
253 def _pprint_set(self, object, stream, indent, allowance, context, level):
254 if not len(object):
255 stream.write(repr(object))
256 return
257 typ = object.__class__
258 if typ is set:
259 stream.write('{')
260 endchar = '}'
261 else:
262 stream.write(typ.__name__ + '({')
263 endchar = '})'
264 indent += len(typ.__name__) + 1
265 object = sorted(object, key=_safe_key)
266 self._format_items(object, stream, indent, allowance + len(endchar),
267 context, level)
268 stream.write(endchar)
269
270 _dispatch[set.__repr__] = _pprint_set
271 _dispatch[frozenset.__repr__] = _pprint_set
272
273 def _pprint_str(self, object, stream, indent, allowance, context, level):
274 write = stream.write
275 if not len(object):
276 write(repr(object))
277 return
278 chunks = []
279 lines = object.splitlines(True)
280 if level == 1:
281 indent += 1
282 allowance += 1
283 max_width1 = max_width = self._width - indent
284 for i, line in enumerate(lines):
285 rep = repr(line)
286 if i == len(lines) - 1:
287 max_width1 -= allowance
288 if len(rep) <= max_width1:
289 chunks.append(rep)
290 else:
291 # A list of alternating (non-space, space) strings
292 parts = re.findall(r'\S*\s*', line)
293 assert parts
294 assert not parts[-1]
295 parts.pop() # drop empty last part
296 max_width2 = max_width
297 current = ''
298 for j, part in enumerate(parts):
299 candidate = current + part
300 if j == len(parts) - 1 and i == len(lines) - 1:
301 max_width2 -= allowance
302 if len(repr(candidate)) > max_width2:
303 if current:
304 chunks.append(repr(current))
305 current = part
306 else:
307 current = candidate
308 if current:
309 chunks.append(repr(current))
310 if len(chunks) == 1:
311 write(rep)
312 return
313 if level == 1:
314 write('(')
315 for i, rep in enumerate(chunks):
316 if i > 0:
317 write('\n' + ' '*indent)
318 write(rep)
319 if level == 1:
320 write(')')
321
322 _dispatch[str.__repr__] = _pprint_str
323
324 def _pprint_bytes(self, object, stream, indent, allowance, context, level):
325 write = stream.write
326 if len(object) <= 4:
327 write(repr(object))
328 return
329 parens = level == 1
330 if parens:
331 indent += 1
332 allowance += 1
333 write('(')
334 delim = ''
335 for rep in _wrap_bytes_repr(object, self._width - indent, allowance):
336 write(delim)
337 write(rep)
338 if not delim:
339 delim = '\n' + ' '*indent
340 if parens:
341 write(')')
342
343 _dispatch[bytes.__repr__] = _pprint_bytes
344
345 def _pprint_bytearray(self, object, stream, indent, allowance, context, level):
346 write = stream.write
347 write('bytearray(')
348 self._pprint_bytes(bytes(object), stream, indent + 10,
349 allowance + 1, context, level + 1)
350 write(')')
351
352 _dispatch[bytearray.__repr__] = _pprint_bytearray
353
354 def _pprint_mappingproxy(self, object, stream, indent, allowance, context, level):
355 stream.write('mappingproxy(')
356 self._format(object.copy(), stream, indent + 13, allowance + 1,
357 context, level)
358 stream.write(')')
359
360 _dispatch[_types.MappingProxyType.__repr__] = _pprint_mappingproxy
361
362 def _pprint_simplenamespace(self, object, stream, indent, allowance, context, level):
363 if type(object) is _types.SimpleNamespace:
364 # The SimpleNamespace repr is "namespace" instead of the class
365 # name, so we do the same here. For subclasses; use the class name.
366 cls_name = 'namespace'
367 else:
368 cls_name = object.__class__.__name__
369 indent += len(cls_name) + 1
370 items = object.__dict__.items()
371 stream.write(cls_name + '(')
372 self._format_namespace_items(items, stream, indent, allowance, context, level)
373 stream.write(')')
374
375 _dispatch[_types.SimpleNamespace.__repr__] = _pprint_simplenamespace
376
377 def _format_dict_items(self, items, stream, indent, allowance, context,
378 level):
379 write = stream.write
380 indent += self._indent_per_level
381 delimnl = ',\n' + ' ' * indent
382 last_index = len(items) - 1
383 for i, (key, ent) in enumerate(items):
384 last = i == last_index
385 rep = self._repr(key, context, level)
386 write(rep)
387 write(': ')
388 self._format(ent, stream, indent + len(rep) + 2,
389 allowance if last else 1,
390 context, level)
391 if not last:
392 write(delimnl)
393
394 def _format_namespace_items(self, items, stream, indent, allowance, context, level):
395 write = stream.write
396 delimnl = ',\n' + ' ' * indent
397 last_index = len(items) - 1
398 for i, (key, ent) in enumerate(items):
399 last = i == last_index
400 write(key)
401 write('=')
402 if id(ent) in context:
403 # Special-case representation of recursion to match standard
404 # recursive dataclass repr.
405 write("...")
406 else:
407 self._format(ent, stream, indent + len(key) + 1,
408 allowance if last else 1,
409 context, level)
410 if not last:
411 write(delimnl)
412
413 def _format_items(self, items, stream, indent, allowance, context, level):
414 write = stream.write
415 indent += self._indent_per_level
416 if self._indent_per_level > 1:
417 write((self._indent_per_level - 1) * ' ')
418 delimnl = ',\n' + ' ' * indent
419 delim = ''
420 width = max_width = self._width - indent + 1
421 it = iter(items)
422 try:
423 next_ent = next(it)
424 except StopIteration:
425 return
426 last = False
427 while not last:
428 ent = next_ent
429 try:
430 next_ent = next(it)
431 except StopIteration:
432 last = True
433 max_width -= allowance
434 width -= allowance
435 if self._compact:
436 rep = self._repr(ent, context, level)
437 w = len(rep) + 2
438 if width < w:
439 width = max_width
440 if delim:
441 delim = delimnl
442 if width >= w:
443 width -= w
444 write(delim)
445 delim = ', '
446 write(rep)
447 continue
448 write(delim)
449 delim = delimnl
450 self._format(ent, stream, indent,
451 allowance if last else 1,
452 context, level)
453
454 def _repr(self, object, context, level):
455 repr, readable, recursive = self.format(object, context.copy(),
456 self._depth, level)
457 if not readable:
458 self._readable = False
459 if recursive:
460 self._recursive = True
461 return repr
462
463 def format(self, object, context, maxlevels, level):
464 """Format object for a specific context, returning a string
465 and flags indicating whether the representation is 'readable'
466 and whether the object represents a recursive construct.
467 """
468 return self._safe_repr(object, context, maxlevels, level)
469
470 def _pprint_default_dict(self, object, stream, indent, allowance, context, level):
471 if not len(object):
472 stream.write(repr(object))
473 return
474 rdf = self._repr(object.default_factory, context, level)
475 cls = object.__class__
476 indent += len(cls.__name__) + 1
477 stream.write('%s(%s,\n%s' % (cls.__name__, rdf, ' ' * indent))
478 self._pprint_dict(object, stream, indent, allowance + 1, context, level)
479 stream.write(')')
480
481 _dispatch[_collections.defaultdict.__repr__] = _pprint_default_dict
482
483 def _pprint_counter(self, object, stream, indent, allowance, context, level):
484 if not len(object):
485 stream.write(repr(object))
486 return
487 cls = object.__class__
488 stream.write(cls.__name__ + '({')
489 if self._indent_per_level > 1:
490 stream.write((self._indent_per_level - 1) * ' ')
491 items = object.most_common()
492 self._format_dict_items(items, stream,
493 indent + len(cls.__name__) + 1, allowance + 2,
494 context, level)
495 stream.write('})')
496
497 _dispatch[_collections.Counter.__repr__] = _pprint_counter
498
499 def _pprint_chain_map(self, object, stream, indent, allowance, context, level):
500 if not len(object.maps):
501 stream.write(repr(object))
502 return
503 cls = object.__class__
504 stream.write(cls.__name__ + '(')
505 indent += len(cls.__name__) + 1
506 for i, m in enumerate(object.maps):
507 if i == len(object.maps) - 1:
508 self._format(m, stream, indent, allowance + 1, context, level)
509 stream.write(')')
510 else:
511 self._format(m, stream, indent, 1, context, level)
512 stream.write(',\n' + ' ' * indent)
513
514 _dispatch[_collections.ChainMap.__repr__] = _pprint_chain_map
515
516 def _pprint_deque(self, object, stream, indent, allowance, context, level):
517 if not len(object):
518 stream.write(repr(object))
519 return
520 cls = object.__class__
521 stream.write(cls.__name__ + '(')
522 indent += len(cls.__name__) + 1
523 stream.write('[')
524 if object.maxlen is None:
525 self._format_items(object, stream, indent, allowance + 2,
526 context, level)
527 stream.write('])')
528 else:
529 self._format_items(object, stream, indent, 2,
530 context, level)
531 rml = self._repr(object.maxlen, context, level)
532 stream.write('],\n%smaxlen=%s)' % (' ' * indent, rml))
533
534 _dispatch[_collections.deque.__repr__] = _pprint_deque
535
536 def _pprint_user_dict(self, object, stream, indent, allowance, context, level):
537 self._format(object.data, stream, indent, allowance, context, level - 1)
538
539 _dispatch[_collections.UserDict.__repr__] = _pprint_user_dict
540
541 def _pprint_user_list(self, object, stream, indent, allowance, context, level):
542 self._format(object.data, stream, indent, allowance, context, level - 1)
543
544 _dispatch[_collections.UserList.__repr__] = _pprint_user_list
545
546 def _pprint_user_string(self, object, stream, indent, allowance, context, level):
547 self._format(object.data, stream, indent, allowance, context, level - 1)
548
549 _dispatch[_collections.UserString.__repr__] = _pprint_user_string
550
551 def _safe_repr(self, object, context, maxlevels, level):
552 # Return triple (repr_string, isreadable, isrecursive).
553 typ = type(object)
554 if typ in _builtin_scalars:
555 return repr(object), True, False
556
557 r = getattr(typ, "__repr__", None)
558
559 if issubclass(typ, int) and r is int.__repr__:
560 if self._underscore_numbers:
561 return f"{object:_d}", True, False
562 else:
563 return repr(object), True, False
564
565 if issubclass(typ, dict) and r is dict.__repr__:
566 if not object:
567 return "{}", True, False
568 objid = id(object)
569 if maxlevels and level >= maxlevels:
570 return "{...}", False, objid in context
571 if objid in context:
572 return _recursion(object), False, True
573 context[objid] = 1
574 readable = True
575 recursive = False
576 components = []
577 append = components.append
578 level += 1
579 if self._sort_dicts:
580 items = sorted(object.items(), key=_safe_tuple)
581 else:
582 items = object.items()
583 for k, v in items:
584 krepr, kreadable, krecur = self.format(
585 k, context, maxlevels, level)
586 vrepr, vreadable, vrecur = self.format(
587 v, context, maxlevels, level)
588 append("%s: %s" % (krepr, vrepr))
589 readable = readable and kreadable and vreadable
590 if krecur or vrecur:
591 recursive = True
592 del context[objid]
593 return "{%s}" % ", ".join(components), readable, recursive
594
595 if (issubclass(typ, list) and r is list.__repr__) or \
596 (issubclass(typ, tuple) and r is tuple.__repr__):
597 if issubclass(typ, list):
598 if not object:
599 return "[]", True, False
600 format = "[%s]"
601 elif len(object) == 1:
602 format = "(%s,)"
603 else:
604 if not object:
605 return "()", True, False
606 format = "(%s)"
607 objid = id(object)
608 if maxlevels and level >= maxlevels:
609 return format % "...", False, objid in context
610 if objid in context:
611 return _recursion(object), False, True
612 context[objid] = 1
613 readable = True
614 recursive = False
615 components = []
616 append = components.append
617 level += 1
618 for o in object:
619 orepr, oreadable, orecur = self.format(
620 o, context, maxlevels, level)
621 append(orepr)
622 if not oreadable:
623 readable = False
624 if orecur:
625 recursive = True
626 del context[objid]
627 return format % ", ".join(components), readable, recursive
628
629 rep = repr(object)
630 return rep, (rep and not rep.startswith('<')), False
631
632 _builtin_scalars = frozenset({str, bytes, bytearray, float, complex,
633 bool, type(None)})
634
635 def _recursion(object):
636 return ("<Recursion on %s with id=%s>"
637 % (type(object).__name__, id(object)))
638
639
640 def _perfcheck(object=None):
641 import time
642 if object is None:
643 object = [("string", (1, 2), [3, 4], {5: 6, 7: 8})] * 100000
644 p = PrettyPrinter()
645 t1 = time.perf_counter()
646 p._safe_repr(object, {}, None, 0, True)
647 t2 = time.perf_counter()
648 p.pformat(object)
649 t3 = time.perf_counter()
650 print("_safe_repr:", t2 - t1)
651 print("pformat:", t3 - t2)
652
653 def _wrap_bytes_repr(object, width, allowance):
654 current = b''
655 last = len(object) // 4 * 4
656 for i in range(0, len(object), 4):
657 part = object[i: i+4]
658 candidate = current + part
659 if i == last:
660 width -= allowance
661 if len(repr(candidate)) > width:
662 if current:
663 yield repr(current)
664 current = part
665 else:
666 current = candidate
667 if current:
668 yield repr(current)
669
670 if __name__ == "__main__":
671 _perfcheck()