aboutsummaryrefslogtreecommitdiffhomepage
path: root/libs/rich/pretty.py
diff options
context:
space:
mode:
Diffstat (limited to 'libs/rich/pretty.py')
-rw-r--r--libs/rich/pretty.py429
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