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