diff --git a/narwhals/_arrow/namespace.py b/narwhals/_arrow/namespace.py index d7561c322e..a44a045d9d 100644 --- a/narwhals/_arrow/namespace.py +++ b/narwhals/_arrow/namespace.py @@ -65,7 +65,7 @@ def len(self: Self) -> ArrowExpr: # coverage bug? this is definitely hit return self._expr( # pragma: no cover lambda df: [ - ArrowSeries._from_iterable( + ArrowSeries.from_iterable( [len(df._native_frame)], name="len", context=self ) ], @@ -79,7 +79,7 @@ def len(self: Self) -> ArrowExpr: def lit(self: Self, value: Any, dtype: DType | None) -> ArrowExpr: def _lit_arrow_series(_: ArrowDataFrame) -> ArrowSeries: - arrow_series = ArrowSeries._from_iterable( + arrow_series = ArrowSeries.from_iterable( data=[value], name="literal", context=self ) if dtype: diff --git a/narwhals/_arrow/series.py b/narwhals/_arrow/series.py index 4642a6dc0c..f8c5e085dc 100644 --- a/narwhals/_arrow/series.py +++ b/narwhals/_arrow/series.py @@ -5,6 +5,7 @@ from typing import Iterable from typing import Iterator from typing import Literal +from typing import Mapping from typing import Sequence from typing import cast from typing import overload @@ -32,6 +33,7 @@ from narwhals.utils import Implementation from narwhals.utils import generate_temporary_column_name from narwhals.utils import import_dtypes_module +from narwhals.utils import not_implemented from narwhals.utils import validate_backend_version if TYPE_CHECKING: @@ -139,12 +141,8 @@ def _from_native_series( ) @classmethod - def _from_iterable( - cls: type[Self], - data: Iterable[Any], - name: str, - *, - context: _FullContext, + def from_iterable( + cls, data: Iterable[Any], *, context: _FullContext, name: str = "" ) -> Self: return cls( chunked_array([data]), @@ -160,8 +158,8 @@ def _from_scalar(self, value: Any) -> Self: @classmethod def from_numpy(cls, data: Into1DArray, /, *, context: _FullContext) -> Self: - return cls._from_iterable( - data if is_numpy_array_1d(data) else [data], name="", context=context + return cls.from_iterable( + data if is_numpy_array_1d(data) else [data], context=context ) def __narwhals_namespace__(self: Self) -> ArrowNamespace: @@ -171,9 +169,6 @@ def __narwhals_namespace__(self: Self) -> ArrowNamespace: backend_version=self._backend_version, version=self._version ) - def __len__(self: Self) -> int: - return len(self.native) - def __eq__(self: Self, other: object) -> Self: # type: ignore[override] ser, other = extract_native(self, other) return self._from_native_series(pc.equal(ser, other)) # type: ignore[arg-type] @@ -391,9 +386,6 @@ def __native_namespace__(self: Self) -> ModuleType: def name(self: Self) -> str: return self._name - def __narwhals_series__(self: Self) -> Self: - return self - @overload def __getitem__(self: Self, idx: int) -> Any: ... @@ -569,7 +561,7 @@ def arg_true(self: Self) -> Self: import numpy as np # ignore-banned-import res = np.flatnonzero(self.native) - return self._from_iterable(res, name=self.name, context=self) + return self.from_iterable(res, name=self.name, context=self) def item(self: Self, index: int | None = None) -> Any: if index is None: @@ -753,7 +745,11 @@ def unique(self: Self, *, maintain_order: bool) -> Self: return self._from_native_series(self.native.unique()) 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: # https://stackoverflow.com/a/79111029/4451315 idxs = pc.index_in(self.native, pa.array(old)) @@ -1217,3 +1213,5 @@ def list(self: Self) -> ArrowSeriesListNamespace: @property def struct(self: Self) -> ArrowSeriesStructNamespace: return ArrowSeriesStructNamespace(self) + + ewm_mean = not_implemented() diff --git a/narwhals/_compliant/__init__.py b/narwhals/_compliant/__init__.py index 2c65296a66..0a8bc59cd8 100644 --- a/narwhals/_compliant/__init__.py +++ b/narwhals/_compliant/__init__.py @@ -24,6 +24,7 @@ from narwhals._compliant.typing import EagerSeriesT from narwhals._compliant.typing import IntoCompliantExpr from narwhals._compliant.typing import NativeFrameT_co +from narwhals._compliant.typing import NativeSeriesT_co __all__ = [ "CompliantDataFrame", @@ -50,4 +51,5 @@ "LazyExpr", "LazySelectorNamespace", "NativeFrameT_co", + "NativeSeriesT_co", ] diff --git a/narwhals/_compliant/expr.py b/narwhals/_compliant/expr.py index dece1372d8..a922c66e78 100644 --- a/narwhals/_compliant/expr.py +++ b/narwhals/_compliant/expr.py @@ -183,7 +183,7 @@ def quantile( ) -> Self: ... def map_batches( self, - function: Callable[[CompliantSeries], CompliantExpr[Any, Any]], + function: Callable[[CompliantSeries[Any]], CompliantExpr[Any, Any]], return_dtype: DType | type[DType] | None, ) -> Self: ... diff --git a/narwhals/_compliant/selectors.py b/narwhals/_compliant/selectors.py index 42803d3e15..f07009c0c4 100644 --- a/narwhals/_compliant/selectors.py +++ b/narwhals/_compliant/selectors.py @@ -62,8 +62,8 @@ ] -SeriesOrExprT = TypeVar("SeriesOrExprT", bound="CompliantSeries | NativeExpr") -SeriesT = TypeVar("SeriesT", bound="CompliantSeries") +SeriesOrExprT = TypeVar("SeriesOrExprT", bound="CompliantSeries[Any] | NativeExpr") +SeriesT = TypeVar("SeriesT", bound="CompliantSeries[Any]") ExprT = TypeVar("ExprT", bound="NativeExpr") FrameT = TypeVar( "FrameT", bound="CompliantDataFrame[Any, Any, Any] | CompliantLazyFrame[Any, Any]" diff --git a/narwhals/_compliant/series.py b/narwhals/_compliant/series.py index 85f4b0a8c6..4e1ef8f354 100644 --- a/narwhals/_compliant/series.py +++ b/narwhals/_compliant/series.py @@ -3,68 +3,284 @@ from typing import TYPE_CHECKING from typing import Any from typing import Iterable +from typing import Iterator +from typing import Literal +from typing import Mapping from typing import Protocol -from typing import TypeVar +from typing import Sequence +from narwhals._compliant.typing import NativeSeriesT_co +from narwhals._translate import FromIterable from narwhals._translate import NumpyConvertible +from narwhals.utils import unstable if TYPE_CHECKING: + from types import ModuleType + + import pandas as pd + import polars as pl from typing_extensions import Self - from narwhals._compliant.expr import CompliantExpr # noqa: F401 + from narwhals._arrow.typing import ArrowArray + from narwhals._compliant.dataframe import CompliantDataFrame + from narwhals._compliant.expr import CompliantExpr from narwhals._compliant.expr import EagerExpr - from narwhals._compliant.namespace import CompliantNamespace # noqa: F401 + from narwhals._compliant.namespace import CompliantNamespace from narwhals._compliant.namespace import EagerNamespace from narwhals.dtypes import DType from narwhals.typing import Into1DArray - from narwhals.typing import NativeSeries - from narwhals.typing import _1DArray # noqa: F401 + from narwhals.typing import _1DArray from narwhals.utils import Implementation from narwhals.utils import Version from narwhals.utils import _FullContext __all__ = ["CompliantSeries", "EagerSeries"] -NativeSeriesT_co = TypeVar("NativeSeriesT_co", bound="NativeSeries", covariant=True) +class CompliantSeries( + NumpyConvertible["_1DArray", "Into1DArray"], + FromIterable, + Protocol[NativeSeriesT_co], +): + _implementation: Implementation + _backend_version: tuple[int, ...] + _version: Version -class CompliantSeries(NumpyConvertible["_1DArray", "Into1DArray"], Protocol): @property def dtype(self) -> DType: ... @property def name(self) -> str: ... @property - def native(self) -> Any: ... - def __narwhals_series__(self) -> CompliantSeries: ... - def alias(self, name: str) -> Self: ... - def __narwhals_namespace__(self) -> Any: ... # CompliantNamespace[Any, Self]: ... + def native(self) -> NativeSeriesT_co: ... + def __narwhals_series__(self) -> Self: + return self + + def __narwhals_namespace__(self) -> CompliantNamespace[Any, Any]: ... + def __native_namespace__(self) -> ModuleType: ... + def __array__(self, dtype: Any, *, copy: bool | None) -> _1DArray: ... + def __contains__(self, other: Any) -> bool: ... + def __getitem__(self, item: Any) -> Any: ... + def __iter__(self) -> Iterator[Any]: ... + def __len__(self) -> int: + return len(self.native) + def _from_native_series(self, series: Any) -> Self: ... - def _to_expr(self) -> Any: ... # CompliantExpr[Any, Self]: ... + def _to_expr(self) -> CompliantExpr[Any, Self]: ... @classmethod def from_numpy(cls, data: Into1DArray, /, *, context: _FullContext) -> Self: ... + @classmethod + def from_iterable( + cls, data: Iterable[Any], /, *, context: _FullContext, name: str = "" + ) -> Self: ... + def _change_version(self, version: Version) -> Self: ... + + # Operators + def __add__(self, other: Any) -> Self: ... + def __and__(self, other: Any) -> Self: ... + def __eq__(self, other: object) -> Self: ... # type: ignore[override] + def __floordiv__(self, other: Any) -> Self: ... + def __ge__(self, other: Any) -> Self: ... + def __gt__(self, other: Any) -> Self: ... + def __invert__(self) -> Self: ... + def __le__(self, other: Any) -> Self: ... + def __lt__(self, other: Any) -> Self: ... + def __mod__(self, other: Any) -> Self: ... + def __mul__(self, other: Any) -> Self: ... + def __ne__(self, other: object) -> Self: ... # type: ignore[override] + def __or__(self, other: Any) -> Self: ... + def __pow__(self, other: Any) -> Self: ... + def __radd__(self, other: Any) -> Self: ... + def __rand__(self, other: Any) -> Self: ... + def __rfloordiv__(self, other: Any) -> Self: ... + def __rmod__(self, other: Any) -> Self: ... + def __rmul__(self, other: Any) -> Self: ... + def __ror__(self, other: Any) -> Self: ... + def __rpow__(self, other: Any) -> Self: ... + def __rsub__(self, other: Any) -> Self: ... + def __rtruediv__(self, other: Any) -> Self: ... + def __sub__(self, other: Any) -> Self: ... + def __truediv__(self, other: Any) -> Self: ... + + def abs(self) -> Self: ... + def alias(self, name: str) -> Self: ... + def all(self) -> bool: ... + def any(self) -> bool: ... + def arg_max(self) -> int: ... + def arg_min(self) -> int: ... + def arg_true(self) -> Self: ... + def cast(self, dtype: DType | type[DType]) -> Self: ... + def clip(self, lower_bound: Any, upper_bound: Any) -> Self: ... + def count(self) -> int: ... + def cum_count(self, *, reverse: bool) -> Self: ... + def cum_max(self, *, reverse: bool) -> Self: ... + def cum_min(self, *, reverse: bool) -> Self: ... + def cum_prod(self, *, reverse: bool) -> Self: ... + def cum_sum(self, *, reverse: bool) -> Self: ... + def diff(self) -> Self: ... + def drop_nulls(self) -> Self: ... + @unstable + def ewm_mean( + self, + *, + com: float | None, + span: float | None, + half_life: float | None, + alpha: float | None, + adjust: bool, + min_samples: int, + ignore_nulls: bool, + ) -> Self: ... + def fill_null( + self, + value: Any | None, + strategy: Literal["forward", "backward"] | None, + limit: int | None, + ) -> Self: ... + def filter(self, predicate: Any) -> Self: ... + def gather_every(self, n: int, offset: int) -> Self: ... + @unstable + def hist( + self: Self, + bins: list[float | int] | None, + *, + bin_count: int | None, + include_breakpoint: bool, + ) -> CompliantDataFrame[Self, Any, Any]: ... + def head(self, n: int) -> Self: ... + def is_between( + self, + lower_bound: Any, + upper_bound: Any, + closed: Literal["left", "right", "none", "both"], + ) -> Self: ... + def is_finite(self) -> Self: ... + def is_first_distinct(self) -> Self: ... + def is_in(self, other: Any) -> Self: ... + def is_last_distinct(self) -> Self: ... + def is_nan(self) -> Self: ... + def is_null(self) -> Self: ... + def is_sorted(self: Self, *, descending: bool) -> bool: ... + def is_unique(self) -> Self: ... + def item(self, index: int | None) -> Any: ... + def len(self) -> int: ... + def max(self) -> Any: ... + def mean(self) -> float: ... + def median(self) -> float: ... + def min(self) -> Any: ... + def mode(self) -> Self: ... + def n_unique(self) -> int: ... + def null_count(self) -> int: ... + def quantile( + self, + quantile: float, + interpolation: Literal["nearest", "higher", "lower", "midpoint", "linear"], + ) -> float: ... + def rank( + self, + method: Literal["average", "min", "max", "dense", "ordinal"], + *, + descending: bool, + ) -> Self: ... + def replace_strict( + self, + old: Sequence[Any] | Mapping[Any, Any], + new: Sequence[Any], + *, + return_dtype: DType | type[DType] | None, + ) -> Self: ... + @unstable + def rolling_mean( + self, + window_size: int, + *, + min_samples: int, + center: bool, + ) -> Self: ... + @unstable + def rolling_std( + self, + window_size: int, + *, + min_samples: int, + center: bool, + ddof: int, + ) -> Self: ... + @unstable + def rolling_sum( + self, + window_size: int, + *, + min_samples: int, + center: bool, + ) -> Self: ... + @unstable + def rolling_var( + self, + window_size: int, + *, + min_samples: int, + center: bool, + ddof: int, + ) -> Self: ... + def round(self, decimals: int) -> Self: ... + def sample( + self, + n: int | None, + *, + fraction: float | None, + with_replacement: bool, + seed: int | None, + ) -> Self: ... + def scatter(self, indices: int | Sequence[int], values: Any) -> Self: ... + def shift(self, n: int) -> Self: ... + def skew(self) -> float | None: ... + def sort(self, *, descending: bool, nulls_last: bool) -> Self: ... + def std(self, *, ddof: int) -> float: ... + def sum(self) -> float: ... + def tail(self, n: int) -> Self: ... + def to_arrow(self) -> ArrowArray: ... + def to_dummies( + self, *, separator: str, drop_first: bool + ) -> CompliantDataFrame[Self, Any, Any]: ... + def to_frame(self) -> CompliantDataFrame[Self, Any, Any]: ... + def to_list(self) -> list[Any]: ... + def to_pandas(self) -> pd.Series[Any]: ... + def to_polars(self) -> pl.Series: ... + def unique(self, *, maintain_order: bool) -> Self: ... + def value_counts( + self, + *, + sort: bool, + parallel: bool, + name: str | None, + normalize: bool, + ) -> CompliantDataFrame[Self, Any, Any]: ... + def var(self, *, ddof: int) -> float: ... + def zip_with(self, mask: Any, other: Any) -> Self: ... + @property + def str(self) -> Any: ... + @property + def dt(self) -> Any: ... + @property + def cat(self) -> Any: ... + @property + def list(self) -> Any: ... + @property + def struct(self) -> Any: ... -class EagerSeries(CompliantSeries, Protocol[NativeSeriesT_co]): + +class EagerSeries(CompliantSeries[NativeSeriesT_co], Protocol[NativeSeriesT_co]): _native_series: Any _implementation: Implementation _backend_version: tuple[int, ...] _version: Version _broadcast: bool - @property - def native(self) -> NativeSeriesT_co: ... - def _from_scalar(self, value: Any) -> Self: - return self._from_iterable([value], name=self.name, context=self) - - @classmethod - def _from_iterable( - cls: type[Self], data: Iterable[Any], name: str, *, context: _FullContext - ) -> Self: ... + return self.from_iterable([value], name=self.name, context=self) def __narwhals_namespace__(self) -> EagerNamespace[Any, Self, Any]: ... def _to_expr(self) -> EagerExpr[Any, Any]: return self.__narwhals_namespace__()._expr._from_series(self) # type: ignore[no-any-return] - - def cast(self, dtype: DType | type[DType]) -> Self: ... diff --git a/narwhals/_compliant/typing.py b/narwhals/_compliant/typing.py index 1da99e6685..9a662e15a2 100644 --- a/narwhals/_compliant/typing.py +++ b/narwhals/_compliant/typing.py @@ -19,6 +19,7 @@ from narwhals._compliant.series import CompliantSeries from narwhals._compliant.series import EagerSeries from narwhals.typing import NativeFrame + from narwhals.typing import NativeSeries __all__ = [ "AliasName", @@ -29,12 +30,14 @@ "CompliantSeriesT", "IntoCompliantExpr", "NativeFrameT_co", + "NativeSeriesT_co", ] NativeExprT_co = TypeVar("NativeExprT_co", bound="NativeExpr", covariant=True) -CompliantSeriesT = TypeVar("CompliantSeriesT", bound="CompliantSeries") +NativeSeriesT_co = TypeVar("NativeSeriesT_co", bound="NativeSeries", covariant=True) +CompliantSeriesT = TypeVar("CompliantSeriesT", bound="CompliantSeries[Any]") CompliantSeriesOrNativeExprT_co = TypeVar( "CompliantSeriesOrNativeExprT_co", - bound="CompliantSeries | NativeExpr", + bound="CompliantSeries[Any] | NativeExpr", covariant=True, ) NativeFrameT_co = TypeVar("NativeFrameT_co", bound="NativeFrame", covariant=True) diff --git a/narwhals/_pandas_like/dataframe.py b/narwhals/_pandas_like/dataframe.py index 49b2e86ed6..be6fce1bf8 100644 --- a/narwhals/_pandas_like/dataframe.py +++ b/narwhals/_pandas_like/dataframe.py @@ -428,8 +428,8 @@ def estimated_size(self: Self, unit: SizeUnit) -> int | float: def with_row_index(self: Self, name: str) -> Self: frame = self.native namespace = self.__narwhals_namespace__() - row_index = namespace._series._from_iterable( - range(len(frame)), name="", context=self, index=frame.index + row_index = namespace._series.from_iterable( + range(len(frame)), context=self, index=frame.index ).alias(name) return self._from_native_frame( horizontal_concat( diff --git a/narwhals/_pandas_like/namespace.py b/narwhals/_pandas_like/namespace.py index ec96167d82..dc525cb634 100644 --- a/narwhals/_pandas_like/namespace.py +++ b/narwhals/_pandas_like/namespace.py @@ -63,7 +63,7 @@ def __init__( # --- selection --- def lit(self: Self, value: Any, dtype: DType | None) -> PandasLikeExpr: def _lit_pandas_series(df: PandasLikeDataFrame) -> PandasLikeSeries: - pandas_series = self._series._from_iterable( + pandas_series = self._series.from_iterable( data=[value], name="literal", index=df._native_frame.index[0:1], @@ -87,7 +87,7 @@ def _lit_pandas_series(df: PandasLikeDataFrame) -> PandasLikeSeries: def len(self: Self) -> PandasLikeExpr: return PandasLikeExpr( lambda df: [ - self._series._from_iterable( + self._series.from_iterable( [len(df._native_frame)], name="len", index=[0], context=self ) ], @@ -293,7 +293,7 @@ def func(df: PandasLikeDataFrame) -> list[PandasLikeSeries]: s.zip_with(~nm, "") for s, nm in zip(series, null_mask) ] - sep_array = init_value._from_iterable( + sep_array = init_value.from_iterable( data=[separator] * len(init_value), name="sep", index=init_value._native_series.index, diff --git a/narwhals/_pandas_like/series.py b/narwhals/_pandas_like/series.py index a62abaff3d..1f75f0647c 100644 --- a/narwhals/_pandas_like/series.py +++ b/narwhals/_pandas_like/series.py @@ -5,6 +5,7 @@ from typing import Iterable from typing import Iterator from typing import Literal +from typing import Mapping from typing import Sequence from typing import cast from typing import overload @@ -135,9 +136,6 @@ def __native_namespace__(self: Self) -> ModuleType: msg = f"Expected pandas/modin/cudf, got: {type(self._implementation)}" # pragma: no cover raise AssertionError(msg) - def __narwhals_series__(self: Self) -> Self: - return self - def __narwhals_namespace__(self) -> PandasLikeNamespace: from narwhals._pandas_like.namespace import PandasLikeNamespace @@ -173,13 +171,13 @@ def _from_native_series(self: Self, series: Any) -> Self: ) @classmethod - def _from_iterable( - cls: type[Self], + def from_iterable( + cls, data: Iterable[Any], - name: str, *, context: _FullContext, - index: Any = None, # NOTE: Originally a liskov substitution principle violation + name: str = "", + index: Any = None, ) -> Self: return cls( native_series_from_iterable( @@ -204,9 +202,6 @@ def from_numpy(cls, data: Into1DArray, /, *, context: _FullContext) -> Self: version=context._version, ) - def __len__(self: Self) -> int: - return len(self.native) - @property def name(self: Self) -> str: return self._name @@ -636,7 +631,11 @@ def shift(self: Self, n: int) -> PandasLikeSeries: return self._from_native_series(self._native_series.shift(n)) 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, ) -> PandasLikeSeries: tmp_name = f"{self.name}_tmp" dtype_backend = get_dtype_backend( @@ -738,7 +737,7 @@ def to_pandas(self: Self) -> pd.Series[Any]: msg = f"Unknown implementation: {self._implementation}" # pragma: no cover raise AssertionError(msg) - def to_polars(self: Self) -> pl.DataFrame: + def to_polars(self: Self) -> pl.Series: import polars as pl # ignore-banned-import if self._implementation is Implementation.PANDAS: diff --git a/narwhals/_translate.py b/narwhals/_translate.py index 870eadc6b1..11aa6afc40 100644 --- a/narwhals/_translate.py +++ b/narwhals/_translate.py @@ -2,6 +2,7 @@ from typing import TYPE_CHECKING from typing import Any +from typing import Iterable from typing import Protocol if TYPE_CHECKING: @@ -59,3 +60,13 @@ class NumpyConvertible( Protocol[ToNumpyT_co, FromNumpyDT_contra], ): def to_numpy(self, dtype: Any, *, copy: bool | None) -> ToNumpyT_co: ... + + +FromIterableT_contra = TypeVar("FromIterableT_contra", contravariant=True, default=Any) + + +class FromIterable(Protocol[FromIterableT_contra]): + @classmethod + def from_iterable( + cls, data: Iterable[FromIterableT_contra], *args: Any, **kwds: Any + ) -> Self: ... diff --git a/narwhals/functions.py b/narwhals/functions.py index 8bc3b45477..1b7bc8d5ea 100644 --- a/narwhals/functions.py +++ b/narwhals/functions.py @@ -45,6 +45,7 @@ from narwhals._compliant import CompliantExpr from narwhals._compliant import CompliantNamespace + from narwhals._pandas_like.series import PandasLikeSeries from narwhals.dataframe import DataFrame from narwhals.dataframe import LazyFrame from narwhals.dtypes import DType @@ -410,7 +411,7 @@ def _from_dict_impl( native_series, series_only=True )._compliant_series if left_most_series is None: - left_most_series = compliant_series + left_most_series = cast("PandasLikeSeries", compliant_series) aligned_data[key] = native_series else: aligned_data[key] = align_and_extract_native( diff --git a/narwhals/series.py b/narwhals/series.py index d7ed5a8970..e06f0cf06d 100644 --- a/narwhals/series.py +++ b/narwhals/series.py @@ -22,7 +22,9 @@ from narwhals.typing import IntoSeriesT from narwhals.utils import _validate_rolling_arguments from narwhals.utils import generate_repr +from narwhals.utils import is_compliant_series from narwhals.utils import parse_version +from narwhals.utils import supports_arrow_c_stream if TYPE_CHECKING: from types import ModuleType @@ -32,6 +34,7 @@ from typing_extensions import Self from narwhals._arrow.typing import ArrowArray + from narwhals._compliant import CompliantSeries from narwhals.dataframe import DataFrame from narwhals.dtypes import DType from narwhals.typing import _1DArray @@ -76,10 +79,10 @@ def __init__( level: Literal["full", "lazy", "interchange"], ) -> None: self._level: Literal["full", "lazy", "interchange"] = level - if hasattr(series, "__narwhals_series__"): - # TODO @dangotbanned: Repeat (#2119) for `CompliantSeries` to support typing - # morally: `CompliantSeries` - self._compliant_series = series.__narwhals_series__() + if is_compliant_series(series): + self._compliant_series: CompliantSeries[IntoSeriesT] = ( + series.__narwhals_series__() + ) else: # pragma: no cover msg = f"Expected Polars Series or an object which implements `__narwhals_series__`, got: {type(series)}." raise AssertionError(msg) @@ -113,10 +116,10 @@ def implementation(self: Self) -> Implementation: >>> s.implementation.is_polars() False """ - return self._compliant_series._implementation # type: ignore[no-any-return] + return self._compliant_series._implementation def __array__(self: Self, dtype: Any = None, copy: bool | None = None) -> _1DArray: # noqa: FBT001 - return self._compliant_series.__array__(dtype=dtype, copy=copy) # type: ignore[no-any-return] + return self._compliant_series.__array__(dtype=dtype, copy=copy) @overload def __getitem__(self: Self, idx: int) -> Any: ... @@ -165,7 +168,7 @@ def __getitem__(self: Self, idx: int | slice | Sequence[int] | Self) -> Any | Se ) def __native_namespace__(self: Self) -> ModuleType: - return self._compliant_series.__native_namespace__() # type: ignore[no-any-return] + return self._compliant_series.__native_namespace__() def __arrow_c_stream__(self: Self, requested_schema: object | None = None) -> object: """Export a Series via the Arrow PyCapsule Interface. @@ -178,8 +181,8 @@ def __arrow_c_stream__(self: Self, requested_schema: object | None = None) -> ob See [PyCapsule Interface](https://arrow.apache.org/docs/dev/format/CDataInterface/PyCapsuleInterface.html) for more. """ - native_series = self._compliant_series._native_series - if hasattr(native_series, "__arrow_c_stream__"): + native_series = self._compliant_series.native + if supports_arrow_c_stream(native_series): return native_series.__arrow_c_stream__(requested_schema=requested_schema) try: import pyarrow as pa # ignore-banned-import @@ -214,7 +217,7 @@ def to_native(self: Self) -> IntoSeriesT: 2 ] """ - return self._compliant_series._native_series # type: ignore[no-any-return] + return self._compliant_series.native def scatter(self: Self, indices: int | Sequence[int], values: Any) -> Self: """Set value(s) at given position(s). @@ -358,7 +361,7 @@ def dtype(self: Self) -> DType: >>> nw.from_native(s_native, series_only=True).dtype Int64 """ - return self._compliant_series.dtype # type: ignore[no-any-return] + return self._compliant_series.dtype @property def name(self: Self) -> str: @@ -375,7 +378,7 @@ def name(self: Self) -> str: >>> nw.from_native(s_native, series_only=True).name 'foo' """ - return self._compliant_series.name # type: ignore[no-any-return] + return self._compliant_series.name def ewm_mean( self: Self, @@ -528,7 +531,7 @@ def to_list(self: Self) -> list[Any]: >>> nw.from_native(s_native, series_only=True).to_list() [1, 2, 3] """ - return self._compliant_series.to_list() # type: ignore[no-any-return] + return self._compliant_series.to_list() def mean(self: Self) -> float: """Reduce this Series to the mean value. @@ -544,7 +547,7 @@ def mean(self: Self) -> float: >>> nw.from_native(s_native, series_only=True).mean() np.float64(2.7) """ - return self._compliant_series.mean() # type: ignore[no-any-return] + return self._compliant_series.mean() def median(self: Self) -> float: """Reduce this Series to the median value. @@ -563,7 +566,7 @@ def median(self: Self) -> float: >>> nw.from_native(s_native, series_only=True).median() 5.0 """ - return self._compliant_series.median() # type: ignore[no-any-return] + return self._compliant_series.median() def skew(self: Self) -> float | None: """Calculate the sample skewness of the Series. @@ -583,7 +586,7 @@ def skew(self: Self) -> float | None: The skewness is a measure of the asymmetry of the probability distribution. A perfectly symmetric distribution has a skewness of 0. """ - return self._compliant_series.skew() # type: ignore[no-any-return] + return self._compliant_series.skew() def count(self: Self) -> int: """Returns the number of non-null elements in the Series. @@ -599,7 +602,7 @@ def count(self: Self) -> int: >>> nw.from_native(s_native, series_only=True).count() 2 """ - return self._compliant_series.count() # type: ignore[no-any-return] + return self._compliant_series.count() def any(self: Self) -> bool: """Return whether any of the values in the Series are True. @@ -618,7 +621,7 @@ def any(self: Self) -> bool: >>> nw.from_native(s_native, series_only=True).any() np.True_ """ - return self._compliant_series.any() # type: ignore[no-any-return] + return self._compliant_series.any() def all(self: Self) -> bool: """Return whether all values in the Series are True. @@ -634,7 +637,7 @@ def all(self: Self) -> bool: >>> nw.from_native(s_native, series_only=True).all() False """ - return self._compliant_series.all() # type: ignore[no-any-return] + return self._compliant_series.all() def min(self: Self) -> Any: """Get the minimal value in this Series. @@ -679,7 +682,7 @@ def arg_min(self: Self) -> int: >>> nw.from_native(s_native, series_only=True).arg_min() 0 """ - return self._compliant_series.arg_min() # type: ignore[no-any-return] + return self._compliant_series.arg_min() def arg_max(self: Self) -> int: """Returns the index of the maximum value. @@ -692,7 +695,7 @@ def arg_max(self: Self) -> int: >>> nw.from_native(s_native, series_only=True).arg_max() 2 """ - return self._compliant_series.arg_max() # type: ignore[no-any-return] + return self._compliant_series.arg_max() def sum(self: Self) -> float: """Reduce this Series to the sum value. @@ -708,7 +711,7 @@ def sum(self: Self) -> float: >>> nw.from_native(s_native, series_only=True).sum() 6 """ - return self._compliant_series.sum() # type: ignore[no-any-return] + return self._compliant_series.sum() def std(self: Self, *, ddof: int = 1) -> float: """Get the standard deviation of this Series. @@ -728,7 +731,7 @@ def std(self: Self, *, ddof: int = 1) -> float: >>> nw.from_native(s_native, series_only=True).std() 1.0 """ - return self._compliant_series.std(ddof=ddof) # type: ignore[no-any-return] + return self._compliant_series.std(ddof=ddof) def var(self: Self, *, ddof: int = 1) -> float: """Get the variance of this Series. @@ -745,7 +748,7 @@ def var(self: Self, *, ddof: int = 1) -> float: >>> nw.from_native(s_native, series_only=True).var() 1.0 """ - return self._compliant_series.var(ddof=ddof) # type: ignore[no-any-return] + return self._compliant_series.var(ddof=ddof) def clip( self: Self, @@ -1396,7 +1399,7 @@ def n_unique(self: Self) -> int: >>> nw.from_native(s_native, series_only=True).n_unique() 3 """ - return self._compliant_series.n_unique() # type: ignore[no-any-return] + return self._compliant_series.n_unique() def to_numpy(self: Self) -> _1DArray: """Convert to numpy. @@ -1412,7 +1415,7 @@ def to_numpy(self: Self) -> _1DArray: >>> nw.from_native(s_native, series_only=True).to_numpy() array([1, 2, 3]...) """ - return self._compliant_series.to_numpy() # type: ignore[no-any-return] + return self._compliant_series.to_numpy(None, copy=None) def to_pandas(self: Self) -> pd.Series[Any]: """Convert to pandas Series. @@ -1431,7 +1434,7 @@ def to_pandas(self: Self) -> pd.Series[Any]: 2 3 Name: a, dtype: int64 """ - return self._compliant_series.to_pandas() # type: ignore[no-any-return] + return self._compliant_series.to_pandas() def to_polars(self: Self) -> pl.Series: """Convert to polars Series. @@ -1455,7 +1458,7 @@ def to_polars(self: Self) -> pl.Series: 3 ] """ - return self._compliant_series.to_polars() # type: ignore[no-any-return] + return self._compliant_series.to_polars() def __add__(self: Self, other: object) -> Self: return self._from_compliant_series( @@ -1648,7 +1651,7 @@ def is_empty(self: Self) -> bool: >>> s_nw.filter(s_nw > 10).is_empty() True """ - return self._compliant_series.len() == 0 # type: ignore[no-any-return] + return self._compliant_series.len() == 0 def is_unique(self: Self) -> Self: r"""Get a mask of all unique rows in the Series. @@ -1689,7 +1692,7 @@ def null_count(self: Self) -> int: >>> nw.from_native(s_native, series_only=True).null_count() 2 """ - return self._compliant_series.null_count() # type: ignore[no-any-return] + return self._compliant_series.null_count() def is_first_distinct(self: Self) -> Self: r"""Return a boolean mask indicating the first occurrence of each distinct value. @@ -1760,7 +1763,7 @@ def is_sorted(self: Self, *, descending: bool = False) -> bool: >>> s_nw.is_sorted(descending=True) True """ - return self._compliant_series.is_sorted(descending=descending) # type: ignore[no-any-return] + return self._compliant_series.is_sorted(descending=descending) def value_counts( self: Self, @@ -1834,7 +1837,7 @@ def quantile( ... ] [5.0, 12.0, 25.0, 37.0, 44.0] """ - return self._compliant_series.quantile( # type: ignore[no-any-return] + return self._compliant_series.quantile( quantile=quantile, interpolation=interpolation ) @@ -2076,7 +2079,7 @@ def to_arrow(self: Self) -> ArrowArray: 4 ] """ - return self._compliant_series.to_arrow() # type: ignore[no-any-return] + return self._compliant_series.to_arrow() def mode(self: Self) -> Self: r"""Compute the most occurring value(s). @@ -2496,7 +2499,7 @@ def __iter__(self: Self) -> Iterator[Any]: yield from self._compliant_series.__iter__() def __contains__(self: Self, other: Any) -> bool: - return self._compliant_series.__contains__(other) # type: ignore[no-any-return] + return self._compliant_series.__contains__(other) def rank( self: Self, diff --git a/narwhals/stable/v1/__init__.py b/narwhals/stable/v1/__init__.py index 344aff9210..42ba70ad9b 100644 --- a/narwhals/stable/v1/__init__.py +++ b/narwhals/stable/v1/__init__.py @@ -1699,7 +1699,7 @@ def to_native( if isinstance(narwhals_object, BaseFrame): return narwhals_object._compliant_frame._native_frame if isinstance(narwhals_object, Series): - return narwhals_object._compliant_series._native_series + return narwhals_object._compliant_series.native if not pass_through: msg = f"Expected Narwhals object, got {type(narwhals_object)}." diff --git a/narwhals/translate.py b/narwhals/translate.py index a482dd8cae..c724eed13a 100644 --- a/narwhals/translate.py +++ b/narwhals/translate.py @@ -119,7 +119,7 @@ def to_native( if isinstance(narwhals_object, BaseFrame): return narwhals_object._compliant_frame._native_frame if isinstance(narwhals_object, Series): - return narwhals_object._compliant_series._native_series + return narwhals_object._compliant_series.native if not pass_through: msg = f"Expected Narwhals object, got {type(narwhals_object)}." diff --git a/narwhals/utils.py b/narwhals/utils.py index a19b287306..10e9e29634 100644 --- a/narwhals/utils.py +++ b/narwhals/utils.py @@ -59,9 +59,11 @@ from narwhals._compliant import CompliantFrameT from narwhals._compliant import CompliantSeriesOrNativeExprT_co from narwhals._compliant import NativeFrameT_co + from narwhals._compliant import NativeSeriesT_co from narwhals.dataframe import DataFrame from narwhals.dataframe import LazyFrame from narwhals.dtypes import DType + from narwhals.functions import ArrowStreamExportable from narwhals.series import Series from narwhals.typing import CompliantDataFrame from narwhals.typing import CompliantLazyFrame @@ -130,7 +132,7 @@ def columns(self) -> Sequence[str]: ... "CompliantExprT_co", bound="CompliantExpr[Any, Any]", covariant=True ) CompliantSeriesT_co = TypeVar( - "CompliantSeriesT_co", bound="CompliantSeries", covariant=True + "CompliantSeriesT_co", bound="CompliantSeries[Any]", covariant=True ) @@ -1488,7 +1490,9 @@ def is_compliant_lazyframe( return _hasattr_static(obj, "__narwhals_lazyframe__") -def is_compliant_series(obj: Any) -> TypeIs[CompliantSeries]: +def is_compliant_series( + obj: CompliantSeries[NativeSeriesT_co] | Any, +) -> TypeIs[CompliantSeries[NativeSeriesT_co]]: return _hasattr_static(obj, "__narwhals_series__") @@ -1506,6 +1510,10 @@ def _supports_dataframe_interchange(obj: Any) -> TypeIs[DataFrameLike]: return hasattr(obj, "__dataframe__") +def supports_arrow_c_stream(obj: Any) -> TypeIs[ArrowStreamExportable]: + return _hasattr_static(obj, "__arrow_c_stream__") + + def is_tracks_depth(obj: Implementation, /) -> TypeIs[_TracksDepth]: # pragma: no cover # Return `True` for implementations that utilize `CompliantExpr._depth`. return obj.is_pandas_like() or obj in {Implementation.PYARROW, Implementation.DASK}