Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
97 commits
Select commit Hold shift + click to select a range
264e717
feat: Improve ``DataFrame.__getitem__`` consistency
MarcoGorelli Apr 17, 2025
8bc2bfa
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Apr 17, 2025
2c4f1c1
fixup
MarcoGorelli Apr 18, 2025
01bd787
fixup
MarcoGorelli Apr 18, 2025
86a057c
gather -> _gather
MarcoGorelli Apr 18, 2025
8c216fc
type alias intindexer and strindexer
MarcoGorelli Apr 18, 2025
944b597
reduce diff
MarcoGorelli Apr 18, 2025
6c98cee
naming
MarcoGorelli Apr 18, 2025
e080956
Merge remote-tracking branch 'upstream/main' into better-indexing
MarcoGorelli Apr 18, 2025
dfbb63f
reduce polars diff
MarcoGorelli Apr 18, 2025
190e265
appease pyright
MarcoGorelli Apr 18, 2025
85940ee
extra test
MarcoGorelli Apr 18, 2025
0d79f24
arrow negative slicing
MarcoGorelli Apr 18, 2025
93110ef
old pyarrow compat
MarcoGorelli Apr 18, 2025
4788a47
old polars compat
MarcoGorelli Apr 18, 2025
2f26c45
pandas fixup
MarcoGorelli Apr 18, 2025
d9c619e
refactor: Reuse `CompliantSeries._is_native`
dangotbanned Apr 18, 2025
2929efb
Merge branch 'better-indexing' of https://github.com/marcogorelli/nar…
dangotbanned Apr 18, 2025
9317d7a
test and cover slicing dataframe with series
MarcoGorelli Apr 18, 2025
2c30a0c
Merge branch 'better-indexing' of github.com:MarcoGorelli/narwhals in…
MarcoGorelli Apr 18, 2025
16a625b
old polars fixup
MarcoGorelli Apr 18, 2025
19e2762
avoid tolist in old polars
MarcoGorelli Apr 18, 2025
0bc31fc
`is_null_slice` typing
dangotbanned Apr 18, 2025
6bc8532
Merge branch 'better-indexing' of https://github.com/marcogorelli/nar…
dangotbanned Apr 18, 2025
426f453
Revert "avoid tolist in old polars"
MarcoGorelli Apr 18, 2025
40d552c
`is_sequence_like` typing
dangotbanned Apr 18, 2025
de06d26
avoid is_sequence_like_ints("") false positive
MarcoGorelli Apr 18, 2025
007795b
t push
MarcoGorelli Apr 18, 2025
e26c6d1
Merge branch 'main' into better-indexing
dangotbanned Apr 18, 2025
b503c2f
fix(typing): Resolve `PolarsSeries` issues
dangotbanned Apr 18, 2025
d3b0224
feat(typing): Add `__getitem__` aliases from polars
dangotbanned Apr 18, 2025
20f61fc
feat: Clone `polars` overloads
dangotbanned Apr 18, 2025
42d5a4e
backport `v1`
dangotbanned Apr 18, 2025
6941029
unhide typing issues
dangotbanned Apr 18, 2025
f4aed5f
chore: Remove `BooleanMask`
dangotbanned Apr 18, 2025
f05c3a2
chore(typing): Fixing `DataFrame.__getitem__`
dangotbanned Apr 18, 2025
1cf8ea8
refactor: Merge branches
dangotbanned Apr 18, 2025
147dbac
early return even earlier 😎
dangotbanned Apr 18, 2025
203376d
Merge remote-tracking branch 'upstream/main' into pr/MarcoGorelli/2393
dangotbanned Apr 18, 2025
46be441
feat(typing): Slice-typing
dangotbanned Apr 19, 2025
e235649
Merge remote-tracking branch 'upstream/main' into pr/MarcoGorelli/2393
dangotbanned Apr 19, 2025
6386996
Merge remote-tracking branch 'upstream/main' into pr/MarcoGorelli/2393
dangotbanned Apr 19, 2025
7a75fb2
fix: `get_column` rename
dangotbanned Apr 19, 2025
d24f1b7
pass compliant down instead of native
MarcoGorelli Apr 19, 2025
e29c2c6
Merge remote-tracking branch 'upstream/main' into better-indexing
MarcoGorelli Apr 20, 2025
e85a1ad
old polars fixup
MarcoGorelli Apr 20, 2025
037c9d7
type __getitem__
MarcoGorelli Apr 20, 2025
0fcbcad
fix modern polars too :sunglasses:
MarcoGorelli Apr 20, 2025
6d2c1aa
log type
MarcoGorelli Apr 20, 2025
5b3cfef
allow slicing Series with native objects
MarcoGorelli Apr 20, 2025
2791f49
coverage
MarcoGorelli Apr 20, 2025
498d6a4
extra test
MarcoGorelli Apr 20, 2025
80bc093
uurgh undo accidental change
MarcoGorelli Apr 20, 2025
5fcc35b
pyright, simplify
MarcoGorelli Apr 20, 2025
9591d10
_arrow/series.py typing
MarcoGorelli Apr 20, 2025
b43feae
refactor(typing): Add `_SliceName`, reuse `_Slice` for `_SliceIndex`
dangotbanned Apr 20, 2025
f5ab0ab
chore(typing): Unhide some issues
dangotbanned Apr 20, 2025
1aa04be
chore: reorganise aliases
dangotbanned Apr 20, 2025
7465b90
refactor: More matching aliases to guards
dangotbanned Apr 20, 2025
de2d9ba
revert(typing): Remove every new `cast`
dangotbanned Apr 20, 2025
9fec62d
fix: `range` != `slice`
dangotbanned Apr 20, 2025
156037f
fix(typing): `EagerDataFrame.__getitem__` exhaustive happy
dangotbanned Apr 20, 2025
7f864c5
chore(typing): Update some signatures
dangotbanned Apr 20, 2025
4276779
fix(typing): Some `PolarsDataFrame` progress
dangotbanned Apr 20, 2025
7a579d3
fix(typing): `Series` allows `_NumpyScalar`
dangotbanned Apr 20, 2025
e07be8b
Merge branch 'main' into better-indexing
dangotbanned Apr 20, 2025
ff6f2eb
refactor: Don't pass down numpy scalar
dangotbanned Apr 20, 2025
6e00a4a
Merge branch 'main' into better-indexing
dangotbanned Apr 20, 2025
7edc0ca
fix: Narrow correctly in `DataFrame.__getitem__`
dangotbanned Apr 20, 2025
becdc48
fix a mypy
dangotbanned Apr 20, 2025
c29ecfd
silence some _polars/_arrow type errors for now
MarcoGorelli Apr 21, 2025
dc71fd9
extra tests, align polars logic more
MarcoGorelli Apr 21, 2025
693c4eb
Start making generic
dangotbanned Apr 21, 2025
bd62c62
Merge branch 'better-indexing' of https://github.com/marcogorelli/nar…
dangotbanned Apr 21, 2025
02cc607
:party: remove many unneded assumes from hypothesis test
MarcoGorelli Apr 21, 2025
f193a29
Merge branch 'better-indexing' of github.com:MarcoGorelli/narwhals in…
MarcoGorelli Apr 21, 2025
e68ee9b
fill in some holes
dangotbanned Apr 21, 2025
17538d3
remove outdated comment
MarcoGorelli Apr 21, 2025
f9f84fe
Merge branch 'better-indexing' of github.com:MarcoGorelli/narwhals in…
MarcoGorelli Apr 21, 2025
2a5e866
more updating annotations
dangotbanned Apr 21, 2025
af8f152
Merge branch 'better-indexing' of https://github.com/marcogorelli/nar…
dangotbanned Apr 21, 2025
308e197
feat(typing): Add `NativeSeriesT` to `EagerDataFrame`
dangotbanned Apr 21, 2025
8e567d5
chore(typing): Update `polars` ignores
dangotbanned Apr 21, 2025
9a2f890
docs(typing): Add note on `pa.Table.select`
dangotbanned Apr 21, 2025
2c01007
refactor: More align names
dangotbanned Apr 21, 2025
1119ad7
refactor: Factor-in `_pandas_like.utils.convert_str_slice_to_int_slice`
dangotbanned Apr 21, 2025
b6aec59
remove outdated comment, type CompliantSeies.__getitem__
MarcoGorelli Apr 22, 2025
f87422e
more consistent naming
MarcoGorelli Apr 22, 2025
230ce6d
remove another Any + dead code
MarcoGorelli Apr 22, 2025
8355ce5
Merge remote-tracking branch 'origin-token/better-indexing' into bett…
MarcoGorelli Apr 22, 2025
427c9c9
remove dead code
MarcoGorelli Apr 22, 2025
dc448ed
fill in Any annotation
MarcoGorelli Apr 22, 2025
e8e5a8a
better typing in _polars
MarcoGorelli Apr 22, 2025
bfb1cf0
yay figured out the pyright error :party:
MarcoGorelli Apr 22, 2025
b725606
remove incorrect Sequence[str]
MarcoGorelli Apr 22, 2025
98fe850
fixup pyright
MarcoGorelli Apr 22, 2025
5b02b59
coverage again
MarcoGorelli Apr 22, 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
179 changes: 62 additions & 117 deletions narwhals/_arrow/dataframe.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,17 @@

from narwhals._arrow.series import ArrowSeries
from narwhals._arrow.utils import align_series_full_broadcast
from narwhals._arrow.utils import convert_str_slice_to_int_slice
from narwhals._arrow.utils import native_to_narwhals_dtype
from narwhals._arrow.utils import select_rows
from narwhals._compliant import EagerDataFrame
from narwhals._expression_parsing import ExprKind
from narwhals.dependencies import is_numpy_array_1d
from narwhals.dependencies import is_numpy_array
from narwhals.exceptions import ShapeError
from narwhals.utils import Implementation
from narwhals.utils import Version
from narwhals.utils import check_column_exists
from narwhals.utils import check_column_names_are_unique
from narwhals.utils import convert_str_slice_to_int_slice
from narwhals.utils import generate_temporary_column_name
from narwhals.utils import is_sequence_but_not_str
from narwhals.utils import not_implemented
from narwhals.utils import parse_columns_to_drop
from narwhals.utils import parse_version
Expand All @@ -51,7 +49,6 @@
from narwhals._arrow.group_by import ArrowGroupBy
from narwhals._arrow.namespace import ArrowNamespace
from narwhals._arrow.typing import ArrowChunkedArray
from narwhals._arrow.typing import Indices # type: ignore[attr-defined]
from narwhals._arrow.typing import Mask # type: ignore[attr-defined]
from narwhals._arrow.typing import Order # type: ignore[attr-defined]
from narwhals._translate import IntoArrowTable
Expand All @@ -60,10 +57,14 @@
from narwhals.typing import CompliantDataFrame
from narwhals.typing import CompliantLazyFrame
from narwhals.typing import JoinStrategy
from narwhals.typing import SizedMultiIndexSelector
from narwhals.typing import SizedMultiNameSelector
from narwhals.typing import SizeUnit
from narwhals.typing import UniqueKeepStrategy
from narwhals.typing import _1DArray
from narwhals.typing import _2DArray
from narwhals.typing import _SliceIndex
from narwhals.typing import _SliceName
from narwhals.utils import Version
from narwhals.utils import _FullContext

Expand All @@ -80,8 +81,9 @@
PromoteOptions: TypeAlias = Literal["none", "default", "permissive"]


class ArrowDataFrame(EagerDataFrame["ArrowSeries", "ArrowExpr", "pa.Table"]):
# --- not in the spec ---
class ArrowDataFrame(
EagerDataFrame["ArrowSeries", "ArrowExpr", "pa.Table", "pa.ChunkedArray[Any]"]
):
def __init__(
self,
native_dataframe: pa.Table,
Expand Down Expand Up @@ -248,118 +250,61 @@ def get_column(self, name: str) -> ArrowSeries:
def __array__(self, dtype: Any, *, copy: bool | None) -> _2DArray:
return self.native.__array__(dtype, copy=copy)

@overload
def __getitem__( # type: ignore[overload-overlap, unused-ignore]
self, item: str | tuple[slice | Sequence[int] | _1DArray, int | str]
) -> ArrowSeries: ...
@overload
def __getitem__(
self,
item: (
int
| slice
| Sequence[int]
| Sequence[str]
| _1DArray
| tuple[
slice | Sequence[int] | _1DArray, slice | Sequence[int] | Sequence[str]
]
),
) -> Self: ...
def __getitem__(
self,
item: (
str
| int
| slice
| Sequence[int]
| Sequence[str]
| _1DArray
| tuple[slice | Sequence[int] | _1DArray, int | str]
| tuple[
slice | Sequence[int] | _1DArray, slice | Sequence[int] | Sequence[str]
]
),
) -> ArrowSeries | Self:
if isinstance(item, tuple):
item = tuple(list(i) if is_sequence_but_not_str(i) else i for i in item) # pyright: ignore[reportAssignmentType]

if isinstance(item, str):
return ArrowSeries.from_native(self.native[item], context=self, name=item)
elif (
isinstance(item, tuple)
and len(item) == 2
and is_sequence_but_not_str(item[1])
and not isinstance(item[0], str)
):
if len(item[1]) == 0:
# Return empty dataframe
return self._with_native(self.native.slice(0, 0).select([]))
selected_rows = select_rows(self.native, item[0])
return self._with_native(selected_rows.select(cast("Indices", item[1])))

elif isinstance(item, tuple) and len(item) == 2:
if isinstance(item[1], slice):
columns = self.columns
indices = cast("Indices", item[0])
if item[1] == slice(None):
if isinstance(item[0], Sequence) and len(item[0]) == 0:
return self._with_native(self.native.slice(0, 0))
return self._with_native(self.native.take(indices))
if isinstance(item[1].start, str) or isinstance(item[1].stop, str):
start, stop, step = convert_str_slice_to_int_slice(item[1], columns)
return self._with_native(
self.native.take(indices).select(columns[start:stop:step])
)
if isinstance(item[1].start, int) or isinstance(item[1].stop, int):
return self._with_native(
self.native.take(indices).select(
columns[item[1].start : item[1].stop : item[1].step]
)
)
msg = f"Expected slice of integers or strings, got: {type(item[1])}" # pragma: no cover
raise TypeError(msg) # pragma: no cover

# PyArrow columns are always strings
col_name = (
item[1]
if isinstance(item[1], str)
else self.columns[cast("int", item[1])]
)
if isinstance(item[0], str): # pragma: no cover
msg = "Can not slice with tuple with the first element as a str"
raise TypeError(msg)
if (isinstance(item[0], slice)) and (item[0] == slice(None)):
return ArrowSeries.from_native(
self.native[col_name], context=self, name=col_name
)
selected_rows = select_rows(self.native, item[0])
return ArrowSeries.from_native(
selected_rows[col_name], context=self, name=col_name
)
def _gather(self, rows: SizedMultiIndexSelector[ArrowChunkedArray]) -> Self:
if len(rows) == 0:
return self._with_native(self.native.slice(0, 0))
if self._backend_version < (18,) and isinstance(rows, tuple):
rows = list(rows)
return self._with_native(self.native.take(rows)) # pyright: ignore[reportArgumentType]

def _gather_slice(self, rows: _SliceIndex | range) -> Self:
start = rows.start or 0
stop = rows.stop if rows.stop is not None else len(self.native)
if start < 0:
start = len(self.native) + start
if stop < 0:
stop = len(self.native) + stop
if rows.step is not None and rows.step != 1:
msg = "Slicing with step is not supported on PyArrow tables"
raise NotImplementedError(msg)
return self._with_native(self.native.slice(start, stop - start))

def _select_slice_name(self, columns: _SliceName) -> Self:
start, stop, step = convert_str_slice_to_int_slice(columns, self.columns)
return self._with_native(self.native.select(self.columns[start:stop:step]))

def _select_slice_index(self, columns: _SliceIndex | range) -> Self:
return self._with_native(
self.native.select(self.columns[columns.start : columns.stop : columns.step])
)

elif isinstance(item, slice):
if item.step is not None and item.step != 1:
msg = "Slicing with step is not supported on PyArrow tables"
raise NotImplementedError(msg)
columns = self.columns
if isinstance(item.start, str) or isinstance(item.stop, str):
start, stop, step = convert_str_slice_to_int_slice(item, columns)
return self._with_native(self.native.select(columns[start:stop:step]))
start = item.start or 0
stop = item.stop if item.stop is not None else len(self.native)
return self._with_native(self.native.slice(start, stop - start))

elif isinstance(item, Sequence) or is_numpy_array_1d(item):
if isinstance(item, Sequence) and len(item) > 0 and isinstance(item[0], str):
return self._with_native(self.native.select(cast("Indices", item)))
if isinstance(item, Sequence) and len(item) == 0:
return self._with_native(self.native.slice(0, 0))
return self._with_native(self.native.take(cast("Indices", item)))
def _select_multi_index(
self, columns: SizedMultiIndexSelector[ArrowChunkedArray]
) -> Self:
selector: Sequence[int]
if isinstance(columns, pa.ChunkedArray):
# TODO @dangotbanned: Fix upstream with `pa.ChunkedArray.to_pylist(self) -> list[Any]:`
selector = cast("Sequence[int]", columns.to_pylist())
# TODO @dangotbanned: Fix upstream, it is actually much narrower
# **Doesn't accept `ndarray`**
elif is_numpy_array(columns):
selector = columns.tolist()
else:
selector = columns
return self._with_native(self.native.select(selector))

else: # pragma: no cover
msg = f"Expected str or slice, got: {type(item)}"
raise TypeError(msg)
def _select_multi_name(
self, columns: SizedMultiNameSelector[ArrowChunkedArray]
) -> Self:
selector: Sequence[str] | _1DArray
if isinstance(columns, pa.ChunkedArray):
# TODO @dangotbanned: Fix upstream with `pa.ChunkedArray.to_pylist(self) -> list[Any]:`
selector = cast("Sequence[str]", columns.to_pylist())
else:
selector = columns
# TODO @dangotbanned: Fix upstream `pa.Table.select` https://github.com/zen-xu/pyarrow-stubs/blob/f899bb35e10b36f7906a728e9f8acf3e0a1f9f64/pyarrow-stubs/__lib_pxi/table.pyi#L597
# NOTE: Investigate what `cython` actually checks
return self._with_native(self.native.select(selector)) # pyright: ignore[reportArgumentType]

@property
def schema(self) -> dict[str, DType]:
Expand Down
40 changes: 23 additions & 17 deletions narwhals/_arrow/series.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,11 @@
from narwhals.typing import PythonLiteral
from narwhals.typing import RankMethod
from narwhals.typing import RollingInterpolationMethod
from narwhals.typing import SizedMultiIndexSelector
from narwhals.typing import TemporalLiteral
from narwhals.typing import _1DArray
from narwhals.typing import _2DArray
from narwhals.typing import _SliceIndex
from narwhals.utils import Version
from narwhals.utils import _FullContext

Expand Down Expand Up @@ -406,20 +408,24 @@ def __native_namespace__(self) -> ModuleType:
def name(self) -> str:
return self._name

@overload
def __getitem__(self, idx: int) -> Any: ...

@overload
def __getitem__(self, idx: slice | Sequence[int] | ArrowChunkedArray) -> Self: ...

def __getitem__(
self, idx: int | slice | Sequence[int] | ArrowChunkedArray
) -> Any | Self:
if isinstance(idx, int):
return maybe_extract_py_scalar(self.native[idx], return_py_scalar=True)
if isinstance(idx, (Sequence, pa.ChunkedArray)):
return self._with_native(self.native.take(idx))
return self._with_native(self.native[idx])
def _gather(self, rows: SizedMultiIndexSelector[ArrowChunkedArray]) -> Self:
if len(rows) == 0:
return self._with_native(self.native.slice(0, 0))
if self._backend_version < (18,) and isinstance(rows, tuple):
rows = list(rows)
return self._with_native(self.native.take(rows))

def _gather_slice(self, rows: _SliceIndex | range) -> Self:
start = rows.start or 0
stop = rows.stop if rows.stop is not None else len(self.native)
if start < 0:
start = len(self.native) + start
if stop < 0:
stop = len(self.native) + stop
if rows.step is not None and rows.step != 1:
msg = "Slicing with step is not supported on PyArrow tables"
raise NotImplementedError(msg)
return self._with_native(self.native.slice(start, stop - start))

def scatter(self, indices: int | Sequence[int], values: Any) -> Self:
import numpy as np # ignore-banned-import
Expand Down Expand Up @@ -911,7 +917,7 @@ def rolling_sum(self, window_size: int, *, min_samples: int, center: bool) -> Se
result = self._with_native(
pc.if_else((count_in_window >= min_samples).native, rolling_sum.native, None)
)
return result[offset:]
return result._gather_slice(slice(offset, None))

def rolling_mean(self, window_size: int, *, min_samples: int, center: bool) -> Self:
min_samples = min_samples if min_samples is not None else window_size
Expand Down Expand Up @@ -940,7 +946,7 @@ def rolling_mean(self, window_size: int, *, min_samples: int, center: bool) -> S
)
/ count_in_window
)
return result[offset:]
return result._gather_slice(slice(offset, None))

def rolling_var(
self, window_size: int, *, min_samples: int, center: bool, ddof: int
Expand Down Expand Up @@ -983,7 +989,7 @@ def rolling_var(
)
) / self._with_native(pc.max_element_wise((count_in_window - ddof).native, 0))

return result[offset:]
return result._gather_slice(slice(offset, None, None))

def rolling_std(
self, window_size: int, *, min_samples: int, center: bool, ddof: int
Expand Down
41 changes: 0 additions & 41 deletions narwhals/_arrow/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
from typing import Iterator
from typing import Sequence
from typing import cast
from typing import overload

import pyarrow as pa
import pyarrow.compute as pc
Expand All @@ -18,8 +17,6 @@
from narwhals.utils import isinstance_or_issubclass

if TYPE_CHECKING:
from typing import TypeVar

from typing_extensions import TypeAlias
from typing_extensions import TypeIs

Expand All @@ -33,15 +30,12 @@
from narwhals._arrow.typing import ScalarAny
from narwhals.dtypes import DType
from narwhals.typing import PythonLiteral
from narwhals.typing import _AnyDArray
from narwhals.utils import Version

# NOTE: stubs don't allow for `ChunkedArray[StructArray]`
# Intended to represent the `.chunks` property storing `list[pa.StructArray]`
ChunkedArrayStructArray: TypeAlias = ArrowChunkedArray

_T = TypeVar("_T")

def is_timestamp(t: Any) -> TypeIs[pa.TimestampType[Any, Any]]: ...
def is_duration(t: Any) -> TypeIs[pa.DurationType[Any]]: ...
def is_list(t: Any) -> TypeIs[pa.ListType[Any]]: ...
Expand Down Expand Up @@ -324,41 +318,6 @@ def cast_for_truediv(
return arrow_array, pa_object


@overload
def convert_slice_to_nparray(num_rows: int, rows_slice: slice) -> _AnyDArray: ...
@overload
def convert_slice_to_nparray(num_rows: int, rows_slice: _T) -> _T: ...
def convert_slice_to_nparray(num_rows: int, rows_slice: slice | _T) -> _AnyDArray | _T:
if isinstance(rows_slice, slice):
import numpy as np # ignore-banned-import

return np.arange(num_rows)[rows_slice]
else:
return rows_slice


def select_rows(
table: pa.Table, rows: slice | int | Sequence[int] | _AnyDArray
) -> pa.Table:
if isinstance(rows, slice) and rows == slice(None):
selected_rows = table
elif isinstance(rows, Sequence) and not rows:
selected_rows = table.slice(0, 0)
else:
range_ = convert_slice_to_nparray(num_rows=len(table), rows_slice=rows)
selected_rows = table.take(cast("list[int]", range_))
return selected_rows


def convert_str_slice_to_int_slice(
str_slice: slice, columns: list[str]
) -> tuple[int | None, int | None, int | None]:
start = columns.index(str_slice.start) if str_slice.start is not None else None
stop = columns.index(str_slice.stop) + 1 if str_slice.stop is not None else None
step = str_slice.step
return (start, stop, step)


# Regex for date, time, separator and timezone components
DATE_RE = r"(?P<date>\d{1,4}[-/.]\d{1,2}[-/.]\d{1,4}|\d{8})"
SEP_RE = r"(?P<sep>\s|T)"
Expand Down
Loading
Loading