# mypy: allow-untyped-defs # This module was imported from the cpython standard library # (https://github.com/python/cpython/) at commit # c5140945c723ae6c4b7ee81ff720ac8ea4b52cfd (python3.12). # # # Original 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. from __future__ import annotations import collections as _collections from collections.abc import Callable from collections.abc import Iterator import dataclasses as _dataclasses from io import StringIO as _StringIO import re import types as _types from typing import Any from typing import IO 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: int = 4, width: int = 80, depth: int | None = None, ) -> None: """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. """ 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 def pformat(self, object: Any) -> str: sio = _StringIO() self._format(object, sio, 0, 0, set(), 0) return sio.getvalue() def _format( self, object: Any, stream: IO[str], indent: int, allowance: int, context: set[int], level: int, ) -> None: objid = id(object) if objid in context: stream.write(_recursion(object)) return p = self._dispatch.get(type(object).__repr__, None) if p is not None: context.add(objid) p(self, object, stream, indent, allowance, context, level + 1) context.remove(objid) elif ( _dataclasses.is_dataclass(object) and not isinstance(object, type) and object.__dataclass_params__.repr # type:ignore[attr-defined] and # Check dataclass has generated repr method. hasattr(object.__repr__, "__wrapped__") and "__create_fn__" in object.__repr__.__wrapped__.__qualname__ ): context.add(objid) self._pprint_dataclass( object, stream, indent, allowance, context, level + 1 ) context.remove(objid) else: stream.write(self._repr(object, context, level)) def _pprint_dataclass( self, object: Any, stream: IO[str], indent: int, allowance: int, context: set[int], level: int, ) -> None: cls_name = object.__class__.__name__ items = [ (f.name, getattr(object, f.name)) for f in _dataclasses.fields(object) if f.repr ] stream.write(cls_name + "(") self._format_namespace_items(items, stream, indent, allowance, context, level) stream.write(")") _dispatch: dict[ Callable[..., str], Callable[[PrettyPrinter, Any, IO[str], int, int, set[int], int], None], ] = {} def _pprint_dict( self, object: Any, stream: IO[str], indent: int, allowance: int, context: set[int], level: int, ) -> None: write = stream.write write("{") items = sorted(object.items(), key=_safe_tuple) self._format_dict_items(items, stream, indent, allowance, context, level) write("}") _dispatch[dict.__repr__] = _pprint_dict def _pprint_ordered_dict( self, object: Any, stream: IO[str], indent: int, allowance: int, context: set[int], level: int, ) -> None: if not len(object): stream.write(repr(object)) return cls = object.__class__ stream.write(cls.__name__ + "(") self._pprint_dict(object, stream, indent, allowance, context, level) stream.write(")") _dispatch[_collections.OrderedDict.__repr__] = _pprint_ordered_dict def _pprint_list( self, object: Any, stream: IO[str], indent: int, allowance: int, context: set[int], level: int, ) -> None: stream.write("[") self._format_items(object, stream, indent, allowance, context, level) stream.write("]") _dispatch[list.__repr__] = _pprint_list def _pprint_tuple( self, object: Any, stream: IO[str], indent: int, allowance: int, context: set[int], level: int, ) -> None: stream.write("(") self._format_items(object, stream, indent, allowance, context, level) stream.write(")") _dispatch[tuple.__repr__] = _pprint_tuple def _pprint_set( self, object: Any, stream: IO[str], indent: int, allowance: int, context: set[int], level: int, ) -> None: 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 = "})" object = sorted(object, key=_safe_key) self._format_items(object, stream, indent, allowance, context, level) stream.write(endchar) _dispatch[set.__repr__] = _pprint_set _dispatch[frozenset.__repr__] = _pprint_set def _pprint_str( self, object: Any, stream: IO[str], indent: int, allowance: int, context: set[int], level: int, ) -> None: 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: Any, stream: IO[str], indent: int, allowance: int, context: set[int], level: int, ) -> None: 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: Any, stream: IO[str], indent: int, allowance: int, context: set[int], level: int, ) -> None: 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: Any, stream: IO[str], indent: int, allowance: int, context: set[int], level: int, ) -> None: stream.write("mappingproxy(") self._format(object.copy(), stream, indent, allowance, context, level) stream.write(")") _dispatch[_types.MappingProxyType.__repr__] = _pprint_mappingproxy def _pprint_simplenamespace( self, object: Any, stream: IO[str], indent: int, allowance: int, context: set[int], level: int, ) -> None: if type(object) is _types.SimpleNamespace: # The SimpleNamespace repr is "namespace" instead of the class # name, so we do the same here. For subclasses; use the class name. cls_name = "namespace" else: cls_name = object.__class__.__name__ items = object.__dict__.items() stream.write(cls_name + "(") self._format_namespace_items(items, stream, indent, allowance, context, level) stream.write(")") _dispatch[_types.SimpleNamespace.__repr__] = _pprint_simplenamespace def _format_dict_items( self, items: list[tuple[Any, Any]], stream: IO[str], indent: int, allowance: int, context: set[int], level: int, ) -> None: if not items: return write = stream.write item_indent = indent + self._indent_per_level delimnl = "\n" + " " * item_indent for key, ent in items: write(delimnl) write(self._repr(key, context, level)) write(": ") self._format(ent, stream, item_indent, 1, context, level) write(",") write("\n" + " " * indent) def _format_namespace_items( self, items: list[tuple[Any, Any]], stream: IO[str], indent: int, allowance: int, context: set[int], level: int, ) -> None: if not items: return write = stream.write item_indent = indent + self._indent_per_level delimnl = "\n" + " " * item_indent for key, ent in items: write(delimnl) write(key) write("=") if id(ent) in context: # Special-case representation of recursion to match standard # recursive dataclass repr. write("...") else: self._format( ent, stream, item_indent + len(key) + 1, 1, context, level, ) write(",") write("\n" + " " * indent) def _format_items( self, items: list[Any], stream: IO[str], indent: int, allowance: int, context: set[int], level: int, ) -> None: if not items: return write = stream.write item_indent = indent + self._indent_per_level delimnl = "\n" + " " * item_indent for item in items: write(delimnl) self._format(item, stream, item_indent, 1, context, level) write(",") write("\n" + " " * indent) def _repr(self, object: Any, context: set[int], level: int) -> str: return self._safe_repr(object, context.copy(), self._depth, level) def _pprint_default_dict( self, object: Any, stream: IO[str], indent: int, allowance: int, context: set[int], level: int, ) -> None: rdf = self._repr(object.default_factory, context, level) stream.write(f"{object.__class__.__name__}({rdf}, ") self._pprint_dict(object, stream, indent, allowance, context, level) stream.write(")") _dispatch[_collections.defaultdict.__repr__] = _pprint_default_dict def _pprint_counter( self, object: Any, stream: IO[str], indent: int, allowance: int, context: set[int], level: int, ) -> None: stream.write(object.__class__.__name__ + "(") if object: stream.write("{") items = object.most_common() self._format_dict_items(items, stream, indent, allowance, context, level) stream.write("}") stream.write(")") _dispatch[_collections.Counter.__repr__] = _pprint_counter def _pprint_chain_map( self, object: Any, stream: IO[str], indent: int, allowance: int, context: set[int], level: int, ) -> None: if not len(object.maps) or (len(object.maps) == 1 and not len(object.maps[0])): stream.write(repr(object)) return stream.write(object.__class__.__name__ + "(") self._format_items(object.maps, stream, indent, allowance, context, level) stream.write(")") _dispatch[_collections.ChainMap.__repr__] = _pprint_chain_map def _pprint_deque( self, object: Any, stream: IO[str], indent: int, allowance: int, context: set[int], level: int, ) -> None: stream.write(object.__class__.__name__ + "(") if object.maxlen is not None: stream.write(f"maxlen={object.maxlen}, ") stream.write("[") self._format_items(object, stream, indent, allowance + 1, context, level) stream.write("])") _dispatch[_collections.deque.__repr__] = _pprint_deque def _pprint_user_dict( self, object: Any, stream: IO[str], indent: int, allowance: int, context: set[int], level: int, ) -> None: self._format(object.data, stream, indent, allowance, context, level - 1) _dispatch[_collections.UserDict.__repr__] = _pprint_user_dict def _pprint_user_list( self, object: Any, stream: IO[str], indent: int, allowance: int, context: set[int], level: int, ) -> None: self._format(object.data, stream, indent, allowance, context, level - 1) _dispatch[_collections.UserList.__repr__] = _pprint_user_list def _pprint_user_string( self, object: Any, stream: IO[str], indent: int, allowance: int, context: set[int], level: int, ) -> None: self._format(object.data, stream, indent, allowance, context, level - 1) _dispatch[_collections.UserString.__repr__] = _pprint_user_string def _safe_repr( self, object: Any, context: set[int], maxlevels: int | None, level: int ) -> str: typ = type(object) if typ in _builtin_scalars: return repr(object) r = getattr(typ, "__repr__", None) if issubclass(typ, dict) and r is dict.__repr__: if not object: return "{}" objid = id(object) if maxlevels and level >= maxlevels: return "{...}" if objid in context: return _recursion(object) context.add(objid) components: list[str] = [] append = components.append level += 1 for k, v in sorted(object.items(), key=_safe_tuple): krepr = self._safe_repr(k, context, maxlevels, level) vrepr = self._safe_repr(v, context, maxlevels, level) append(f"{krepr}: {vrepr}") context.remove(objid) return "{{{}}}".format(", ".join(components)) 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 "[]" format = "[%s]" elif len(object) == 1: format = "(%s,)" else: if not object: return "()" format = "(%s)" objid = id(object) if maxlevels and level >= maxlevels: return format % "..." if objid in context: return _recursion(object) context.add(objid) components = [] append = components.append level += 1 for o in object: orepr = self._safe_repr(o, context, maxlevels, level) append(orepr) context.remove(objid) return format % ", ".join(components) return repr(object) _builtin_scalars = frozenset( {str, bytes, bytearray, float, complex, bool, type(None), int} ) def _recursion(object: Any) -> str: return f"" def _wrap_bytes_repr(object: Any, width: int, allowance: int) -> Iterator[str]: 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)