Extract ipy display hook and add tests around it

This commit is contained in:
Darren Burns 2022-01-06 14:17:34 +00:00
parent 23bc0a2d7c
commit a3fcdbe210
2 changed files with 159 additions and 65 deletions

View File

@ -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)

View File

@ -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