diff --git a/rich/pretty.py b/rich/pretty.py index 4231b314..a866f184 100644 --- a/rich/pretty.py +++ b/rich/pretty.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import builtins import inspect import os @@ -52,9 +54,6 @@ if TYPE_CHECKING: RenderResult, ) -# Matches Jupyter's special methods -_re_jupyter_repr = re.compile(f"^_repr_.+_$") - def _is_attr_object(obj: Any) -> bool: """Check if an object was created with attrs module.""" @@ -83,6 +82,70 @@ def _is_dataclass_repr(obj: object) -> bool: return False +def _ipy_display_hook( + value: Any, + console: Console | None = 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: Optional["Console"] = None, overflow: "OverflowMethod" = "ignore", @@ -107,8 +170,6 @@ def install( """ 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 @@ -132,59 +193,6 @@ 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 don't use rich - - 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, - ) - try: # pragma: no cover ip = get_ipython() # type: ignore from IPython.core.formatters import BaseFormatter @@ -194,7 +202,15 @@ def install( def __call__(self, value: Any) -> Any: if self.pprint: - return ipy_display_hook(value) + 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) diff --git a/tests/test_pretty.py b/tests/test_pretty.py index ab83d930..954721f7 100644 --- a/tests/test_pretty.py +++ b/tests/test_pretty.py @@ -1,16 +1,16 @@ -from array import array -from collections import defaultdict, UserDict, UserList -from dataclasses import dataclass, field import io import sys +from array import array +from collections import defaultdict, UserDict from typing import List import attr import pytest +from dataclasses import dataclass, field from rich.console import Console -from rich.pretty import install, Pretty, pprint, pretty_repr, Node - +from rich.pretty import install, Pretty, pprint, pretty_repr, Node, _ipy_display_hook +from rich.text import Text skip_py36 = pytest.mark.skipif( sys.version_info.minor == 6 and sys.version_info.major == 3, @@ -44,6 +44,85 @@ def test_install(): assert sys.displayhook is not dh +def test_ipy_display_hook__repr_html(): + console = Console(file=io.StringIO(), force_jupyter=True) + + class Thing: + def _repr_html_(self): + return "hello" + + console.begin_capture() + _ipy_display_hook(Thing(), console=console) + + # Rendering delegated to notebook because _repr_html_ method exists + assert console.end_capture() == "" + + +def test_ipy_display_hook__multiple_special_reprs(): + """ + The case where there are multiple IPython special _repr_*_ + methods on the object, and one of them returns None but another + one does not. + """ + console = Console(file=io.StringIO(), force_jupyter=True) + + class Thing: + def _repr_latex_(self): + return None + + def _repr_html_(self): + return "hello" + + console.begin_capture() + _ipy_display_hook(Thing(), console=console) + + assert console.end_capture() == "" + + +def test_ipy_display_hook__no_special_repr_methods(): + console = Console(file=io.StringIO(), force_jupyter=True) + + class Thing: + def __repr__(self) -> str: + return "hello" + + console.begin_capture() + _ipy_display_hook(Thing(), console=console) + + # No IPython special repr methods, so printed by Rich + assert console.end_capture() == "hello\n" + + +def test_ipy_display_hook__special_repr_raises_exception(): + """ + When an IPython special repr method raises an exception, + we treat it as if it doesn't exist and look for the next. + """ + console = Console(file=io.StringIO(), force_jupyter=True) + + class Thing: + def _repr_markdown_(self): + raise Exception() + + def _repr_latex_(self): + return None + + def _repr_html_(self): + return "hello" + + console.begin_capture() + _ipy_display_hook(Thing(), console=console) + + assert console.end_capture() == "" + + +def test_ipy_display_hook__console_renderables_on_newline(): + console = Console(file=io.StringIO(), force_jupyter=True) + console.begin_capture() + _ipy_display_hook(Text("hello"), console=console) + assert console.end_capture() == "\nhello\n" + + def test_pretty(): test = { "foo": [1, 2, 3, (4, 5, {6}, 7, 8, {9}), {}], @@ -55,7 +134,6 @@ def test_pretty(): result = pretty_repr(test, max_width=80) print(result) - # print(repr(result)) expected = "{\n 'foo': [1, 2, 3, (4, 5, {6}, 7, 8, {9}), {}],\n 'bar': {\n 'egg': 'baz',\n 'words': [\n 'Hello World',\n 'Hello World',\n 'Hello World',\n 'Hello World',\n 'Hello World',\n 'Hello World',\n 'Hello World',\n 'Hello World',\n 'Hello World',\n 'Hello World'\n ]\n },\n False: 'foo',\n True: '',\n 'text': ('Hello World', 'foo bar baz egg')\n}" print(expected) assert result == expected