| #  Author:      Fred L. Drake, Jr. | 
 | #               fdrake@acm.org | 
 | # | 
 | #  This is a simple little module I wrote to make life easier.  I didn't | 
 | #  see anything quite like it in the library, though I may have overlooked | 
 | #  something.  I wrote this when I was trying to read some heavily nested | 
 | #  tuples with fairly non-descriptive content.  This is modeled very much | 
 | #  after Lisp/Scheme - style pretty-printing of lists.  If you find it | 
 | #  useful, thank small children who sleep at night. | 
 |  | 
 | """Support to pretty-print lists, tuples, & dictionaries recursively. | 
 |  | 
 | Very simple, but useful, especially in debugging data structures. | 
 |  | 
 | Classes | 
 | ------- | 
 |  | 
 | PrettyPrinter() | 
 |     Handle pretty-printing operations onto a stream using a configured | 
 |     set of formatting parameters. | 
 |  | 
 | Functions | 
 | --------- | 
 |  | 
 | pformat() | 
 |     Format a Python object into a pretty-printed representation. | 
 |  | 
 | pprint() | 
 |     Pretty-print a Python object to a stream [default is sys.stdout]. | 
 |  | 
 | saferepr() | 
 |     Generate a 'standard' repr()-like value, but protect against recursive | 
 |     data structures. | 
 |  | 
 | """ | 
 |  | 
 | import collections as _collections | 
 | import re | 
 | import sys as _sys | 
 | import types as _types | 
 | from io import StringIO as _StringIO | 
 |  | 
 | __all__ = ["pprint","pformat","isreadable","isrecursive","saferepr", | 
 |            "PrettyPrinter"] | 
 |  | 
 |  | 
 | def pprint(object, stream=None, indent=1, width=80, depth=None, *, | 
 |            compact=False): | 
 |     """Pretty-print a Python object to a stream [default is sys.stdout].""" | 
 |     printer = PrettyPrinter( | 
 |         stream=stream, indent=indent, width=width, depth=depth, | 
 |         compact=compact) | 
 |     printer.pprint(object) | 
 |  | 
 | def pformat(object, indent=1, width=80, depth=None, *, compact=False): | 
 |     """Format a Python object into a pretty-printed representation.""" | 
 |     return PrettyPrinter(indent=indent, width=width, depth=depth, | 
 |                          compact=compact).pformat(object) | 
 |  | 
 | def saferepr(object): | 
 |     """Version of repr() which can handle recursive data structures.""" | 
 |     return _safe_repr(object, {}, None, 0)[0] | 
 |  | 
 | def isreadable(object): | 
 |     """Determine if saferepr(object) is readable by eval().""" | 
 |     return _safe_repr(object, {}, None, 0)[1] | 
 |  | 
 | def isrecursive(object): | 
 |     """Determine if object requires a recursive representation.""" | 
 |     return _safe_repr(object, {}, None, 0)[2] | 
 |  | 
 | class _safe_key: | 
 |     """Helper function for key functions when sorting unorderable objects. | 
 |  | 
 |     The wrapped-object will fallback to a Py2.x style comparison for | 
 |     unorderable types (sorting first comparing the type name and then by | 
 |     the obj ids).  Does not work recursively, so dict.items() must have | 
 |     _safe_key applied to both the key and the value. | 
 |  | 
 |     """ | 
 |  | 
 |     __slots__ = ['obj'] | 
 |  | 
 |     def __init__(self, obj): | 
 |         self.obj = obj | 
 |  | 
 |     def __lt__(self, other): | 
 |         try: | 
 |             return self.obj < other.obj | 
 |         except TypeError: | 
 |             return ((str(type(self.obj)), id(self.obj)) < \ | 
 |                     (str(type(other.obj)), id(other.obj))) | 
 |  | 
 | def _safe_tuple(t): | 
 |     "Helper function for comparing 2-tuples" | 
 |     return _safe_key(t[0]), _safe_key(t[1]) | 
 |  | 
 | class PrettyPrinter: | 
 |     def __init__(self, indent=1, width=80, depth=None, stream=None, *, | 
 |                  compact=False): | 
 |         """Handle pretty printing operations onto a stream using a set of | 
 |         configured parameters. | 
 |  | 
 |         indent | 
 |             Number of spaces to indent for each level of nesting. | 
 |  | 
 |         width | 
 |             Attempted maximum number of columns in the output. | 
 |  | 
 |         depth | 
 |             The maximum depth to print out nested structures. | 
 |  | 
 |         stream | 
 |             The desired output stream.  If omitted (or false), the standard | 
 |             output stream available at construction will be used. | 
 |  | 
 |         compact | 
 |             If true, several items will be combined in one line. | 
 |  | 
 |         """ | 
 |         indent = int(indent) | 
 |         width = int(width) | 
 |         if indent < 0: | 
 |             raise ValueError('indent must be >= 0') | 
 |         if depth is not None and depth <= 0: | 
 |             raise ValueError('depth must be > 0') | 
 |         if not width: | 
 |             raise ValueError('width must be != 0') | 
 |         self._depth = depth | 
 |         self._indent_per_level = indent | 
 |         self._width = width | 
 |         if stream is not None: | 
 |             self._stream = stream | 
 |         else: | 
 |             self._stream = _sys.stdout | 
 |         self._compact = bool(compact) | 
 |  | 
 |     def pprint(self, object): | 
 |         self._format(object, self._stream, 0, 0, {}, 0) | 
 |         self._stream.write("\n") | 
 |  | 
 |     def pformat(self, object): | 
 |         sio = _StringIO() | 
 |         self._format(object, sio, 0, 0, {}, 0) | 
 |         return sio.getvalue() | 
 |  | 
 |     def isrecursive(self, object): | 
 |         return self.format(object, {}, 0, 0)[2] | 
 |  | 
 |     def isreadable(self, object): | 
 |         s, readable, recursive = self.format(object, {}, 0, 0) | 
 |         return readable and not recursive | 
 |  | 
 |     def _format(self, object, stream, indent, allowance, context, level): | 
 |         objid = id(object) | 
 |         if objid in context: | 
 |             stream.write(_recursion(object)) | 
 |             self._recursive = True | 
 |             self._readable = False | 
 |             return | 
 |         rep = self._repr(object, context, level) | 
 |         max_width = self._width - indent - allowance | 
 |         if len(rep) > max_width: | 
 |             p = self._dispatch.get(type(object).__repr__, None) | 
 |             if p is not None: | 
 |                 context[objid] = 1 | 
 |                 p(self, object, stream, indent, allowance, context, level + 1) | 
 |                 del context[objid] | 
 |                 return | 
 |             elif isinstance(object, dict): | 
 |                 context[objid] = 1 | 
 |                 self._pprint_dict(object, stream, indent, allowance, | 
 |                                   context, level + 1) | 
 |                 del context[objid] | 
 |                 return | 
 |         stream.write(rep) | 
 |  | 
 |     _dispatch = {} | 
 |  | 
 |     def _pprint_dict(self, object, stream, indent, allowance, context, level): | 
 |         write = stream.write | 
 |         write('{') | 
 |         if self._indent_per_level > 1: | 
 |             write((self._indent_per_level - 1) * ' ') | 
 |         length = len(object) | 
 |         if length: | 
 |             items = sorted(object.items(), key=_safe_tuple) | 
 |             self._format_dict_items(items, stream, indent, allowance + 1, | 
 |                                     context, level) | 
 |         write('}') | 
 |  | 
 |     _dispatch[dict.__repr__] = _pprint_dict | 
 |  | 
 |     def _pprint_ordered_dict(self, object, stream, indent, allowance, context, level): | 
 |         if not len(object): | 
 |             stream.write(repr(object)) | 
 |             return | 
 |         cls = object.__class__ | 
 |         stream.write(cls.__name__ + '(') | 
 |         self._format(list(object.items()), stream, | 
 |                      indent + len(cls.__name__) + 1, allowance + 1, | 
 |                      context, level) | 
 |         stream.write(')') | 
 |  | 
 |     _dispatch[_collections.OrderedDict.__repr__] = _pprint_ordered_dict | 
 |  | 
 |     def _pprint_list(self, object, stream, indent, allowance, context, level): | 
 |         stream.write('[') | 
 |         self._format_items(object, stream, indent, allowance + 1, | 
 |                            context, level) | 
 |         stream.write(']') | 
 |  | 
 |     _dispatch[list.__repr__] = _pprint_list | 
 |  | 
 |     def _pprint_tuple(self, object, stream, indent, allowance, context, level): | 
 |         stream.write('(') | 
 |         endchar = ',)' if len(object) == 1 else ')' | 
 |         self._format_items(object, stream, indent, allowance + len(endchar), | 
 |                            context, level) | 
 |         stream.write(endchar) | 
 |  | 
 |     _dispatch[tuple.__repr__] = _pprint_tuple | 
 |  | 
 |     def _pprint_set(self, object, stream, indent, allowance, context, level): | 
 |         if not len(object): | 
 |             stream.write(repr(object)) | 
 |             return | 
 |         typ = object.__class__ | 
 |         if typ is set: | 
 |             stream.write('{') | 
 |             endchar = '}' | 
 |         else: | 
 |             stream.write(typ.__name__ + '({') | 
 |             endchar = '})' | 
 |             indent += len(typ.__name__) + 1 | 
 |         object = sorted(object, key=_safe_key) | 
 |         self._format_items(object, stream, indent, allowance + len(endchar), | 
 |                            context, level) | 
 |         stream.write(endchar) | 
 |  | 
 |     _dispatch[set.__repr__] = _pprint_set | 
 |     _dispatch[frozenset.__repr__] = _pprint_set | 
 |  | 
 |     def _pprint_str(self, object, stream, indent, allowance, context, level): | 
 |         write = stream.write | 
 |         if not len(object): | 
 |             write(repr(object)) | 
 |             return | 
 |         chunks = [] | 
 |         lines = object.splitlines(True) | 
 |         if level == 1: | 
 |             indent += 1 | 
 |             allowance += 1 | 
 |         max_width1 = max_width = self._width - indent | 
 |         for i, line in enumerate(lines): | 
 |             rep = repr(line) | 
 |             if i == len(lines) - 1: | 
 |                 max_width1 -= allowance | 
 |             if len(rep) <= max_width1: | 
 |                 chunks.append(rep) | 
 |             else: | 
 |                 # A list of alternating (non-space, space) strings | 
 |                 parts = re.findall(r'\S*\s*', line) | 
 |                 assert parts | 
 |                 assert not parts[-1] | 
 |                 parts.pop()  # drop empty last part | 
 |                 max_width2 = max_width | 
 |                 current = '' | 
 |                 for j, part in enumerate(parts): | 
 |                     candidate = current + part | 
 |                     if j == len(parts) - 1 and i == len(lines) - 1: | 
 |                         max_width2 -= allowance | 
 |                     if len(repr(candidate)) > max_width2: | 
 |                         if current: | 
 |                             chunks.append(repr(current)) | 
 |                         current = part | 
 |                     else: | 
 |                         current = candidate | 
 |                 if current: | 
 |                     chunks.append(repr(current)) | 
 |         if len(chunks) == 1: | 
 |             write(rep) | 
 |             return | 
 |         if level == 1: | 
 |             write('(') | 
 |         for i, rep in enumerate(chunks): | 
 |             if i > 0: | 
 |                 write('\n' + ' '*indent) | 
 |             write(rep) | 
 |         if level == 1: | 
 |             write(')') | 
 |  | 
 |     _dispatch[str.__repr__] = _pprint_str | 
 |  | 
 |     def _pprint_bytes(self, object, stream, indent, allowance, context, level): | 
 |         write = stream.write | 
 |         if len(object) <= 4: | 
 |             write(repr(object)) | 
 |             return | 
 |         parens = level == 1 | 
 |         if parens: | 
 |             indent += 1 | 
 |             allowance += 1 | 
 |             write('(') | 
 |         delim = '' | 
 |         for rep in _wrap_bytes_repr(object, self._width - indent, allowance): | 
 |             write(delim) | 
 |             write(rep) | 
 |             if not delim: | 
 |                 delim = '\n' + ' '*indent | 
 |         if parens: | 
 |             write(')') | 
 |  | 
 |     _dispatch[bytes.__repr__] = _pprint_bytes | 
 |  | 
 |     def _pprint_bytearray(self, object, stream, indent, allowance, context, level): | 
 |         write = stream.write | 
 |         write('bytearray(') | 
 |         self._pprint_bytes(bytes(object), stream, indent + 10, | 
 |                            allowance + 1, context, level + 1) | 
 |         write(')') | 
 |  | 
 |     _dispatch[bytearray.__repr__] = _pprint_bytearray | 
 |  | 
 |     def _pprint_mappingproxy(self, object, stream, indent, allowance, context, level): | 
 |         stream.write('mappingproxy(') | 
 |         self._format(object.copy(), stream, indent + 13, allowance + 1, | 
 |                      context, level) | 
 |         stream.write(')') | 
 |  | 
 |     _dispatch[_types.MappingProxyType.__repr__] = _pprint_mappingproxy | 
 |  | 
 |     def _format_dict_items(self, items, stream, indent, allowance, context, | 
 |                            level): | 
 |         write = stream.write | 
 |         indent += self._indent_per_level | 
 |         delimnl = ',\n' + ' ' * indent | 
 |         last_index = len(items) - 1 | 
 |         for i, (key, ent) in enumerate(items): | 
 |             last = i == last_index | 
 |             rep = self._repr(key, context, level) | 
 |             write(rep) | 
 |             write(': ') | 
 |             self._format(ent, stream, indent + len(rep) + 2, | 
 |                          allowance if last else 1, | 
 |                          context, level) | 
 |             if not last: | 
 |                 write(delimnl) | 
 |  | 
 |     def _format_items(self, items, stream, indent, allowance, context, level): | 
 |         write = stream.write | 
 |         indent += self._indent_per_level | 
 |         if self._indent_per_level > 1: | 
 |             write((self._indent_per_level - 1) * ' ') | 
 |         delimnl = ',\n' + ' ' * indent | 
 |         delim = '' | 
 |         width = max_width = self._width - indent + 1 | 
 |         it = iter(items) | 
 |         try: | 
 |             next_ent = next(it) | 
 |         except StopIteration: | 
 |             return | 
 |         last = False | 
 |         while not last: | 
 |             ent = next_ent | 
 |             try: | 
 |                 next_ent = next(it) | 
 |             except StopIteration: | 
 |                 last = True | 
 |                 max_width -= allowance | 
 |                 width -= allowance | 
 |             if self._compact: | 
 |                 rep = self._repr(ent, context, level) | 
 |                 w = len(rep) + 2 | 
 |                 if width < w: | 
 |                     width = max_width | 
 |                     if delim: | 
 |                         delim = delimnl | 
 |                 if width >= w: | 
 |                     width -= w | 
 |                     write(delim) | 
 |                     delim = ', ' | 
 |                     write(rep) | 
 |                     continue | 
 |             write(delim) | 
 |             delim = delimnl | 
 |             self._format(ent, stream, indent, | 
 |                          allowance if last else 1, | 
 |                          context, level) | 
 |  | 
 |     def _repr(self, object, context, level): | 
 |         repr, readable, recursive = self.format(object, context.copy(), | 
 |                                                 self._depth, level) | 
 |         if not readable: | 
 |             self._readable = False | 
 |         if recursive: | 
 |             self._recursive = True | 
 |         return repr | 
 |  | 
 |     def format(self, object, context, maxlevels, level): | 
 |         """Format object for a specific context, returning a string | 
 |         and flags indicating whether the representation is 'readable' | 
 |         and whether the object represents a recursive construct. | 
 |         """ | 
 |         return _safe_repr(object, context, maxlevels, level) | 
 |  | 
 |     def _pprint_default_dict(self, object, stream, indent, allowance, context, level): | 
 |         if not len(object): | 
 |             stream.write(repr(object)) | 
 |             return | 
 |         rdf = self._repr(object.default_factory, context, level) | 
 |         cls = object.__class__ | 
 |         indent += len(cls.__name__) + 1 | 
 |         stream.write('%s(%s,\n%s' % (cls.__name__, rdf, ' ' * indent)) | 
 |         self._pprint_dict(object, stream, indent, allowance + 1, context, level) | 
 |         stream.write(')') | 
 |  | 
 |     _dispatch[_collections.defaultdict.__repr__] = _pprint_default_dict | 
 |  | 
 |     def _pprint_counter(self, object, stream, indent, allowance, context, level): | 
 |         if not len(object): | 
 |             stream.write(repr(object)) | 
 |             return | 
 |         cls = object.__class__ | 
 |         stream.write(cls.__name__ + '({') | 
 |         if self._indent_per_level > 1: | 
 |             stream.write((self._indent_per_level - 1) * ' ') | 
 |         items = object.most_common() | 
 |         self._format_dict_items(items, stream, | 
 |                                 indent + len(cls.__name__) + 1, allowance + 2, | 
 |                                 context, level) | 
 |         stream.write('})') | 
 |  | 
 |     _dispatch[_collections.Counter.__repr__] = _pprint_counter | 
 |  | 
 |     def _pprint_chain_map(self, object, stream, indent, allowance, context, level): | 
 |         if not len(object.maps): | 
 |             stream.write(repr(object)) | 
 |             return | 
 |         cls = object.__class__ | 
 |         stream.write(cls.__name__ + '(') | 
 |         indent += len(cls.__name__) + 1 | 
 |         for i, m in enumerate(object.maps): | 
 |             if i == len(object.maps) - 1: | 
 |                 self._format(m, stream, indent, allowance + 1, context, level) | 
 |                 stream.write(')') | 
 |             else: | 
 |                 self._format(m, stream, indent, 1, context, level) | 
 |                 stream.write(',\n' + ' ' * indent) | 
 |  | 
 |     _dispatch[_collections.ChainMap.__repr__] = _pprint_chain_map | 
 |  | 
 |     def _pprint_deque(self, object, stream, indent, allowance, context, level): | 
 |         if not len(object): | 
 |             stream.write(repr(object)) | 
 |             return | 
 |         cls = object.__class__ | 
 |         stream.write(cls.__name__ + '(') | 
 |         indent += len(cls.__name__) + 1 | 
 |         stream.write('[') | 
 |         if object.maxlen is None: | 
 |             self._format_items(object, stream, indent, allowance + 2, | 
 |                                context, level) | 
 |             stream.write('])') | 
 |         else: | 
 |             self._format_items(object, stream, indent, 2, | 
 |                                context, level) | 
 |             rml = self._repr(object.maxlen, context, level) | 
 |             stream.write('],\n%smaxlen=%s)' % (' ' * indent, rml)) | 
 |  | 
 |     _dispatch[_collections.deque.__repr__] = _pprint_deque | 
 |  | 
 |     def _pprint_user_dict(self, object, stream, indent, allowance, context, level): | 
 |         self._format(object.data, stream, indent, allowance, context, level - 1) | 
 |  | 
 |     _dispatch[_collections.UserDict.__repr__] = _pprint_user_dict | 
 |  | 
 |     def _pprint_user_list(self, object, stream, indent, allowance, context, level): | 
 |         self._format(object.data, stream, indent, allowance, context, level - 1) | 
 |  | 
 |     _dispatch[_collections.UserList.__repr__] = _pprint_user_list | 
 |  | 
 |     def _pprint_user_string(self, object, stream, indent, allowance, context, level): | 
 |         self._format(object.data, stream, indent, allowance, context, level - 1) | 
 |  | 
 |     _dispatch[_collections.UserString.__repr__] = _pprint_user_string | 
 |  | 
 | # Return triple (repr_string, isreadable, isrecursive). | 
 |  | 
 | def _safe_repr(object, context, maxlevels, level): | 
 |     typ = type(object) | 
 |     if typ in _builtin_scalars: | 
 |         return repr(object), True, False | 
 |  | 
 |     r = getattr(typ, "__repr__", None) | 
 |     if issubclass(typ, dict) and r is dict.__repr__: | 
 |         if not object: | 
 |             return "{}", True, False | 
 |         objid = id(object) | 
 |         if maxlevels and level >= maxlevels: | 
 |             return "{...}", False, objid in context | 
 |         if objid in context: | 
 |             return _recursion(object), False, True | 
 |         context[objid] = 1 | 
 |         readable = True | 
 |         recursive = False | 
 |         components = [] | 
 |         append = components.append | 
 |         level += 1 | 
 |         saferepr = _safe_repr | 
 |         items = sorted(object.items(), key=_safe_tuple) | 
 |         for k, v in items: | 
 |             krepr, kreadable, krecur = saferepr(k, context, maxlevels, level) | 
 |             vrepr, vreadable, vrecur = saferepr(v, context, maxlevels, level) | 
 |             append("%s: %s" % (krepr, vrepr)) | 
 |             readable = readable and kreadable and vreadable | 
 |             if krecur or vrecur: | 
 |                 recursive = True | 
 |         del context[objid] | 
 |         return "{%s}" % ", ".join(components), readable, recursive | 
 |  | 
 |     if (issubclass(typ, list) and r is list.__repr__) or \ | 
 |        (issubclass(typ, tuple) and r is tuple.__repr__): | 
 |         if issubclass(typ, list): | 
 |             if not object: | 
 |                 return "[]", True, False | 
 |             format = "[%s]" | 
 |         elif len(object) == 1: | 
 |             format = "(%s,)" | 
 |         else: | 
 |             if not object: | 
 |                 return "()", True, False | 
 |             format = "(%s)" | 
 |         objid = id(object) | 
 |         if maxlevels and level >= maxlevels: | 
 |             return format % "...", False, objid in context | 
 |         if objid in context: | 
 |             return _recursion(object), False, True | 
 |         context[objid] = 1 | 
 |         readable = True | 
 |         recursive = False | 
 |         components = [] | 
 |         append = components.append | 
 |         level += 1 | 
 |         for o in object: | 
 |             orepr, oreadable, orecur = _safe_repr(o, context, maxlevels, level) | 
 |             append(orepr) | 
 |             if not oreadable: | 
 |                 readable = False | 
 |             if orecur: | 
 |                 recursive = True | 
 |         del context[objid] | 
 |         return format % ", ".join(components), readable, recursive | 
 |  | 
 |     rep = repr(object) | 
 |     return rep, (rep and not rep.startswith('<')), False | 
 |  | 
 | _builtin_scalars = frozenset({str, bytes, bytearray, int, float, complex, | 
 |                               bool, type(None)}) | 
 |  | 
 | def _recursion(object): | 
 |     return ("<Recursion on %s with id=%s>" | 
 |             % (type(object).__name__, id(object))) | 
 |  | 
 |  | 
 | def _perfcheck(object=None): | 
 |     import time | 
 |     if object is None: | 
 |         object = [("string", (1, 2), [3, 4], {5: 6, 7: 8})] * 100000 | 
 |     p = PrettyPrinter() | 
 |     t1 = time.time() | 
 |     _safe_repr(object, {}, None, 0) | 
 |     t2 = time.time() | 
 |     p.pformat(object) | 
 |     t3 = time.time() | 
 |     print("_safe_repr:", t2 - t1) | 
 |     print("pformat:", t3 - t2) | 
 |  | 
 | def _wrap_bytes_repr(object, width, allowance): | 
 |     current = b'' | 
 |     last = len(object) // 4 * 4 | 
 |     for i in range(0, len(object), 4): | 
 |         part = object[i: i+4] | 
 |         candidate = current + part | 
 |         if i == last: | 
 |             width -= allowance | 
 |         if len(repr(candidate)) > width: | 
 |             if current: | 
 |                 yield repr(current) | 
 |             current = part | 
 |         else: | 
 |             current = candidate | 
 |     if current: | 
 |         yield repr(current) | 
 |  | 
 | if __name__ == "__main__": | 
 |     _perfcheck() |