Skip to content

Commit

Permalink
fix: improve rich repr support
Browse files Browse the repository at this point in the history
  • Loading branch information
jcrist authored and cpcloud committed Oct 5, 2022
1 parent 2fd33cd commit 522db9c
Show file tree
Hide file tree
Showing 8 changed files with 554 additions and 203 deletions.
52 changes: 41 additions & 11 deletions ibis/backends/tests/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -756,18 +756,48 @@ def test_dunder_array_column(alltypes, df):
np.testing.assert_array_equal(result, expected)


def test_repr_html(alltypes):
t = alltypes.limit(5)
@pytest.mark.parametrize("interactive", [True, False])
def test_repr(alltypes, interactive):
expr = alltypes.select("id", "int_col")

assert t._repr_html_() is None
assert t.int_col._repr_html_() is None
assert t.int_col.sum()._repr_html_() is None
val = str(alltypes.limit(5).id.execute().iloc[0])

interactive = ibis.options.interactive
ibis.options.interactive = True
old = ibis.options.interactive
ibis.options.interactive = interactive
try:
assert 'table' in t._repr_html_()
assert 'table' in t.int_col._repr_html_()
assert t.int_col.sum()._repr_html_() is None
s = repr(expr)
# no control characters
assert all(c.isprintable() or c in "\n\r\t" for c in s)
assert "id" in s
if interactive:
assert val in s
else:
assert val not in s
finally:
ibis.options.interactive = interactive
ibis.options.interactive = old


@pytest.mark.parametrize("expr_type", ["table", "column"])
@pytest.mark.parametrize("interactive", [True, False])
def test_repr_mimebundle(alltypes, interactive, expr_type):
if expr_type == "column":
expr = alltypes.id
else:
expr = alltypes.select("id", "int_col")

val = str(alltypes.limit(5).id.execute().iloc[0])

old = ibis.options.interactive
ibis.options.interactive = interactive
try:
reprs = expr._repr_mimebundle_(
include=["text/plain", "text/html"], exclude=[]
)
for format in ["text/plain", "text/html"]:
assert "id" in reprs[format]
if interactive:
assert val in reprs[format]
else:
assert val not in reprs[format]
finally:
ibis.options.interactive = old
78 changes: 0 additions & 78 deletions ibis/common/pretty.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,11 @@
from __future__ import annotations

import datetime
import decimal
from typing import IO

import rich
from rich.console import Console

import ibis
import ibis.common.exceptions as com
import ibis.expr.datatypes as dt
import ibis.expr.types as ir

console = Console()

_IBIS_TO_SQLGLOT_NAME_MAP = {
# not 100% accurate, but very close
"impala": "hive",
Expand Down Expand Up @@ -114,73 +106,3 @@ def to_sql(expr: ir.Expr, dialect: str | None = None) -> str:
pretty=True,
)
return pretty


def _pretty_value(v, dtype: dt.DataType):
if isinstance(v, str):
return v

if isinstance(v, decimal.Decimal):
return f"[bold][light_salmon1]{v}[/][/]"

if isinstance(v, datetime.datetime):
if isinstance(dtype, dt.Date):
return f"[magenta]{v.date()}[/]"

fmt = v.isoformat(timespec='milliseconds').replace("T", " ")
return f"[magenta]{fmt}[/]"

if isinstance(v, datetime.timedelta):
return f"[magenta]{v}[/]"

interactive = ibis.options.repr.interactive
return rich.pretty.Pretty(
v,
max_length=interactive.max_length,
max_string=interactive.max_string,
max_depth=interactive.max_depth,
)


def _format_value(v) -> str:
if v is None:
# render NULL values as the empty set
return "[dim][yellow]∅[/][/]"

if isinstance(v, str):
if not v:
return "[dim][yellow]~[/][/]"

v = (
# replace spaces with dots
v.replace(" ", "[dim]·[/]")
# tab
.replace("\t", r"[dim]\t[/]")
# carriage return
.replace("\r", r"[dim]\r[/]")
# line feed
.replace("\n", r"[dim]\n[/]")
# vertical tab
.replace("\v", r"[dim]\v[/]")
# form feed (page break)
.replace("\f", r"[dim]\f[/]")
)
# display all unprintable characters as a dimmed version of their repr
return "".join(
f"[dim]{repr(c)[1:-1]}[/]" if not c.isprintable() else c for c in v
)
return v


def _format_dtype(dtype):
strtyp = str(dtype)
max_string = ibis.options.repr.interactive.max_string
return (
("[bold][dark_orange]![/][/]" * (not dtype.nullable))
+ "[bold][blue]"
+ (
strtyp[(not dtype.nullable) : max_string]
+ "[orange1]…[/]" * (len(strtyp) > max_string)
)
+ "[/][/]"
)
4 changes: 2 additions & 2 deletions ibis/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,9 +89,9 @@ class Interactive(Config):
"""

max_rows: int = 10
max_length: int = 5
max_length: int = 2
max_string: int = 80
max_depth: int = 2
max_depth: int = 1
show_types: bool = True


Expand Down
7 changes: 4 additions & 3 deletions ibis/expr/types/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
import ibis.expr.operations as ops
from ibis.common.exceptions import IbisError, IbisTypeError, TranslationError
from ibis.common.grounds import Immutable
from ibis.common.pretty import console
from ibis.config import _default_backend, options
from ibis.expr.typing import TimeContext
from ibis.util import UnnamedMarker
Expand All @@ -32,12 +31,14 @@ def __init__(self, arg: ops.Node) -> None:
object.__setattr__(self, "_arg", arg)

def __repr__(self) -> str:
from ibis.expr.types.pretty import simple_console

if not options.interactive:
return self._repr()

with console.capture() as capture:
with simple_console.capture() as capture:
try:
console.print(self)
simple_console.print(self)
except TranslationError as e:
lines = [
"Translation to backend failed",
Expand Down
20 changes: 6 additions & 14 deletions ibis/expr/types/generic.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@
import ibis.expr.window as win

from public import public
from rich.jupyter import JupyterMixin

import ibis
import ibis.common.exceptions as com
import ibis.expr.datatypes as dt
import ibis.expr.operations as ops
from ibis.common.pretty import _format_value, _pretty_value
from ibis.expr.types.core import Expr, _binop


Expand Down Expand Up @@ -500,17 +500,15 @@ def to_projection(self) -> ir.Table:
@public
class Scalar(Value):
def __rich_console__(self, console, options):
return console.render(
_pretty_value(_format_value(self.execute()), self.type()),
options=options,
)
from rich.text import Text

def _repr_html_(self) -> str | None:
return None
if not ibis.options.interactive:
return console.render(Text(self._repr()), options=options)
return console.render(repr(self.execute()), options=options)


@public
class Column(Value):
class Column(Value, JupyterMixin):
# Higher than numpy & dask objects
__array_priority__ = 20

Expand All @@ -519,12 +517,6 @@ class Column(Value):
def __array__(self):
return self.execute().__array__()

def _repr_html_(self) -> str | None:
if not ibis.options.interactive:
return None

return self.execute().to_frame()._repr_html_()

def __rich_console__(self, console, options):
named = self.name(self.op().name)
projection = named.to_projection()
Expand Down
Loading

0 comments on commit 522db9c

Please sign in to comment.