python (3.11.7)
       1  import builtins
       2  import collections
       3  import dataclasses
       4  import inspect
       5  import os
       6  import sys
       7  from array import array
       8  from collections import Counter, UserDict, UserList, defaultdict, deque
       9  from dataclasses import dataclass, fields, is_dataclass
      10  from inspect import isclass
      11  from itertools import islice
      12  from types import MappingProxyType
      13  from 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  
      28  from pip._vendor.rich.repr import RichReprResult
      29  
      30  try:
      31      import attr as _attr_module
      32  
      33      _has_attrs = hasattr(_attr_module, "ib")
      34  except ImportError:  # pragma: no cover
      35      _has_attrs = False
      36  
      37  from . import get_console
      38  from ._loop import loop_last
      39  from ._pick import pick_bool
      40  from .abc import RichRenderable
      41  from .cells import cell_len
      42  from .highlighter import ReprHighlighter
      43  from .jupyter import JupyterMixin, JupyterRenderable
      44  from .measure import Measurement
      45  from .text import Text
      46  
      47  if TYPE_CHECKING:
      48      from .console import (
      49          Console,
      50          ConsoleOptions,
      51          HighlighterType,
      52          JustifyMethod,
      53          OverflowMethod,
      54          RenderResult,
      55      )
      56  
      57  
      58  def _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  
      63  def _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  
      68  def _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  
      88  def _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  
     108  def _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  
     154  def _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  
     164  def 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 pip._vendor.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 ESC[4;38;5;81mRichFormatter(ESC[4;38;5;149mBaseFormatter):  # 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  
     243  class ESC[4;38;5;81mPretty(ESC[4;38;5;149mJupyterMixin):
     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  
     347  def _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  
     355  def _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  
     378  def 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
     389  class ESC[4;38;5;81mNode:
     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
     474  class ESC[4;38;5;81m_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  
     541  def _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  
     560  def 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  
     856  def 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  
     896  def 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  
     933  if __name__ == "__main__":  # pragma: no cover
     934  
     935      class ESC[4;38;5;81mBrokenRepr:
     936          def __repr__(self) -> str:
     937              1 / 0
     938              return "this will fail"
     939  
     940      from typing import NamedTuple
     941  
     942      class ESC[4;38;5;81mStockKeepingUnit(ESC[4;38;5;149mNamedTuple):
     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 pip._vendor.rich import print
     987  
     988      # print(Pretty(data, indent_guides=True, max_string=20))
     989  
     990      class ESC[4;38;5;81mThing:
     991          def __repr__(self) -> str:
     992              return "Hello\x1b[38;5;239m World!"
     993  
     994      print(Pretty(Thing()))