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