Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/rich/pretty.py: 21%

392 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-06-07 06:35 +0000

1import builtins 

2import collections 

3import dataclasses 

4import inspect 

5import os 

6import sys 

7from array import array 

8from collections import Counter, UserDict, UserList, defaultdict, deque 

9from dataclasses import dataclass, fields, is_dataclass 

10from inspect import isclass 

11from itertools import islice 

12from types import MappingProxyType 

13from typing import ( 

14 TYPE_CHECKING, 

15 Any, 

16 Callable, 

17 DefaultDict, 

18 Dict, 

19 Iterable, 

20 List, 

21 Optional, 

22 Sequence, 

23 Set, 

24 Tuple, 

25 Union, 

26) 

27 

28from rich.repr import RichReprResult 

29 

30try: 

31 import attr as _attr_module 

32 

33 _has_attrs = hasattr(_attr_module, "ib") 

34except ImportError: # pragma: no cover 

35 _has_attrs = False 

36 

37from . import get_console 

38from ._loop import loop_last 

39from ._pick import pick_bool 

40from .abc import RichRenderable 

41from .cells import cell_len 

42from .highlighter import ReprHighlighter 

43from .jupyter import JupyterMixin, JupyterRenderable 

44from .measure import Measurement 

45from .text import Text 

46 

47if TYPE_CHECKING: 

48 from .console import ( 

49 Console, 

50 ConsoleOptions, 

51 HighlighterType, 

52 JustifyMethod, 

53 OverflowMethod, 

54 RenderResult, 

55 ) 

56 

57 

58def _is_attr_object(obj: Any) -> bool: 

59 """Check if an object was created with attrs module.""" 

60 return _has_attrs and _attr_module.has(type(obj)) 

61 

62 

63def _get_attr_fields(obj: Any) -> Sequence["_attr_module.Attribute[Any]"]: 

64 """Get fields for an attrs object.""" 

65 return _attr_module.fields(type(obj)) if _has_attrs else [] 

66 

67 

68def _is_dataclass_repr(obj: object) -> bool: 

69 """Check if an instance of a dataclass contains the default repr. 

70 

71 Args: 

72 obj (object): A dataclass instance. 

73 

74 Returns: 

75 bool: True if the default repr is used, False if there is a custom repr. 

76 """ 

77 # Digging in to a lot of internals here 

78 # Catching all exceptions in case something is missing on a non CPython implementation 

79 try: 

80 return obj.__repr__.__code__.co_filename == dataclasses.__file__ 

81 except Exception: # pragma: no coverage 

82 return False 

83 

84 

85_dummy_namedtuple = collections.namedtuple("_dummy_namedtuple", []) 

86 

87 

88def _has_default_namedtuple_repr(obj: object) -> bool: 

89 """Check if an instance of namedtuple contains the default repr 

90 

91 Args: 

92 obj (object): A namedtuple 

93 

94 Returns: 

95 bool: True if the default repr is used, False if there's a custom repr. 

96 """ 

97 obj_file = None 

98 try: 

99 obj_file = inspect.getfile(obj.__repr__) 

100 except (OSError, TypeError): 

101 # OSError handles case where object is defined in __main__ scope, e.g. REPL - no filename available. 

102 # TypeError trapped defensively, in case of object without filename slips through. 

103 pass 

104 default_repr_file = inspect.getfile(_dummy_namedtuple.__repr__) 

105 return obj_file == default_repr_file 

106 

107 

108def _ipy_display_hook( 

109 value: Any, 

110 console: Optional["Console"] = None, 

111 overflow: "OverflowMethod" = "ignore", 

112 crop: bool = False, 

113 indent_guides: bool = False, 

114 max_length: Optional[int] = None, 

115 max_string: Optional[int] = None, 

116 max_depth: Optional[int] = None, 

117 expand_all: bool = False, 

118) -> Union[str, None]: 

119 # needed here to prevent circular import: 

120 from .console import ConsoleRenderable 

121 

122 # always skip rich generated jupyter renderables or None values 

123 if _safe_isinstance(value, JupyterRenderable) or value is None: 

124 return None 

125 

126 console = console or get_console() 

127 

128 with console.capture() as capture: 

129 # certain renderables should start on a new line 

130 if _safe_isinstance(value, ConsoleRenderable): 

131 console.line() 

132 console.print( 

133 value 

134 if _safe_isinstance(value, RichRenderable) 

135 else Pretty( 

136 value, 

137 overflow=overflow, 

138 indent_guides=indent_guides, 

139 max_length=max_length, 

140 max_string=max_string, 

141 max_depth=max_depth, 

142 expand_all=expand_all, 

143 margin=12, 

144 ), 

145 crop=crop, 

146 new_line_start=True, 

147 end="", 

148 ) 

149 # strip trailing newline, not usually part of a text repr 

150 # I'm not sure if this should be prevented at a lower level 

151 return capture.get().rstrip("\n") 

152 

153 

154def _safe_isinstance( 

155 obj: object, class_or_tuple: Union[type, Tuple[type, ...]] 

156) -> bool: 

157 """isinstance can fail in rare cases, for example types with no __class__""" 

158 try: 

159 return isinstance(obj, class_or_tuple) 

160 except Exception: 

161 return False 

162 

163 

164def install( 

165 console: Optional["Console"] = None, 

166 overflow: "OverflowMethod" = "ignore", 

167 crop: bool = False, 

168 indent_guides: bool = False, 

169 max_length: Optional[int] = None, 

170 max_string: Optional[int] = None, 

171 max_depth: Optional[int] = None, 

172 expand_all: bool = False, 

173) -> None: 

174 """Install automatic pretty printing in the Python REPL. 

175 

176 Args: 

177 console (Console, optional): Console instance or ``None`` to use global console. Defaults to None. 

178 overflow (Optional[OverflowMethod], optional): Overflow method. Defaults to "ignore". 

179 crop (Optional[bool], optional): Enable cropping of long lines. Defaults to False. 

180 indent_guides (bool, optional): Enable indentation guides. Defaults to False. 

181 max_length (int, optional): Maximum length of containers before abbreviating, or None for no abbreviation. 

182 Defaults to None. 

183 max_string (int, optional): Maximum length of string before truncating, or None to disable. Defaults to None. 

184 max_depth (int, optional): Maximum depth of nested data structures, or None for no maximum. Defaults to None. 

185 expand_all (bool, optional): Expand all containers. Defaults to False. 

186 max_frames (int): Maximum number of frames to show in a traceback, 0 for no maximum. Defaults to 100. 

187 """ 

188 from rich import get_console 

189 

190 console = console or get_console() 

191 assert console is not None 

192 

193 def display_hook(value: Any) -> None: 

194 """Replacement sys.displayhook which prettifies objects with Rich.""" 

195 if value is not None: 

196 assert console is not None 

197 builtins._ = None # type: ignore[attr-defined] 

198 console.print( 

199 value 

200 if _safe_isinstance(value, RichRenderable) 

201 else Pretty( 

202 value, 

203 overflow=overflow, 

204 indent_guides=indent_guides, 

205 max_length=max_length, 

206 max_string=max_string, 

207 max_depth=max_depth, 

208 expand_all=expand_all, 

209 ), 

210 crop=crop, 

211 ) 

212 builtins._ = value # type: ignore[attr-defined] 

213 

214 if "get_ipython" in globals(): 

215 ip = get_ipython() # type: ignore[name-defined] 

216 from IPython.core.formatters import BaseFormatter 

217 

218 class RichFormatter(BaseFormatter): # type: ignore[misc] 

219 pprint: bool = True 

220 

221 def __call__(self, value: Any) -> Any: 

222 if self.pprint: 

223 return _ipy_display_hook( 

224 value, 

225 console=get_console(), 

226 overflow=overflow, 

227 indent_guides=indent_guides, 

228 max_length=max_length, 

229 max_string=max_string, 

230 max_depth=max_depth, 

231 expand_all=expand_all, 

232 ) 

233 else: 

234 return repr(value) 

235 

236 # replace plain text formatter with rich formatter 

237 rich_formatter = RichFormatter() 

238 ip.display_formatter.formatters["text/plain"] = rich_formatter 

239 else: 

240 sys.displayhook = display_hook 

241 

242 

243class Pretty(JupyterMixin): 

244 """A rich renderable that pretty prints an object. 

245 

246 Args: 

247 _object (Any): An object to pretty print. 

248 highlighter (HighlighterType, optional): Highlighter object to apply to result, or None for ReprHighlighter. Defaults to None. 

249 indent_size (int, optional): Number of spaces in indent. Defaults to 4. 

250 justify (JustifyMethod, optional): Justify method, or None for default. Defaults to None. 

251 overflow (OverflowMethod, optional): Overflow method, or None for default. Defaults to None. 

252 no_wrap (Optional[bool], optional): Disable word wrapping. Defaults to False. 

253 indent_guides (bool, optional): Enable indentation guides. Defaults to False. 

254 max_length (int, optional): Maximum length of containers before abbreviating, or None for no abbreviation. 

255 Defaults to None. 

256 max_string (int, optional): Maximum length of string before truncating, or None to disable. Defaults to None. 

257 max_depth (int, optional): Maximum depth of nested data structures, or None for no maximum. Defaults to None. 

258 expand_all (bool, optional): Expand all containers. Defaults to False. 

259 margin (int, optional): Subtrace a margin from width to force containers to expand earlier. Defaults to 0. 

260 insert_line (bool, optional): Insert a new line if the output has multiple new lines. Defaults to False. 

261 """ 

262 

263 def __init__( 

264 self, 

265 _object: Any, 

266 highlighter: Optional["HighlighterType"] = None, 

267 *, 

268 indent_size: int = 4, 

269 justify: Optional["JustifyMethod"] = None, 

270 overflow: Optional["OverflowMethod"] = None, 

271 no_wrap: Optional[bool] = False, 

272 indent_guides: bool = False, 

273 max_length: Optional[int] = None, 

274 max_string: Optional[int] = None, 

275 max_depth: Optional[int] = None, 

276 expand_all: bool = False, 

277 margin: int = 0, 

278 insert_line: bool = False, 

279 ) -> None: 

280 self._object = _object 

281 self.highlighter = highlighter or ReprHighlighter() 

282 self.indent_size = indent_size 

283 self.justify: Optional["JustifyMethod"] = justify 

284 self.overflow: Optional["OverflowMethod"] = overflow 

285 self.no_wrap = no_wrap 

286 self.indent_guides = indent_guides 

287 self.max_length = max_length 

288 self.max_string = max_string 

289 self.max_depth = max_depth 

290 self.expand_all = expand_all 

291 self.margin = margin 

292 self.insert_line = insert_line 

293 

294 def __rich_console__( 

295 self, console: "Console", options: "ConsoleOptions" 

296 ) -> "RenderResult": 

297 pretty_str = pretty_repr( 

298 self._object, 

299 max_width=options.max_width - self.margin, 

300 indent_size=self.indent_size, 

301 max_length=self.max_length, 

302 max_string=self.max_string, 

303 max_depth=self.max_depth, 

304 expand_all=self.expand_all, 

305 ) 

306 pretty_text = Text.from_ansi( 

307 pretty_str, 

308 justify=self.justify or options.justify, 

309 overflow=self.overflow or options.overflow, 

310 no_wrap=pick_bool(self.no_wrap, options.no_wrap), 

311 style="pretty", 

312 ) 

313 pretty_text = ( 

314 self.highlighter(pretty_text) 

315 if pretty_text 

316 else Text( 

317 f"{type(self._object)}.__repr__ returned empty string", 

318 style="dim italic", 

319 ) 

320 ) 

321 if self.indent_guides and not options.ascii_only: 

322 pretty_text = pretty_text.with_indent_guides( 

323 self.indent_size, style="repr.indent" 

324 ) 

325 if self.insert_line and "\n" in pretty_text: 

326 yield "" 

327 yield pretty_text 

328 

329 def __rich_measure__( 

330 self, console: "Console", options: "ConsoleOptions" 

331 ) -> "Measurement": 

332 pretty_str = pretty_repr( 

333 self._object, 

334 max_width=options.max_width, 

335 indent_size=self.indent_size, 

336 max_length=self.max_length, 

337 max_string=self.max_string, 

338 max_depth=self.max_depth, 

339 expand_all=self.expand_all, 

340 ) 

341 text_width = ( 

342 max(cell_len(line) for line in pretty_str.splitlines()) if pretty_str else 0 

343 ) 

344 return Measurement(text_width, text_width) 

345 

346 

347def _get_braces_for_defaultdict(_object: DefaultDict[Any, Any]) -> Tuple[str, str, str]: 

348 return ( 

349 f"defaultdict({_object.default_factory!r}, {{", 

350 "})", 

351 f"defaultdict({_object.default_factory!r}, {{}})", 

352 ) 

353 

354 

355def _get_braces_for_array(_object: "array[Any]") -> Tuple[str, str, str]: 

356 return (f"array({_object.typecode!r}, [", "])", f"array({_object.typecode!r})") 

357 

358 

359_BRACES: Dict[type, Callable[[Any], Tuple[str, str, str]]] = { 

360 os._Environ: lambda _object: ("environ({", "})", "environ({})"), 

361 array: _get_braces_for_array, 

362 defaultdict: _get_braces_for_defaultdict, 

363 Counter: lambda _object: ("Counter({", "})", "Counter()"), 

364 deque: lambda _object: ("deque([", "])", "deque()"), 

365 dict: lambda _object: ("{", "}", "{}"), 

366 UserDict: lambda _object: ("{", "}", "{}"), 

367 frozenset: lambda _object: ("frozenset({", "})", "frozenset()"), 

368 list: lambda _object: ("[", "]", "[]"), 

369 UserList: lambda _object: ("[", "]", "[]"), 

370 set: lambda _object: ("{", "}", "set()"), 

371 tuple: lambda _object: ("(", ")", "()"), 

372 MappingProxyType: lambda _object: ("mappingproxy({", "})", "mappingproxy({})"), 

373} 

374_CONTAINERS = tuple(_BRACES.keys()) 

375_MAPPING_CONTAINERS = (dict, os._Environ, MappingProxyType, UserDict) 

376 

377 

378def is_expandable(obj: Any) -> bool: 

379 """Check if an object may be expanded by pretty print.""" 

380 return ( 

381 _safe_isinstance(obj, _CONTAINERS) 

382 or (is_dataclass(obj)) 

383 or (hasattr(obj, "__rich_repr__")) 

384 or _is_attr_object(obj) 

385 ) and not isclass(obj) 

386 

387 

388@dataclass 

389class Node: 

390 """A node in a repr tree. May be atomic or a container.""" 

391 

392 key_repr: str = "" 

393 value_repr: str = "" 

394 open_brace: str = "" 

395 close_brace: str = "" 

396 empty: str = "" 

397 last: bool = False 

398 is_tuple: bool = False 

399 is_namedtuple: bool = False 

400 children: Optional[List["Node"]] = None 

401 key_separator: str = ": " 

402 separator: str = ", " 

403 

404 def iter_tokens(self) -> Iterable[str]: 

405 """Generate tokens for this node.""" 

406 if self.key_repr: 

407 yield self.key_repr 

408 yield self.key_separator 

409 if self.value_repr: 

410 yield self.value_repr 

411 elif self.children is not None: 

412 if self.children: 

413 yield self.open_brace 

414 if self.is_tuple and not self.is_namedtuple and len(self.children) == 1: 

415 yield from self.children[0].iter_tokens() 

416 yield "," 

417 else: 

418 for child in self.children: 

419 yield from child.iter_tokens() 

420 if not child.last: 

421 yield self.separator 

422 yield self.close_brace 

423 else: 

424 yield self.empty 

425 

426 def check_length(self, start_length: int, max_length: int) -> bool: 

427 """Check the length fits within a limit. 

428 

429 Args: 

430 start_length (int): Starting length of the line (indent, prefix, suffix). 

431 max_length (int): Maximum length. 

432 

433 Returns: 

434 bool: True if the node can be rendered within max length, otherwise False. 

435 """ 

436 total_length = start_length 

437 for token in self.iter_tokens(): 

438 total_length += cell_len(token) 

439 if total_length > max_length: 

440 return False 

441 return True 

442 

443 def __str__(self) -> str: 

444 repr_text = "".join(self.iter_tokens()) 

445 return repr_text 

446 

447 def render( 

448 self, max_width: int = 80, indent_size: int = 4, expand_all: bool = False 

449 ) -> str: 

450 """Render the node to a pretty repr. 

451 

452 Args: 

453 max_width (int, optional): Maximum width of the repr. Defaults to 80. 

454 indent_size (int, optional): Size of indents. Defaults to 4. 

455 expand_all (bool, optional): Expand all levels. Defaults to False. 

456 

457 Returns: 

458 str: A repr string of the original object. 

459 """ 

460 lines = [_Line(node=self, is_root=True)] 

461 line_no = 0 

462 while line_no < len(lines): 

463 line = lines[line_no] 

464 if line.expandable and not line.expanded: 

465 if expand_all or not line.check_length(max_width): 

466 lines[line_no : line_no + 1] = line.expand(indent_size) 

467 line_no += 1 

468 

469 repr_str = "\n".join(str(line) for line in lines) 

470 return repr_str 

471 

472 

473@dataclass 

474class _Line: 

475 """A line in repr output.""" 

476 

477 parent: Optional["_Line"] = None 

478 is_root: bool = False 

479 node: Optional[Node] = None 

480 text: str = "" 

481 suffix: str = "" 

482 whitespace: str = "" 

483 expanded: bool = False 

484 last: bool = False 

485 

486 @property 

487 def expandable(self) -> bool: 

488 """Check if the line may be expanded.""" 

489 return bool(self.node is not None and self.node.children) 

490 

491 def check_length(self, max_length: int) -> bool: 

492 """Check this line fits within a given number of cells.""" 

493 start_length = ( 

494 len(self.whitespace) + cell_len(self.text) + cell_len(self.suffix) 

495 ) 

496 assert self.node is not None 

497 return self.node.check_length(start_length, max_length) 

498 

499 def expand(self, indent_size: int) -> Iterable["_Line"]: 

500 """Expand this line by adding children on their own line.""" 

501 node = self.node 

502 assert node is not None 

503 whitespace = self.whitespace 

504 assert node.children 

505 if node.key_repr: 

506 new_line = yield _Line( 

507 text=f"{node.key_repr}{node.key_separator}{node.open_brace}", 

508 whitespace=whitespace, 

509 ) 

510 else: 

511 new_line = yield _Line(text=node.open_brace, whitespace=whitespace) 

512 child_whitespace = self.whitespace + " " * indent_size 

513 tuple_of_one = node.is_tuple and len(node.children) == 1 

514 for last, child in loop_last(node.children): 

515 separator = "," if tuple_of_one else node.separator 

516 line = _Line( 

517 parent=new_line, 

518 node=child, 

519 whitespace=child_whitespace, 

520 suffix=separator, 

521 last=last and not tuple_of_one, 

522 ) 

523 yield line 

524 

525 yield _Line( 

526 text=node.close_brace, 

527 whitespace=whitespace, 

528 suffix=self.suffix, 

529 last=self.last, 

530 ) 

531 

532 def __str__(self) -> str: 

533 if self.last: 

534 return f"{self.whitespace}{self.text}{self.node or ''}" 

535 else: 

536 return ( 

537 f"{self.whitespace}{self.text}{self.node or ''}{self.suffix.rstrip()}" 

538 ) 

539 

540 

541def _is_namedtuple(obj: Any) -> bool: 

542 """Checks if an object is most likely a namedtuple. It is possible 

543 to craft an object that passes this check and isn't a namedtuple, but 

544 there is only a minuscule chance of this happening unintentionally. 

545 

546 Args: 

547 obj (Any): The object to test 

548 

549 Returns: 

550 bool: True if the object is a namedtuple. False otherwise. 

551 """ 

552 try: 

553 fields = getattr(obj, "_fields", None) 

554 except Exception: 

555 # Being very defensive - if we cannot get the attr then its not a namedtuple 

556 return False 

557 return isinstance(obj, tuple) and isinstance(fields, tuple) 

558 

559 

560def traverse( 

561 _object: Any, 

562 max_length: Optional[int] = None, 

563 max_string: Optional[int] = None, 

564 max_depth: Optional[int] = None, 

565) -> Node: 

566 """Traverse object and generate a tree. 

567 

568 Args: 

569 _object (Any): Object to be traversed. 

570 max_length (int, optional): Maximum length of containers before abbreviating, or None for no abbreviation. 

571 Defaults to None. 

572 max_string (int, optional): Maximum length of string before truncating, or None to disable truncating. 

573 Defaults to None. 

574 max_depth (int, optional): Maximum depth of data structures, or None for no maximum. 

575 Defaults to None. 

576 

577 Returns: 

578 Node: The root of a tree structure which can be used to render a pretty repr. 

579 """ 

580 

581 def to_repr(obj: Any) -> str: 

582 """Get repr string for an object, but catch errors.""" 

583 if ( 

584 max_string is not None 

585 and _safe_isinstance(obj, (bytes, str)) 

586 and len(obj) > max_string 

587 ): 

588 truncated = len(obj) - max_string 

589 obj_repr = f"{obj[:max_string]!r}+{truncated}" 

590 else: 

591 try: 

592 obj_repr = repr(obj) 

593 except Exception as error: 

594 obj_repr = f"<repr-error {str(error)!r}>" 

595 return obj_repr 

596 

597 visited_ids: Set[int] = set() 

598 push_visited = visited_ids.add 

599 pop_visited = visited_ids.remove 

600 

601 def _traverse(obj: Any, root: bool = False, depth: int = 0) -> Node: 

602 """Walk the object depth first.""" 

603 

604 obj_id = id(obj) 

605 if obj_id in visited_ids: 

606 # Recursion detected 

607 return Node(value_repr="...") 

608 

609 obj_type = type(obj) 

610 children: List[Node] 

611 reached_max_depth = max_depth is not None and depth >= max_depth 

612 

613 def iter_rich_args(rich_args: Any) -> Iterable[Union[Any, Tuple[str, Any]]]: 

614 for arg in rich_args: 

615 if _safe_isinstance(arg, tuple): 

616 if len(arg) == 3: 

617 key, child, default = arg 

618 if default == child: 

619 continue 

620 yield key, child 

621 elif len(arg) == 2: 

622 key, child = arg 

623 yield key, child 

624 elif len(arg) == 1: 

625 yield arg[0] 

626 else: 

627 yield arg 

628 

629 try: 

630 fake_attributes = hasattr( 

631 obj, "awehoi234_wdfjwljet234_234wdfoijsdfmmnxpi492" 

632 ) 

633 except Exception: 

634 fake_attributes = False 

635 

636 rich_repr_result: Optional[RichReprResult] = None 

637 if not fake_attributes: 

638 try: 

639 if hasattr(obj, "__rich_repr__") and not isclass(obj): 

640 rich_repr_result = obj.__rich_repr__() 

641 except Exception: 

642 pass 

643 

644 if rich_repr_result is not None: 

645 push_visited(obj_id) 

646 angular = getattr(obj.__rich_repr__, "angular", False) 

647 args = list(iter_rich_args(rich_repr_result)) 

648 class_name = obj.__class__.__name__ 

649 

650 if args: 

651 children = [] 

652 append = children.append 

653 

654 if reached_max_depth: 

655 if angular: 

656 node = Node(value_repr=f"<{class_name}...>") 

657 else: 

658 node = Node(value_repr=f"{class_name}(...)") 

659 else: 

660 if angular: 

661 node = Node( 

662 open_brace=f"<{class_name} ", 

663 close_brace=">", 

664 children=children, 

665 last=root, 

666 separator=" ", 

667 ) 

668 else: 

669 node = Node( 

670 open_brace=f"{class_name}(", 

671 close_brace=")", 

672 children=children, 

673 last=root, 

674 ) 

675 for last, arg in loop_last(args): 

676 if _safe_isinstance(arg, tuple): 

677 key, child = arg 

678 child_node = _traverse(child, depth=depth + 1) 

679 child_node.last = last 

680 child_node.key_repr = key 

681 child_node.key_separator = "=" 

682 append(child_node) 

683 else: 

684 child_node = _traverse(arg, depth=depth + 1) 

685 child_node.last = last 

686 append(child_node) 

687 else: 

688 node = Node( 

689 value_repr=f"<{class_name}>" if angular else f"{class_name}()", 

690 children=[], 

691 last=root, 

692 ) 

693 pop_visited(obj_id) 

694 elif _is_attr_object(obj) and not fake_attributes: 

695 push_visited(obj_id) 

696 children = [] 

697 append = children.append 

698 

699 attr_fields = _get_attr_fields(obj) 

700 if attr_fields: 

701 if reached_max_depth: 

702 node = Node(value_repr=f"{obj.__class__.__name__}(...)") 

703 else: 

704 node = Node( 

705 open_brace=f"{obj.__class__.__name__}(", 

706 close_brace=")", 

707 children=children, 

708 last=root, 

709 ) 

710 

711 def iter_attrs() -> Iterable[ 

712 Tuple[str, Any, Optional[Callable[[Any], str]]] 

713 ]: 

714 """Iterate over attr fields and values.""" 

715 for attr in attr_fields: 

716 if attr.repr: 

717 try: 

718 value = getattr(obj, attr.name) 

719 except Exception as error: 

720 # Can happen, albeit rarely 

721 yield (attr.name, error, None) 

722 else: 

723 yield ( 

724 attr.name, 

725 value, 

726 attr.repr if callable(attr.repr) else None, 

727 ) 

728 

729 for last, (name, value, repr_callable) in loop_last(iter_attrs()): 

730 if repr_callable: 

731 child_node = Node(value_repr=str(repr_callable(value))) 

732 else: 

733 child_node = _traverse(value, depth=depth + 1) 

734 child_node.last = last 

735 child_node.key_repr = name 

736 child_node.key_separator = "=" 

737 append(child_node) 

738 else: 

739 node = Node( 

740 value_repr=f"{obj.__class__.__name__}()", children=[], last=root 

741 ) 

742 pop_visited(obj_id) 

743 elif ( 

744 is_dataclass(obj) 

745 and not _safe_isinstance(obj, type) 

746 and not fake_attributes 

747 and _is_dataclass_repr(obj) 

748 ): 

749 push_visited(obj_id) 

750 children = [] 

751 append = children.append 

752 if reached_max_depth: 

753 node = Node(value_repr=f"{obj.__class__.__name__}(...)") 

754 else: 

755 node = Node( 

756 open_brace=f"{obj.__class__.__name__}(", 

757 close_brace=")", 

758 children=children, 

759 last=root, 

760 empty=f"{obj.__class__.__name__}()", 

761 ) 

762 

763 for last, field in loop_last( 

764 field for field in fields(obj) if field.repr 

765 ): 

766 child_node = _traverse(getattr(obj, field.name), depth=depth + 1) 

767 child_node.key_repr = field.name 

768 child_node.last = last 

769 child_node.key_separator = "=" 

770 append(child_node) 

771 

772 pop_visited(obj_id) 

773 elif _is_namedtuple(obj) and _has_default_namedtuple_repr(obj): 

774 push_visited(obj_id) 

775 class_name = obj.__class__.__name__ 

776 if reached_max_depth: 

777 # If we've reached the max depth, we still show the class name, but not its contents 

778 node = Node( 

779 value_repr=f"{class_name}(...)", 

780 ) 

781 else: 

782 children = [] 

783 append = children.append 

784 node = Node( 

785 open_brace=f"{class_name}(", 

786 close_brace=")", 

787 children=children, 

788 empty=f"{class_name}()", 

789 ) 

790 for last, (key, value) in loop_last(obj._asdict().items()): 

791 child_node = _traverse(value, depth=depth + 1) 

792 child_node.key_repr = key 

793 child_node.last = last 

794 child_node.key_separator = "=" 

795 append(child_node) 

796 pop_visited(obj_id) 

797 elif _safe_isinstance(obj, _CONTAINERS): 

798 for container_type in _CONTAINERS: 

799 if _safe_isinstance(obj, container_type): 

800 obj_type = container_type 

801 break 

802 

803 push_visited(obj_id) 

804 

805 open_brace, close_brace, empty = _BRACES[obj_type](obj) 

806 

807 if reached_max_depth: 

808 node = Node(value_repr=f"{open_brace}...{close_brace}") 

809 elif obj_type.__repr__ != type(obj).__repr__: 

810 node = Node(value_repr=to_repr(obj), last=root) 

811 elif obj: 

812 children = [] 

813 node = Node( 

814 open_brace=open_brace, 

815 close_brace=close_brace, 

816 children=children, 

817 last=root, 

818 ) 

819 append = children.append 

820 num_items = len(obj) 

821 last_item_index = num_items - 1 

822 

823 if _safe_isinstance(obj, _MAPPING_CONTAINERS): 

824 iter_items = iter(obj.items()) 

825 if max_length is not None: 

826 iter_items = islice(iter_items, max_length) 

827 for index, (key, child) in enumerate(iter_items): 

828 child_node = _traverse(child, depth=depth + 1) 

829 child_node.key_repr = to_repr(key) 

830 child_node.last = index == last_item_index 

831 append(child_node) 

832 else: 

833 iter_values = iter(obj) 

834 if max_length is not None: 

835 iter_values = islice(iter_values, max_length) 

836 for index, child in enumerate(iter_values): 

837 child_node = _traverse(child, depth=depth + 1) 

838 child_node.last = index == last_item_index 

839 append(child_node) 

840 if max_length is not None and num_items > max_length: 

841 append(Node(value_repr=f"... +{num_items - max_length}", last=True)) 

842 else: 

843 node = Node(empty=empty, children=[], last=root) 

844 

845 pop_visited(obj_id) 

846 else: 

847 node = Node(value_repr=to_repr(obj), last=root) 

848 node.is_tuple = _safe_isinstance(obj, tuple) 

849 node.is_namedtuple = _is_namedtuple(obj) 

850 return node 

851 

852 node = _traverse(_object, root=True) 

853 return node 

854 

855 

856def pretty_repr( 

857 _object: Any, 

858 *, 

859 max_width: int = 80, 

860 indent_size: int = 4, 

861 max_length: Optional[int] = None, 

862 max_string: Optional[int] = None, 

863 max_depth: Optional[int] = None, 

864 expand_all: bool = False, 

865) -> str: 

866 """Prettify repr string by expanding on to new lines to fit within a given width. 

867 

868 Args: 

869 _object (Any): Object to repr. 

870 max_width (int, optional): Desired maximum width of repr string. Defaults to 80. 

871 indent_size (int, optional): Number of spaces to indent. Defaults to 4. 

872 max_length (int, optional): Maximum length of containers before abbreviating, or None for no abbreviation. 

873 Defaults to None. 

874 max_string (int, optional): Maximum length of string before truncating, or None to disable truncating. 

875 Defaults to None. 

876 max_depth (int, optional): Maximum depth of nested data structure, or None for no depth. 

877 Defaults to None. 

878 expand_all (bool, optional): Expand all containers regardless of available width. Defaults to False. 

879 

880 Returns: 

881 str: A possibly multi-line representation of the object. 

882 """ 

883 

884 if _safe_isinstance(_object, Node): 

885 node = _object 

886 else: 

887 node = traverse( 

888 _object, max_length=max_length, max_string=max_string, max_depth=max_depth 

889 ) 

890 repr_str: str = node.render( 

891 max_width=max_width, indent_size=indent_size, expand_all=expand_all 

892 ) 

893 return repr_str 

894 

895 

896def pprint( 

897 _object: Any, 

898 *, 

899 console: Optional["Console"] = None, 

900 indent_guides: bool = True, 

901 max_length: Optional[int] = None, 

902 max_string: Optional[int] = None, 

903 max_depth: Optional[int] = None, 

904 expand_all: bool = False, 

905) -> None: 

906 """A convenience function for pretty printing. 

907 

908 Args: 

909 _object (Any): Object to pretty print. 

910 console (Console, optional): Console instance, or None to use default. Defaults to None. 

911 max_length (int, optional): Maximum length of containers before abbreviating, or None for no abbreviation. 

912 Defaults to None. 

913 max_string (int, optional): Maximum length of strings before truncating, or None to disable. Defaults to None. 

914 max_depth (int, optional): Maximum depth for nested data structures, or None for unlimited depth. Defaults to None. 

915 indent_guides (bool, optional): Enable indentation guides. Defaults to True. 

916 expand_all (bool, optional): Expand all containers. Defaults to False. 

917 """ 

918 _console = get_console() if console is None else console 

919 _console.print( 

920 Pretty( 

921 _object, 

922 max_length=max_length, 

923 max_string=max_string, 

924 max_depth=max_depth, 

925 indent_guides=indent_guides, 

926 expand_all=expand_all, 

927 overflow="ignore", 

928 ), 

929 soft_wrap=True, 

930 ) 

931 

932 

933if __name__ == "__main__": # pragma: no cover 

934 

935 class BrokenRepr: 

936 def __repr__(self) -> str: 

937 1 / 0 

938 return "this will fail" 

939 

940 from typing import NamedTuple 

941 

942 class StockKeepingUnit(NamedTuple): 

943 name: str 

944 description: str 

945 price: float 

946 category: str 

947 reviews: List[str] 

948 

949 d = defaultdict(int) 

950 d["foo"] = 5 

951 data = { 

952 "foo": [ 

953 1, 

954 "Hello World!", 

955 100.123, 

956 323.232, 

957 432324.0, 

958 {5, 6, 7, (1, 2, 3, 4), 8}, 

959 ], 

960 "bar": frozenset({1, 2, 3}), 

961 "defaultdict": defaultdict( 

962 list, {"crumble": ["apple", "rhubarb", "butter", "sugar", "flour"]} 

963 ), 

964 "counter": Counter( 

965 [ 

966 "apple", 

967 "orange", 

968 "pear", 

969 "kumquat", 

970 "kumquat", 

971 "durian" * 100, 

972 ] 

973 ), 

974 "atomic": (False, True, None), 

975 "namedtuple": StockKeepingUnit( 

976 "Sparkling British Spring Water", 

977 "Carbonated spring water", 

978 0.9, 

979 "water", 

980 ["its amazing!", "its terrible!"], 

981 ), 

982 "Broken": BrokenRepr(), 

983 } 

984 data["foo"].append(data) # type: ignore[attr-defined] 

985 

986 from rich import print 

987 

988 # print(Pretty(data, indent_guides=True, max_string=20)) 

989 

990 class Thing: 

991 def __repr__(self) -> str: 

992 return "Hello\x1b[38;5;239m World!" 

993 

994 print(Pretty(Thing()))