Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
104 commits
Select commit Hold shift + click to select a range
9df31dc
refactor: Split up selectors
dangotbanned Oct 22, 2025
f44e015
refactor: Separate `DTypeSelector`
dangotbanned Oct 22, 2025
b57bb10
oops missed one!
dangotbanned Oct 22, 2025
e17d18b
feat(DRAFT): Re/implement `By{Index,Name}`, `into_columns`
dangotbanned Oct 22, 2025
11489b3
chore: More planning
dangotbanned Oct 22, 2025
f9be688
test: Start porting some upstream tests
dangotbanned Oct 23, 2025
67d7eb4
test: Restructure tests
dangotbanned Oct 23, 2025
14b891f
test: Port `by_index` tests
dangotbanned Oct 23, 2025
5158d94
test(DRAFT): Add placeholders for everything that looks interesting
dangotbanned Oct 23, 2025
18b4a22
test: Port port port
dangotbanned Oct 23, 2025
a032f49
test: 4x more
dangotbanned Oct 23, 2025
e93dc46
test: Add `test_selector_result_order`
dangotbanned Oct 23, 2025
5d60906
cov temp
dangotbanned Oct 23, 2025
e733dbc
test: oh nice, this already works!
dangotbanned Oct 23, 2025
2f66d16
test: More `by_name` tests
dangotbanned Oct 23, 2025
bde633a
fix: Validate selectors inputs more
dangotbanned Oct 23, 2025
632de2e
fix: Allow empty `by_dtype`
dangotbanned Oct 23, 2025
198b296
fix: Re-align selector binary ops w/ polars
dangotbanned Oct 23, 2025
db4e331
test(typing): Silence unfixable ignore
dangotbanned Oct 23, 2025
f493c72
test: All of `test_selector_sets` works!
dangotbanned Oct 24, 2025
e5dd9e0
test: All of `test_selector_datetime` works too!
dangotbanned Oct 24, 2025
9df5700
test: `parametrize` big `datetime` test
dangotbanned Oct 24, 2025
e502e73
fix: Actually check the inner dtype for `cs.{list,array}` 🤦‍♂️
dangotbanned Oct 24, 2025
21ef672
fix: Ensure `DTypeSelector` only nests other `DTypeSelector`s
dangotbanned Oct 24, 2025
35f8bac
feat(DRAFT): Roughly add all the new concepts
dangotbanned Oct 24, 2025
66b2501
fix: Swap implementation of `expand_selector_irs_names`
dangotbanned Oct 24, 2025
4e3433d
refactor(DRAFT): Integrating concrete `Selector` in expansion
dangotbanned Oct 24, 2025
f83c79b
refactor: Unravelling `expand_expression_by_combination` part 1
dangotbanned Oct 25, 2025
74a200f
refactor: Unravelling `expand_expression_by_combination` part 2
dangotbanned Oct 25, 2025
debf883
refactor: Fill out the `FunctionExpr` parts too
dangotbanned Oct 25, 2025
4a8dc31
chore: Housekeeping
dangotbanned Oct 25, 2025
5db6d57
chore: Align some more reprs
dangotbanned Oct 25, 2025
34b51a8
oooh missed a spot
dangotbanned Oct 25, 2025
814d4f8
tidy
dangotbanned Oct 25, 2025
961a5f4
fix: Ensure selectors reduce in predicates
dangotbanned Oct 26, 2025
1c0d059
just keep swimming
dangotbanned Oct 26, 2025
b96dfd7
disallow multi-output in `when` (for now)
dangotbanned Oct 27, 2025
53cc24e
tweak that error message
dangotbanned Oct 27, 2025
8c69f13
fix: Ensure exprs are ordered for grouping in error
dangotbanned Oct 27, 2025
3747155
feat(DRAFT): Huge simplify, remove fiddly leaf/root zip expansion
dangotbanned Oct 27, 2025
9047af6
refactor: Remove more complexity
dangotbanned Oct 27, 2025
2499b88
test: Add failing tests for new `name` behaviors
dangotbanned Oct 27, 2025
2cfd725
feat: Allow multiple `name` ops
dangotbanned Oct 27, 2025
303d091
fix: Get `KeepName` working
dangotbanned Oct 27, 2025
f1a8d0d
test: All of `selectors_test` passes!!!!! 🥳🥳🥳🥳🥳
dangotbanned Oct 27, 2025
d478420
refactor: Use new selectors in `_rewrites`
dangotbanned Oct 28, 2025
7b31d65
refactor: (Partially) Use new selectors in `group_by`
dangotbanned Oct 28, 2025
d7654e1
Use new selectors in `DataFrame.filter`, fix `''` output name
dangotbanned Oct 28, 2025
d572f0a
refactor: Use new selectors in `DataFrame.sort`
dangotbanned Oct 28, 2025
b2124e0
refactor: Use new selectors in `DataFrame.with_columns`
dangotbanned Oct 28, 2025
03bc5e8
test: Flesh-out `Frame` util some more
dangotbanned Oct 28, 2025
439ebf5
test: Use new selectors in most of `expr_expansion_test`
dangotbanned Oct 28, 2025
1321c21
test: Finish `expr_expansion_test` transition
dangotbanned Oct 28, 2025
75065ca
refactor: Use new selectors in `DataFrame.select`
dangotbanned Oct 28, 2025
952f70d
refactor: Remove a whole bunch of old selectors
dangotbanned Oct 28, 2025
c4a9cea
refactor: Remove `Nth`, `IndexColumns`, `Exclude`
dangotbanned Oct 28, 2025
3ae9a7b
refactor: Remove `Columns`
dangotbanned Oct 28, 2025
ef7ba4e
refactor: Remove `All`, `_ColumnSelection`
dangotbanned Oct 28, 2025
b926152
tidy tidy tidy
dangotbanned Oct 28, 2025
5cdb984
fix: Always ignore for `group_by`
dangotbanned Oct 28, 2025
9f96467
okay what errors?
dangotbanned Oct 28, 2025
f16603d
ci: Fix `check_docstrings`
dangotbanned Oct 28, 2025
b7a5d30
perf: Re-introduce cache for `DTypeSelector`s
dangotbanned Oct 29, 2025
273454c
fix(typing): Widen `ByDType._matches`
dangotbanned Oct 29, 2025
0094bbc
fix: Create and handle more selectors edge cases
dangotbanned Oct 29, 2025
3e84d9c
chore: remove outdated comment
dangotbanned Oct 29, 2025
677be9f
exclude
dangotbanned Oct 29, 2025
a9493ad
docs: restore, adapt old doc
dangotbanned Oct 29, 2025
7c42f78
fix(typing): update ignore
dangotbanned Oct 29, 2025
271389b
refactor: Remove `_s` suffixes
dangotbanned Oct 29, 2025
303efd7
perf: Make `needs_expansion` a method
dangotbanned Oct 29, 2025
bf25c9a
refactor: Structure around `Expander`
dangotbanned Oct 29, 2025
9b2f617
`resolve_names` -> `Expander.prepare_projection`
dangotbanned Oct 29, 2025
936c78b
chore: planning fancy binary combination expansion
dangotbanned Oct 29, 2025
d09aa74
adapt old `BinaryExpr` tests and make them fail
dangotbanned Oct 29, 2025
fbb1c8e
feat: Allow multi-output `Expr` on either or both* sides of a binary op
dangotbanned Oct 30, 2025
d60a32e
feat: Re-introduce error, display shape mismatch
dangotbanned Oct 30, 2025
ebe8fc8
revert: temp change `sum`
dangotbanned Oct 30, 2025
9832de2
Update narwhals/_plan/expressions/selectors.py
dangotbanned Oct 30, 2025
2847cbb
chore: Remove dead code
dangotbanned Oct 30, 2025
d829677
test: Port over upstream `test_meta.py`
dangotbanned Oct 30, 2025
124706c
feat: Support `ncs.by_name("name").meta.output_name()`
dangotbanned Oct 30, 2025
e8eb22e
chore: More coverage, simplify
dangotbanned Oct 30, 2025
19ca6d3
chore: Fix `_expr_output_name` coverage
dangotbanned Oct 31, 2025
109537c
test: cover error in `meta.as_selector`
dangotbanned Oct 31, 2025
2a8212c
Merge branch 'expr-ir/over-and-over-and-over-again' into expr-ir/stri…
dangotbanned Nov 1, 2025
90104e6
chore(expr-ir): Improve coverage (#3265)
dangotbanned Nov 1, 2025
9de3552
Merge branch 'expr-ir/over-and-over-and-over-again' into expr-ir/stri…
dangotbanned Nov 2, 2025
1c5be8f
cov
dangotbanned Nov 2, 2025
a8f70d7
feat: Support `ncs.matches(re.Pattern[str])`
dangotbanned Nov 2, 2025
67e7918
feat: Add `ncs.{float,integer,temporal}`#
dangotbanned Nov 2, 2025
3557728
feat: Add `ncs.{first,last}`
dangotbanned Nov 2, 2025
d081ef9
chore: Re-order selectors in `expressions.expr`
dangotbanned Nov 2, 2025
ebf945a
chore: Add `_plan.selectors.__all__`
dangotbanned Nov 2, 2025
35047c7
chore: Simplify, test, stabilize `ByDType` repr
dangotbanned Nov 2, 2025
46bdd51
chore: Remove some bad docs
dangotbanned Nov 2, 2025
a826204
fix: Align `group_by` agg expansion with `polars`
dangotbanned Nov 2, 2025
2ba8edd
refactor: Rename, re-doc `into_columns` -> `iter_expand(_names)`
dangotbanned Nov 2, 2025
616a5d5
feat: Support `drop_nulls(OneOrIterable[ColumnNameOrSelector])`
dangotbanned Nov 2, 2025
9447959
feat: Support `drop(*columns: OneOrIterable[ColumnNameOrSelector])`
dangotbanned Nov 3, 2025
003972b
test: Fully cover selectors in `drop`
dangotbanned Nov 3, 2025
20ac1d3
cov
dangotbanned Nov 3, 2025
cc780de
refactor: Start transitioning `DataFrame.sort` to selectors-only
dangotbanned Nov 3, 2025
50bcb9c
refactor: Finish `sort_by_ir` removal
dangotbanned Nov 3, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions narwhals/_plan/__init__.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
from __future__ import annotations

from narwhals._plan import selectors
from narwhals._plan.dataframe import DataFrame
from narwhals._plan.expr import Expr, Selector
from narwhals._plan.expressions import selectors
from narwhals._plan.expr import Expr
from narwhals._plan.functions import (
all,
all_horizontal,
Expand All @@ -25,6 +25,7 @@
sum_horizontal,
when,
)
from narwhals._plan.selectors import Selector
from narwhals._plan.series import Series

__all__ = [
Expand Down
9 changes: 5 additions & 4 deletions narwhals/_plan/_expr_ir.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,10 @@
from typing_extensions import Self

from narwhals._plan.compliant.typing import Ctx, FrameT_contra, R_co
from narwhals._plan.expr import Expr, Selector
from narwhals._plan.expr import Expr
from narwhals._plan.expressions.expr import Alias, Cast, Column
from narwhals._plan.meta import MetaNamespace
from narwhals._plan.selectors import Selector
from narwhals._plan.typing import ExprIRT2, MapIR, Seq
from narwhals.dtypes import DType

Expand Down Expand Up @@ -191,11 +192,11 @@ def _map_ir_child(obj: ExprIR | Seq[ExprIR], fn: MapIR, /) -> ExprIR | Seq[ExprI

class SelectorIR(ExprIR, config=ExprIROptions.no_dispatch()):
def to_narwhals(self, version: Version = Version.MAIN) -> Selector:
from narwhals._plan import expr
from narwhals._plan.selectors import Selector, SelectorV1

if version is Version.MAIN:
return expr.Selector._from_ir(self)
return expr.SelectorV1._from_ir(self)
return Selector._from_ir(self)
return SelectorV1._from_ir(self)

def matches_column(self, name: str, dtype: DType) -> bool:
"""Return True if we can select this column.
Expand Down
15 changes: 11 additions & 4 deletions narwhals/_plan/_guards.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@
if TYPE_CHECKING:
from typing_extensions import TypeIs

from narwhals._plan import expr, expressions as ir
from narwhals._plan import expressions as ir
from narwhals._plan.compliant.series import CompliantSeries
from narwhals._plan.expr import Expr
from narwhals._plan.selectors import Selector
from narwhals._plan.series import Series
from narwhals._plan.typing import (
ColumnNameOrSelector,
Expand Down Expand Up @@ -49,6 +50,12 @@ def _expr(*_: Any): # type: ignore[no-untyped-def] # noqa: ANN202
return expr


def _selectors(*_: Any): # type: ignore[no-untyped-def] # noqa: ANN202
from narwhals._plan import selectors

return selectors


def _series(*_: Any): # type: ignore[no-untyped-def] # noqa: ANN202
from narwhals._plan import series

Expand All @@ -63,8 +70,8 @@ def is_expr(obj: Any) -> TypeIs[Expr]:
return isinstance(obj, _expr().Expr)


def is_selector(obj: Any) -> TypeIs[expr.Selector]:
return isinstance(obj, _expr().Selector)
def is_selector(obj: Any) -> TypeIs[Selector]:
return isinstance(obj, _selectors().Selector)


def is_column(obj: Any) -> TypeIs[Expr]:
Expand All @@ -83,7 +90,7 @@ def is_into_expr_column(obj: Any) -> TypeIs[IntoExprColumn]:
def is_column_name_or_selector(
obj: Any, *, allow_expr: bool = False
) -> TypeIs[ColumnNameOrSelector]:
tps = (str, _expr().Selector) if not allow_expr else (str, _expr().Expr)
tps = (str, _selectors().Selector) if not allow_expr else (str, _expr().Expr)
return isinstance(obj, tps)


Expand Down
121 changes: 9 additions & 112 deletions narwhals/_plan/expr.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@

import math
from collections.abc import Iterable, Mapping, Sequence
from typing import TYPE_CHECKING, Any, ClassVar, overload
from typing import TYPE_CHECKING, Any, ClassVar

from narwhals._plan import common, expressions as ir
from narwhals._plan._guards import is_column, is_expr, is_series
from narwhals._plan._guards import is_expr, is_series
from narwhals._plan._parse import (
parse_into_expr_ir,
parse_into_seq_of_expr_ir,
Expand All @@ -17,7 +17,6 @@
functions as F,
operators as ops,
)
from narwhals._plan.expressions.selectors import by_name
from narwhals._plan.options import (
EWMOptions,
RankOptions,
Expand All @@ -29,7 +28,7 @@
from narwhals.exceptions import ComputeError

if TYPE_CHECKING:
from typing_extensions import Never, Self
from typing_extensions import Self

from narwhals._plan._function import Function
from narwhals._plan.expressions.categorical import ExprCatNamespace
Expand All @@ -51,8 +50,6 @@
)


# NOTE: Overly simplified placeholders for mocking typing
# Entirely ignoring namespace + function binding
class Expr:
_ir: ir.ExprIR
_version: ClassVar[Version] = Version.MAIN
Expand Down Expand Up @@ -83,9 +80,15 @@ def alias(self, name: str) -> Self:
def cast(self, dtype: IntoDType) -> Self:
return self._from_ir(self._ir.cast(common.into_dtype(dtype)))

# TODO @dangotbanned: Swap out with `exclude_s`
def exclude(self, *names: OneOrIterable[str]) -> Self:
return self._from_ir(ir.Exclude.from_names(self._ir, *names))

def exclude_s(self, *names: OneOrIterable[str]) -> Expr:
from narwhals._plan import selectors as cs

return (self.meta.as_selector() - cs.by_name(*names)).as_expr()

def count(self) -> Self:
return self._from_ir(agg.Count(expr=self._ir))

Expand Down Expand Up @@ -573,111 +576,5 @@ def str(self) -> ExprStringNamespace:
return ExprStringNamespace(_expr=self)


class Selector(Expr):
_ir: ir.SelectorIR

def __repr__(self) -> str:
return f"nw._plan.Selector({self.version.name.lower()}):\n{self._ir!r}"

@classmethod
def _from_ir(cls, selector_ir: ir.SelectorIR, /) -> Self: # type: ignore[override]
obj = cls.__new__(cls)
obj._ir = selector_ir
return obj

def _to_expr(self) -> Expr:
return self._ir.to_narwhals(self.version)

@overload # type: ignore[override]
def __or__(self, other: Self) -> Self: ...
@overload
def __or__(self, other: IntoExprColumn | int | bool) -> Expr: ...
def __or__(self, other: IntoExprColumn | int | bool) -> Self | Expr:
if isinstance(other, type(self)):
op = ops.Or()
return self._from_ir(op.to_binary_selector(self._ir, other._ir))
return self._to_expr() | other

@overload # type: ignore[override]
def __and__(self, other: Self) -> Self: ...
@overload
def __and__(self, other: IntoExprColumn | int | bool) -> Expr: ...
def __and__(self, other: IntoExprColumn | int | bool) -> Self | Expr:
if is_column(other) and (name := other.meta.output_name()):
other = by_name(name)
if isinstance(other, type(self)):
op = ops.And()
return self._from_ir(op.to_binary_selector(self._ir, other._ir))
return self._to_expr() & other

@overload # type: ignore[override]
def __sub__(self, other: Self) -> Self: ...
@overload
def __sub__(self, other: IntoExpr) -> Expr: ...
def __sub__(self, other: IntoExpr) -> Self | Expr:
if isinstance(other, type(self)):
op = ops.Sub()
return self._from_ir(op.to_binary_selector(self._ir, other._ir))
return self._to_expr() - other

@overload # type: ignore[override]
def __xor__(self, other: Self) -> Self: ...
@overload
def __xor__(self, other: IntoExprColumn | int | bool) -> Expr: ...
def __xor__(self, other: IntoExprColumn | int | bool) -> Self | Expr:
if isinstance(other, type(self)):
op = ops.ExclusiveOr()
return self._from_ir(op.to_binary_selector(self._ir, other._ir))
return self._to_expr() ^ other

def __invert__(self) -> Self:
return self._from_ir(ir.InvertSelector(selector=self._ir))

def __add__(self, other: Any) -> Expr: # type: ignore[override]
if isinstance(other, type(self)):
msg = "unsupported operand type(s) for op: ('Selector' + 'Selector')"
raise TypeError(msg)
return self._to_expr() + other # type: ignore[no-any-return]

def __radd__(self, other: Any) -> Never:
msg = "unsupported operand type(s) for op: ('Expr' + 'Selector')"
raise TypeError(msg)

def __rsub__(self, other: Any) -> Never:
msg = "unsupported operand type(s) for op: ('Expr' - 'Selector')"
raise TypeError(msg)

@overload # type: ignore[override]
def __rand__(self, other: Self) -> Self: ...
@overload
def __rand__(self, other: IntoExprColumn | int | bool) -> Expr: ...
def __rand__(self, other: IntoExprColumn | int | bool) -> Self | Expr:
if is_column(other) and (name := other.meta.output_name()):
return by_name(name) & self
return self._to_expr().__rand__(other)

@overload # type: ignore[override]
def __ror__(self, other: Self) -> Self: ...
@overload
def __ror__(self, other: IntoExprColumn | int | bool) -> Expr: ...
def __ror__(self, other: IntoExprColumn | int | bool) -> Self | Expr:
if is_column(other) and (name := other.meta.output_name()):
return by_name(name) | self
return self._to_expr().__ror__(other)

@overload # type: ignore[override]
def __rxor__(self, other: Self) -> Self: ...
@overload
def __rxor__(self, other: IntoExprColumn | int | bool) -> Expr: ...
def __rxor__(self, other: IntoExprColumn | int | bool) -> Self | Expr:
if is_column(other) and (name := other.meta.output_name()):
return by_name(name) ^ self
return self._to_expr().__rxor__(other)


class ExprV1(Expr):
_version: ClassVar[Version] = Version.V1


class SelectorV1(Selector):
_version: ClassVar[Version] = Version.V1
18 changes: 15 additions & 3 deletions narwhals/_plan/expressions/expr.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,14 +68,17 @@ def col(name: str, /) -> Column:
return Column(name=name)


# TODO @dangotbanned: Scheduled to be removed, not needed with new selectors
def cols(*names: str) -> Columns:
return Columns(names=names)


# TODO @dangotbanned: Scheduled to be removed, not needed with new selectors
def nth(index: int, /) -> Nth:
return Nth(index=index)


# TODO @dangotbanned: Scheduled to be removed, not needed with new selectors
def index_columns(*indices: int) -> IndexColumns:
return IndexColumns(indices=indices)

Expand Down Expand Up @@ -104,10 +107,12 @@ def to_selector_ir(self) -> RootSelector:
return cs.ByName.from_name(self.name).to_selector_ir()


# TODO @dangotbanned: Scheduled to be removed, not needed with new selectors
class _ColumnSelection(ExprIR, config=ExprIROptions.no_dispatch()):
"""Nodes which can resolve to `Column`(s) with a `Schema`."""


# TODO @dangotbanned: Scheduled to be removed, not needed with new selectors
class Columns(_ColumnSelection):
__slots__ = ("names",)
names: Seq[str]
Expand All @@ -119,24 +124,31 @@ def to_selector_ir(self) -> RootSelector:
return cs.ByName.from_names(*self.names).to_selector_ir()


# TODO @dangotbanned: Add `selectors.by_index`
# TODO @dangotbanned: Scheduled to be removed, not needed with new selectors
class Nth(_ColumnSelection):
__slots__ = ("index",)
index: int

def __repr__(self) -> str:
return f"nth({self.index})"

def to_selector_ir(self) -> RootSelector:
return cs.ByIndex.from_index(self.index).to_selector_ir()


# TODO @dangotbanned: Add `selectors.by_index`
# TODO @dangotbanned: Scheduled to be removed, not needed with new selectors
class IndexColumns(_ColumnSelection):
__slots__ = ("indices",)
indices: Seq[int]

def __repr__(self) -> str:
return f"index_columns({self.indices!r})"

def to_selector_ir(self) -> RootSelector:
return cs.ByIndex.from_indices(self.indices).to_selector_ir()


# TODO @dangotbanned: Scheduled to be removed, not needed with new selectors
class All(_ColumnSelection):
def __repr__(self) -> str:
return "all()"
Expand All @@ -145,7 +157,7 @@ def to_selector_ir(self) -> RootSelector:
return cs.All().to_selector_ir()


# TODO @dangotbanned: Add `selectors.exclude`
# TODO @dangotbanned: Scheduled to be removed, not needed with new selectors
class Exclude(_ColumnSelection, child=("expr",)):
__slots__ = ("expr", "names")
expr: ExprIR
Expand Down
Loading
Loading