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