diff options
Diffstat (limited to 'libs/rich/pretty.py')
-rw-r--r-- | libs/rich/pretty.py | 429 |
1 files changed, 319 insertions, 110 deletions
diff --git a/libs/rich/pretty.py b/libs/rich/pretty.py index 7903b2cc2..57e743df4 100644 --- a/libs/rich/pretty.py +++ b/libs/rich/pretty.py @@ -1,24 +1,36 @@ import builtins +import dataclasses +import inspect import os +import re import sys from array import array -from collections import Counter, defaultdict, deque +from collections import Counter, UserDict, UserList, defaultdict, deque from dataclasses import dataclass, fields, is_dataclass +from inspect import isclass from itertools import islice +from types import MappingProxyType from typing import ( TYPE_CHECKING, Any, Callable, + DefaultDict, Dict, Iterable, List, Optional, Set, - Union, Tuple, + Union, ) -from rich.highlighter import ReprHighlighter +from rich.repr import RichReprResult + +try: + import attr as _attr_module +except ImportError: # pragma: no cover + _attr_module = None # type: ignore + from . import get_console from ._loop import loop_last @@ -41,13 +53,104 @@ if TYPE_CHECKING: ) +def _is_attr_object(obj: Any) -> bool: + """Check if an object was created with attrs module.""" + return _attr_module is not None and _attr_module.has(type(obj)) + + +def _get_attr_fields(obj: Any) -> Iterable["_attr_module.Attribute[Any]"]: + """Get fields for an attrs object.""" + return _attr_module.fields(type(obj)) if _attr_module is not None else [] + + +def _is_dataclass_repr(obj: object) -> bool: + """Check if an instance of a dataclass contains the default repr. + + Args: + obj (object): A dataclass instance. + + Returns: + bool: True if the default repr is used, False if there is a custom repr. + """ + # Digging in to a lot of internals here + # Catching all exceptions in case something is missing on a non CPython implementation + try: + return obj.__repr__.__code__.co_filename == dataclasses.__file__ + except Exception: # pragma: no coverage + return False + + +def _ipy_display_hook( + value: Any, + console: Optional["Console"] = None, + overflow: "OverflowMethod" = "ignore", + crop: bool = False, + indent_guides: bool = False, + max_length: Optional[int] = None, + max_string: Optional[int] = None, + expand_all: bool = False, +) -> None: + from .console import ConsoleRenderable # needed here to prevent circular import + + # always skip rich generated jupyter renderables or None values + if isinstance(value, JupyterRenderable) or value is None: + return + + console = console or get_console() + if console.is_jupyter: + # Delegate rendering to IPython if the object (and IPython) supports it + # https://ipython.readthedocs.io/en/stable/config/integrating.html#rich-display + ipython_repr_methods = [ + "_repr_html_", + "_repr_markdown_", + "_repr_json_", + "_repr_latex_", + "_repr_jpeg_", + "_repr_png_", + "_repr_svg_", + "_repr_mimebundle_", + ] + for repr_method in ipython_repr_methods: + method = getattr(value, repr_method, None) + if inspect.ismethod(method): + # Calling the method ourselves isn't ideal. The interface for the `_repr_*_` methods + # specifies that if they return None, then they should not be rendered + # by the notebook. + try: + repr_result = method() + except Exception: + continue # If the method raises, treat it as if it doesn't exist, try any others + if repr_result is not None: + return # Delegate rendering to IPython + + # certain renderables should start on a new line + if isinstance(value, ConsoleRenderable): + console.line() + + console.print( + value + if isinstance(value, RichRenderable) + else Pretty( + value, + overflow=overflow, + indent_guides=indent_guides, + max_length=max_length, + max_string=max_string, + expand_all=expand_all, + margin=12, + ), + crop=crop, + new_line_start=True, + ) + + def install( - console: "Console" = None, + console: Optional["Console"] = None, overflow: "OverflowMethod" = "ignore", crop: bool = False, indent_guides: bool = False, - max_length: int = None, - max_string: int = None, + max_length: Optional[int] = None, + max_string: Optional[int] = None, expand_all: bool = False, ) -> None: """Install automatic pretty printing in the Python REPL. @@ -60,12 +163,11 @@ def install( max_length (int, optional): Maximum length of containers before abbreviating, or None for no abbreviation. Defaults to None. max_string (int, optional): Maximum length of string before truncating, or None to disable. Defaults to None. - expand_all (bool, optional): Expand all containers. Defaults to False + expand_all (bool, optional): Expand all containers. Defaults to False. + max_frames (int): Maximum number of frames to show in a traceback, 0 for no maximum. Defaults to 100. """ from rich import get_console - from .console import ConsoleRenderable # needed here to prevent circular import - console = console or get_console() assert console is not None @@ -89,44 +191,29 @@ def install( ) builtins._ = value # type: ignore - def ipy_display_hook(value: Any) -> None: # pragma: no cover - assert console is not None - # always skip rich generated jupyter renderables or None values - if isinstance(value, JupyterRenderable) or value is None: - return - # on jupyter rich display, if using one of the special representations dont use rich - if console.is_jupyter and any(attr.startswith("_repr_") for attr in dir(value)): - return - - if hasattr(value, "_repr_mimebundle_"): - return - - # certain renderables should start on a new line - if isinstance(value, ConsoleRenderable): - console.line() - - console.print( - value - if isinstance(value, RichRenderable) - else Pretty( - value, - overflow=overflow, - indent_guides=indent_guides, - max_length=max_length, - max_string=max_string, - expand_all=expand_all, - margin=12, - ), - crop=crop, - ) - try: # pragma: no cover ip = get_ipython() # type: ignore from IPython.core.formatters import BaseFormatter + class RichFormatter(BaseFormatter): # type: ignore + pprint: bool = True + + def __call__(self, value: Any) -> Any: + if self.pprint: + return _ipy_display_hook( + value, + console=get_console(), + overflow=overflow, + indent_guides=indent_guides, + max_length=max_length, + max_string=max_string, + expand_all=expand_all, + ) + else: + return repr(value) + # replace plain text formatter with rich formatter - rich_formatter = BaseFormatter() - rich_formatter.for_type(object, func=ipy_display_hook) + rich_formatter = RichFormatter() ip.display_formatter.formatters["text/plain"] = rich_formatter except Exception: sys.displayhook = display_hook @@ -146,6 +233,7 @@ class Pretty(JupyterMixin): max_length (int, optional): Maximum length of containers before abbreviating, or None for no abbreviation. Defaults to None. max_string (int, optional): Maximum length of string before truncating, or None to disable. Defaults to None. + max_depth (int, optional): Maximum depth of nested data structures, or None for no maximum. Defaults to None. expand_all (bool, optional): Expand all containers. Defaults to False. margin (int, optional): Subtrace a margin from width to force containers to expand earlier. Defaults to 0. insert_line (bool, optional): Insert a new line if the output has multiple new lines. Defaults to False. @@ -154,15 +242,16 @@ class Pretty(JupyterMixin): def __init__( self, _object: Any, - highlighter: "HighlighterType" = None, + highlighter: Optional["HighlighterType"] = None, *, indent_size: int = 4, - justify: "JustifyMethod" = None, + justify: Optional["JustifyMethod"] = None, overflow: Optional["OverflowMethod"] = None, no_wrap: Optional[bool] = False, indent_guides: bool = False, - max_length: int = None, - max_string: int = None, + max_length: Optional[int] = None, + max_string: Optional[int] = None, + max_depth: Optional[int] = None, expand_all: bool = False, margin: int = 0, insert_line: bool = False, @@ -170,12 +259,13 @@ class Pretty(JupyterMixin): self._object = _object self.highlighter = highlighter or ReprHighlighter() self.indent_size = indent_size - self.justify = justify - self.overflow = overflow + self.justify: Optional["JustifyMethod"] = justify + self.overflow: Optional["OverflowMethod"] = overflow self.no_wrap = no_wrap self.indent_guides = indent_guides self.max_length = max_length self.max_string = max_string + self.max_depth = max_depth self.expand_all = expand_all self.margin = margin self.insert_line = insert_line @@ -189,6 +279,7 @@ class Pretty(JupyterMixin): indent_size=self.indent_size, max_length=self.max_length, max_string=self.max_string, + max_depth=self.max_depth, expand_all=self.expand_all, ) pretty_text = Text( @@ -230,7 +321,7 @@ class Pretty(JupyterMixin): return Measurement(text_width, text_width) -def _get_braces_for_defaultdict(_object: defaultdict) -> Tuple[str, str, str]: +def _get_braces_for_defaultdict(_object: DefaultDict[Any, Any]) -> Tuple[str, str, str]: return ( f"defaultdict({_object.default_factory!r}, {{", "})", @@ -238,7 +329,7 @@ def _get_braces_for_defaultdict(_object: defaultdict) -> Tuple[str, str, str]: ) -def _get_braces_for_array(_object: array) -> Tuple[str, str, str]: +def _get_braces_for_array(_object: "array[Any]") -> Tuple[str, str, str]: return (f"array({_object.typecode!r}, [", "])", "array({_object.typecode!r})") @@ -249,22 +340,26 @@ _BRACES: Dict[type, Callable[[Any], Tuple[str, str, str]]] = { Counter: lambda _object: ("Counter({", "})", "Counter()"), deque: lambda _object: ("deque([", "])", "deque()"), dict: lambda _object: ("{", "}", "{}"), + UserDict: lambda _object: ("{", "}", "{}"), frozenset: lambda _object: ("frozenset({", "})", "frozenset()"), list: lambda _object: ("[", "]", "[]"), + UserList: lambda _object: ("[", "]", "[]"), set: lambda _object: ("{", "}", "set()"), tuple: lambda _object: ("(", ")", "()"), + MappingProxyType: lambda _object: ("mappingproxy({", "})", "mappingproxy({})"), } _CONTAINERS = tuple(_BRACES.keys()) -_MAPPING_CONTAINERS = (dict, os._Environ) +_MAPPING_CONTAINERS = (dict, os._Environ, MappingProxyType, UserDict) def is_expandable(obj: Any) -> bool: """Check if an object may be expanded by pretty print.""" return ( isinstance(obj, _CONTAINERS) - or (is_dataclass(obj) and not isinstance(obj, type)) - or hasattr(obj, "__rich_repr__") - ) + or (is_dataclass(obj)) + or (hasattr(obj, "__rich_repr__")) + or _is_attr_object(obj) + ) and not isclass(obj) @dataclass @@ -280,11 +375,7 @@ class Node: is_tuple: bool = False children: Optional[List["Node"]] = None key_separator = ": " - - @property - def separator(self) -> str: - """Get separator between items.""" - return "" if self.last else "," + separator: str = ", " def iter_tokens(self) -> Iterable[str]: """Generate tokens for this node.""" @@ -303,7 +394,7 @@ class Node: for child in self.children: yield from child.iter_tokens() if not child.last: - yield ", " + yield self.separator yield self.close_brace else: yield self.empty @@ -359,12 +450,14 @@ class Node: class _Line: """A line in repr output.""" + parent: Optional["_Line"] = None is_root: bool = False node: Optional[Node] = None text: str = "" suffix: str = "" whitespace: str = "" expanded: bool = False + last: bool = False @property def expandable(self) -> bool: @@ -386,34 +479,47 @@ class _Line: whitespace = self.whitespace assert node.children if node.key_repr: - yield _Line( + new_line = yield _Line( text=f"{node.key_repr}{node.key_separator}{node.open_brace}", whitespace=whitespace, ) else: - yield _Line(text=node.open_brace, whitespace=whitespace) + new_line = yield _Line(text=node.open_brace, whitespace=whitespace) child_whitespace = self.whitespace + " " * indent_size tuple_of_one = node.is_tuple and len(node.children) == 1 - for child in node.children: - separator = "," if tuple_of_one else child.separator + for last, child in loop_last(node.children): + separator = "," if tuple_of_one else node.separator line = _Line( + parent=new_line, node=child, whitespace=child_whitespace, suffix=separator, + last=last and not tuple_of_one, ) yield line yield _Line( text=node.close_brace, whitespace=whitespace, - suffix="," if (tuple_of_one and not self.is_root) else node.separator, + suffix=self.suffix, + last=self.last, ) def __str__(self) -> str: - return f"{self.whitespace}{self.text}{self.node or ''}{self.suffix}" + if self.last: + return f"{self.whitespace}{self.text}{self.node or ''}" + else: + return ( + f"{self.whitespace}{self.text}{self.node or ''}{self.suffix.rstrip()}" + ) -def traverse(_object: Any, max_length: int = None, max_string: int = None) -> Node: +def traverse( + _object: Any, + max_length: Optional[int] = None, + max_string: Optional[int] = None, + max_depth: Optional[int] = None, +) -> Node: """Traverse object and generate a tree. Args: @@ -422,6 +528,8 @@ def traverse(_object: Any, max_length: int = None, max_string: int = None) -> No Defaults to None. max_string (int, optional): Maximum length of string before truncating, or None to disable truncating. Defaults to None. + max_depth (int, optional): Maximum depth of data structures, or None for no maximum. + Defaults to None. Returns: Node: The root of a tree structure which can be used to render a pretty repr. @@ -440,20 +548,22 @@ def traverse(_object: Any, max_length: int = None, max_string: int = None) -> No try: obj_repr = repr(obj) except Exception as error: - obj_repr = f"<repr-error '{error}'>" + obj_repr = f"<repr-error {str(error)!r}>" return obj_repr visited_ids: Set[int] = set() push_visited = visited_ids.add pop_visited = visited_ids.remove - def _traverse(obj: Any, root: bool = False) -> Node: + def _traverse(obj: Any, root: bool = False, depth: int = 0) -> Node: """Walk the object depth first.""" + obj_type = type(obj) py_version = (sys.version_info.major, sys.version_info.minor) children: List[Node] + reached_max_depth = max_depth is not None and depth >= max_depth - def iter_rich_args(rich_args) -> Iterable[Union[Any, Tuple[str, Any]]]: + def iter_rich_args(rich_args: Any) -> Iterable[Union[Any, Tuple[str, Any]]]: for arg in rich_args: if isinstance(arg, tuple): if len(arg) == 3: @@ -469,31 +579,109 @@ def traverse(_object: Any, max_length: int = None, max_string: int = None) -> No else: yield arg - if hasattr(obj, "__rich_repr__"): - args = list(iter_rich_args(obj.__rich_repr__())) + try: + fake_attributes = hasattr( + obj, "awehoi234_wdfjwljet234_234wdfoijsdfmmnxpi492" + ) + except Exception: + fake_attributes = False + + rich_repr_result: Optional[RichReprResult] = None + if not fake_attributes: + try: + if hasattr(obj, "__rich_repr__") and not isclass(obj): + rich_repr_result = obj.__rich_repr__() + except Exception: + pass + + if rich_repr_result is not None: + angular = getattr(obj.__rich_repr__, "angular", False) + args = list(iter_rich_args(rich_repr_result)) + class_name = obj.__class__.__name__ if args: children = [] append = children.append + + if reached_max_depth: + node = Node(value_repr=f"...") + else: + if angular: + node = Node( + open_brace=f"<{class_name} ", + close_brace=">", + children=children, + last=root, + separator=" ", + ) + else: + node = Node( + open_brace=f"{class_name}(", + close_brace=")", + children=children, + last=root, + ) + for last, arg in loop_last(args): + if isinstance(arg, tuple): + key, child = arg + child_node = _traverse(child, depth=depth + 1) + child_node.last = last + child_node.key_repr = key + child_node.key_separator = "=" + append(child_node) + else: + child_node = _traverse(arg, depth=depth + 1) + child_node.last = last + append(child_node) + else: node = Node( - open_brace=f"{obj.__class__.__name__}(", - close_brace=")", - children=children, + value_repr=f"<{class_name}>" if angular else f"{class_name}()", + children=[], last=root, ) - for last, arg in loop_last(args): - if isinstance(arg, tuple): - key, child = arg - child_node = _traverse(child) - child_node.last = last - child_node.key_repr = key + elif _is_attr_object(obj) and not fake_attributes: + children = [] + append = children.append + + attr_fields = _get_attr_fields(obj) + if attr_fields: + if reached_max_depth: + node = Node(value_repr=f"...") + else: + node = Node( + open_brace=f"{obj.__class__.__name__}(", + close_brace=")", + children=children, + last=root, + ) + + def iter_attrs() -> Iterable[ + Tuple[str, Any, Optional[Callable[[Any], str]]] + ]: + """Iterate over attr fields and values.""" + for attr in attr_fields: + if attr.repr: + try: + value = getattr(obj, attr.name) + except Exception as error: + # Can happen, albeit rarely + yield (attr.name, error, None) + else: + yield ( + attr.name, + value, + attr.repr if callable(attr.repr) else None, + ) + + for last, (name, value, repr_callable) in loop_last(iter_attrs()): + if repr_callable: + child_node = Node(value_repr=str(repr_callable(value))) + else: + child_node = _traverse(value, depth=depth + 1) child_node.last = last + child_node.key_repr = name child_node.key_separator = "=" append(child_node) - else: - child_node = _traverse(arg) - child_node.last = last - append(child_node) else: node = Node( value_repr=f"{obj.__class__.__name__}()", children=[], last=root @@ -502,9 +690,8 @@ def traverse(_object: Any, max_length: int = None, max_string: int = None) -> No elif ( is_dataclass(obj) and not isinstance(obj, type) - and ( - "__create_fn__" in obj.__repr__.__qualname__ or py_version == (3, 6) - ) # Check if __repr__ wasn't overriden + and not fake_attributes + and (_is_dataclass_repr(obj) or py_version == (3, 6)) ): obj_id = id(obj) if obj_id in visited_ids: @@ -514,24 +701,33 @@ def traverse(_object: Any, max_length: int = None, max_string: int = None) -> No children = [] append = children.append - node = Node( - open_brace=f"{obj.__class__.__name__}(", - close_brace=")", - children=children, - last=root, - ) + if reached_max_depth: + node = Node(value_repr=f"...") + else: + node = Node( + open_brace=f"{obj.__class__.__name__}(", + close_brace=")", + children=children, + last=root, + ) - for last, field in loop_last(fields(obj)): - if field.repr: - child_node = _traverse(getattr(obj, field.name)) + for last, field in loop_last( + field for field in fields(obj) if field.repr + ): + child_node = _traverse(getattr(obj, field.name), depth=depth + 1) child_node.key_repr = field.name child_node.last = last child_node.key_separator = "=" append(child_node) - pop_visited(obj_id) + pop_visited(obj_id) + + elif isinstance(obj, _CONTAINERS): + for container_type in _CONTAINERS: + if isinstance(obj, container_type): + obj_type = container_type + break - elif obj_type in _CONTAINERS: obj_id = id(obj) if obj_id in visited_ids: # Recursion detected @@ -540,7 +736,11 @@ def traverse(_object: Any, max_length: int = None, max_string: int = None) -> No open_brace, close_brace, empty = _BRACES[obj_type](obj) - if obj: + if reached_max_depth: + node = Node(value_repr=f"...", last=root) + elif obj_type.__repr__ != type(obj).__repr__: + node = Node(value_repr=to_repr(obj), last=root) + elif obj: children = [] node = Node( open_brace=open_brace, @@ -557,7 +757,7 @@ def traverse(_object: Any, max_length: int = None, max_string: int = None) -> No if max_length is not None: iter_items = islice(iter_items, max_length) for index, (key, child) in enumerate(iter_items): - child_node = _traverse(child) + child_node = _traverse(child, depth=depth + 1) child_node.key_repr = to_repr(key) child_node.last = index == last_item_index append(child_node) @@ -566,7 +766,7 @@ def traverse(_object: Any, max_length: int = None, max_string: int = None) -> No if max_length is not None: iter_values = islice(iter_values, max_length) for index, child in enumerate(iter_values): - child_node = _traverse(child) + child_node = _traverse(child, depth=depth + 1) child_node.last = index == last_item_index append(child_node) if max_length is not None and num_items > max_length: @@ -589,8 +789,9 @@ def pretty_repr( *, max_width: int = 80, indent_size: int = 4, - max_length: int = None, - max_string: int = None, + max_length: Optional[int] = None, + max_string: Optional[int] = None, + max_depth: Optional[int] = None, expand_all: bool = False, ) -> str: """Prettify repr string by expanding on to new lines to fit within a given width. @@ -603,6 +804,8 @@ def pretty_repr( Defaults to None. max_string (int, optional): Maximum length of string before truncating, or None to disable truncating. Defaults to None. + max_depth (int, optional): Maximum depth of nested data structure, or None for no depth. + Defaults to None. expand_all (bool, optional): Expand all containers regardless of available width. Defaults to False. Returns: @@ -612,7 +815,9 @@ def pretty_repr( if isinstance(_object, Node): node = _object else: - node = traverse(_object, max_length=max_length, max_string=max_string) + node = traverse( + _object, max_length=max_length, max_string=max_string, max_depth=max_depth + ) repr_str = node.render( max_width=max_width, indent_size=indent_size, expand_all=expand_all ) @@ -622,12 +827,13 @@ def pretty_repr( def pprint( _object: Any, *, - console: "Console" = None, + console: Optional["Console"] = None, indent_guides: bool = True, - max_length: int = None, - max_string: int = None, + max_length: Optional[int] = None, + max_string: Optional[int] = None, + max_depth: Optional[int] = None, expand_all: bool = False, -): +) -> None: """A convenience function for pretty printing. Args: @@ -636,6 +842,7 @@ def pprint( max_length (int, optional): Maximum length of containers before abbreviating, or None for no abbreviation. Defaults to None. max_string (int, optional): Maximum length of strings before truncating, or None to disable. Defaults to None. + max_depth (int, optional): Maximum depth for nested data structures, or None for unlimited depth. Defaults to None. indent_guides (bool, optional): Enable indentation guides. Defaults to True. expand_all (bool, optional): Expand all containers. Defaults to False. """ @@ -645,6 +852,7 @@ def pprint( _object, max_length=max_length, max_string=max_string, + max_depth=max_depth, indent_guides=indent_guides, expand_all=expand_all, overflow="ignore", @@ -656,8 +864,9 @@ def pprint( if __name__ == "__main__": # pragma: no cover class BrokenRepr: - def __repr__(self): + def __repr__(self) -> str: 1 / 0 + return "this will fail" d = defaultdict(int) d["foo"] = 5 |