Skip to content
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
0a95012
chore(typing): Add alias to identify holes
dangotbanned Feb 28, 2025
b752965
feat(typing): Add all methods used in `expr.py`
dangotbanned Feb 28, 2025
2b87d80
fix(typing): `map_batches` return type
dangotbanned Feb 28, 2025
44f3c6f
feat(typing): add `Expr` namespaces
dangotbanned Feb 28, 2025
01ea1b3
refactor: mark `ewm_mean` as unstable
dangotbanned Feb 28, 2025
26b967f
fix: start adding `NotImplementedError`s
dangotbanned Feb 28, 2025
39d6ced
fix(typing): add missing `[type-var]`
dangotbanned Feb 28, 2025
4ca309c
fix: permit `over(keys=...)` widening
dangotbanned Feb 28, 2025
0a93329
feat: replace boilerplate w/ `not_implemented` marker
dangotbanned Feb 28, 2025
6a138dd
refactor: replace the other ones
dangotbanned Feb 28, 2025
6b57795
feat(typing): mark all missing `DaskExpr`
dangotbanned Feb 28, 2025
7622439
feat(typing): mark all missing `DuckDBExpr`
dangotbanned Feb 28, 2025
ccd68f5
feat(typing): mark all missing `SparkLikeExpr`
dangotbanned Feb 28, 2025
a1a1461
fix(typing): resolve more out-of-sync signatures
dangotbanned Feb 28, 2025
f8ef0c7
ignore pretty reasonable warning
dangotbanned Feb 28, 2025
b2c19e7
fix(typing): remove defaults from protocol
dangotbanned Feb 28, 2025
6131455
test: add `test_not_implemented`
dangotbanned Feb 28, 2025
50bcef0
feat: kinda resolve the `property` case
dangotbanned Feb 28, 2025
427c8d9
docs: add "Returns"
dangotbanned Feb 28, 2025
fc02399
test: skip `duckdb` if not available
dangotbanned Feb 28, 2025
31f3506
fix: unbreak doctest
dangotbanned Feb 28, 2025
4c6b639
Merge branch 'main' into compliant-expr-spec
dangotbanned Mar 1, 2025
e599caa
Merge branch 'main' into compliant-expr-spec
dangotbanned Mar 1, 2025
85a691c
Merge remote-tracking branch 'upstream/main' into compliant-expr-spec
dangotbanned Mar 2, 2025
d3bc414
refactor: Convert `DaskExpr.replace_strict` into `not_implemented`
dangotbanned Mar 2, 2025
4804ab4
Merge remote-tracking branch 'upstream/main' into compliant-expr-spec
dangotbanned Mar 2, 2025
d8e32c0
feat(DRAFT): Add descriptor version `not_implemented_alt`
dangotbanned Mar 3, 2025
85df827
typo
dangotbanned Mar 3, 2025
88a073e
test: coverage for `@classmethod`-like?
dangotbanned Mar 3, 2025
6784a12
feat(DRAFT): Improved `@property` support
dangotbanned Mar 3, 2025
68b37d8
feat(DRAFT): Add a second option for `@property`
dangotbanned Mar 3, 2025
50a9376
fix: coverage, fix alias
dangotbanned Mar 3, 2025
f64b466
Merge remote-tracking branch 'upstream/main' into compliant-expr-spec
dangotbanned Mar 3, 2025
fc0071c
Merge remote-tracking branch 'upstream/main' into compliant-expr-spec
dangotbanned Mar 4, 2025
990ee7b
docs(DRAFT): Add IDE benefit demo video
dangotbanned Mar 4, 2025
f125b97
refactor: Fully replace original `not_implemented` callable
dangotbanned Mar 4, 2025
a076349
chore(typing): ignore pyright
dangotbanned Mar 4, 2025
6928c1e
docs: Handle `not_implemented` in `generate_backend_completeness`
dangotbanned Mar 4, 2025
fae840f
Merge remote-tracking branch 'upstream/main' into compliant-expr-spec
dangotbanned Mar 4, 2025
b4caa96
Merge branch 'main' into compliant-expr-spec
dangotbanned Mar 4, 2025
a124302
chore: todo -> to-done
dangotbanned Mar 4, 2025
7eab760
Merge branch 'compliant-expr-spec' of https://github.com/narwhals-dev…
dangotbanned Mar 4, 2025
233ba14
style: tidy up whitespace
dangotbanned Mar 4, 2025
3b24293
Merge branch 'main' into compliant-expr-spec
dangotbanned Mar 4, 2025
7400a22
refactor: move `@deprecated` to `utils`
dangotbanned Mar 4, 2025
400bbe4
docs: Finalize `unstable`, `not_implemented`
dangotbanned Mar 5, 2025
885c16b
docs: add missing import
dangotbanned Mar 5, 2025
7962cbb
revert: remove video
dangotbanned Mar 5, 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
19 changes: 14 additions & 5 deletions narwhals/_arrow/expr.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from typing import Any
from typing import Callable
from typing import Literal
from typing import Mapping
from typing import Sequence

from narwhals._arrow.expr_cat import ArrowExprCatNamespace
Expand All @@ -21,6 +22,7 @@
from narwhals.exceptions import ColumnNotFoundError
from narwhals.typing import CompliantExpr
from narwhals.utils import Implementation
from narwhals.utils import not_implemented

if TYPE_CHECKING:
from typing_extensions import Self
Expand Down Expand Up @@ -259,7 +261,7 @@ def var(self: Self, ddof: int) -> Self:
def skew(self: Self) -> Self:
return reuse_series_implementation(self, "skew", returns_scalar=True)

def cast(self: Self, dtype: DType) -> Self:
def cast(self: Self, dtype: DType | type[DType]) -> Self:
Comment thread
dangotbanned marked this conversation as resolved.
return reuse_series_implementation(self, "cast", dtype=dtype)

def abs(self: Self) -> Self:
Expand Down Expand Up @@ -382,7 +384,11 @@ def unique(self: Self) -> Self:
return reuse_series_implementation(self, "unique", maintain_order=False)

def replace_strict(
self: Self, old: Sequence[Any], new: Sequence[Any], *, return_dtype: DType | None
self: Self,
old: Sequence[Any] | Mapping[Any, Any],
new: Sequence[Any],
*,
return_dtype: DType | type[DType] | None,
) -> Self:
return reuse_series_implementation(
self, "replace_strict", old=old, new=new, return_dtype=return_dtype
Expand Down Expand Up @@ -414,7 +420,7 @@ def clip(self: Self, lower_bound: Any | None, upper_bound: Any | None) -> Self:
self, "clip", lower_bound=lower_bound, upper_bound=upper_bound
)

def over(self: Self, keys: list[str], kind: ExprKind) -> Self:
def over(self: Self, keys: Sequence[str], kind: ExprKind) -> Self:
if not is_scalar_like(kind):
msg = "Only aggregation or literal operations are supported in `over` context for PyArrow."
raise NotImplementedError(msg)
Expand All @@ -431,8 +437,9 @@ def func(df: ArrowDataFrame) -> list[ArrowSeries]:
raise NotImplementedError(msg)

tmp = df.group_by(*keys, drop_null_keys=False).agg(self)
on = list(keys)
tmp = df.simple_select(*keys).join(
tmp, how="left", left_on=keys, right_on=keys, suffix="_right"
tmp, how="left", left_on=on, right_on=on, suffix="_right"
)
return [tmp[alias] for alias in aliases]

Expand All @@ -452,7 +459,7 @@ def mode(self: Self) -> Self:
def map_batches(
self: Self,
function: Callable[[Any], Any],
return_dtype: DType | None,
return_dtype: DType | type[DType] | None,
) -> Self:
def func(df: ArrowDataFrame) -> list[ArrowSeries]:
input_series_list = self._call(df)
Expand Down Expand Up @@ -576,6 +583,8 @@ def rank(
self, "rank", method=method, descending=descending
)

ewm_mean = not_implemented("ewm_mean")

@property
def dt(self: Self) -> ArrowExprDateTimeNamespace:
return ArrowExprDateTimeNamespace(self)
Expand Down
2 changes: 1 addition & 1 deletion narwhals/_arrow/series.py
Original file line number Diff line number Diff line change
Expand Up @@ -517,7 +517,7 @@ def is_null(self: Self) -> Self:
def is_nan(self: Self) -> Self:
return self._from_native_series(pc.is_nan(self._native_series))

def cast(self: Self, dtype: DType) -> Self:
def cast(self: Self, dtype: DType | type[DType]) -> Self:
ser = self._native_series
data_type = narwhals_to_native_dtype(dtype, self._version)
return self._from_native_series(pc.cast(ser, data_type))
Expand Down
33 changes: 30 additions & 3 deletions narwhals/_dask/expr.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from typing import Any
from typing import Callable
from typing import Literal
from typing import Mapping
from typing import Sequence

from narwhals._dask.expr_dt import DaskExprDateTimeNamespace
Expand All @@ -20,6 +21,7 @@
from narwhals.typing import CompliantExpr
from narwhals.utils import Implementation
from narwhals.utils import generate_temporary_column_name
from narwhals.utils import not_implemented

if TYPE_CHECKING:
try:
Expand Down Expand Up @@ -382,7 +384,11 @@ def drop_nulls(self: Self) -> Self:
return self._from_call(lambda _input: _input.dropna(), "drop_nulls")

def replace_strict(
self: Self, old: Sequence[Any], new: Sequence[Any], *, return_dtype: DType | None
self: Self,
old: Sequence[Any] | Mapping[Any, Any],
new: Sequence[Any],
*,
return_dtype: DType | type[DType] | None,
) -> Self:
msg = "`replace_strict` is not yet supported for Dask expressions"
raise NotImplementedError(msg)
Expand Down Expand Up @@ -527,7 +533,7 @@ def null_count(self: Self) -> Self:
lambda _input: _input.isna().sum().to_series(), "null_count"
)

def over(self: Self, keys: list[str], kind: ExprKind) -> Self:
def over(self: Self, keys: Sequence[str], kind: ExprKind) -> Self:
def func(df: DaskLazyFrame) -> list[Any]:
output_names, aliases = evaluate_output_names_and_aliases(self, df, [])
if overlap := set(output_names).intersection(keys):
Expand All @@ -540,9 +546,10 @@ def func(df: DaskLazyFrame) -> list[Any]:
raise NotImplementedError(msg)
if df._native_frame.npartitions == 1: # pragma: no cover
tmp = df.group_by(*keys, drop_null_keys=False).agg(self)
on = list(keys)
tmp_native = (
df.simple_select(*keys)
.join(tmp, how="left", left_on=keys, right_on=keys, suffix="_right")
.join(tmp, how="left", left_on=on, right_on=on, suffix="_right")
._native_frame
)
return [tmp_native[name] for name in aliases]
Expand Down Expand Up @@ -586,3 +593,23 @@ def dt(self: Self) -> DaskExprDateTimeNamespace:
@property
def name(self: Self) -> DaskExprNameNamespace:
return DaskExprNameNamespace(self)

arg_min = not_implemented("arg_min")
arg_max = not_implemented("arg_max")
arg_true = not_implemented("arg_true")
head = not_implemented("head")
tail = not_implemented("tail")
mode = not_implemented("mode")
sort = not_implemented("sort")
rank = not_implemented("rank")
sample = not_implemented("sample")

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@camriddell camriddell Mar 3, 2025

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a particular reason for this implementation over a not_implemented decorator? There could definitely be a technical detail that I missed.

My thoughts boil down to

  1. Eliminate the repetition of the name, we wouldn't need to repeat the name as both class variable and as a string
  2. Searchability, I often use simple greps through code bases so if I was trying to find the arg_min function/method I would use the specific pattern 'def arg_min' which wouldn't work here.

alternatively, we could have not_implemented return a descriptor- this would keep the usage pattern the exact same but let us avoid needing to repeat the name.

Here's a couple of examples for the current impl vs decorator vs descriptor.

# current
class T:
    arg_min = not_implemented("arg_min")

# decorator pattern
class T:
    @not_implemented
    def arg_min(self):
        ... # elipsis to indicate that this just hasn't been written yet

# descriptor
class T:
    arg_min = not_implemented()

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@camriddell glad to see you here!

I will try to respond in a bit more detail tomorrow, but on this point specifically:

Eliminate the repetition of the name, we wouldn't need to repeat the name as both class variable and as a string

I've got a descriptor implementation that solves that issue
#2119 (comment)

I'm leaning towards that most strongly at the moment - but it is mainly used in tests only at the moment (not_implemented_alt)

@dangotbanned dangotbanned Mar 4, 2025

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Searchability, I often use simple greps through code bases so if I was trying to find the arg_min function/method I would use the specific pattern 'def arg_min' which wouldn't work here.

@camriddell I've added this (no talking) video to try and show off what the improved typing lets you do instead of grepping the code base:

2025-03-04.13-13-19.-.Expr-Not.Implemented-30-1.mp4

Important

We can now statically know what is and is not implemented in VSCode
I imagine other IDEs probably have a similar functionality as well

Is there a particular reason for this implementation over a not_implemented decorator? There could definitely be a technical detail that I missed.

This gives us a clear line between not/implemented while also being very compact.

Adding decorated signatures for just 4 methods creates this diff:

diff --git a/narwhals/_dask/expr.py b/narwhals/_dask/expr.py
index a1bfb0b8..41c86132 100644
--- a/narwhals/_dask/expr.py
+++ b/narwhals/_dask/expr.py
@@ -25,6 +25,7 @@ from narwhals.utils import Implementation
 from narwhals.utils import generate_temporary_column_name
 from narwhals.utils import not_implemented
 from narwhals.utils import not_implemented_alt
+from narwhals.utils import unstable  # <------------------------ using just for syntactically valid decorator demo
 
 if TYPE_CHECKING:
     try:
@@ -615,10 +616,27 @@ class DaskExpr(CompliantExpr["DaskLazyFrame", "dx.Series"]):  # pyright: ignore[
     sample = not_implemented("sample")
     map_batches = not_implemented("map_batches")
     ewm_mean = not_implemented("ewm_mean")
-    rolling_sum = not_implemented("rolling_sum")
-    rolling_mean = not_implemented("rolling_mean")
-    rolling_var = not_implemented("rolling_var")
-    rolling_std = not_implemented("rolling_std")
+
+    @unstable
+    def rolling_mean(
+        self, window_size: int, *, min_samples: int | None, center: bool
+    ) -> Self: ...
+
+    @unstable
+    def rolling_std(
+        self, window_size: int, *, min_samples: int | None, center: bool, ddof: int
+    ) -> Self: ...
+
+    @unstable
+    def rolling_sum(
+        self, window_size: int, *, min_samples: int | None, center: bool
+    ) -> Self: ...
+
+    @unstable
+    def rolling_var(
+        self, window_size: int, *, min_samples: int | None, center: bool, ddof: int
+    ) -> Self: ...
+
     gather_every = not_implemented("gather_every")
     replace_strict = not_implemented_alt()
 

They now all fail type checking - because ... was misused:

>>> mypy
narwhals/_dask/expr.py:621: error: Missing return statement  [empty-body]
        def rolling_mean(
        ^
narwhals/_dask/expr.py:621: note: If the method is meant to be abstract, use @abc.abstractmethod
narwhals/_dask/expr.py:626: error: Missing return statement  [empty-body]
        def rolling_std(
        ^
narwhals/_dask/expr.py:626: note: If the method is meant to be abstract, use @abc.abstractmethod
narwhals/_dask/expr.py:631: error: Missing return statement  [empty-body]
        def rolling_sum(
        ^
narwhals/_dask/expr.py:631: note: If the method is meant to be abstract, use @abc.abstractmethod
narwhals/_dask/expr.py:636: error: Missing return statement  [empty-body]
        def rolling_var(
        ^
narwhals/_dask/expr.py:636: note: If the method is meant to be abstract, use @abc.abstractmethod
Found 4 errors in 1 file (checked 348 source files)

But lets say that wasn't an issue.
We now have the additional maintenance burden of keeping the signature annotations in sync - for a no-op 😞

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@camriddell
the big thing that's changed since your work on #1807 (for which I'm super grateful 😍) is the expanded CompliantExpr protocol in (https://github.com/narwhals-dev/narwhals/pull/2119/files#diff-705564b396213c63fc71e45d1bf5e3836b72b13d290ffa52b442fcaf05625b4d)

In my big PR description ramble I mentioned that this was where the PR started.
It just so happened to tie in really well with your not_implemented idea - but with the goalposts moved this tweak on it seemed like the best compromise to me personally

This comment was marked as outdated.

map_batches = not_implemented("map_batches")
ewm_mean = not_implemented("ewm_mean")
rolling_sum = not_implemented("rolling_sum")
rolling_mean = not_implemented("rolling_mean")
rolling_var = not_implemented("rolling_var")
rolling_std = not_implemented("rolling_std")
gather_every = not_implemented("gather_every")

cat = not_implemented("cat", is_property=True)
list = not_implemented("list", is_property=True)
35 changes: 35 additions & 0 deletions narwhals/_duckdb/expr.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
from narwhals._expression_parsing import ExprKind
from narwhals.typing import CompliantExpr
from narwhals.utils import Implementation
from narwhals.utils import not_implemented

if TYPE_CHECKING:
import duckdb
Expand Down Expand Up @@ -476,3 +477,37 @@ def name(self: Self) -> DuckDBExprNameNamespace:
@property
def list(self: Self) -> DuckDBExprListNamespace:
return DuckDBExprListNamespace(self)

arg_min = not_implemented("arg_min")
arg_max = not_implemented("arg_max")
arg_true = not_implemented("arg_true")
head = not_implemented("head")
tail = not_implemented("tail")
mode = not_implemented("mode")
sort = not_implemented("sort")
rank = not_implemented("rank")
sample = not_implemented("sample")
map_batches = not_implemented("map_batches")
ewm_mean = not_implemented("ewm_mean")
rolling_sum = not_implemented("rolling_sum")
rolling_mean = not_implemented("rolling_mean")
rolling_var = not_implemented("rolling_var")
rolling_std = not_implemented("rolling_std")
gather_every = not_implemented("gather_every")

drop_nulls = not_implemented("drop_nulls")
diff = not_implemented("diff")
unique = not_implemented("unique")
shift = not_implemented("shift")
is_unique = not_implemented("is_unique")
is_first_distinct = not_implemented("is_first_distinct")
is_last_distinct = not_implemented("is_last_distinct")
cum_sum = not_implemented("cum_sum")
cum_count = not_implemented("cum_count")
cum_min = not_implemented("cum_min")
cum_max = not_implemented("cum_max")
cum_prod = not_implemented("cum_prod")
replace_strict = not_implemented("replace_strict")
over = not_implemented("over")

cat = not_implemented("cat", is_property=True)
14 changes: 10 additions & 4 deletions narwhals/_pandas_like/expr.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from typing import Any
from typing import Callable
from typing import Literal
from typing import Mapping
from typing import Sequence

from narwhals._expression_parsing import ExprKind
Expand Down Expand Up @@ -360,7 +361,11 @@ def drop_nulls(self: Self) -> Self:
return reuse_series_implementation(self, "drop_nulls")

def replace_strict(
self: Self, old: Sequence[Any], new: Sequence[Any], *, return_dtype: DType | None
self: Self,
old: Sequence[Any] | Mapping[Any, Any],
new: Sequence[Any],
*,
return_dtype: DType | type[DType] | None,
) -> Self:
return reuse_series_implementation(
self, "replace_strict", old=old, new=new, return_dtype=return_dtype
Expand Down Expand Up @@ -426,7 +431,7 @@ def alias_output_names(names: Sequence[str]) -> Sequence[str]:
call_kwargs=self._call_kwargs,
)

def over(self: Self, keys: list[str], kind: ExprKind) -> Self:
def over(self: Self, keys: Sequence[str], kind: ExprKind) -> Self:
if (
is_simple_aggregation(self)
and (function_name := re.sub(r"(\w+->)", "", self._function_name))
Expand Down Expand Up @@ -497,8 +502,9 @@ def func(df: PandasLikeDataFrame) -> list[PandasLikeSeries]:
raise NotImplementedError(msg)

tmp = df.group_by(*keys, drop_null_keys=False).agg(self)
on = list(keys)
tmp = df.simple_select(*keys).join(
tmp, how="left", left_on=keys, right_on=keys, suffix="_right"
tmp, how="left", left_on=on, right_on=on, suffix="_right"
)
return [tmp[name] for name in aliases]

Expand Down Expand Up @@ -556,7 +562,7 @@ def mode(self: Self) -> Self:
def map_batches(
self: Self,
function: Callable[[Any], Any],
return_dtype: DType | None,
return_dtype: DType | type[DType] | None,
) -> Self:
def func(df: PandasLikeDataFrame) -> list[PandasLikeSeries]:
input_series_list = self._call(df)
Expand Down
37 changes: 36 additions & 1 deletion narwhals/_spark_like/expr.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from narwhals.dependencies import get_pyspark
from narwhals.typing import CompliantExpr
from narwhals.utils import Implementation
from narwhals.utils import not_implemented
from narwhals.utils import parse_version

if TYPE_CHECKING:
Expand Down Expand Up @@ -471,7 +472,7 @@ def _n_unique(_input: Column) -> Column:

return self._from_call(_n_unique, "n_unique")

def over(self: Self, keys: list[str], kind: ExprKind) -> Self:
def over(self: Self, keys: Sequence[str], kind: ExprKind) -> Self:
def func(df: SparkLikeLazyFrame) -> list[Column]:
return [expr.over(self._Window.partitionBy(*keys)) for expr in self._call(df)]

Expand Down Expand Up @@ -511,3 +512,37 @@ def dt(self: Self) -> SparkLikeExprDateTimeNamespace:
@property
def list(self: Self) -> SparkLikeExprListNamespace:
return SparkLikeExprListNamespace(self)

arg_min = not_implemented("arg_min")
arg_max = not_implemented("arg_max")
arg_true = not_implemented("arg_true")
head = not_implemented("head")
tail = not_implemented("tail")
mode = not_implemented("mode")
sort = not_implemented("sort")
rank = not_implemented("rank")
sample = not_implemented("sample")
map_batches = not_implemented("map_batches")
ewm_mean = not_implemented("ewm_mean")
rolling_sum = not_implemented("rolling_sum")
rolling_mean = not_implemented("rolling_mean")
rolling_var = not_implemented("rolling_var")
rolling_std = not_implemented("rolling_std")
gather_every = not_implemented("gather_every")

drop_nulls = not_implemented("drop_nulls")
diff = not_implemented("diff")
unique = not_implemented("unique")
shift = not_implemented("shift")
is_first_distinct = not_implemented("is_first_distinct")
is_last_distinct = not_implemented("is_last_distinct")
cum_sum = not_implemented("cum_sum")
cum_count = not_implemented("cum_count")
cum_min = not_implemented("cum_min")
cum_max = not_implemented("cum_max")
cum_prod = not_implemented("cum_prod")
replace_strict = not_implemented("replace_strict")
fill_null = not_implemented("fill_null")
quantile = not_implemented("quantile")

cat = not_implemented("cat", is_property=True)
9 changes: 7 additions & 2 deletions narwhals/expr.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,18 @@
from typing_extensions import Concatenate
from typing_extensions import ParamSpec
from typing_extensions import Self
from typing_extensions import TypeAlias

from narwhals.dtypes import DType
from narwhals.typing import CompliantExpr
from narwhals.typing import CompliantNamespace
from narwhals.typing import IntoExpr

PS = ParamSpec("PS")
R = TypeVar("R")
_ToCompliant: TypeAlias = Callable[
[CompliantNamespace[Any, Any]], CompliantExpr[Any, Any]
]


class Expr:
Expand All @@ -46,7 +51,7 @@ def __init__(
metadata: ExprMetadata,
) -> None:
# callable from CompliantNamespace to CompliantExpr
self._to_compliant_expr = to_compliant_expr
self._to_compliant_expr: _ToCompliant = to_compliant_expr
self._metadata = metadata

def _from_callable(self, to_compliant_expr: Callable[[Any], Any]) -> Self:
Expand Down Expand Up @@ -607,7 +612,7 @@ def var(self: Self, *, ddof: int = 1) -> Self:

def map_batches(
self: Self,
function: Callable[[Any], Self],
function: Callable[[Any], CompliantExpr[Any, Any]],
return_dtype: DType | None = None,
) -> Self:
"""Apply a custom python function to a whole Series or sequence of Series.
Expand Down
2 changes: 1 addition & 1 deletion narwhals/functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -1453,7 +1453,7 @@ def func(plx: CompliantNamespace[Any, Any]) -> CompliantExpr[Any, Any]:
compliant_value = extract_compliant(plx, value, str_as_lit=False)
if is_scalar_like(kind) and is_compliant_expr(compliant_value):
compliant_value = compliant_value.broadcast(kind)
return compliant_expr.otherwise(compliant_value) # type: ignore[no-any-return]
return compliant_expr.otherwise(compliant_value) # type: ignore[attr-defined, no-any-return]

return Expr(
func,
Expand Down
Loading