Skip to content

Commit 616a5d5

Browse files
committed
feat: Support drop_nulls(OneOrIterable[ColumnNameOrSelector])
1 parent 2ba8edd commit 616a5d5

File tree

3 files changed

+77
-9
lines changed

3 files changed

+77
-9
lines changed

narwhals/_plan/_parse.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -136,10 +136,10 @@ def parse_into_selector_ir(input: ColumnNameOrSelector | Expr, /) -> SelectorIR:
136136
from narwhals._plan import selectors as cs
137137

138138
selector = cs.by_name(input)
139-
elif is_expr(input): # pragma: no cover
139+
elif is_expr(input):
140140
selector = input.meta.as_selector()
141-
else: # pragma: no cover
142-
msg = f"cannot turn {qualified_type_name(input)!r} into selector"
141+
else:
142+
msg = f"cannot turn {qualified_type_name(input)!r} into a selector"
143143
raise TypeError(msg)
144144
return selector._ir
145145

narwhals/_plan/dataframe.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -114,9 +114,11 @@ def drop(self, *columns: str, strict: bool = True) -> Self:
114114
return self._with_compliant(self._compliant.drop(columns, strict=strict))
115115

116116
def drop_nulls(
117-
self, subset: str | Sequence[str] | None = None
118-
) -> Self: # pragma: no cover
119-
subset = [subset] if isinstance(subset, str) else subset
117+
self, subset: OneOrIterable[ColumnNameOrSelector] | None = None
118+
) -> Self:
119+
if subset is not None:
120+
s_irs = _parse.parse_into_seq_of_selector_ir(subset)
121+
subset = expand_selector_irs_names(s_irs, schema=self)
120122
return self._with_compliant(self._compliant.drop_nulls(subset))
121123

122124
def rename(self, mapping: Mapping[str, str]) -> Self:

tests/plan/compliant_test.py

Lines changed: 69 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import pytest
66

77
from narwhals._plan import selectors as ncs
8+
from narwhals.exceptions import ColumnNotFoundError, InvalidOperationError
89

910
pytest.importorskip("pyarrow")
1011
pytest.importorskip("numpy")
@@ -19,7 +20,9 @@
1920
if TYPE_CHECKING:
2021
from collections.abc import Sequence
2122

23+
from narwhals._plan.typing import ColumnNameOrSelector, OneOrIterable
2224
from narwhals.typing import PythonLiteral
25+
from tests.conftest import Data
2326

2427

2528
@pytest.fixture
@@ -44,12 +47,19 @@ def data_small() -> dict[str, Any]:
4447

4548

4649
@pytest.fixture
47-
def data_smaller(data_small: dict[str, Any]) -> dict[str, Any]:
50+
def data_small_af(data_small: dict[str, Any]) -> dict[str, Any]:
4851
"""Use only columns `"a"-"f"`."""
4952
keep = {"a", "b", "c", "d", "e", "f"}
5053
return {k: v for k, v in data_small.items() if k in keep}
5154

5255

56+
@pytest.fixture
57+
def data_small_dh(data_small: dict[str, Any]) -> dict[str, Any]:
58+
"""Use only columns `"d"-"h"`."""
59+
keep = {"d", "e", "f", "g", "h"}
60+
return {k: v for k, v in data_small.items() if k in keep}
61+
62+
5363
@pytest.fixture
5464
def data_indexed() -> dict[str, Any]:
5565
"""Used in https://github.com/narwhals-dev/narwhals/pull/2528."""
@@ -472,9 +482,9 @@ def test_select(
472482
def test_with_columns(
473483
expr: nwp.Expr | Sequence[nwp.Expr],
474484
expected: dict[str, Any],
475-
data_smaller: dict[str, Any],
485+
data_small_af: dict[str, Any],
476486
) -> None:
477-
result = dataframe(data_smaller).with_columns(expr)
487+
result = dataframe(data_small_af).with_columns(expr)
478488
assert_equal_data(result, expected)
479489

480490

@@ -518,6 +528,62 @@ def test_row_is_py_literal(
518528
assert result == polars_result
519529

520530

531+
def test_drop_nulls(data_small_dh: Data) -> None:
532+
df = dataframe(data_small_dh)
533+
expected: Data = {"d": [], "e": [], "f": [], "g": [], "h": []}
534+
result = df.drop_nulls()
535+
assert_equal_data(result, expected)
536+
537+
538+
def test_drop_nulls_invalid(data_small_dh: Data) -> None:
539+
df = dataframe(data_small_dh)
540+
with pytest.raises(TypeError, match=r"cannot turn.+int.+into a selector"):
541+
df.drop_nulls(123) # type: ignore[arg-type]
542+
with pytest.raises(
543+
InvalidOperationError, match=r"cannot turn.+col\('a'\).first\(\).+into a selector"
544+
):
545+
df.drop_nulls(nwp.col("a").first()) # type: ignore[arg-type]
546+
547+
with pytest.raises(ColumnNotFoundError):
548+
df.drop_nulls(["j", "k"])
549+
550+
with pytest.raises(ColumnNotFoundError):
551+
df.drop_nulls(ncs.by_name("j", "k"))
552+
553+
with pytest.raises(ColumnNotFoundError):
554+
df.drop_nulls(ncs.by_index(-999))
555+
556+
557+
DROP_ROW_1: Data = {
558+
"d": [7, 8],
559+
"e": [9, 7],
560+
"f": [False, None],
561+
"g": [None, False],
562+
"h": [None, True],
563+
}
564+
KEEP_ROW_3: Data = {"d": [8], "e": [7], "f": [None], "g": [False], "h": [True]}
565+
566+
567+
@pytest.mark.parametrize(
568+
("subset", "expected"),
569+
[
570+
("e", DROP_ROW_1),
571+
(nwp.col("e"), DROP_ROW_1),
572+
(ncs.by_index(1), DROP_ROW_1),
573+
(ncs.integer(), DROP_ROW_1),
574+
([ncs.numeric() | ~ncs.boolean()], DROP_ROW_1),
575+
(["g", "h"], KEEP_ROW_3),
576+
([ncs.by_name("g", "h"), "d"], KEEP_ROW_3),
577+
],
578+
)
579+
def test_drop_nulls_subset(
580+
data_small_dh: Data, subset: OneOrIterable[ColumnNameOrSelector], expected: Data
581+
) -> None:
582+
df = dataframe(data_small_dh)
583+
result = df.drop_nulls(subset)
584+
assert_equal_data(result, expected)
585+
586+
521587
if TYPE_CHECKING:
522588
from typing_extensions import assert_type
523589

0 commit comments

Comments
 (0)