Skip to content

Commit ca01346

Browse files
committed
Merge branch 'main' into create_fromnative_daft
2 parents c3a8c4d + dbc7ecf commit ca01346

File tree

12 files changed

+119
-111
lines changed

12 files changed

+119
-111
lines changed

.github/workflows/extremes.yml

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -152,8 +152,10 @@ jobs:
152152
uv pip install pandas --pre --index https://pypi.anaconda.org/scientific-python-nightly-wheels/simple --system -U
153153
- name: install pyarrow nightly
154154
run: |
155-
uv pip uninstall pyarrow --system
156-
uv pip install pyarrow --pre --index https://pypi.fury.io/arrow-nightlies/ --system -U
155+
# commented out nightly whilst it fails to install
156+
uv pip install -U pyarrow --system
157+
# uv pip uninstall pyarrow --system
158+
# uv pip install pyarrow --pre --index https://pypi.fury.io/arrow-nightlies/ --system -U
157159
- name: install numpy nightly
158160
run: |
159161
uv pip uninstall numpy --system
@@ -172,7 +174,7 @@ jobs:
172174
run: |
173175
DEPS=$(uv pip freeze)
174176
echo "$DEPS" | grep 'pandas.*dev'
175-
echo "$DEPS" | grep 'pyarrow.*dev'
177+
# echo "$DEPS" | grep 'pyarrow.*dev'
176178
echo "$DEPS" | grep 'numpy.*dev'
177179
echo "$DEPS" | grep 'dask.*@'
178180
- name: Run pytest

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,7 @@ Join the party!
181181
- [plotly](https://plotly.com)
182182
- [pointblank](https://github.com/posit-dev/pointblank)
183183
- [pymarginaleffects](https://github.com/vincentarelbundock/pymarginaleffects)
184+
- [pyreadstat](https://github.com/Roche/pyreadstat)
184185
- [py-shiny](https://github.com/posit-dev/py-shiny)
185186
- [rio](https://github.com/rio-labs/rio)
186187
- [scikit-lego](https://github.com/koaning/scikit-lego)

docs/ecosystem.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ for their dataframe interoperability needs:
1818
* [plotly](https://github.com/plotly/plotly.py)
1919
* [pointblank](https://github.com/posit-dev/pointblank)
2020
* [pymarginaleffects](https://github.com/vincentarelbundock/pymarginaleffects)
21+
* [pyreadstat](https://github.com/Roche/pyreadstat)
2122
* [py-shiny](https://github.com/posit-dev/py-shiny)
2223
* [rio](https://github.com/rio-labs/rio)
2324
* [scikit-lego](https://github.com/koaning/scikit-lego)

narwhals/_dask/expr.py

Lines changed: 63 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -186,105 +186,92 @@ def _with_alias_output_names(self, func: AliasNames | None, /) -> Self:
186186
scalar_kwargs=self._scalar_kwargs,
187187
)
188188

189-
def __add__(self, other: Any) -> Self:
190-
return self._with_callable(
191-
lambda expr, other: expr.__add__(other), "__add__", other=other
189+
def _with_binary(
190+
self,
191+
call: Callable[[dx.Series, Any], dx.Series],
192+
name: str,
193+
other: Any,
194+
*,
195+
reverse: bool = False,
196+
) -> Self:
197+
result = self._with_callable(
198+
lambda expr, other: call(expr, other), name, other=other
192199
)
200+
if reverse:
201+
result = result.alias("literal")
202+
return result
193203

194-
def __sub__(self, other: Any) -> Self:
195-
return self._with_callable(
196-
lambda expr, other: expr.__sub__(other), "__sub__", other=other
204+
def _binary_op(self, op_name: str, other: Any) -> Self:
205+
return self._with_binary(
206+
lambda expr, other: getattr(expr, op_name)(other), op_name, other
197207
)
198208

199-
def __rsub__(self, other: Any) -> Self:
200-
return self._with_callable(
201-
lambda expr, other: other - expr, "__rsub__", other=other
202-
).alias("literal")
209+
def _reverse_binary_op(
210+
self, op_name: str, operator_func: Callable[..., dx.Series], other: Any
211+
) -> Self:
212+
return self._with_binary(
213+
lambda expr, other: operator_func(other, expr), op_name, other, reverse=True
214+
)
215+
216+
def __add__(self, other: Any) -> Self:
217+
return self._binary_op("__add__", other)
218+
219+
def __sub__(self, other: Any) -> Self:
220+
return self._binary_op("__sub__", other)
203221

204222
def __mul__(self, other: Any) -> Self:
205-
return self._with_callable(
206-
lambda expr, other: expr.__mul__(other), "__mul__", other=other
207-
)
223+
return self._binary_op("__mul__", other)
208224

209225
def __truediv__(self, other: Any) -> Self:
210-
return self._with_callable(
211-
lambda expr, other: expr.__truediv__(other), "__truediv__", other=other
212-
)
213-
214-
def __rtruediv__(self, other: Any) -> Self:
215-
return self._with_callable(
216-
lambda expr, other: other / expr, "__rtruediv__", other=other
217-
).alias("literal")
226+
return self._binary_op("__truediv__", other)
218227

219228
def __floordiv__(self, other: Any) -> Self:
220-
return self._with_callable(
221-
lambda expr, other: expr.__floordiv__(other), "__floordiv__", other=other
222-
)
223-
224-
def __rfloordiv__(self, other: Any) -> Self:
225-
return self._with_callable(
226-
lambda expr, other: other // expr, "__rfloordiv__", other=other
227-
).alias("literal")
229+
return self._binary_op("__floordiv__", other)
228230

229231
def __pow__(self, other: Any) -> Self:
230-
return self._with_callable(
231-
lambda expr, other: expr.__pow__(other), "__pow__", other=other
232-
)
233-
234-
def __rpow__(self, other: Any) -> Self:
235-
return self._with_callable(
236-
lambda expr, other: other**expr, "__rpow__", other=other
237-
).alias("literal")
232+
return self._binary_op("__pow__", other)
238233

239234
def __mod__(self, other: Any) -> Self:
240-
return self._with_callable(
241-
lambda expr, other: expr.__mod__(other), "__mod__", other=other
242-
)
235+
return self._binary_op("__mod__", other)
243236

244-
def __rmod__(self, other: Any) -> Self:
245-
return self._with_callable(
246-
lambda expr, other: other % expr, "__rmod__", other=other
247-
).alias("literal")
237+
def __eq__(self, other: object) -> Self: # type: ignore[override]
238+
return self._binary_op("__eq__", other)
248239

249-
def __eq__(self, other: DaskExpr) -> Self: # type: ignore[override]
250-
return self._with_callable(
251-
lambda expr, other: expr.__eq__(other), "__eq__", other=other
252-
)
240+
def __ne__(self, other: object) -> Self: # type: ignore[override]
241+
return self._binary_op("__ne__", other)
253242

254-
def __ne__(self, other: DaskExpr) -> Self: # type: ignore[override]
255-
return self._with_callable(
256-
lambda expr, other: expr.__ne__(other), "__ne__", other=other
257-
)
243+
def __ge__(self, other: Any) -> Self:
244+
return self._binary_op("__ge__", other)
258245

259-
def __ge__(self, other: DaskExpr | Any) -> Self:
260-
return self._with_callable(
261-
lambda expr, other: expr.__ge__(other), "__ge__", other=other
262-
)
246+
def __gt__(self, other: Any) -> Self:
247+
return self._binary_op("__gt__", other)
263248

264-
def __gt__(self, other: DaskExpr) -> Self:
265-
return self._with_callable(
266-
lambda expr, other: expr.__gt__(other), "__gt__", other=other
267-
)
249+
def __le__(self, other: Any) -> Self:
250+
return self._binary_op("__le__", other)
268251

269-
def __le__(self, other: DaskExpr) -> Self:
270-
return self._with_callable(
271-
lambda expr, other: expr.__le__(other), "__le__", other=other
272-
)
252+
def __lt__(self, other: Any) -> Self:
253+
return self._binary_op("__lt__", other)
273254

274-
def __lt__(self, other: DaskExpr) -> Self:
275-
return self._with_callable(
276-
lambda expr, other: expr.__lt__(other), "__lt__", other=other
277-
)
255+
def __and__(self, other: Any) -> Self:
256+
return self._binary_op("__and__", other)
278257

279-
def __and__(self, other: DaskExpr | Any) -> Self:
280-
return self._with_callable(
281-
lambda expr, other: expr.__and__(other), "__and__", other=other
282-
)
258+
def __or__(self, other: Any) -> Self:
259+
return self._binary_op("__or__", other)
283260

284-
def __or__(self, other: DaskExpr) -> Self:
285-
return self._with_callable(
286-
lambda expr, other: expr.__or__(other), "__or__", other=other
287-
)
261+
def __rsub__(self, other: Any) -> Self:
262+
return self._reverse_binary_op("__rsub__", lambda a, b: a - b, other)
263+
264+
def __rtruediv__(self, other: Any) -> Self:
265+
return self._reverse_binary_op("__rtruediv__", lambda a, b: a / b, other)
266+
267+
def __rfloordiv__(self, other: Any) -> Self:
268+
return self._reverse_binary_op("__rfloordiv__", lambda a, b: a // b, other)
269+
270+
def __rpow__(self, other: Any) -> Self:
271+
return self._reverse_binary_op("__rpow__", lambda a, b: a**b, other)
272+
273+
def __rmod__(self, other: Any) -> Self:
274+
return self._reverse_binary_op("__rmod__", lambda a, b: a % b, other)
288275

289276
def __invert__(self) -> Self:
290277
return self._with_callable(lambda expr: expr.__invert__(), "__invert__")

narwhals/_ibis/dataframe.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -337,12 +337,12 @@ def sort(self, *by: str, descending: bool | Sequence[bool], nulls_last: bool) ->
337337
if isinstance(descending, bool):
338338
descending = [descending for _ in range(len(by))]
339339

340-
sort_cols = []
340+
sort_cols: list[Any] = []
341341

342342
for i in range(len(by)):
343343
direction_fn = ibis.desc if descending[i] else ibis.asc
344344
col = direction_fn(by[i], nulls_first=not nulls_last)
345-
sort_cols.append(cast("ir.Column", col))
345+
sort_cols.append(col)
346346

347347
return self._with_native(self.native.order_by(*sort_cols))
348348

narwhals/_ibis/expr.py

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,7 @@ def _std(expr: ir.NumericColumn, ddof: int) -> ir.Value:
227227
return expr.std(how="sample")
228228
n_samples = expr.count()
229229
std_pop = expr.std(how="pop")
230-
ddof_lit = cast("ir.IntegerScalar", ibis.literal(ddof))
230+
ddof_lit = lit(ddof)
231231
return std_pop * n_samples.sqrt() / (n_samples - ddof_lit).sqrt()
232232

233233
return self._with_callable(lambda expr: _std(expr, ddof))
@@ -240,7 +240,7 @@ def _var(expr: ir.NumericColumn, ddof: int) -> ir.Value:
240240
return expr.var(how="sample")
241241
n_samples = expr.count()
242242
var_pop = expr.var(how="pop")
243-
ddof_lit = cast("ir.IntegerScalar", ibis.literal(ddof))
243+
ddof_lit = lit(ddof)
244244
return var_pop * n_samples / (n_samples - ddof_lit)
245245

246246
return self._with_callable(lambda expr: _var(expr, ddof))
@@ -290,35 +290,33 @@ def is_unique(self) -> Self:
290290
)
291291

292292
def rank(self, method: RankMethod, *, descending: bool) -> Self:
293-
def _rank(expr: ir.Column) -> ir.Column:
293+
def _rank(expr: ir.Column) -> ir.Value:
294294
order_by = next(self._sort(expr, descending=[descending], nulls_last=[True]))
295295
window = ibis.window(order_by=order_by)
296296

297297
if method == "dense":
298298
rank_ = order_by.dense_rank()
299299
elif method == "ordinal":
300-
rank_ = cast("ir.IntegerColumn", ibis.row_number().over(window))
300+
rank_ = ibis.row_number().over(window)
301301
else:
302302
rank_ = order_by.rank()
303303

304304
# Ibis uses 0-based ranking. Add 1 to match polars 1-based rank.
305-
rank_ = rank_ + cast("ir.IntegerValue", lit(1))
305+
rank_ = rank_ + lit(1)
306306

307307
# For "max" and "average", adjust using the count of rows in the partition.
308308
if method == "max":
309309
# Define a window partitioned by expr (i.e. each distinct value)
310310
partition = ibis.window(group_by=[expr])
311-
cnt = cast("ir.IntegerValue", expr.count().over(partition))
312-
rank_ = rank_ + cnt - cast("ir.IntegerValue", lit(1))
311+
cnt = expr.count().over(partition)
312+
rank_ = rank_ + cnt - lit(1)
313313
elif method == "average":
314314
partition = ibis.window(group_by=[expr])
315-
cnt = cast("ir.IntegerValue", expr.count().over(partition))
316-
avg = cast(
317-
"ir.NumericValue", (cnt - cast("ir.IntegerScalar", lit(1))) / lit(2.0)
318-
)
315+
cnt = expr.count().over(partition)
316+
avg = cast("ir.NumericValue", (cnt - lit(1)) / lit(2.0))
319317
rank_ = rank_ + avg
320318

321-
return cast("ir.Column", ibis.cases((expr.notnull(), rank_)))
319+
return ibis.cases((expr.notnull(), rank_))
322320

323321
return self._with_callable(_rank)
324322

narwhals/_ibis/namespace.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import operator
44
from functools import reduce
55
from itertools import chain
6-
from typing import TYPE_CHECKING, Any, cast
6+
from typing import TYPE_CHECKING, Any
77

88
import ibis
99
import ibis.expr.types as ir
@@ -88,8 +88,7 @@ def func(df: IbisLazyFrame) -> list[ir.Value]:
8888
for col in cols_casted[1:]:
8989
result = result + separator + col
9090
else:
91-
sep = cast("ir.StringValue", lit(separator))
92-
result = sep.join(cols_casted)
91+
result = lit(separator).join(cols_casted)
9392

9493
return [result]
9594

narwhals/_ibis/utils.py

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from __future__ import annotations
22

33
from functools import lru_cache
4-
from typing import TYPE_CHECKING, Any, Literal, cast
4+
from typing import TYPE_CHECKING, Any, Literal, cast, overload
55

66
import ibis
77
import ibis.expr.datatypes as ibis_dtypes
@@ -23,8 +23,27 @@
2323
from narwhals.dtypes import DType
2424
from narwhals.typing import IntoDType, PythonLiteral
2525

26-
lit = ibis.literal
27-
"""Alias for `ibis.literal`."""
26+
Incomplete: TypeAlias = Any
27+
"""Marker for upstream issues."""
28+
29+
30+
@overload
31+
def lit(value: bool, dtype: None = ...) -> ir.BooleanScalar: ... # noqa: FBT001
32+
@overload
33+
def lit(value: int, dtype: None = ...) -> ir.IntegerScalar: ...
34+
@overload
35+
def lit(value: float, dtype: None = ...) -> ir.FloatingScalar: ...
36+
@overload
37+
def lit(value: str, dtype: None = ...) -> ir.StringScalar: ...
38+
@overload
39+
def lit(value: PythonLiteral | ir.Value, dtype: None = ...) -> ir.Scalar: ...
40+
@overload
41+
def lit(value: Any, dtype: Any) -> Incomplete: ...
42+
def lit(value: Any, dtype: Any | None = None) -> Incomplete:
43+
"""Alias for `ibis.literal`."""
44+
literal: Incomplete = ibis.literal
45+
return literal(value, dtype)
46+
2847

2948
BucketUnit: TypeAlias = Literal[
3049
"years",
@@ -231,11 +250,11 @@ def timedelta_to_ibis_interval(td: timedelta) -> ibis.expr.types.temporal.Interv
231250
def function(name: str, *args: ir.Value | PythonLiteral) -> ir.Value:
232251
# Workaround SQL vs Ibis differences.
233252
if name == "row_number":
234-
return ibis.row_number() + 1 # pyright: ignore[reportOperatorIssue]
253+
return ibis.row_number() + lit(1)
235254
if name == "least":
236-
return ibis.least(*args) # pyright: ignore[reportOperatorIssue]
255+
return ibis.least(*args)
237256
if name == "greatest":
238-
return ibis.greatest(*args) # pyright: ignore[reportOperatorIssue]
257+
return ibis.greatest(*args)
239258
expr = args[0]
240259
if name == "var_pop":
241260
return cast("ir.NumericColumn", expr).var(how="pop")

narwhals/_utils.py

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -603,11 +603,7 @@ def backend_version(implementation: Implementation, /) -> tuple[int, ...]:
603603
impl = implementation
604604
module_name = _IMPLEMENTATION_TO_MODULE_NAME.get(impl, impl.value)
605605
native_namespace = _import_native_namespace(module_name)
606-
if impl.is_polars():
607-
from importlib import metadata
608-
609-
into_version = metadata.version("polars")
610-
elif impl.is_sqlframe():
606+
if impl.is_sqlframe():
611607
import sqlframe._version
612608

613609
into_version = sqlframe._version

tests/expr_and_series/map_batches_test.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
from __future__ import annotations
22

3+
import pytest
4+
35
import narwhals as nw
46
from tests.utils import ConstructorEager, assert_equal_data
57

@@ -12,7 +14,12 @@ def test_map_batches_expr(constructor_eager: ConstructorEager) -> None:
1214
assert_equal_data(expected, {"a": [2, 3, 4], "b": [5, 6, 7]})
1315

1416

15-
def test_map_batches_expr_numpy(constructor_eager: ConstructorEager) -> None:
17+
def test_map_batches_expr_numpy(
18+
constructor_eager: ConstructorEager, request: pytest.FixtureRequest
19+
) -> None:
20+
if "polars" in str(constructor_eager):
21+
# https://github.com/narwhals-dev/narwhals/issues/2995
22+
request.applymarker(pytest.mark.xfail(strict=False))
1623
df = nw.from_native(constructor_eager(data))
1724
expected = df.select(
1825
nw.col("a")

0 commit comments

Comments
 (0)