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