Skip to content

Commit

Permalink
fix(interactive-repr): ensure that null scalars can be repr'd interac…
Browse files Browse the repository at this point in the history
…tively (#10784)

Fixes #10780.
  • Loading branch information
cpcloud authored Feb 5, 2025
1 parent ec41d4c commit 5f1c75a
Show file tree
Hide file tree
Showing 4 changed files with 86 additions and 19 deletions.
59 changes: 59 additions & 0 deletions ibis/backends/duckdb/tests/test_datatypes.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
from __future__ import annotations

import string

import hypothesis as h
import hypothesis.strategies as st
import numpy as np
import pytest
from pytest import param

import ibis
import ibis.expr.datatypes as dt
import ibis.tests.strategies as its
from ibis.backends.sql.datatypes import DuckDBType


Expand Down Expand Up @@ -115,3 +120,57 @@ def test_cast_to_floating_point_type(con, snapshot, typ):

sql = str(ibis.to_sql(expr, dialect="duckdb"))
snapshot.assert_match(sql, "out.sql")


def null_literal(array_min_size: int = 0, array_max_size: int | None = None):
true = st.just(True)

field_names = st.text(alphabet=string.ascii_lowercase + string.digits, min_size=1)
num_fields = st.integers(min_value=1, max_value=20)

recursive = st.deferred(
lambda: (
its.primitive_dtypes(nullable=true, include_null=False)
| its.string_dtype(nullable=true)
| its.binary_dtype(nullable=true)
| st.builds(dt.JSON, binary=st.just(False), nullable=true)
| its.macaddr_dtype(nullable=true)
| its.uuid_dtype(nullable=true)
| its.temporal_dtypes(nullable=true)
| its.array_dtypes(recursive, nullable=true, length=st.none())
| its.map_dtypes(recursive, recursive, nullable=true)
| its.struct_dtypes(
recursive, nullable=true, names=field_names, num_fields=num_fields
)
)
)

return st.builds(
ibis.null,
its.primitive_dtypes(nullable=true, include_null=False)
| its.string_dtype(nullable=true)
| its.binary_dtype(nullable=true)
| st.builds(dt.JSON, binary=st.just(False), nullable=true)
| its.macaddr_dtype(nullable=true)
| its.uuid_dtype(nullable=true)
| its.temporal_dtypes(nullable=true)
| its.array_dtypes(recursive, nullable=true, length=st.none())
| its.map_dtypes(recursive, recursive, nullable=true)
| its.struct_dtypes(
recursive, nullable=true, names=field_names, num_fields=num_fields
),
)


@h.given(lit=null_literal(array_min_size=1, array_max_size=8192))
@h.settings(suppress_health_check=[h.HealthCheck.function_scoped_fixture])
def test_null_scalar(con, lit, monkeypatch):
monkeypatch.setattr(ibis.options, "default_backend", con)
monkeypatch.setattr(ibis.options, "interactive", True)

result = repr(lit)
expected = """\
┌──────┐
│ NULL │
└──────┘"""
assert result == expected
2 changes: 1 addition & 1 deletion ibis/expr/types/generic.py
Original file line number Diff line number Diff line change
Expand Up @@ -2895,7 +2895,7 @@ def null(type: dt.DataType | str | None = None, /) -> Value:
AttributeError: 'NullScalar' object has no attribute 'upper'
>>> ibis.null(str).upper()
┌──────┐
None
NULL
└──────┘
>>> ibis.null(str).upper().isnull()
┌──────┐
Expand Down
22 changes: 14 additions & 8 deletions ibis/expr/types/pretty.py
Original file line number Diff line number Diff line change
Expand Up @@ -294,14 +294,20 @@ def _to_rich_scalar(
max_string: int | None = None,
max_depth: int | None = None,
) -> Pretty:
value = format_values(
expr.type(),
[expr.to_pyarrow().as_py()],
max_length=max_length or ibis.options.repr.interactive.max_length,
max_string=max_string or ibis.options.repr.interactive.max_string,
max_depth=max_depth or ibis.options.repr.interactive.max_depth,
)[0]
return Panel(value, expand=False, box=box.SQUARE)
value = expr.to_pyarrow().as_py()

if value is None:
formatted_value = Text.styled("NULL", style="dim")
else:
(formatted_value,) = format_values(
expr.type(),
[value],
max_length=max_length or ibis.options.repr.interactive.max_length,
max_string=max_string or ibis.options.repr.interactive.max_string,
max_depth=max_depth or ibis.options.repr.interactive.max_depth,
)

return Panel(formatted_value, expand=False, box=box.SQUARE)


def _to_rich_table(
Expand Down
22 changes: 12 additions & 10 deletions ibis/tests/strategies.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,23 +128,25 @@ def interval_dtype(interval=_interval, nullable=_nullable):
return st.builds(dt.Interval, unit=interval, nullable=nullable)


def temporal_dtypes(timezone=_timezone, interval=_interval, nullable=_nullable):
def temporal_dtypes(timezone=_timezone, nullable=_nullable):
return st.one_of(
date_dtype(nullable=nullable),
time_dtype(nullable=nullable),
timestamp_dtype(timezone=timezone, nullable=nullable),
)


def primitive_dtypes(nullable=_nullable):
return st.one_of(
null_dtype,
boolean_dtype(nullable=nullable),
integer_dtypes(nullable=nullable),
floating_dtypes(nullable=nullable),
date_dtype(nullable=nullable),
time_dtype(nullable=nullable),
def primitive_dtypes(nullable=_nullable, include_null=True):
primitive = (
boolean_dtype(nullable=nullable)
| integer_dtypes(nullable=nullable)
| floating_dtypes(nullable=nullable)
| date_dtype(nullable=nullable)
| time_dtype(nullable=nullable)
)
if include_null:
primitive |= null_dtype
return primitive


_item_strategy = primitive_dtypes()
Expand Down Expand Up @@ -174,7 +176,7 @@ def struct_dtypes(
nullable=_nullable,
):
num_fields = draw(num_fields)
names = draw(st.lists(names, min_size=num_fields, max_size=num_fields))
names = draw(st.lists(names, min_size=num_fields, max_size=num_fields, unique=True))
types = draw(st.lists(types, min_size=num_fields, max_size=num_fields))
fields = dict(zip(names, types))
return dt.Struct(fields, nullable=draw(nullable))
Expand Down

0 comments on commit 5f1c75a

Please sign in to comment.