Skip to content

Commit

Permalink
feat(api): support type arg to ibis.null()
Browse files Browse the repository at this point in the history
  • Loading branch information
NickCrews committed May 6, 2024
1 parent 711bf9f commit 8db686e
Show file tree
Hide file tree
Showing 4 changed files with 88 additions and 13 deletions.
24 changes: 20 additions & 4 deletions ibis/backends/pandas/kernels.py
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,8 @@ def arbitrary(arg):
ops.ArrayCollect: lambda x: x.tolist(),
}

generic = {

_generic = {
ops.Abs: abs,
ops.Acos: np.arccos,
ops.Add: operator.add,
Expand Down Expand Up @@ -315,8 +316,6 @@ def arbitrary(arg):
ops.IntervalFloorDivide: operator.floordiv,
ops.IntervalMultiply: operator.mul,
ops.IntervalSubtract: operator.sub,
ops.IsInf: np.isinf,
ops.IsNull: pd.isnull,
ops.Less: operator.lt,
ops.LessEqual: operator.le,
ops.Ln: np.log,
Expand All @@ -327,7 +326,6 @@ def arbitrary(arg):
ops.Negate: lambda x: not x if isinstance(x, (bool, np.bool_)) else -x,
ops.Not: lambda x: not x if isinstance(x, (bool, np.bool_)) else ~x,
ops.NotEquals: operator.ne,
ops.NotNull: pd.notnull,
ops.Or: operator.or_,
ops.Power: operator.pow,
ops.Radians: np.radians,
Expand All @@ -349,6 +347,24 @@ def arbitrary(arg):
ops.Log: lambda x, base: np.log(x) if base is None else np.log(x) / np.log(base),
}


def none_proof(func):
def wrapper(*args, **kwargs):
if any(map(isnull, args)):
return None
return func(*args, **kwargs)

return wrapper


generic = {
**{k: none_proof(v) for k, v in _generic.items()},
ops.IsNull: pd.isnull,
ops.NotNull: pd.notnull,
ops.IsInf: np.isinf,
}


columnwise = {
ops.Clip: lambda df: df["arg"].clip(lower=df["lower"], upper=df["upper"]),
ops.IfElse: lambda df: df["true_expr"].where(
Expand Down
39 changes: 35 additions & 4 deletions ibis/backends/tests/test_generic.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,13 +53,32 @@
@pytest.mark.notyet(["flink"], "The runtime does not support untyped `NULL` values.")
def test_null_literal(con, backend):
expr = ibis.null()
result = con.execute(expr)
assert pd.isna(result)
assert pd.isna(con.execute(expr))

with contextlib.suppress(com.OperationNotDefinedError):
backend_name = backend.name()
assert con.execute(expr.typeof()) == NULL_BACKEND_TYPES[backend_name]

with pytest.raises(AttributeError):
expr.upper()
with pytest.raises(AttributeError):
expr.cast(str).max()
assert pd.isna(con.execute(expr.cast(str).upper()))


@pytest.mark.broken(
"mssql",
reason="https://github.com/ibis-project/ibis/issues/9109",
raises=AssertionError,
)
def test_null_literal_typed(con, backend):
expr = ibis.null(bool)
assert pd.isna(con.execute(expr))
assert pd.isna(con.execute(expr.negate()))
assert pd.isna(con.execute(expr.cast(str).upper()))
with pytest.raises(AttributeError):
expr.upper()


BOOLEAN_BACKEND_TYPE = {
"bigquery": "BOOL",
Expand All @@ -75,15 +94,27 @@ def test_null_literal(con, backend):
}


def test_null_literal_typed_typeof(con, backend):
expr = ibis.null(bool)
TYPES = {
**BOOLEAN_BACKEND_TYPE,
"clickhouse": "Nullable(Bool)",
"flink": "BOOLEAN",
"sqlite": "null", # in sqlite, typeof(x) is determined by the VALUE of x at runtime, not it's static type
}

with contextlib.suppress(com.OperationNotDefinedError):
assert con.execute(expr.typeof()) == TYPES[backend.name()]


def test_boolean_literal(con, backend):
expr = ibis.literal(False, type=dt.boolean)
result = con.execute(expr)
assert not result
assert type(result) in (np.bool_, bool)

with contextlib.suppress(com.OperationNotDefinedError):
backend_name = backend.name()
assert con.execute(expr.typeof()) == BOOLEAN_BACKEND_TYPE[backend_name]
assert con.execute(expr.typeof()) == BOOLEAN_BACKEND_TYPE[backend.name()]


@pytest.mark.parametrize(
Expand Down
18 changes: 16 additions & 2 deletions ibis/expr/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -200,11 +200,25 @@
NA = null()
"""The NULL scalar.
This is an untyped NULL. If you want a typed NULL, use eg `ibis.null(str)`.
Examples
--------
>>> import ibis
>>> my_null = ibis.NA
>>> my_null.isnull()
>>> assert ibis.NA.execute() is None
>>> ibis.NA.isnull().execute()
True
datatype-specific methods aren't available on `NA`:
>>> ibis.NA.upper().execute() is None # quartodoc: +EXPECTED_FAILURE
Traceback (most recent call last):
...
AttributeError: 'NullScalar' object has no attribute 'upper'
Instead, use the typed `ibis.null`:
>>> ibis.null(str).upper().execute() is None
True
"""

Expand Down
20 changes: 17 additions & 3 deletions ibis/expr/types/generic.py
Original file line number Diff line number Diff line change
Expand Up @@ -2227,9 +2227,23 @@ class NullColumn(Column, NullValue):


@public
def null():
"""Create a NULL/NA scalar."""
return ops.NULL.to_expr()
def null(type: dt.DataType | str | None = None) -> Value:
"""Create a NULL/NA scalar.
By default, the type will be NULLTYPE. This is castable and comparable to any type,
but lacks datatype-specific methods:
>>> import ibis
>>> ibis.null().upper().execute() is None
Traceback (most recent call last):
...
AttributeError: 'NullScalar' object has no attribute 'upper'
>>> ibis.null(str).upper().execute() is None
True
"""
if type is None:
type = dt.null
return ops.Literal(None, type).to_expr()


@public
Expand Down

0 comments on commit 8db686e

Please sign in to comment.