Home | History | Annotate | Download | only in Lib
      1 #  Author:      Fred L. Drake, Jr.
      2 #               fdrake (at] acm.org
      3 #
      4 #  This is a simple little module I wrote to make life easier.  I didn't
      5 #  see anything quite like it in the library, though I may have overlooked
      6 #  something.  I wrote this when I was trying to read some heavily nested
      7 #  tuples with fairly non-descriptive content.  This is modeled very much
      8 #  after Lisp/Scheme - style pretty-printing of lists.  If you find it
      9 #  useful, thank small children who sleep at night.
     10 
     11 """Support to pretty-print lists, tuples, & dictionaries recursively.
     12 
     13 Very simple, but useful, especially in debugging data structures.
     14 
     15 Classes
     16 -------
     17 
     18 PrettyPrinter()
     19     Handle pretty-printing operations onto a stream using a configured
     20     set of formatting parameters.
     21 
     22 Functions
     23 ---------
     24 
     25 pformat()
     26     Format a Python object into a pretty-printed representation.
     27 
     28 pprint()
     29     Pretty-print a Python object to a stream [default is sys.stdout].
     30 
     31 saferepr()
     32     Generate a 'standard' repr()-like value, but protect against recursive
     33     data structures.
     34 
     35 """
     36 
     37 import collections as _collections
     38 import re
     39 import sys as _sys
     40 import types as _types
     41 from io import StringIO as _StringIO
     42 
     43 __all__ = ["pprint","pformat","isreadable","isrecursive","saferepr",
     44            "PrettyPrinter"]
     45 
     46 
     47 def 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 
     55 def 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 
     60 def saferepr(object):
     61     """Version of repr() which can handle recursive data structures."""
     62     return _safe_repr(object, {}, None, 0)[0]
     63 
     64 def isreadable(object):
     65     """Determine if saferepr(object) is readable by eval()."""
     66     return _safe_repr(object, {}, None, 0)[1]
     67 
     68 def isrecursive(object):
     69     """Determine if object requires a recursive representation."""
     70     return _safe_repr(object, {}, None, 0)[2]
     71 
     72 class _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 
     94 def _safe_tuple(t):
     95     "Helper function for comparing 2-tuples"
     96     return _safe_key(t[0]), _safe_key(t[1])
     97 
     98 class 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 
    490 def _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 
    561 def _recursion(object):
    562     return ("<Recursion on %s with id=%s>"
    563             % (type(object).__name__, id(object)))
    564 
    565 
    566 def _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 
    579 def _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 
    596 if __name__ == "__main__":
    597     _perfcheck()
    598