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