• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
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 dataclasses as _dataclasses
39import re
40import sys as _sys
41import types as _types
42from io import StringIO as _StringIO
43
44__all__ = ["pprint","pformat","isreadable","isrecursive","saferepr",
45           "PrettyPrinter", "pp"]
46
47
48def 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
57def 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
64def pp(object, *args, sort_dicts=False, **kwargs):
65    """Pretty-print a Python object"""
66    pprint(object, *args, sort_dicts=sort_dicts, **kwargs)
67
68def saferepr(object):
69    """Version of repr() which can handle recursive data structures."""
70    return PrettyPrinter()._safe_repr(object, {}, None, 0)[0]
71
72def isreadable(object):
73    """Determine if saferepr(object) is readable by eval()."""
74    return PrettyPrinter()._safe_repr(object, {}, None, 0)[1]
75
76def isrecursive(object):
77    """Determine if object requires a recursive representation."""
78    return PrettyPrinter()._safe_repr(object, {}, None, 0)[2]
79
80class _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
102def _safe_tuple(t):
103    "Helper function for comparing 2-tuples"
104    return _safe_key(t[0]), _safe_key(t[1])
105
106class PrettyPrinter:
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        self._format(object, self._stream, 0, 0, {}, 0)
153        self._stream.write("\n")
154
155    def pformat(self, object):
156        sio = _StringIO()
157        self._format(object, sio, 0, 0, {}, 0)
158        return sio.getvalue()
159
160    def isrecursive(self, object):
161        return self.format(object, {}, 0, 0)[2]
162
163    def isreadable(self, object):
164        s, readable, recursive = self.format(object, {}, 0, 0)
165        return readable and not recursive
166
167    def _format(self, object, stream, indent, allowance, context, level):
168        objid = id(object)
169        if objid in context:
170            stream.write(_recursion(object))
171            self._recursive = True
172            self._readable = False
173            return
174        rep = self._repr(object, context, level)
175        max_width = self._width - indent - allowance
176        if len(rep) > max_width:
177            p = self._dispatch.get(type(object).__repr__, None)
178            if p is not None:
179                context[objid] = 1
180                p(self, object, stream, indent, allowance, context, level + 1)
181                del context[objid]
182                return
183            elif (_dataclasses.is_dataclass(object) and
184                  not isinstance(object, type) and
185                  object.__dataclass_params__.repr and
186                  # Check dataclass has generated repr method.
187                  hasattr(object.__repr__, "__wrapped__") and
188                  "__create_fn__" in object.__repr__.__wrapped__.__qualname__):
189                context[objid] = 1
190                self._pprint_dataclass(object, stream, indent, allowance, context, level + 1)
191                del context[objid]
192                return
193        stream.write(rep)
194
195    def _pprint_dataclass(self, object, stream, indent, allowance, context, level):
196        cls_name = object.__class__.__name__
197        indent += len(cls_name) + 1
198        items = [(f.name, getattr(object, f.name)) for f in _dataclasses.fields(object) if f.repr]
199        stream.write(cls_name + '(')
200        self._format_namespace_items(items, stream, indent, allowance, context, level)
201        stream.write(')')
202
203    _dispatch = {}
204
205    def _pprint_dict(self, object, stream, indent, allowance, context, level):
206        write = stream.write
207        write('{')
208        if self._indent_per_level > 1:
209            write((self._indent_per_level - 1) * ' ')
210        length = len(object)
211        if length:
212            if self._sort_dicts:
213                items = sorted(object.items(), key=_safe_tuple)
214            else:
215                items = object.items()
216            self._format_dict_items(items, stream, indent, allowance + 1,
217                                    context, level)
218        write('}')
219
220    _dispatch[dict.__repr__] = _pprint_dict
221
222    def _pprint_ordered_dict(self, object, stream, indent, allowance, context, level):
223        if not len(object):
224            stream.write(repr(object))
225            return
226        cls = object.__class__
227        stream.write(cls.__name__ + '(')
228        self._format(list(object.items()), stream,
229                     indent + len(cls.__name__) + 1, allowance + 1,
230                     context, level)
231        stream.write(')')
232
233    _dispatch[_collections.OrderedDict.__repr__] = _pprint_ordered_dict
234
235    def _pprint_list(self, object, stream, indent, allowance, context, level):
236        stream.write('[')
237        self._format_items(object, stream, indent, allowance + 1,
238                           context, level)
239        stream.write(']')
240
241    _dispatch[list.__repr__] = _pprint_list
242
243    def _pprint_tuple(self, object, stream, indent, allowance, context, level):
244        stream.write('(')
245        endchar = ',)' if len(object) == 1 else ')'
246        self._format_items(object, stream, indent, allowance + len(endchar),
247                           context, level)
248        stream.write(endchar)
249
250    _dispatch[tuple.__repr__] = _pprint_tuple
251
252    def _pprint_set(self, object, stream, indent, allowance, context, level):
253        if not len(object):
254            stream.write(repr(object))
255            return
256        typ = object.__class__
257        if typ is set:
258            stream.write('{')
259            endchar = '}'
260        else:
261            stream.write(typ.__name__ + '({')
262            endchar = '})'
263            indent += len(typ.__name__) + 1
264        object = sorted(object, key=_safe_key)
265        self._format_items(object, stream, indent, allowance + len(endchar),
266                           context, level)
267        stream.write(endchar)
268
269    _dispatch[set.__repr__] = _pprint_set
270    _dispatch[frozenset.__repr__] = _pprint_set
271
272    def _pprint_str(self, object, stream, indent, allowance, context, level):
273        write = stream.write
274        if not len(object):
275            write(repr(object))
276            return
277        chunks = []
278        lines = object.splitlines(True)
279        if level == 1:
280            indent += 1
281            allowance += 1
282        max_width1 = max_width = self._width - indent
283        for i, line in enumerate(lines):
284            rep = repr(line)
285            if i == len(lines) - 1:
286                max_width1 -= allowance
287            if len(rep) <= max_width1:
288                chunks.append(rep)
289            else:
290                # A list of alternating (non-space, space) strings
291                parts = re.findall(r'\S*\s*', line)
292                assert parts
293                assert not parts[-1]
294                parts.pop()  # drop empty last part
295                max_width2 = max_width
296                current = ''
297                for j, part in enumerate(parts):
298                    candidate = current + part
299                    if j == len(parts) - 1 and i == len(lines) - 1:
300                        max_width2 -= allowance
301                    if len(repr(candidate)) > max_width2:
302                        if current:
303                            chunks.append(repr(current))
304                        current = part
305                    else:
306                        current = candidate
307                if current:
308                    chunks.append(repr(current))
309        if len(chunks) == 1:
310            write(rep)
311            return
312        if level == 1:
313            write('(')
314        for i, rep in enumerate(chunks):
315            if i > 0:
316                write('\n' + ' '*indent)
317            write(rep)
318        if level == 1:
319            write(')')
320
321    _dispatch[str.__repr__] = _pprint_str
322
323    def _pprint_bytes(self, object, stream, indent, allowance, context, level):
324        write = stream.write
325        if len(object) <= 4:
326            write(repr(object))
327            return
328        parens = level == 1
329        if parens:
330            indent += 1
331            allowance += 1
332            write('(')
333        delim = ''
334        for rep in _wrap_bytes_repr(object, self._width - indent, allowance):
335            write(delim)
336            write(rep)
337            if not delim:
338                delim = '\n' + ' '*indent
339        if parens:
340            write(')')
341
342    _dispatch[bytes.__repr__] = _pprint_bytes
343
344    def _pprint_bytearray(self, object, stream, indent, allowance, context, level):
345        write = stream.write
346        write('bytearray(')
347        self._pprint_bytes(bytes(object), stream, indent + 10,
348                           allowance + 1, context, level + 1)
349        write(')')
350
351    _dispatch[bytearray.__repr__] = _pprint_bytearray
352
353    def _pprint_mappingproxy(self, object, stream, indent, allowance, context, level):
354        stream.write('mappingproxy(')
355        self._format(object.copy(), stream, indent + 13, allowance + 1,
356                     context, level)
357        stream.write(')')
358
359    _dispatch[_types.MappingProxyType.__repr__] = _pprint_mappingproxy
360
361    def _pprint_simplenamespace(self, object, stream, indent, allowance, context, level):
362        if type(object) is _types.SimpleNamespace:
363            # The SimpleNamespace repr is "namespace" instead of the class
364            # name, so we do the same here. For subclasses; use the class name.
365            cls_name = 'namespace'
366        else:
367            cls_name = object.__class__.__name__
368        indent += len(cls_name) + 1
369        items = object.__dict__.items()
370        stream.write(cls_name + '(')
371        self._format_namespace_items(items, stream, indent, allowance, context, level)
372        stream.write(')')
373
374    _dispatch[_types.SimpleNamespace.__repr__] = _pprint_simplenamespace
375
376    def _format_dict_items(self, items, stream, indent, allowance, context,
377                           level):
378        write = stream.write
379        indent += self._indent_per_level
380        delimnl = ',\n' + ' ' * indent
381        last_index = len(items) - 1
382        for i, (key, ent) in enumerate(items):
383            last = i == last_index
384            rep = self._repr(key, context, level)
385            write(rep)
386            write(': ')
387            self._format(ent, stream, indent + len(rep) + 2,
388                         allowance if last else 1,
389                         context, level)
390            if not last:
391                write(delimnl)
392
393    def _format_namespace_items(self, items, stream, indent, allowance, context, level):
394        write = stream.write
395        delimnl = ',\n' + ' ' * indent
396        last_index = len(items) - 1
397        for i, (key, ent) in enumerate(items):
398            last = i == last_index
399            write(key)
400            write('=')
401            if id(ent) in context:
402                # Special-case representation of recursion to match standard
403                # recursive dataclass repr.
404                write("...")
405            else:
406                self._format(ent, stream, indent + len(key) + 1,
407                             allowance if last else 1,
408                             context, level)
409            if not last:
410                write(delimnl)
411
412    def _format_items(self, items, stream, indent, allowance, context, level):
413        write = stream.write
414        indent += self._indent_per_level
415        if self._indent_per_level > 1:
416            write((self._indent_per_level - 1) * ' ')
417        delimnl = ',\n' + ' ' * indent
418        delim = ''
419        width = max_width = self._width - indent + 1
420        it = iter(items)
421        try:
422            next_ent = next(it)
423        except StopIteration:
424            return
425        last = False
426        while not last:
427            ent = next_ent
428            try:
429                next_ent = next(it)
430            except StopIteration:
431                last = True
432                max_width -= allowance
433                width -= allowance
434            if self._compact:
435                rep = self._repr(ent, context, level)
436                w = len(rep) + 2
437                if width < w:
438                    width = max_width
439                    if delim:
440                        delim = delimnl
441                if width >= w:
442                    width -= w
443                    write(delim)
444                    delim = ', '
445                    write(rep)
446                    continue
447            write(delim)
448            delim = delimnl
449            self._format(ent, stream, indent,
450                         allowance if last else 1,
451                         context, level)
452
453    def _repr(self, object, context, level):
454        repr, readable, recursive = self.format(object, context.copy(),
455                                                self._depth, level)
456        if not readable:
457            self._readable = False
458        if recursive:
459            self._recursive = True
460        return repr
461
462    def format(self, object, context, maxlevels, level):
463        """Format object for a specific context, returning a string
464        and flags indicating whether the representation is 'readable'
465        and whether the object represents a recursive construct.
466        """
467        return self._safe_repr(object, context, maxlevels, level)
468
469    def _pprint_default_dict(self, object, stream, indent, allowance, context, level):
470        if not len(object):
471            stream.write(repr(object))
472            return
473        rdf = self._repr(object.default_factory, context, level)
474        cls = object.__class__
475        indent += len(cls.__name__) + 1
476        stream.write('%s(%s,\n%s' % (cls.__name__, rdf, ' ' * indent))
477        self._pprint_dict(object, stream, indent, allowance + 1, context, level)
478        stream.write(')')
479
480    _dispatch[_collections.defaultdict.__repr__] = _pprint_default_dict
481
482    def _pprint_counter(self, object, stream, indent, allowance, context, level):
483        if not len(object):
484            stream.write(repr(object))
485            return
486        cls = object.__class__
487        stream.write(cls.__name__ + '({')
488        if self._indent_per_level > 1:
489            stream.write((self._indent_per_level - 1) * ' ')
490        items = object.most_common()
491        self._format_dict_items(items, stream,
492                                indent + len(cls.__name__) + 1, allowance + 2,
493                                context, level)
494        stream.write('})')
495
496    _dispatch[_collections.Counter.__repr__] = _pprint_counter
497
498    def _pprint_chain_map(self, object, stream, indent, allowance, context, level):
499        if not len(object.maps):
500            stream.write(repr(object))
501            return
502        cls = object.__class__
503        stream.write(cls.__name__ + '(')
504        indent += len(cls.__name__) + 1
505        for i, m in enumerate(object.maps):
506            if i == len(object.maps) - 1:
507                self._format(m, stream, indent, allowance + 1, context, level)
508                stream.write(')')
509            else:
510                self._format(m, stream, indent, 1, context, level)
511                stream.write(',\n' + ' ' * indent)
512
513    _dispatch[_collections.ChainMap.__repr__] = _pprint_chain_map
514
515    def _pprint_deque(self, object, stream, indent, allowance, context, level):
516        if not len(object):
517            stream.write(repr(object))
518            return
519        cls = object.__class__
520        stream.write(cls.__name__ + '(')
521        indent += len(cls.__name__) + 1
522        stream.write('[')
523        if object.maxlen is None:
524            self._format_items(object, stream, indent, allowance + 2,
525                               context, level)
526            stream.write('])')
527        else:
528            self._format_items(object, stream, indent, 2,
529                               context, level)
530            rml = self._repr(object.maxlen, context, level)
531            stream.write('],\n%smaxlen=%s)' % (' ' * indent, rml))
532
533    _dispatch[_collections.deque.__repr__] = _pprint_deque
534
535    def _pprint_user_dict(self, object, stream, indent, allowance, context, level):
536        self._format(object.data, stream, indent, allowance, context, level - 1)
537
538    _dispatch[_collections.UserDict.__repr__] = _pprint_user_dict
539
540    def _pprint_user_list(self, object, stream, indent, allowance, context, level):
541        self._format(object.data, stream, indent, allowance, context, level - 1)
542
543    _dispatch[_collections.UserList.__repr__] = _pprint_user_list
544
545    def _pprint_user_string(self, object, stream, indent, allowance, context, level):
546        self._format(object.data, stream, indent, allowance, context, level - 1)
547
548    _dispatch[_collections.UserString.__repr__] = _pprint_user_string
549
550    def _safe_repr(self, object, context, maxlevels, level):
551        # Return triple (repr_string, isreadable, isrecursive).
552        typ = type(object)
553        if typ in _builtin_scalars:
554            return repr(object), True, False
555
556        r = getattr(typ, "__repr__", None)
557
558        if issubclass(typ, int) and r is int.__repr__:
559            if self._underscore_numbers:
560                return f"{object:_d}", True, False
561            else:
562                return repr(object), True, False
563
564        if issubclass(typ, dict) and r is dict.__repr__:
565            if not object:
566                return "{}", True, False
567            objid = id(object)
568            if maxlevels and level >= maxlevels:
569                return "{...}", False, objid in context
570            if objid in context:
571                return _recursion(object), False, True
572            context[objid] = 1
573            readable = True
574            recursive = False
575            components = []
576            append = components.append
577            level += 1
578            if self._sort_dicts:
579                items = sorted(object.items(), key=_safe_tuple)
580            else:
581                items = object.items()
582            for k, v in items:
583                krepr, kreadable, krecur = self.format(
584                    k, context, maxlevels, level)
585                vrepr, vreadable, vrecur = self.format(
586                    v, context, maxlevels, level)
587                append("%s: %s" % (krepr, vrepr))
588                readable = readable and kreadable and vreadable
589                if krecur or vrecur:
590                    recursive = True
591            del context[objid]
592            return "{%s}" % ", ".join(components), readable, recursive
593
594        if (issubclass(typ, list) and r is list.__repr__) or \
595           (issubclass(typ, tuple) and r is tuple.__repr__):
596            if issubclass(typ, list):
597                if not object:
598                    return "[]", True, False
599                format = "[%s]"
600            elif len(object) == 1:
601                format = "(%s,)"
602            else:
603                if not object:
604                    return "()", True, False
605                format = "(%s)"
606            objid = id(object)
607            if maxlevels and level >= maxlevels:
608                return format % "...", False, objid in context
609            if objid in context:
610                return _recursion(object), False, True
611            context[objid] = 1
612            readable = True
613            recursive = False
614            components = []
615            append = components.append
616            level += 1
617            for o in object:
618                orepr, oreadable, orecur = self.format(
619                    o, context, maxlevels, level)
620                append(orepr)
621                if not oreadable:
622                    readable = False
623                if orecur:
624                    recursive = True
625            del context[objid]
626            return format % ", ".join(components), readable, recursive
627
628        rep = repr(object)
629        return rep, (rep and not rep.startswith('<')), False
630
631_builtin_scalars = frozenset({str, bytes, bytearray, float, complex,
632                              bool, type(None)})
633
634def _recursion(object):
635    return ("<Recursion on %s with id=%s>"
636            % (type(object).__name__, id(object)))
637
638
639def _perfcheck(object=None):
640    import time
641    if object is None:
642        object = [("string", (1, 2), [3, 4], {5: 6, 7: 8})] * 100000
643    p = PrettyPrinter()
644    t1 = time.perf_counter()
645    p._safe_repr(object, {}, None, 0, True)
646    t2 = time.perf_counter()
647    p.pformat(object)
648    t3 = time.perf_counter()
649    print("_safe_repr:", t2 - t1)
650    print("pformat:", t3 - t2)
651
652def _wrap_bytes_repr(object, width, allowance):
653    current = b''
654    last = len(object) // 4 * 4
655    for i in range(0, len(object), 4):
656        part = object[i: i+4]
657        candidate = current + part
658        if i == last:
659            width -= allowance
660        if len(repr(candidate)) > width:
661            if current:
662                yield repr(current)
663            current = part
664        else:
665            current = candidate
666    if current:
667        yield repr(current)
668
669if __name__ == "__main__":
670    _perfcheck()
671