From 720bd7d175cc4e70a9ad452abd3ba2816d35b486 Mon Sep 17 00:00:00 2001 From: Edoardo Abati <29585319+EdAbati@users.noreply.github.com> Date: Wed, 13 Nov 2024 13:29:16 +0100 Subject: [PATCH 1/6] first draft --- narwhals/dataframe.py | 20 ++++++++++---------- narwhals/series.py | 5 +++-- narwhals/stable/v1/__init__.py | 2 +- narwhals/translate.py | 16 +++++++++------- narwhals/typing.py | 4 ++-- narwhals/utils.py | 6 +++--- tests/expr_and_series/dt/timestamp_test.py | 6 +++++- 7 files changed, 33 insertions(+), 26 deletions(-) diff --git a/narwhals/dataframe.py b/narwhals/dataframe.py index bb163b28d..9267c569b 100644 --- a/narwhals/dataframe.py +++ b/narwhals/dataframe.py @@ -319,7 +319,7 @@ class DataFrame(BaseFrame[DataFrameT]): """ @property - def _series(self) -> type[Series]: + def _series(self) -> type[Series[Any]]: from narwhals.series import Series return Series @@ -661,7 +661,7 @@ def shape(self) -> tuple[int, int]: """ return self._compliant_frame.shape # type: ignore[no-any-return] - def get_column(self, name: str) -> Series: + def get_column(self, name: str) -> Series[Any]: """ Get a single column by name. @@ -713,23 +713,23 @@ def __getitem__(self, item: tuple[Sequence[int], Sequence[int]]) -> Self: ... @overload def __getitem__(self, item: tuple[slice, Sequence[int]]) -> Self: ... @overload - def __getitem__(self, item: tuple[Sequence[int], str]) -> Series: ... # type: ignore[overload-overlap] + def __getitem__(self, item: tuple[Sequence[int], str]) -> Series[Any]: ... # type: ignore[overload-overlap] @overload - def __getitem__(self, item: tuple[slice, str]) -> Series: ... # type: ignore[overload-overlap] + def __getitem__(self, item: tuple[slice, str]) -> Series[Any]: ... # type: ignore[overload-overlap] @overload def __getitem__(self, item: tuple[Sequence[int], Sequence[str]]) -> Self: ... @overload def __getitem__(self, item: tuple[slice, Sequence[str]]) -> Self: ... @overload - def __getitem__(self, item: tuple[Sequence[int], int]) -> Series: ... # type: ignore[overload-overlap] + def __getitem__(self, item: tuple[Sequence[int], int]) -> Series[Any]: ... # type: ignore[overload-overlap] @overload - def __getitem__(self, item: tuple[slice, int]) -> Series: ... # type: ignore[overload-overlap] + def __getitem__(self, item: tuple[slice, int]) -> Series[Any]: ... # type: ignore[overload-overlap] @overload def __getitem__(self, item: Sequence[int]) -> Self: ... @overload - def __getitem__(self, item: str) -> Series: ... # type: ignore[overload-overlap] + def __getitem__(self, item: str) -> Series[Any]: ... # type: ignore[overload-overlap] @overload def __getitem__(self, item: Sequence[str]) -> Self: ... @@ -752,7 +752,7 @@ def __getitem__( | tuple[slice | Sequence[int], Sequence[int] | Sequence[str] | slice] | tuple[slice, slice] ), - ) -> Series | Self: + ) -> Series[Any] | Self: """ Extract column or slice of DataFrame. @@ -871,14 +871,14 @@ def __contains__(self, key: str) -> bool: return key in self.columns @overload - def to_dict(self, *, as_series: Literal[True] = ...) -> dict[str, Series]: ... + def to_dict(self, *, as_series: Literal[True] = ...) -> dict[str, Series[Any]]: ... @overload def to_dict(self, *, as_series: Literal[False]) -> dict[str, list[Any]]: ... @overload def to_dict(self, *, as_series: bool) -> dict[str, Series] | dict[str, list[Any]]: ... def to_dict( self, *, as_series: bool = True - ) -> dict[str, Series] | dict[str, list[Any]]: + ) -> dict[str, Series[Any]] | dict[str, list[Any]]: """ Convert DataFrame to a dictionary mapping column name to values. diff --git a/narwhals/series.py b/narwhals/series.py index 0b5dfcc1d..30dcd70f4 100644 --- a/narwhals/series.py +++ b/narwhals/series.py @@ -11,6 +11,7 @@ from typing import TypeVar from typing import overload +from narwhals.typing import IntoSeriesT from narwhals.utils import parse_version if TYPE_CHECKING: @@ -25,7 +26,7 @@ from narwhals.dtypes import DType -class Series: +class Series(Generic[IntoSeriesT]): """ Narwhals Series, backed by a native series. @@ -2705,7 +2706,7 @@ def cat(self: Self) -> SeriesCatNamespace[Self]: return SeriesCatNamespace(self) -T = TypeVar("T", bound=Series) +T = TypeVar("T", bound=Series[Any]) class SeriesCatNamespace(Generic[T]): diff --git a/narwhals/stable/v1/__init__.py b/narwhals/stable/v1/__init__.py index 0ca2a682d..33c125f3d 100644 --- a/narwhals/stable/v1/__init__.py +++ b/narwhals/stable/v1/__init__.py @@ -388,7 +388,7 @@ def _l1_norm(self: Self) -> Self: return self.select(all()._l1_norm()) -class Series(NwSeries): +class Series(NwSeries[IntoSeriesT]): """ Narwhals Series, backed by a native series. diff --git a/narwhals/translate.py b/narwhals/translate.py index 129fe671f..be34148d6 100644 --- a/narwhals/translate.py +++ b/narwhals/translate.py @@ -64,13 +64,15 @@ def to_native( narwhals_object: LazyFrame[IntoFrameT], *, pass_through: Literal[False] = ... ) -> IntoFrameT: ... @overload -def to_native(narwhals_object: Series, *, pass_through: Literal[False] = ...) -> Any: ... +def to_native( + narwhals_object: Series[IntoSeriesT], *, pass_through: Literal[False] = ... +) -> Any: ... @overload def to_native(narwhals_object: Any, *, pass_through: bool) -> Any: ... def to_native( - narwhals_object: DataFrame[IntoFrameT] | LazyFrame[IntoFrameT] | Series, + narwhals_object: DataFrame[IntoFrameT] | LazyFrame[IntoFrameT] | Series[IntoSeriesT], *, strict: bool | None = None, pass_through: bool | None = None, @@ -125,7 +127,7 @@ def from_native( eager_or_interchange_only: None = ..., series_only: None = ..., allow_series: Literal[True], -) -> DataFrame[IntoDataFrameT] | Series: ... +) -> DataFrame[IntoDataFrameT] | Series[IntoSeriesT]: ... @overload @@ -185,7 +187,7 @@ def from_native( eager_or_interchange_only: None = ..., series_only: None = ..., allow_series: Literal[True], -) -> DataFrame[IntoFrameT] | LazyFrame[IntoFrameT] | Series: ... +) -> DataFrame[IntoFrameT] | LazyFrame[IntoFrameT] | Series[IntoSeriesT]: ... @overload @@ -197,7 +199,7 @@ def from_native( eager_or_interchange_only: None = ..., series_only: Literal[True], allow_series: None = ..., -) -> Series: ... +) -> Series[IntoSeriesT]: ... @overload @@ -265,7 +267,7 @@ def from_native( eager_or_interchange_only: None = ..., series_only: None = ..., allow_series: Literal[True], -) -> DataFrame[Any] | LazyFrame[Any] | Series: +) -> DataFrame[Any] | LazyFrame[Any] | Series[Any]: """ from_native(df, pass_through=False, allow_series=True) from_native(df, allow_series=True) @@ -281,7 +283,7 @@ def from_native( eager_or_interchange_only: None = ..., series_only: Literal[True], allow_series: None = ..., -) -> Series: +) -> Series[IntoSeriesT]: """ from_native(df, pass_through=False, series_only=True) from_native(df, series_only=True) diff --git a/narwhals/typing.py b/narwhals/typing.py index 044962ac3..7bd7a988e 100644 --- a/narwhals/typing.py +++ b/narwhals/typing.py @@ -36,7 +36,7 @@ class DataFrameLike(Protocol): def __dataframe__(self, *args: Any, **kwargs: Any) -> Any: ... -IntoExpr: TypeAlias = Union["Expr", str, "Series"] +IntoExpr: TypeAlias = Union["Expr", str, "Series[Any]"] """Anything which can be converted to an expression.""" IntoDataFrame: TypeAlias = Union["NativeFrame", "DataFrame[Any]", "DataFrameLike"] @@ -50,7 +50,7 @@ def __dataframe__(self, *args: Any, **kwargs: Any) -> Any: ... Frame: TypeAlias = Union["DataFrame[Any]", "LazyFrame[Any]"] """Narwhals DataFrame or Narwhals LazyFrame""" -IntoSeries: TypeAlias = Union["Series", "NativeSeries"] +IntoSeries: TypeAlias = Union["Series[Any]", "NativeSeries"] """Anything which can be converted to a Narwhals Series.""" # TypeVars for some of the above diff --git a/narwhals/utils.py b/narwhals/utils.py index e1a7b812d..6a4a875ab 100644 --- a/narwhals/utils.py +++ b/narwhals/utils.py @@ -154,7 +154,7 @@ def validate_laziness(items: Iterable[Any]) -> None: raise NotImplementedError(msg) -def maybe_align_index(lhs: T, rhs: Series | BaseFrame[Any]) -> T: +def maybe_align_index(lhs: T, rhs: Series[Any] | BaseFrame[Any]) -> T: """ Align `lhs` to the Index of `rhs`, if they're both pandas-like. @@ -278,7 +278,7 @@ def maybe_set_index( obj: T, column_names: str | list[str] | None = None, *, - index: Series | list[Series] | None = None, + index: Series[Any] | list[Series[Any]] | None = None, ) -> T: """ Set the index of a DataFrame or a Series, if it's pandas-like. @@ -470,7 +470,7 @@ def maybe_convert_dtypes(obj: T, *args: bool, **kwargs: bool | str) -> T: return obj_any # type: ignore[no-any-return] -def is_ordered_categorical(series: Series) -> bool: +def is_ordered_categorical(series: Series[Any]) -> bool: """ Return whether indices of categories are semantically meaningful. diff --git a/tests/expr_and_series/dt/timestamp_test.py b/tests/expr_and_series/dt/timestamp_test.py index 212926628..e20086c65 100644 --- a/tests/expr_and_series/dt/timestamp_test.py +++ b/tests/expr_and_series/dt/timestamp_test.py @@ -1,6 +1,7 @@ from __future__ import annotations from datetime import datetime +from typing import TYPE_CHECKING from typing import Literal import hypothesis.strategies as st @@ -18,6 +19,9 @@ from tests.utils import assert_equal_data from tests.utils import is_windows +if TYPE_CHECKING: + from narwhals.typing import IntoSeriesT + data = { "a": [ datetime(2021, 3, 1, 12, 34, 56, 49000), @@ -207,7 +211,7 @@ def test_timestamp_hypothesis( import polars as pl @nw.narwhalify - def func(s: nw.Series) -> nw.Series: + def func(s: nw.Series[IntoSeriesT]) -> nw.Series[IntoSeriesT]: return s.dt.timestamp(time_unit) result_pl = func(pl.Series([inputs], dtype=pl.Datetime(starting_time_unit))) From e658e04e5aa446defc13ad1d2138816eaf9a2fd4 Mon Sep 17 00:00:00 2001 From: Edoardo Abati <29585319+EdAbati@users.noreply.github.com> Date: Wed, 13 Nov 2024 18:36:36 +0100 Subject: [PATCH 2/6] move all series to generic type --- narwhals/dataframe.py | 8 ++-- narwhals/functions.py | 7 ++-- narwhals/stable/v1/__init__.py | 68 +++++++++++++++++++--------------- narwhals/stable/v1/typing.py | 4 +- narwhals/utils.py | 3 +- tests/utils_test.py | 5 ++- 6 files changed, 55 insertions(+), 40 deletions(-) diff --git a/narwhals/dataframe.py b/narwhals/dataframe.py index 9267c569b..8856670f7 100644 --- a/narwhals/dataframe.py +++ b/narwhals/dataframe.py @@ -875,7 +875,9 @@ def to_dict(self, *, as_series: Literal[True] = ...) -> dict[str, Series[Any]]: @overload def to_dict(self, *, as_series: Literal[False]) -> dict[str, list[Any]]: ... @overload - def to_dict(self, *, as_series: bool) -> dict[str, Series] | dict[str, list[Any]]: ... + def to_dict( + self, *, as_series: bool + ) -> dict[str, Series[Any]] | dict[str, list[Any]]: ... def to_dict( self, *, as_series: bool = True ) -> dict[str, Series[Any]] | dict[str, list[Any]]: @@ -2274,7 +2276,7 @@ def join_asof( ) # --- descriptive --- - def is_duplicated(self: Self) -> Series: + def is_duplicated(self: Self) -> Series[Any]: r""" Get a mask of all duplicated rows in this DataFrame. @@ -2355,7 +2357,7 @@ def is_empty(self: Self) -> bool: """ return self._compliant_frame.is_empty() # type: ignore[no-any-return] - def is_unique(self: Self) -> Series: + def is_unique(self: Self) -> Series[Any]: r""" Get a mask of all unique rows in this DataFrame. diff --git a/narwhals/functions.py b/narwhals/functions.py index 3fd2025a6..a1aa1685b 100644 --- a/narwhals/functions.py +++ b/narwhals/functions.py @@ -30,6 +30,7 @@ from narwhals.schema import Schema from narwhals.series import Series from narwhals.typing import DTypes + from narwhals.typing import IntoSeriesT class ArrowStreamExportable(Protocol): def __arrow_c_stream__( @@ -162,7 +163,7 @@ def new_series( dtype: DType | type[DType] | None = None, *, native_namespace: ModuleType, -) -> Series: +) -> Series[Any]: """ Instantiate Narwhals Series from raw data. @@ -224,7 +225,7 @@ def _new_series_impl( *, native_namespace: ModuleType, dtypes: DTypes, -) -> Series: +) -> Series[Any]: implementation = Implementation.from_native_namespace(native_namespace) if implementation is Implementation.POLARS: @@ -603,7 +604,7 @@ def show_versions() -> None: def get_level( - obj: DataFrame[Any] | LazyFrame[Any] | Series, + obj: DataFrame[Any] | LazyFrame[Any] | Series[IntoSeriesT], ) -> Literal["full", "interchange"]: """ Level of support Narwhals has for current object. diff --git a/narwhals/stable/v1/__init__.py b/narwhals/stable/v1/__init__.py index 33c125f3d..55be1d79e 100644 --- a/narwhals/stable/v1/__init__.py +++ b/narwhals/stable/v1/__init__.py @@ -90,7 +90,7 @@ class DataFrame(NwDataFrame[IntoDataFrameT]): # annotations are correct. @property - def _series(self) -> type[Series]: + def _series(self) -> type[Series[Any]]: return Series @property @@ -104,23 +104,23 @@ def __getitem__(self, item: tuple[Sequence[int], Sequence[int]]) -> Self: ... @overload def __getitem__(self, item: tuple[slice, Sequence[int]]) -> Self: ... @overload - def __getitem__(self, item: tuple[Sequence[int], str]) -> Series: ... # type: ignore[overload-overlap] + def __getitem__(self, item: tuple[Sequence[int], str]) -> Series[Any]: ... # type: ignore[overload-overlap] @overload - def __getitem__(self, item: tuple[slice, str]) -> Series: ... # type: ignore[overload-overlap] + def __getitem__(self, item: tuple[slice, str]) -> Series[Any]: ... # type: ignore[overload-overlap] @overload def __getitem__(self, item: tuple[Sequence[int], Sequence[str]]) -> Self: ... @overload def __getitem__(self, item: tuple[slice, Sequence[str]]) -> Self: ... @overload - def __getitem__(self, item: tuple[Sequence[int], int]) -> Series: ... # type: ignore[overload-overlap] + def __getitem__(self, item: tuple[Sequence[int], int]) -> Series[Any]: ... # type: ignore[overload-overlap] @overload - def __getitem__(self, item: tuple[slice, int]) -> Series: ... # type: ignore[overload-overlap] + def __getitem__(self, item: tuple[slice, int]) -> Series[Any]: ... # type: ignore[overload-overlap] @overload def __getitem__(self, item: Sequence[int]) -> Self: ... @overload - def __getitem__(self, item: str) -> Series: ... # type: ignore[overload-overlap] + def __getitem__(self, item: str) -> Series[Any]: ... # type: ignore[overload-overlap] @overload def __getitem__(self, item: Sequence[str]) -> Self: ... @@ -182,14 +182,16 @@ def lazy(self) -> LazyFrame[Any]: # Not sure what mypy is complaining about, probably some fancy # thing that I need to understand category theory for @overload # type: ignore[override] - def to_dict(self, *, as_series: Literal[True] = ...) -> dict[str, Series]: ... + def to_dict(self, *, as_series: Literal[True] = ...) -> dict[str, Series[Any]]: ... @overload def to_dict(self, *, as_series: Literal[False]) -> dict[str, list[Any]]: ... @overload - def to_dict(self, *, as_series: bool) -> dict[str, Series] | dict[str, list[Any]]: ... + def to_dict( + self, *, as_series: bool + ) -> dict[str, Series[Any]] | dict[str, list[Any]]: ... def to_dict( self, *, as_series: bool = True - ) -> dict[str, Series] | dict[str, list[Any]]: + ) -> dict[str, Series[Any]] | dict[str, list[Any]]: """ Convert DataFrame to a dictionary mapping column name to values. @@ -230,7 +232,7 @@ def to_dict( """ return super().to_dict(as_series=as_series) # type: ignore[return-value] - def is_duplicated(self: Self) -> Series: + def is_duplicated(self: Self) -> Series[Any]: r""" Get a mask of all duplicated rows in this DataFrame. @@ -278,7 +280,7 @@ def is_duplicated(self: Self) -> Series: """ return super().is_duplicated() # type: ignore[return-value] - def is_unique(self: Self) -> Series: + def is_unique(self: Self) -> Series[Any]: r""" Get a mask of all unique rows in this DataFrame. @@ -545,7 +547,7 @@ def _stableify(obj: NwDataFrame[IntoFrameT]) -> DataFrame[IntoFrameT]: ... @overload def _stableify(obj: NwLazyFrame[IntoFrameT]) -> LazyFrame[IntoFrameT]: ... @overload -def _stableify(obj: NwSeries) -> Series: ... +def _stableify(obj: NwSeries[IntoSeriesT]) -> Series[IntoSeriesT]: ... @overload def _stableify(obj: NwExpr) -> Expr: ... @overload @@ -553,8 +555,12 @@ def _stableify(obj: Any) -> Any: ... def _stableify( - obj: NwDataFrame[IntoFrameT] | NwLazyFrame[IntoFrameT] | NwSeries | NwExpr | Any, -) -> DataFrame[IntoFrameT] | LazyFrame[IntoFrameT] | Series | Expr | Any: + obj: NwDataFrame[IntoFrameT] + | NwLazyFrame[IntoFrameT] + | NwSeries[IntoSeriesT] + | NwExpr + | Any, +) -> DataFrame[IntoFrameT] | LazyFrame[IntoFrameT] | Series[IntoSeriesT] | Expr | Any: if isinstance(obj, NwDataFrame): return DataFrame( obj._compliant_frame, @@ -584,7 +590,7 @@ def from_native( eager_or_interchange_only: Literal[True], series_only: None = ..., allow_series: Literal[True], -) -> DataFrame[IntoFrameT] | Series: ... +) -> DataFrame[IntoFrameT] | Series[IntoSeriesT]: ... @overload @@ -596,7 +602,7 @@ def from_native( eager_or_interchange_only: None = ..., series_only: None = ..., allow_series: Literal[True], -) -> DataFrame[IntoDataFrameT] | Series: ... +) -> DataFrame[IntoDataFrameT] | Series[IntoSeriesT]: ... @overload @@ -656,7 +662,7 @@ def from_native( eager_or_interchange_only: None = ..., series_only: None = ..., allow_series: Literal[True], -) -> DataFrame[IntoFrameT] | LazyFrame[IntoFrameT] | Series: ... +) -> DataFrame[IntoFrameT] | LazyFrame[IntoFrameT] | Series[IntoSeriesT]: ... @overload @@ -668,7 +674,7 @@ def from_native( eager_or_interchange_only: None = ..., series_only: Literal[True], allow_series: None = ..., -) -> Series: ... +) -> Series[IntoSeriesT]: ... @overload @@ -736,7 +742,7 @@ def from_native( eager_or_interchange_only: None = ..., series_only: None = ..., allow_series: Literal[True], -) -> DataFrame[Any] | LazyFrame[Any] | Series: +) -> DataFrame[Any] | LazyFrame[Any] | Series[Any]: """ from_native(df, strict=True, allow_series=True) from_native(df, allow_series=True) @@ -752,7 +758,7 @@ def from_native( eager_or_interchange_only: None = ..., series_only: Literal[True], allow_series: None = ..., -) -> Series: +) -> Series[Any]: """ from_native(df, strict=True, series_only=True) from_native(df, series_only=True) @@ -796,7 +802,7 @@ def from_native( eager_or_interchange_only: None = ..., series_only: None = ..., allow_series: Literal[True], -) -> DataFrame[IntoDataFrameT] | Series: ... +) -> DataFrame[IntoDataFrameT] | Series[IntoSeriesT]: ... @overload @@ -856,7 +862,7 @@ def from_native( eager_or_interchange_only: None = ..., series_only: None = ..., allow_series: Literal[True], -) -> DataFrame[IntoFrameT] | LazyFrame[IntoFrameT] | Series: ... +) -> DataFrame[IntoFrameT] | LazyFrame[IntoFrameT] | Series[IntoSeriesT]: ... @overload @@ -868,7 +874,7 @@ def from_native( eager_or_interchange_only: None = ..., series_only: Literal[True], allow_series: None = ..., -) -> Series: ... +) -> Series[IntoSeriesT]: ... @overload @@ -936,7 +942,7 @@ def from_native( eager_or_interchange_only: None = ..., series_only: None = ..., allow_series: Literal[True], -) -> DataFrame[Any] | LazyFrame[Any] | Series: +) -> DataFrame[Any] | LazyFrame[Any] | Series[IntoSeriesT]: """ from_native(df, pass_through=False, allow_series=True) from_native(df, allow_series=True) @@ -952,7 +958,7 @@ def from_native( eager_or_interchange_only: None = ..., series_only: Literal[True], allow_series: None = ..., -) -> Series: +) -> Series[IntoSeriesT]: """ from_native(df, pass_through=False, series_only=True) from_native(df, series_only=True) @@ -1067,7 +1073,9 @@ def to_native( narwhals_object: LazyFrame[IntoFrameT], *, strict: Literal[True] = ... ) -> IntoFrameT: ... @overload -def to_native(narwhals_object: Series, *, strict: Literal[True] = ...) -> Any: ... +def to_native( + narwhals_object: Series[IntoSeriesT], *, strict: Literal[True] = ... +) -> Any: ... @overload def to_native(narwhals_object: Any, *, strict: bool) -> Any: ... @overload @@ -1079,13 +1087,15 @@ def to_native( narwhals_object: LazyFrame[IntoFrameT], *, pass_through: Literal[False] = ... ) -> IntoFrameT: ... @overload -def to_native(narwhals_object: Series, *, pass_through: Literal[False] = ...) -> Any: ... +def to_native( + narwhals_object: Series[IntoSeriesT], *, pass_through: Literal[False] = ... +) -> Any: ... @overload def to_native(narwhals_object: Any, *, pass_through: bool) -> Any: ... def to_native( - narwhals_object: DataFrame[IntoFrameT] | LazyFrame[IntoFrameT] | Series, + narwhals_object: DataFrame[IntoFrameT] | LazyFrame[IntoFrameT] | Series[IntoSeriesT], *, strict: bool | None = None, pass_through: bool | None = None, @@ -2361,7 +2371,7 @@ def new_series( dtype: DType | type[DType] | None = None, *, native_namespace: ModuleType, -) -> Series: +) -> Series[Any]: """ Instantiate Narwhals Series from raw data. diff --git a/narwhals/stable/v1/typing.py b/narwhals/stable/v1/typing.py index 79adf5063..e3979185c 100644 --- a/narwhals/stable/v1/typing.py +++ b/narwhals/stable/v1/typing.py @@ -36,7 +36,7 @@ class DataFrameLike(Protocol): def __dataframe__(self, *args: Any, **kwargs: Any) -> Any: ... -IntoExpr: TypeAlias = Union["Expr", str, "Series"] +IntoExpr: TypeAlias = Union["Expr", str, "Series[Any]"] """Anything which can be converted to an expression.""" IntoDataFrame: TypeAlias = Union["NativeFrame", "DataFrame[Any]", "DataFrameLike"] @@ -50,7 +50,7 @@ def __dataframe__(self, *args: Any, **kwargs: Any) -> Any: ... Frame: TypeAlias = Union["DataFrame[Any]", "LazyFrame[Any]"] """Narwhals DataFrame or Narwhals LazyFrame""" -IntoSeries: TypeAlias = Union["Series", "NativeSeries"] +IntoSeries: TypeAlias = Union["Series[Any]", "NativeSeries"] """Anything which can be converted to a Narwhals Series.""" # TypeVars for some of the above diff --git a/narwhals/utils.py b/narwhals/utils.py index 6a4a875ab..35843f641 100644 --- a/narwhals/utils.py +++ b/narwhals/utils.py @@ -38,6 +38,7 @@ from narwhals.dataframe import BaseFrame from narwhals.series import Series + from narwhals.typing import IntoSeriesT T = TypeVar("T") @@ -278,7 +279,7 @@ def maybe_set_index( obj: T, column_names: str | list[str] | None = None, *, - index: Series[Any] | list[Series[Any]] | None = None, + index: Series[IntoSeriesT] | list[Series[IntoSeriesT]] | None = None, ) -> T: """ Set the index of a DataFrame or a Series, if it's pandas-like. diff --git a/tests/utils_test.py b/tests/utils_test.py index dc2415c8d..c36cb1686 100644 --- a/tests/utils_test.py +++ b/tests/utils_test.py @@ -18,6 +18,7 @@ if TYPE_CHECKING: from narwhals.series import Series + from narwhals.typing import IntoSeriesT def test_maybe_align_index_pandas() -> None: @@ -111,7 +112,7 @@ def test_maybe_set_index_polars_column_names( ], ) def test_maybe_set_index_pandas_direct_index( - narwhals_index: Series | list[Series] | None, + narwhals_index: Series[IntoSeriesT] | list[Series[IntoSeriesT]] | None, pandas_index: pd.Series | list[pd.Series] | None, native_df_or_series: pd.DataFrame | pd.Series, ) -> None: @@ -136,7 +137,7 @@ def test_maybe_set_index_pandas_direct_index( ], ) def test_maybe_set_index_polars_direct_index( - index: Series | list[Series] | None, + index: Series[IntoSeriesT] | list[Series[IntoSeriesT]] | None, ) -> None: df = nw.from_native(pl.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]})) result = nw.maybe_set_index(df, index=index) From eea19c6dc48adaf328c353ded86c006d4a444be7 Mon Sep 17 00:00:00 2001 From: Edoardo Abati <29585319+EdAbati@users.noreply.github.com> Date: Wed, 20 Nov 2024 23:56:12 +0100 Subject: [PATCH 3/6] restore stable typing --- narwhals/stable/v1/__init__.py | 64 ++++++++++------------ narwhals/stable/v1/typing.py | 4 +- tests/expr_and_series/dt/timestamp_test.py | 6 +- 3 files changed, 32 insertions(+), 42 deletions(-) diff --git a/narwhals/stable/v1/__init__.py b/narwhals/stable/v1/__init__.py index d3b4cd818..2f8090a33 100644 --- a/narwhals/stable/v1/__init__.py +++ b/narwhals/stable/v1/__init__.py @@ -91,7 +91,7 @@ class DataFrame(NwDataFrame[IntoDataFrameT]): # annotations are correct. @property - def _series(self) -> type[Series[Any]]: + def _series(self) -> type[Series]: return Series @property @@ -105,23 +105,23 @@ def __getitem__(self, item: tuple[Sequence[int], Sequence[int]]) -> Self: ... @overload def __getitem__(self, item: tuple[slice, Sequence[int]]) -> Self: ... @overload - def __getitem__(self, item: tuple[Sequence[int], str]) -> Series[Any]: ... # type: ignore[overload-overlap] + def __getitem__(self, item: tuple[Sequence[int], str]) -> Series: ... # type: ignore[overload-overlap] @overload - def __getitem__(self, item: tuple[slice, str]) -> Series[Any]: ... # type: ignore[overload-overlap] + def __getitem__(self, item: tuple[slice, str]) -> Series: ... # type: ignore[overload-overlap] @overload def __getitem__(self, item: tuple[Sequence[int], Sequence[str]]) -> Self: ... @overload def __getitem__(self, item: tuple[slice, Sequence[str]]) -> Self: ... @overload - def __getitem__(self, item: tuple[Sequence[int], int]) -> Series[Any]: ... # type: ignore[overload-overlap] + def __getitem__(self, item: tuple[Sequence[int], int]) -> Series: ... # type: ignore[overload-overlap] @overload - def __getitem__(self, item: tuple[slice, int]) -> Series[Any]: ... # type: ignore[overload-overlap] + def __getitem__(self, item: tuple[slice, int]) -> Series: ... # type: ignore[overload-overlap] @overload def __getitem__(self, item: Sequence[int]) -> Self: ... @overload - def __getitem__(self, item: str) -> Series[Any]: ... # type: ignore[overload-overlap] + def __getitem__(self, item: str) -> Series: ... # type: ignore[overload-overlap] @overload def __getitem__(self, item: Sequence[str]) -> Self: ... @@ -185,16 +185,14 @@ def lazy(self) -> LazyFrame[Any]: # Not sure what mypy is complaining about, probably some fancy # thing that I need to understand category theory for @overload # type: ignore[override] - def to_dict(self, *, as_series: Literal[True] = ...) -> dict[str, Series[Any]]: ... + def to_dict(self, *, as_series: Literal[True] = ...) -> dict[str, Series]: ... @overload def to_dict(self, *, as_series: Literal[False]) -> dict[str, list[Any]]: ... @overload - def to_dict( - self, *, as_series: bool - ) -> dict[str, Series[Any]] | dict[str, list[Any]]: ... + def to_dict(self, *, as_series: bool) -> dict[str, Series] | dict[str, list[Any]]: ... def to_dict( self, *, as_series: bool = True - ) -> dict[str, Series[Any]] | dict[str, list[Any]]: + ) -> dict[str, Series] | dict[str, list[Any]]: """Convert DataFrame to a dictionary mapping column name to values. Arguments: @@ -237,7 +235,7 @@ def to_dict( """ return super().to_dict(as_series=as_series) # type: ignore[return-value] - def is_duplicated(self: Self) -> Series[Any]: + def is_duplicated(self: Self) -> Series: r"""Get a mask of all duplicated rows in this DataFrame. Returns: @@ -287,7 +285,7 @@ def is_duplicated(self: Self) -> Series[Any]: """ return super().is_duplicated() # type: ignore[return-value] - def is_unique(self: Self) -> Series[Any]: + def is_unique(self: Self) -> Series: r"""Get a mask of all unique rows in this DataFrame. Returns: @@ -405,7 +403,7 @@ def _l1_norm(self: Self) -> Self: return self.select(all()._l1_norm()) -class Series(NwSeries[IntoSeriesT]): +class Series(NwSeries[Any]): """Narwhals Series, backed by a native series. The native series might be pandas.Series, polars.Series, ... @@ -960,7 +958,7 @@ def _stableify(obj: NwDataFrame[IntoFrameT]) -> DataFrame[IntoFrameT]: ... @overload def _stableify(obj: NwLazyFrame[IntoFrameT]) -> LazyFrame[IntoFrameT]: ... @overload -def _stableify(obj: NwSeries[IntoSeriesT]) -> Series[IntoSeriesT]: ... +def _stableify(obj: NwSeries[IntoSeriesT]) -> Series: ... @overload def _stableify(obj: NwExpr) -> Expr: ... @overload @@ -973,7 +971,7 @@ def _stableify( | NwSeries[IntoSeriesT] | NwExpr | Any, -) -> DataFrame[IntoFrameT] | LazyFrame[IntoFrameT] | Series[IntoSeriesT] | Expr | Any: +) -> DataFrame[IntoFrameT] | LazyFrame[IntoFrameT] | Series | Expr | Any: if isinstance(obj, NwDataFrame): return DataFrame( obj._compliant_frame, @@ -1003,7 +1001,7 @@ def from_native( eager_or_interchange_only: Literal[True], series_only: None = ..., allow_series: Literal[True], -) -> DataFrame[IntoFrameT] | Series[IntoSeriesT]: ... +) -> DataFrame[IntoFrameT] | Series: ... @overload @@ -1015,7 +1013,7 @@ def from_native( eager_or_interchange_only: None = ..., series_only: None = ..., allow_series: Literal[True], -) -> DataFrame[IntoDataFrameT] | Series[IntoSeriesT]: ... +) -> DataFrame[IntoDataFrameT] | Series: ... @overload @@ -1075,7 +1073,7 @@ def from_native( eager_or_interchange_only: None = ..., series_only: None = ..., allow_series: Literal[True], -) -> DataFrame[IntoFrameT] | LazyFrame[IntoFrameT] | Series[IntoSeriesT]: ... +) -> DataFrame[IntoFrameT] | LazyFrame[IntoFrameT] | Series: ... @overload @@ -1087,7 +1085,7 @@ def from_native( eager_or_interchange_only: None = ..., series_only: Literal[True], allow_series: None = ..., -) -> Series[IntoSeriesT]: ... +) -> Series: ... @overload @@ -1147,7 +1145,7 @@ def from_native( eager_or_interchange_only: None = ..., series_only: None = ..., allow_series: Literal[True], -) -> DataFrame[Any] | LazyFrame[Any] | Series[Any]: ... +) -> DataFrame[Any] | LazyFrame[Any] | Series: ... @overload @@ -1159,7 +1157,7 @@ def from_native( eager_or_interchange_only: None = ..., series_only: Literal[True], allow_series: None = ..., -) -> Series[Any]: ... +) -> Series: ... @overload @@ -1195,7 +1193,7 @@ def from_native( eager_or_interchange_only: None = ..., series_only: None = ..., allow_series: Literal[True], -) -> DataFrame[IntoDataFrameT] | Series[IntoSeriesT]: ... +) -> DataFrame[IntoDataFrameT] | Series: ... @overload @@ -1255,7 +1253,7 @@ def from_native( eager_or_interchange_only: None = ..., series_only: None = ..., allow_series: Literal[True], -) -> DataFrame[IntoFrameT] | LazyFrame[IntoFrameT] | Series[IntoSeriesT]: ... +) -> DataFrame[IntoFrameT] | LazyFrame[IntoFrameT] | Series: ... @overload @@ -1267,7 +1265,7 @@ def from_native( eager_or_interchange_only: None = ..., series_only: Literal[True], allow_series: None = ..., -) -> Series[IntoSeriesT]: ... +) -> Series: ... @overload @@ -1327,7 +1325,7 @@ def from_native( eager_or_interchange_only: None = ..., series_only: None = ..., allow_series: Literal[True], -) -> DataFrame[Any] | LazyFrame[Any] | Series[IntoSeriesT]: ... +) -> DataFrame[Any] | LazyFrame[Any] | Series: ... @overload @@ -1339,7 +1337,7 @@ def from_native( eager_or_interchange_only: None = ..., series_only: Literal[True], allow_series: None = ..., -) -> Series[IntoSeriesT]: ... +) -> Series: ... @overload @@ -1445,9 +1443,7 @@ def to_native( narwhals_object: LazyFrame[IntoFrameT], *, strict: Literal[True] = ... ) -> IntoFrameT: ... @overload -def to_native( - narwhals_object: Series[IntoSeriesT], *, strict: Literal[True] = ... -) -> Any: ... +def to_native(narwhals_object: Series, *, strict: Literal[True] = ...) -> Any: ... @overload def to_native(narwhals_object: Any, *, strict: bool) -> Any: ... @overload @@ -1459,15 +1455,13 @@ def to_native( narwhals_object: LazyFrame[IntoFrameT], *, pass_through: Literal[False] = ... ) -> IntoFrameT: ... @overload -def to_native( - narwhals_object: Series[IntoSeriesT], *, pass_through: Literal[False] = ... -) -> Any: ... +def to_native(narwhals_object: Series, *, pass_through: Literal[False] = ...) -> Any: ... @overload def to_native(narwhals_object: Any, *, pass_through: bool) -> Any: ... def to_native( - narwhals_object: DataFrame[IntoFrameT] | LazyFrame[IntoFrameT] | Series[IntoSeriesT], + narwhals_object: DataFrame[IntoFrameT] | LazyFrame[IntoFrameT] | Series, *, strict: bool | None = None, pass_through: bool | None = None, @@ -2782,7 +2776,7 @@ def new_series( dtype: DType | type[DType] | None = None, *, native_namespace: ModuleType, -) -> Series[Any]: +) -> Series: """Instantiate Narwhals Series from iterable (e.g. list or array). Arguments: diff --git a/narwhals/stable/v1/typing.py b/narwhals/stable/v1/typing.py index e3979185c..79adf5063 100644 --- a/narwhals/stable/v1/typing.py +++ b/narwhals/stable/v1/typing.py @@ -36,7 +36,7 @@ class DataFrameLike(Protocol): def __dataframe__(self, *args: Any, **kwargs: Any) -> Any: ... -IntoExpr: TypeAlias = Union["Expr", str, "Series[Any]"] +IntoExpr: TypeAlias = Union["Expr", str, "Series"] """Anything which can be converted to an expression.""" IntoDataFrame: TypeAlias = Union["NativeFrame", "DataFrame[Any]", "DataFrameLike"] @@ -50,7 +50,7 @@ def __dataframe__(self, *args: Any, **kwargs: Any) -> Any: ... Frame: TypeAlias = Union["DataFrame[Any]", "LazyFrame[Any]"] """Narwhals DataFrame or Narwhals LazyFrame""" -IntoSeries: TypeAlias = Union["Series[Any]", "NativeSeries"] +IntoSeries: TypeAlias = Union["Series", "NativeSeries"] """Anything which can be converted to a Narwhals Series.""" # TypeVars for some of the above diff --git a/tests/expr_and_series/dt/timestamp_test.py b/tests/expr_and_series/dt/timestamp_test.py index e20086c65..212926628 100644 --- a/tests/expr_and_series/dt/timestamp_test.py +++ b/tests/expr_and_series/dt/timestamp_test.py @@ -1,7 +1,6 @@ from __future__ import annotations from datetime import datetime -from typing import TYPE_CHECKING from typing import Literal import hypothesis.strategies as st @@ -19,9 +18,6 @@ from tests.utils import assert_equal_data from tests.utils import is_windows -if TYPE_CHECKING: - from narwhals.typing import IntoSeriesT - data = { "a": [ datetime(2021, 3, 1, 12, 34, 56, 49000), @@ -211,7 +207,7 @@ def test_timestamp_hypothesis( import polars as pl @nw.narwhalify - def func(s: nw.Series[IntoSeriesT]) -> nw.Series[IntoSeriesT]: + def func(s: nw.Series) -> nw.Series: return s.dt.timestamp(time_unit) result_pl = func(pl.Series([inputs], dtype=pl.Datetime(starting_time_unit))) From 1e2632b0e2fe67a6890636a4a9c713a69b3c62c7 Mon Sep 17 00:00:00 2001 From: Edoardo Abati <29585319+EdAbati@users.noreply.github.com> Date: Tue, 26 Nov 2024 07:54:52 +0100 Subject: [PATCH 4/6] Any in stable/v1 --- narwhals/stable/v1/__init__.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/narwhals/stable/v1/__init__.py b/narwhals/stable/v1/__init__.py index 075e4393c..0dfcc2191 100644 --- a/narwhals/stable/v1/__init__.py +++ b/narwhals/stable/v1/__init__.py @@ -1150,7 +1150,7 @@ def _stableify(obj: NwDataFrame[IntoFrameT]) -> DataFrame[IntoFrameT]: ... @overload def _stableify(obj: NwLazyFrame[IntoFrameT]) -> LazyFrame[IntoFrameT]: ... @overload -def _stableify(obj: NwSeries[IntoSeriesT]) -> Series: ... +def _stableify(obj: NwSeries[Any]) -> Series: ... @overload def _stableify(obj: NwExpr) -> Expr: ... @overload @@ -1158,11 +1158,7 @@ def _stableify(obj: Any) -> Any: ... def _stableify( - obj: NwDataFrame[IntoFrameT] - | NwLazyFrame[IntoFrameT] - | NwSeries[IntoSeriesT] - | NwExpr - | Any, + obj: NwDataFrame[IntoFrameT] | NwLazyFrame[IntoFrameT] | NwSeries[Any] | NwExpr | Any, ) -> DataFrame[IntoFrameT] | LazyFrame[IntoFrameT] | Series | Expr | Any: if isinstance(obj, NwDataFrame): return DataFrame( From 716c3cd14415cdb8ef25fa5e72320fa927ddbb6c Mon Sep 17 00:00:00 2001 From: Marco Gorelli <33491632+MarcoGorelli@users.noreply.github.com> Date: Fri, 29 Nov 2024 16:44:27 +0000 Subject: [PATCH 5/6] use typevar in to_native --- narwhals/series.py | 4 ++-- narwhals/translate.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/narwhals/series.py b/narwhals/series.py index 5aff4b440..9fae34ec0 100644 --- a/narwhals/series.py +++ b/narwhals/series.py @@ -99,7 +99,7 @@ def __arrow_c_stream__(self, requested_schema: object | None = None) -> object: ca = pa.chunked_array([self.to_arrow()]) return ca.__arrow_c_stream__(requested_schema=requested_schema) - def to_native(self) -> Any: + def to_native(self) -> IntoSeriesT: """Convert Narwhals series to native series. Returns: @@ -136,7 +136,7 @@ def to_native(self) -> Any: 3 ] """ - return self._compliant_series._native_series + return self._compliant_series._native_series # type: ignore[no-any-return] def scatter(self, indices: int | Sequence[int], values: Any) -> Self: """Set value(s) at given position(s). diff --git a/narwhals/translate.py b/narwhals/translate.py index 1acb24ce7..1f9371a23 100644 --- a/narwhals/translate.py +++ b/narwhals/translate.py @@ -67,7 +67,7 @@ def to_native( @overload def to_native( narwhals_object: Series[IntoSeriesT], *, pass_through: Literal[False] = ... -) -> Any: ... +) -> IntoSeriesT: ... @overload def to_native(narwhals_object: Any, *, pass_through: bool) -> Any: ... From 00e3a7e1770a3d166e03705f9a2aa69cceff9314 Mon Sep 17 00:00:00 2001 From: Marco Gorelli <33491632+MarcoGorelli@users.noreply.github.com> Date: Fri, 29 Nov 2024 16:57:52 +0000 Subject: [PATCH 6/6] add extra docs note --- docs/backcompat.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/docs/backcompat.md b/docs/backcompat.md index e44c38ebf..787df393d 100644 --- a/docs/backcompat.md +++ b/docs/backcompat.md @@ -99,6 +99,20 @@ before making any change. ### After `stable.v1` +- Since Narwhals 1.15, `Series` is generic in the native Series, meaning that you can + write: + ```python + import narwhals as nw + import polars as pl + + s_pl = pl.Series([1, 2, 3]) + s = nw.from_native(s, series_only=True) + # mypy infers `s.to_native()` to be `polars.Series` + reveal_type(s.to_native()) + ``` + Previously, `Series` was not generic, so in the above example + `s.to_native()` would have been inferred as `Any`. + - Since Narwhals 1.13.0, the `strict` parameter in `from_native`, `to_native`, and `narwhalify` has been deprecated in favour of `pass_through`. This is because several users expressed confusion/surprise over what `strict=False` did.