diff --git a/narwhals/_compliant/dataframe.py b/narwhals/_compliant/dataframe.py index 3e7810616c..507f422846 100644 --- a/narwhals/_compliant/dataframe.py +++ b/narwhals/_compliant/dataframe.py @@ -330,7 +330,7 @@ def __narwhals_namespace__( ]: ... def to_narwhals(self) -> DataFrame[NativeDataFrameT]: - return self._version.dataframe(self, level="full") + return self._version.dataframe(self) def aggregate(self, *exprs: EagerExprT) -> Self: # pyright: ignore[reportIncompatibleMethodOverride] # NOTE: Ignore intermittent [False Negative] (1) diff --git a/narwhals/_compliant/series.py b/narwhals/_compliant/series.py index 077e6a86ad..652a671c68 100644 --- a/narwhals/_compliant/series.py +++ b/narwhals/_compliant/series.py @@ -93,7 +93,7 @@ def __native_namespace__(self) -> ModuleType: ... @classmethod def from_native(cls, data: NativeSeriesT, /, *, context: _LimitedContext) -> Self: ... def to_narwhals(self) -> Series[NativeSeriesT]: - return self._version.series(self, level="full") + return self._version.series(self) def _with_native(self, series: Any) -> Self: ... def _with_version(self, version: Version) -> Self: ... diff --git a/narwhals/_dask/dataframe.py b/narwhals/_dask/dataframe.py index 15bc3fab7e..7e3ce722e3 100644 --- a/narwhals/_dask/dataframe.py +++ b/narwhals/_dask/dataframe.py @@ -78,7 +78,7 @@ def from_native(cls, data: dd.DataFrame, /, *, context: _LimitedContext) -> Self return cls(data, version=context._version) def to_narwhals(self) -> LazyFrame[dd.DataFrame]: - return self._version.lazyframe(self, level="lazy") + return self._version.lazyframe(self) def __native_namespace__(self) -> ModuleType: if self._implementation is Implementation.DASK: diff --git a/narwhals/_duckdb/dataframe.py b/narwhals/_duckdb/dataframe.py index 23ef4daa65..35bab5a3da 100644 --- a/narwhals/_duckdb/dataframe.py +++ b/narwhals/_duckdb/dataframe.py @@ -102,7 +102,7 @@ def to_narwhals( from narwhals.stable.v1 import DataFrame as DataFrameV1 return DataFrameV1(self, level="interchange") # type: ignore[no-any-return] - return self._version.lazyframe(self, level="lazy") + return self._version.lazyframe(self) def __narwhals_dataframe__(self) -> Self: # pragma: no cover # Keep around for backcompat. diff --git a/narwhals/_ibis/dataframe.py b/narwhals/_ibis/dataframe.py index 43b4f2b6ce..79a6da3c6e 100644 --- a/narwhals/_ibis/dataframe.py +++ b/narwhals/_ibis/dataframe.py @@ -75,7 +75,7 @@ def to_narwhals(self) -> LazyFrame[ir.Table] | DataFrameV1[ir.Table]: from narwhals.stable.v1 import DataFrame return DataFrame(self, level="interchange") - return self._version.lazyframe(self, level="lazy") + return self._version.lazyframe(self) def __narwhals_dataframe__(self) -> Self: # pragma: no cover # Keep around for backcompat. diff --git a/narwhals/_interchange/dataframe.py b/narwhals/_interchange/dataframe.py index a07f1fad4c..c9e88ef56e 100644 --- a/narwhals/_interchange/dataframe.py +++ b/narwhals/_interchange/dataframe.py @@ -1,7 +1,7 @@ from __future__ import annotations import enum -from typing import TYPE_CHECKING, Any, NoReturn +from typing import TYPE_CHECKING, Any, NoReturn, Protocol from narwhals._utils import Version, parse_version @@ -12,7 +12,10 @@ from narwhals._interchange.series import InterchangeSeries from narwhals.dtypes import DType - from narwhals.stable.v1.typing import DataFrameLike + + +class DataFrameLike(Protocol): + def __dataframe__(self, *args: Any, **kwargs: Any) -> Any: ... class DtypeKind(enum.IntEnum): diff --git a/narwhals/_polars/dataframe.py b/narwhals/_polars/dataframe.py index 0848967e9a..412bea5227 100644 --- a/narwhals/_polars/dataframe.py +++ b/narwhals/_polars/dataframe.py @@ -384,7 +384,7 @@ def from_numpy( return cls.from_native(pl.from_numpy(data, pl_schema), context=context) def to_narwhals(self) -> DataFrame[pl.DataFrame]: - return self._version.dataframe(self, level="full") + return self._version.dataframe(self) def __repr__(self) -> str: # pragma: no cover return "PolarsDataFrame" @@ -681,7 +681,7 @@ def _is_native(obj: pl.LazyFrame | Any) -> TypeIs[pl.LazyFrame]: return isinstance(obj, pl.LazyFrame) def to_narwhals(self) -> LazyFrame[pl.LazyFrame]: - return self._version.lazyframe(self, level="lazy") + return self._version.lazyframe(self) def __repr__(self) -> str: # pragma: no cover return "PolarsLazyFrame" diff --git a/narwhals/_polars/series.py b/narwhals/_polars/series.py index 5a8397522a..1f2bf14aca 100644 --- a/narwhals/_polars/series.py +++ b/narwhals/_polars/series.py @@ -216,7 +216,7 @@ def from_numpy(cls, data: Into1DArray, /, *, context: _LimitedContext) -> Self: return cls.from_native(native, context=context) def to_narwhals(self) -> Series[pl.Series]: - return self._version.series(self, level="full") + return self._version.series(self) def _with_native(self, series: pl.Series) -> Self: return self.__class__(series, version=self._version) diff --git a/narwhals/_spark_like/dataframe.py b/narwhals/_spark_like/dataframe.py index 18eb945843..293b055ec9 100644 --- a/narwhals/_spark_like/dataframe.py +++ b/narwhals/_spark_like/dataframe.py @@ -114,7 +114,7 @@ def from_native(cls, data: SQLFrameDataFrame, /, *, context: _LimitedContext) -> return cls(data, version=context._version, implementation=context._implementation) def to_narwhals(self) -> LazyFrame[SQLFrameDataFrame]: - return self._version.lazyframe(self, level="lazy") + return self._version.lazyframe(self) def __native_namespace__(self) -> ModuleType: # pragma: no cover return self._implementation.to_native_namespace() diff --git a/narwhals/_utils.py b/narwhals/_utils.py index 70b6f5d342..647986ffef 100644 --- a/narwhals/_utils.py +++ b/narwhals/_utils.py @@ -1922,7 +1922,7 @@ def convert_str_slice_to_int_slice( def inherit_doc( - tp_parent: Callable[P, R1], / + tp_parent: Callable[..., R1], / ) -> Callable[[_Constructor[_T, P, R2]], _Constructor[_T, P, R2]]: """Steal the class-level docstring from parent and attach to child `__init__`. diff --git a/narwhals/dataframe.py b/narwhals/dataframe.py index bd602efd52..22fe74b5e7 100644 --- a/narwhals/dataframe.py +++ b/narwhals/dataframe.py @@ -88,6 +88,7 @@ SingleColSelector, SingleIndexSelector, SizeUnit, + SupportLevel, UniqueKeepStrategy, _2DArray, ) @@ -106,7 +107,10 @@ class BaseFrame(Generic[_FrameT]): _compliant_frame: Any - _level: Literal["full", "lazy", "interchange"] + # `_level` is stored on subclasses: + # * For stable.v1 "interchange" is still supported. + # * For main and stable.v2 only "full" and "lazy" are supported. + _level: SupportLevel implementation: _Implementation = _Implementation() """Return [`narwhals.Implementation`][] of native frame. @@ -141,7 +145,7 @@ def __narwhals_namespace__(self) -> Any: def _with_compliant(self, df: Any) -> Self: # construct, preserving properties - return self.__class__(df, level=self._level) # type: ignore[call-arg] + return self.__class__(df) # type: ignore[call-arg] def _flatten_and_extract( self, *exprs: IntoExpr | Iterable[IntoExpr], **named_exprs: IntoExpr @@ -473,6 +477,7 @@ class DataFrame(BaseFrame[DataFrameT]): """ _version: ClassVar[Version] = Version.MAIN + _level: SupportLevel = "full" @property def _compliant(self) -> CompliantDataFrame[Any, Any, DataFrameT, Self]: @@ -490,8 +495,7 @@ def _validate_metadata(self, metadata: ExprMetadata) -> None: # all is valid in eager case. pass - def __init__(self, df: Any, *, level: Literal["full", "lazy", "interchange"]) -> None: - self._level: Literal["full", "lazy", "interchange"] = level + def __init__(self, df: Any) -> None: self._compliant_frame: CompliantDataFrame[Any, Any, DataFrameT, Self] if is_compliant_dataframe(df): self._compliant_frame = df.__narwhals_dataframe__() @@ -544,7 +548,7 @@ def from_arrow( if is_eager_allowed(implementation): ns = cls._version.namespace.from_backend(implementation).compliant compliant = ns._dataframe.from_arrow(native_frame, context=ns) - return cls(compliant, level="full") + return cls(compliant) msg = ( f"{implementation} support in Narwhals is lazy-only, but `DataFrame.from_arrow` is an eager-only function.\n\n" "Hint: you may want to use an eager backend and then call `.lazy`, e.g.:\n\n" @@ -604,7 +608,7 @@ def from_dict( if is_eager_allowed(implementation): ns = cls._version.namespace.from_backend(implementation).compliant compliant = ns._dataframe.from_dict(data, schema=schema, context=ns) - return cls(compliant, level="full") + return cls(compliant) # NOTE: (#2786) needs resolving for extensions msg = ( f"{implementation} support in Narwhals is lazy-only, but `DataFrame.from_dict` is an eager-only function.\n\n" @@ -676,7 +680,7 @@ def from_dicts( if is_eager_allowed(implementation): ns = cls._version.namespace.from_backend(implementation).compliant compliant = ns._dataframe.from_dicts(data, schema=schema, context=ns) - return cls(compliant, level="full") + return cls(compliant) # NOTE: (#2786) needs resolving for extensions msg = ( f"{implementation} support in Narwhals is lazy-only, but `DataFrame.from_dicts` is an eager-only function.\n\n" @@ -748,7 +752,7 @@ def from_numpy( implementation = Implementation.from_backend(backend) if is_eager_allowed(implementation): ns = cls._version.namespace.from_backend(implementation).compliant - return cls(ns.from_numpy(data, schema), level="full") + return cls(ns.from_numpy(data, schema)) msg = ( f"{implementation} support in Narwhals is lazy-only, but `DataFrame.from_numpy` is an eager-only function.\n\n" "Hint: you may want to use an eager backend and then call `.lazy`, e.g.:\n\n" @@ -864,10 +868,10 @@ def lazy( """ lazy = self._compliant_frame.lazy if backend is None: - return self._lazyframe(lazy(None, session=session), level="lazy") + return self._lazyframe(lazy(None, session=session)) lazy_backend = Implementation.from_backend(backend) if is_lazy_allowed(lazy_backend): - return self._lazyframe(lazy(lazy_backend, session=session), level="lazy") + return self._lazyframe(lazy(lazy_backend, session=session)) msg = f"Not-supported backend.\n\nExpected one of {get_args(_LazyAllowedImpl)} or `None`, got {lazy_backend}" raise ValueError(msg) @@ -1025,7 +1029,7 @@ def get_column(self, name: str) -> Series[Any]: 1 2 Name: a, dtype: int64 """ - return self._series(self._compliant_frame.get_column(name), level=self._level) + return self._series(self._compliant_frame.get_column(name)) def estimated_size(self, unit: SizeUnit = "b") -> int | float: """Return an estimation of the total (heap) allocated size of the `DataFrame`. @@ -1211,7 +1215,7 @@ def to_dict( """ if as_series: return { - key: self._series(value, level=self._level) + key: self._series(value) for key, value in self._compliant_frame.to_dict( as_series=as_series ).items() @@ -1419,7 +1423,7 @@ def iter_columns(self) -> Iterator[Series[Any]]: └─────────────────────────┘ """ for series in self._compliant_frame.iter_columns(): - yield self._series(series, level=self._level) + yield self._series(series) @overload def iter_rows( @@ -2055,7 +2059,7 @@ def is_unique(self) -> Series[Any]: | dtype: bool | └───────────────┘ """ - return self._series(self._compliant_frame.is_unique(), level=self._level) + return self._series(self._compliant_frame.is_unique()) def null_count(self) -> Self: r"""Create a new DataFrame that shows the null counts per column. @@ -2362,6 +2366,7 @@ class LazyFrame(BaseFrame[LazyFrameT]): """ _version: ClassVar[Version] = Version.MAIN + _level: SupportLevel = "lazy" @property def _compliant(self) -> CompliantLazyFrame[Any, LazyFrameT, Self]: @@ -2395,8 +2400,7 @@ def _validate_metadata(self, metadata: ExprMetadata) -> None: ) raise InvalidOperationError(msg) - def __init__(self, df: Any, *, level: Literal["full", "lazy", "interchange"]) -> None: - self._level = level + def __init__(self, df: Any) -> None: self._compliant_frame: CompliantLazyFrame[Any, LazyFrameT, Self] if is_compliant_lazyframe(df): self._compliant_frame = df.__narwhals_lazyframe__() @@ -2473,10 +2477,10 @@ def collect( """ collect = self._compliant_frame.collect if backend is None: - return self._dataframe(collect(None, **kwargs), level="full") + return self._dataframe(collect(None, **kwargs)) eager_backend = Implementation.from_backend(backend) if can_lazyframe_collect(eager_backend): - return self._dataframe(collect(eager_backend, **kwargs), level="full") + return self._dataframe(collect(eager_backend, **kwargs)) msg = f"Unsupported `backend` value.\nExpected one of {get_args(_LazyFrameCollectImpl)} or None, got: {eager_backend}." raise ValueError(msg) diff --git a/narwhals/series.py b/narwhals/series.py index 6acc476245..301f800810 100644 --- a/narwhals/series.py +++ b/narwhals/series.py @@ -67,6 +67,7 @@ RankMethod, RollingInterpolationMethod, SingleIndexSelector, + SupportLevel, TemporalLiteral, _1DArray, ) @@ -94,6 +95,8 @@ class Series(Generic[IntoSeriesT]): """ _version: ClassVar[Version] = Version.MAIN + # See note on `BaseFrame._level`. Subclasses override at class or instance level. + _level: SupportLevel = "full" @property def _compliant(self) -> CompliantSeries[IntoSeriesT]: @@ -110,10 +113,7 @@ def _to_expr(self) -> Expr: ExprNode(ExprKind.SERIES, "_expr._from_series", series=self._compliant) ) - def __init__( - self, series: Any, *, level: Literal["full", "lazy", "interchange"] - ) -> None: - self._level: Literal["full", "lazy", "interchange"] = level + def __init__(self, series: Any) -> None: if is_compliant_series(series): self._compliant_series: CompliantSeries[IntoSeriesT] = ( series.__narwhals_series__() @@ -178,8 +178,8 @@ def from_numpy( ns = cls._version.namespace.from_backend(implementation).compliant compliant = ns.from_numpy(values).alias(name) if dtype: - return cls(compliant.cast(dtype), level="full") - return cls(compliant, level="full") + return cls(compliant.cast(dtype)) + return cls(compliant) msg = ( # pragma: no cover f"{implementation} support in Narwhals is lazy-only, but `Series.from_numpy` is an eager-only function.\n\n" "Hint: you may want to use an eager backend and then call `.lazy`, e.g.:\n\n" @@ -241,7 +241,7 @@ def from_iterable( compliant = ns._series.from_iterable( values, context=ns, name=name, dtype=dtype ) - return cls(compliant, level="full") + return cls(compliant) msg = ( f"{implementation} support in Narwhals is lazy-only, but `Series.from_iterable` is an eager-only function.\n\n" "Hint: you may want to use an eager backend and then call `.lazy`, e.g.:\n\n" @@ -482,7 +482,7 @@ def _extract_native(self, arg: Any) -> Any: return arg def _with_compliant(self, series: Any) -> Self: - return self.__class__(series, level=self._level) + return self.__class__(series) def pipe(self, function: Callable[[Any], Self], *args: Any, **kwargs: Any) -> Self: """Pipe function call. @@ -665,7 +665,7 @@ def to_frame(self) -> DataFrame[Any]: │ 2 │ └─────┘ """ - return self._dataframe(self._compliant_series.to_frame(), level=self._level) + return self._dataframe(self._compliant_series.to_frame()) def to_list(self) -> list[Any]: """Convert to list. @@ -1962,8 +1962,7 @@ def value_counts( return self._dataframe( self._compliant_series.value_counts( sort=sort, parallel=parallel, name=name, normalize=normalize - ), - level=self._level, + ) ) def quantile( @@ -2201,8 +2200,7 @@ def to_dummies( 2 0 1 """ return self._dataframe( - self._compliant_series.to_dummies(separator=separator, drop_first=drop_first), - level=self._level, + self._compliant_series.to_dummies(separator=separator, drop_first=drop_first) ) def gather_every(self, n: int, offset: int = 0) -> Self: @@ -2732,7 +2730,7 @@ def hist( bin_count=bin_count, include_breakpoint=include_breakpoint ) - return self._dataframe(result, level=self._level) + return self._dataframe(result) def log(self, base: float = math.e) -> Self: r"""Compute the logarithm to a given base. diff --git a/narwhals/sql.py b/narwhals/sql.py index cd503f0dcd..f0d1939e7a 100644 --- a/narwhals/sql.py +++ b/narwhals/sql.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Literal +from typing import TYPE_CHECKING from narwhals._duckdb.utils import DeferredTimeZone, narwhals_to_native_dtype from narwhals.dataframe import LazyFrame @@ -29,10 +29,8 @@ class SQLTable(LazyFrame[duckdb.DuckDBPyRelation]): """A LazyFrame with an additional `to_sql` method.""" - def __init__( - self, df: CompliantLazyFrameAny, level: Literal["full", "interchange", "lazy"] - ) -> None: - super().__init__(df, level=level) + def __init__(self, df: CompliantLazyFrameAny) -> None: + super().__init__(df) def to_sql(self, *, pretty: bool = False) -> str: """Convert to SQL query. @@ -99,7 +97,7 @@ def table(name: str, schema: IntoSchema) -> SQLTable: ({dtypes}); """) lf = from_native(CONN.table(name)) - return SQLTable(lf._compliant_frame, level=lf._level) + return SQLTable(lf._compliant_frame) __all__ = ["table"] diff --git a/narwhals/stable/v1/__init__.py b/narwhals/stable/v1/__init__.py index a974d3c565..ae5b6c2532 100644 --- a/narwhals/stable/v1/__init__.py +++ b/narwhals/stable/v1/__init__.py @@ -115,6 +115,7 @@ PythonLiteral, SingleColSelector, SingleIndexSelector, + SupportLevel, _1DArray, _2DArray, ) @@ -129,9 +130,15 @@ class DataFrame(NwDataFrame[IntoDataFrameT]): # type: ignore[type-var] _version = Version.V1 @inherit_doc(NwDataFrame) - def __init__(self, df: Any, *, level: Literal["full", "lazy", "interchange"]) -> None: + def __init__(self, df: Any, *, level: SupportLevel | None = None) -> None: + from narwhals._interchange.dataframe import InterchangeFrame + assert df._version is Version.V1 # noqa: S101 - super().__init__(df, level=level) + super().__init__(df) + if level is not None: + self._level = level + elif isinstance(self._compliant_frame, InterchangeFrame): + self._level = "interchange" # We need to override any method which don't return Self so that type # annotations are correct. @@ -261,9 +268,11 @@ class LazyFrame(NwLazyFrame[IntoLazyFrameT]): _version = Version.V1 @inherit_doc(NwLazyFrame) - def __init__(self, df: Any, *, level: Literal["full", "lazy", "interchange"]) -> None: + def __init__(self, df: Any, *, level: SupportLevel | None = None) -> None: assert df._version is Version.V1 # noqa: S101 - super().__init__(df, level=level) + super().__init__(df) + if level is not None: + self._level = level @property def _dataframe(self) -> type[DataFrame[Any]]: @@ -313,11 +322,15 @@ class Series(NwSeries[IntoSeriesT]): _version = Version.V1 @inherit_doc(NwSeries) - def __init__( - self, series: Any, *, level: Literal["full", "lazy", "interchange"] - ) -> None: + def __init__(self, series: Any, *, level: SupportLevel | None = None) -> None: + from narwhals._interchange.series import InterchangeSeries + assert series._version is Version.V1 # noqa: S101 - super().__init__(series, level=level) + super().__init__(series) + if level is not None: + self._level = level + elif isinstance(self._compliant_series, InterchangeSeries): + self._level = "interchange" # We need to override any method which don't return Self so that type # annotations are correct. @@ -521,11 +534,11 @@ def _stableify( | NwExpr, ) -> DataFrame[IntoDataFrameT] | LazyFrame[IntoLazyFrameT] | Series[IntoSeriesT] | Expr: if isinstance(obj, NwDataFrame): - return DataFrame(obj._compliant_frame._with_version(Version.V1), level=obj._level) + return DataFrame(obj._compliant_frame._with_version(Version.V1)) if isinstance(obj, NwLazyFrame): - return LazyFrame(obj._compliant_frame._with_version(Version.V1), level=obj._level) + return LazyFrame(obj._compliant_frame._with_version(Version.V1)) if isinstance(obj, NwSeries): - return Series(obj._compliant_series._with_version(Version.V1), level=obj._level) + return Series(obj._compliant_series._with_version(Version.V1)) if isinstance(obj, NwExpr): return Expr(*obj._nodes) assert_never(obj) @@ -880,9 +893,7 @@ def coalesce(exprs: IntoExpr | Iterable[IntoExpr], *more_exprs: IntoExpr) -> Exp return _stableify(nw.coalesce(exprs, *more_exprs)) -def get_level( - obj: DataFrame[Any] | LazyFrame[Any] | Series[IntoSeriesT], -) -> Literal["full", "lazy", "interchange"]: +def get_level(obj: DataFrame[Any] | LazyFrame[Any] | Series[IntoSeriesT]) -> SupportLevel: """Level of support Narwhals has for current object. Arguments: diff --git a/narwhals/stable/v1/typing.py b/narwhals/stable/v1/typing.py index a154a1a3f8..7d23ab6c74 100644 --- a/narwhals/stable/v1/typing.py +++ b/narwhals/stable/v1/typing.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any, Protocol, Union +from typing import TYPE_CHECKING, Any, Union from narwhals._native import IntoSeries from narwhals._typing_compat import TypeVar @@ -8,12 +8,10 @@ if TYPE_CHECKING: from typing_extensions import TypeAlias + from narwhals._interchange.dataframe import DataFrameLike from narwhals._native import NativeDataFrame, NativeDuckDB, NativeLazyFrame from narwhals.stable.v1 import DataFrame, Expr, LazyFrame, Series - class DataFrameLike(Protocol): - def __dataframe__(self, *args: Any, **kwargs: Any) -> Any: ... - IntoExpr: TypeAlias = Union["Expr", str, "Series[Any]"] """Anything which can be converted to an expression. diff --git a/narwhals/stable/v2/__init__.py b/narwhals/stable/v2/__init__.py index 3f480b4ca8..1bd26c5da6 100644 --- a/narwhals/stable/v2/__init__.py +++ b/narwhals/stable/v2/__init__.py @@ -116,9 +116,9 @@ class DataFrame(NwDataFrame[IntoDataFrameT]): _version = Version.V2 @inherit_doc(NwDataFrame) - def __init__(self, df: Any, *, level: Literal["full", "lazy", "interchange"]) -> None: + def __init__(self, df: Any) -> None: assert df._version is Version.V2 # noqa: S101 - super().__init__(df, level=level) + super().__init__(df) # We need to override any method which don't return Self so that type # annotations are correct. @@ -244,9 +244,9 @@ class LazyFrame(NwLazyFrame[IntoLazyFrameT]): _version = Version.V2 @inherit_doc(NwLazyFrame) - def __init__(self, df: Any, *, level: Literal["full", "lazy", "interchange"]) -> None: + def __init__(self, df: Any) -> None: assert df._version is Version.V2 # noqa: S101 - super().__init__(df, level=level) + super().__init__(df) @property def _dataframe(self) -> type[DataFrame[Any]]: @@ -262,11 +262,9 @@ class Series(NwSeries[IntoSeriesT]): _version = Version.V2 @inherit_doc(NwSeries) - def __init__( - self, series: Any, *, level: Literal["full", "lazy", "interchange"] - ) -> None: + def __init__(self, series: Any) -> None: assert series._version is Version.V2 # noqa: S101 - super().__init__(series, level=level) + super().__init__(series) # We need to override any method which don't return Self so that type # annotations are correct. @@ -375,11 +373,11 @@ def _stableify( | NwExpr, ) -> DataFrame[IntoDataFrameT] | LazyFrame[IntoLazyFrameT] | Series[IntoSeriesT] | Expr: if isinstance(obj, NwDataFrame): - return DataFrame(obj._compliant_frame._with_version(Version.V2), level=obj._level) + return DataFrame(obj._compliant_frame._with_version(Version.V2)) if isinstance(obj, NwLazyFrame): - return LazyFrame(obj._compliant_frame._with_version(Version.V2), level=obj._level) + return LazyFrame(obj._compliant_frame._with_version(Version.V2)) if isinstance(obj, NwSeries): - return Series(obj._compliant_series._with_version(Version.V2), level=obj._level) + return Series(obj._compliant_series._with_version(Version.V2)) if isinstance(obj, NwExpr): return Expr(*obj._nodes) assert_never(obj) diff --git a/narwhals/translate.py b/narwhals/translate.py index 9452111bdd..8a129e09a0 100644 --- a/narwhals/translate.py +++ b/narwhals/translate.py @@ -236,7 +236,7 @@ def _translate_if_compliant( # noqa: C901,PLR0911 raise TypeError(msg) return compliant_object return version.dataframe( - compliant_object.__narwhals_dataframe__()._with_version(version), level="full" + compliant_object.__narwhals_dataframe__()._with_version(version) ) if is_compliant_lazyframe(compliant_object): if series_only: @@ -250,7 +250,7 @@ def _translate_if_compliant( # noqa: C901,PLR0911 raise TypeError(msg) return compliant_object return version.lazyframe( - compliant_object.__narwhals_lazyframe__()._with_version(version), level="full" + compliant_object.__narwhals_lazyframe__()._with_version(version) ) if is_compliant_series(compliant_object): if not allow_series: @@ -259,7 +259,7 @@ def _translate_if_compliant( # noqa: C901,PLR0911 raise TypeError(msg) return compliant_object return version.series( - compliant_object.__narwhals_series__()._with_version(version), level="full" + compliant_object.__narwhals_series__()._with_version(version) ) # Object wasn't compliant, can't translate here. return None @@ -463,7 +463,7 @@ def _from_native_impl( # noqa: C901, PLR0911, PLR0912, PLR0915 ) raise TypeError(msg) return native_object - return Version.V1.dataframe(InterchangeFrame(native_object), level="interchange") + return Version.V1.dataframe(InterchangeFrame(native_object)) if (compliant_object := plugins.from_native(native_object, version)) is not None: return _translate_if_compliant( diff --git a/narwhals/typing.py b/narwhals/typing.py index c99507f380..b66cb240ad 100644 --- a/narwhals/typing.py +++ b/narwhals/typing.py @@ -263,6 +263,8 @@ def Binary(self) -> type[dtypes.Binary]: ... - *"all"*: Keeps all the mode's. """ +SupportLevel: TypeAlias = Literal["full", "lazy", "interchange"] +"""Level of Narwhals support for the object.""" PandasLikeDType: TypeAlias = "pd.api.extensions.ExtensionDtype | np.dtype[Any]" diff --git a/tests/series_only/is_ordered_categorical_test.py b/tests/series_only/is_ordered_categorical_test.py index 8833957b1c..2f8393c82a 100644 --- a/tests/series_only/is_ordered_categorical_test.py +++ b/tests/series_only/is_ordered_categorical_test.py @@ -91,5 +91,5 @@ def test_is_ordered_categorical_pyarrow() -> None: def test_is_ordered_categorical_unknown_series() -> None: - series: nw.Series[Any] = nw.Series(MockCompliantSeries(), level="full") + series: nw.Series[Any] = nw.Series(MockCompliantSeries()) assert nw.is_ordered_categorical(series) is False diff --git a/tests/translate/from_native_test.py b/tests/translate/from_native_test.py index 8d076699c0..bacbe75ec9 100644 --- a/tests/translate/from_native_test.py +++ b/tests/translate/from_native_test.py @@ -365,7 +365,7 @@ def test_dataframe_recursive() -> None: pl_frame = pl.DataFrame({"a": [1, 2, 3]}) nw_frame = nw.from_native(pl_frame) with pytest.raises(AssertionError): - nw.DataFrame(nw_frame, level="full") + nw.DataFrame(nw_frame) nw_frame_early_return = nw.from_native(nw_frame) @@ -373,7 +373,7 @@ def test_dataframe_recursive() -> None: assert_type(pl_frame, pl.DataFrame) assert_type(nw_frame, nw.DataFrame[pl.DataFrame]) - nw_frame_depth_2 = nw.DataFrame(nw_frame, level="full") # type: ignore[var-annotated] + nw_frame_depth_2 = nw.DataFrame(nw_frame) # type: ignore[var-annotated] # NOTE: Checking that the type is `DataFrame[Unknown]` assert_type(nw_frame_depth_2, nw.DataFrame[Any]) assert_type(nw_frame_early_return, nw.DataFrame[pl.DataFrame]) @@ -400,7 +400,7 @@ def test_lazyframe_recursive() -> None: pl_frame = pl.DataFrame({"a": [1, 2, 3]}).lazy() nw_frame = nw.from_native(pl_frame) with pytest.raises(AssertionError): - nw.LazyFrame(nw_frame, level="lazy") + nw.LazyFrame(nw_frame) nw_frame_early_return = nw.from_native(nw_frame) @@ -408,7 +408,7 @@ def test_lazyframe_recursive() -> None: assert_type(pl_frame, pl.LazyFrame) assert_type(nw_frame, nw.LazyFrame[pl.LazyFrame]) - nw_frame_depth_2 = nw.LazyFrame(nw_frame, level="lazy") # type: ignore[var-annotated] + nw_frame_depth_2 = nw.LazyFrame(nw_frame) # type: ignore[var-annotated] # NOTE: Checking that the type is `LazyFrame[Unknown]` assert_type(nw_frame_depth_2, nw.LazyFrame[Any]) assert_type(nw_frame_early_return, nw.LazyFrame[pl.LazyFrame]) @@ -441,7 +441,7 @@ def test_series_recursive() -> None: pl_series = pl.Series(name="test", values=[1, 2, 3]) nw_series = nw.from_native(pl_series, series_only=True) with pytest.raises(AssertionError): - nw.Series(nw_series, level="full") + nw.Series(nw_series) nw_series_early_return = nw.from_native(nw_series, series_only=True) @@ -449,7 +449,7 @@ def test_series_recursive() -> None: assert_type(pl_series, pl.Series) assert_type(nw_series, nw.Series[pl.Series]) - nw_series_depth_2 = nw.Series(nw_series, level="full") # type: ignore[var-annotated] + nw_series_depth_2 = nw.Series(nw_series) # type: ignore[var-annotated] # NOTE: Checking that the type is `Series[Unknown]` assert_type(nw_series_depth_2, nw.Series[Any]) assert_type(nw_series_early_return, nw.Series[pl.Series]) diff --git a/tests/v1_test.py b/tests/v1_test.py index 9882c4ed15..8418f87281 100644 --- a/tests/v1_test.py +++ b/tests/v1_test.py @@ -416,6 +416,21 @@ def test_get_level() -> None: ) +def test_v1_explicit_level_kwarg() -> None: + # `_duckdb`/`_ibis` pass `level="interchange"` explicitly for a v1 DataFrame. + # We have no such _real_ cases for LazyFrame and Series + pytest.importorskip("polars") + import polars as pl + + nw_lf = nw_v1.from_native(pl.LazyFrame({"a": [1]})) + rewrapped_lf = nw_v1.LazyFrame[pl.LazyFrame](nw_lf._compliant_frame, level="lazy") + assert nw_v1.get_level(rewrapped_lf) == "lazy" + + nw_s = nw_v1.from_native(pl.Series(name="a", values=[1]), series_only=True) + rewrapped_s = nw_v1.Series(nw_s._compliant_series, level="full") + assert nw_v1.get_level(rewrapped_s) == "full" + + def test_any_horizontal() -> None: # here, it defaults to Kleene logic. pytest.importorskip("polars") @@ -614,14 +629,14 @@ def test_dataframe_recursive_v1() -> None: nw_frame = nw_v1.from_native(pl_frame) # NOTE: (#2629) combined with passing in `nw_v1.DataFrame` (w/ a `_version`) into itself changes the error with pytest.raises(AssertionError): - nw_v1.DataFrame(nw_frame, level="full") + nw_v1.DataFrame(nw_frame) nw_frame_early_return = nw_v1.from_native(nw_frame) if TYPE_CHECKING: assert_type(pl_frame, pl.DataFrame) assert_type(nw_frame, "nw_v1.DataFrame[pl.DataFrame]") - nw_frame_depth_2 = nw_v1.DataFrame(nw_frame, level="full") # type: ignore[var-annotated] + nw_frame_depth_2 = nw_v1.DataFrame(nw_frame) # type: ignore[var-annotated] assert_type(nw_frame_depth_2, nw_v1.DataFrame[Any]) # NOTE: Checking that the type is `DataFrame[Unknown]` assert_type(nw_frame_early_return, "nw_v1.DataFrame[pl.DataFrame]") @@ -634,7 +649,7 @@ def test_lazyframe_recursive_v1() -> None: pl_frame = pl.DataFrame({"a": [1, 2, 3]}).lazy() nw_frame = nw_v1.from_native(pl_frame) with pytest.raises(AssertionError): - nw_v1.LazyFrame(nw_frame, level="lazy") + nw_v1.LazyFrame(nw_frame) nw_frame_early_return = nw_v1.from_native(nw_frame) @@ -642,7 +657,7 @@ def test_lazyframe_recursive_v1() -> None: assert_type(pl_frame, pl.LazyFrame) assert_type(nw_frame, nw_v1.LazyFrame[pl.LazyFrame]) - nw_frame_depth_2 = nw_v1.LazyFrame(nw_frame, level="lazy") # type: ignore[var-annotated] + nw_frame_depth_2 = nw_v1.LazyFrame(nw_frame) # type: ignore[var-annotated] # NOTE: Checking that the type is `LazyFrame[Unknown]` assert_type(nw_frame_depth_2, nw_v1.LazyFrame[Any]) assert_type(nw_frame_early_return, nw_v1.LazyFrame[pl.LazyFrame]) @@ -657,7 +672,7 @@ def test_series_recursive_v1() -> None: nw_series = nw_v1.from_native(pl_series, series_only=True) # NOTE: (#2629) combined with passing in `nw_v1.Series` (w/ a `_version`) into itself changes the error with pytest.raises(AssertionError): - nw_v1.Series(nw_series, level="full") + nw_v1.Series(nw_series) nw_series_early_return = nw_v1.from_native(nw_series, series_only=True) @@ -665,7 +680,7 @@ def test_series_recursive_v1() -> None: assert_type(pl_series, pl.Series) assert_type(nw_series, nw_v1.Series[pl.Series]) - nw_series_depth_2 = nw_v1.Series(nw_series, level="full") + nw_series_depth_2 = nw_v1.Series(nw_series) # NOTE: `Unknown` isn't possible for `v1`, as it has a `TypeVar` default assert_type(nw_series_depth_2, nw_v1.Series[Any]) assert_type(nw_series_early_return, nw_v1.Series[pl.Series]) diff --git a/tests/v2_test.py b/tests/v2_test.py index 7a1903425c..963119acb8 100644 --- a/tests/v2_test.py +++ b/tests/v2_test.py @@ -558,3 +558,17 @@ def test_first_last() -> None: result = df.select(b=nw_v2.col("a").first(), c=nw_v2.col("a").last()) expected = {"b": [0], "c": [-1]} assert_equal_data(result, expected) + + +def test_from_mock_interchange_protocol_rejected() -> None: + """v2 must reject objects which only implement `__dataframe__`.""" + + class MockDf: + def __dataframe__(self) -> None: ... # pragma: no cover + + mockdf = MockDf() + with pytest.raises(TypeError, match="Unsupported dataframe type"): + # Typing rejection **is** expected in v2, since IntoDataFrame excludes + # DataFrameLike objects! + nw_v2.from_native(mockdf) # type: ignore[call-overload] + assert nw_v2.from_native(mockdf, pass_through=True) is mockdf