diff --git a/docs/backcompat.md b/docs/backcompat.md index b2d312e0a6..78e01e46a2 100644 --- a/docs/backcompat.md +++ b/docs/backcompat.md @@ -111,6 +111,16 @@ before making any change. ### After `stable.v1` +The following are differences between the main Narwhals namespace and `narwhals.stable.v1`: + +- Since Narwhals 1.23: + + - Passing an `ibis.Table` to `from_native` returns a `LazyFrame`. In + `narwhals.stable.v1`, it returns a `DataFrame` with `level='interchange'`. + - `eager_or_interchange_only` has been removed from `from_native` and `narwhalify`. + - Order-dependent expressions can no longer be used with `narwhals.LazyFrame`. + - The following expressions have been deprecated from the main namespace: `Expr.head`, + `Expr.tail`, `Expr.gather_every`, `Expr.sample`, `Expr.arg_true`, `Expr.sort`. - Since Narwhals 1.21, passing a `DuckDBPyRelation` to `from_native` returns a `LazyFrame`. In `narwhals.stable.v1`, it returns a `DataFrame` with `level='interchange'`. diff --git a/narwhals/_ibis/dataframe.py b/narwhals/_ibis/dataframe.py index 6fe8997a93..6adb0f08d3 100644 --- a/narwhals/_ibis/dataframe.py +++ b/narwhals/_ibis/dataframe.py @@ -6,6 +6,7 @@ from narwhals.dependencies import get_ibis from narwhals.utils import Implementation +from narwhals.utils import Version from narwhals.utils import import_dtypes_module from narwhals.utils import validate_backend_version @@ -18,7 +19,6 @@ from narwhals._ibis.series import IbisInterchangeSeries from narwhals.dtypes import DType - from narwhals.utils import Version @lru_cache(maxsize=16) @@ -70,7 +70,7 @@ def native_to_narwhals_dtype(ibis_dtype: Any, version: Version) -> DType: return dtypes.Unknown() # pragma: no cover -class IbisInterchangeFrame: +class IbisLazyFrame: _implementation = Implementation.IBIS def __init__( @@ -81,7 +81,14 @@ def __init__( self._backend_version = backend_version validate_backend_version(self._implementation, self._backend_version) - def __narwhals_dataframe__(self) -> Any: + def __narwhals_dataframe__(self) -> Any: # pragma: no cover + # Keep around for backcompat. + if self._version is not Version.V1: + msg = "__narwhals_dataframe__ is not implemented for IbisLazyFrame" + raise AttributeError(msg) + return self + + def __narwhals_lazyframe__(self) -> Any: return self def __native_namespace__(self: Self) -> ModuleType: diff --git a/narwhals/_pandas_like/utils.py b/narwhals/_pandas_like/utils.py index 28b7a9030a..5075a2b40d 100644 --- a/narwhals/_pandas_like/utils.py +++ b/narwhals/_pandas_like/utils.py @@ -509,6 +509,8 @@ def native_to_narwhals_dtype( ) except Exception: # noqa: BLE001, S110 pass + # The most useful assumption is probably String + return dtypes.String() return dtypes.Unknown() # pragma: no cover diff --git a/narwhals/translate.py b/narwhals/translate.py index 6ed82326d8..9c455055a0 100644 --- a/narwhals/translate.py +++ b/narwhals/translate.py @@ -132,7 +132,6 @@ def from_native( *, pass_through: Literal[True], eager_only: Literal[False] = ..., - eager_or_interchange_only: Literal[True], series_only: Literal[False] = ..., allow_series: Literal[True], ) -> DataFrame[IntoDataFrameT]: ... @@ -144,7 +143,6 @@ def from_native( *, pass_through: Literal[True], eager_only: Literal[True], - eager_or_interchange_only: Literal[False] = ..., series_only: Literal[False] = ..., allow_series: Literal[True], ) -> DataFrame[IntoDataFrameT] | Series[IntoSeriesT]: ... @@ -156,7 +154,6 @@ def from_native( *, pass_through: Literal[True], eager_only: Literal[False] = ..., - eager_or_interchange_only: Literal[True], series_only: Literal[False] = ..., allow_series: None = ..., ) -> DataFrame[IntoDataFrameT]: ... @@ -168,7 +165,6 @@ def from_native( *, pass_through: Literal[True], eager_only: Literal[False] = ..., - eager_or_interchange_only: Literal[True], series_only: Literal[False] = ..., allow_series: None = ..., ) -> T: ... @@ -180,7 +176,6 @@ def from_native( *, pass_through: Literal[True], eager_only: Literal[True], - eager_or_interchange_only: Literal[False] = ..., series_only: Literal[False] = ..., allow_series: None = ..., ) -> DataFrame[IntoDataFrameT]: ... @@ -192,7 +187,6 @@ def from_native( *, pass_through: Literal[True], eager_only: Literal[True], - eager_or_interchange_only: Literal[False] = ..., series_only: Literal[False] = ..., allow_series: None = ..., ) -> T: ... @@ -204,7 +198,6 @@ def from_native( *, pass_through: Literal[True], eager_only: Literal[False] = ..., - eager_or_interchange_only: Literal[False] = ..., series_only: Literal[False] = ..., allow_series: Literal[True], ) -> DataFrame[IntoFrameT] | LazyFrame[IntoFrameT] | Series[IntoSeriesT]: ... @@ -216,43 +209,17 @@ def from_native( *, pass_through: Literal[True], eager_only: Literal[False] = ..., - eager_or_interchange_only: Literal[False] = ..., series_only: Literal[True], allow_series: None = ..., ) -> Series[IntoSeriesT]: ... -@overload -def from_native( - native_object: IntoFrameT, - *, - pass_through: Literal[True], - eager_only: Literal[False] = ..., - eager_or_interchange_only: Literal[False] = ..., - series_only: Literal[False] = ..., - allow_series: None = ..., -) -> DataFrame[IntoFrameT] | LazyFrame[IntoFrameT]: ... - - -@overload -def from_native( - native_object: T, - *, - pass_through: Literal[True], - eager_only: Literal[False] = ..., - eager_or_interchange_only: Literal[False] = ..., - series_only: Literal[False] = ..., - allow_series: None = ..., -) -> T: ... - - @overload def from_native( native_object: IntoDataFrameT, *, pass_through: Literal[False] = ..., eager_only: Literal[False] = ..., - eager_or_interchange_only: Literal[True], series_only: Literal[False] = ..., allow_series: None = ..., ) -> DataFrame[IntoDataFrameT]: ... @@ -264,7 +231,6 @@ def from_native( *, pass_through: Literal[False] = ..., eager_only: Literal[True], - eager_or_interchange_only: Literal[False] = ..., series_only: Literal[False] = ..., allow_series: None = ..., ) -> DataFrame[IntoDataFrameT]: ... @@ -276,7 +242,6 @@ def from_native( *, pass_through: Literal[False] = ..., eager_only: Literal[False] = ..., - eager_or_interchange_only: Literal[False] = ..., series_only: Literal[False] = ..., allow_series: Literal[True], ) -> DataFrame[Any] | LazyFrame[Any] | Series[Any]: ... @@ -288,7 +253,6 @@ def from_native( *, pass_through: Literal[False] = ..., eager_only: Literal[False] = ..., - eager_or_interchange_only: Literal[False] = ..., series_only: Literal[True], allow_series: None = ..., ) -> Series[IntoSeriesT]: ... @@ -300,7 +264,6 @@ def from_native( *, pass_through: Literal[False] = ..., eager_only: Literal[False] = ..., - eager_or_interchange_only: Literal[False] = ..., series_only: Literal[False] = ..., allow_series: None = ..., ) -> DataFrame[IntoFrameT] | LazyFrame[IntoFrameT]: ... @@ -313,7 +276,6 @@ def from_native( *, pass_through: bool, eager_only: bool, - eager_or_interchange_only: bool = False, series_only: bool, allow_series: bool | None, ) -> Any: ... @@ -325,7 +287,6 @@ def from_native( strict: bool | None = None, pass_through: bool | None = None, eager_only: bool = False, - eager_or_interchange_only: bool = False, series_only: bool = False, allow_series: bool | None = None, ) -> LazyFrame[IntoFrameT] | DataFrame[IntoFrameT] | Series[IntoSeriesT] | T: @@ -355,16 +316,6 @@ def from_native( - `False` (default): don't require `native_object` to be eager - `True`: only convert to Narwhals if `native_object` is eager - eager_or_interchange_only: Whether to only allow eager objects or objects which - have interchange-level support in Narwhals: - - - `False` (default): don't require `native_object` to either be eager or to - have interchange-level support in Narwhals - - `True`: only convert to Narwhals if `native_object` is eager or has - interchange-level support in Narwhals - - See [interchange-only support](../extending.md/#interchange-only-support) - for more details. series_only: Whether to only allow Series: - `False` (default): don't require `native_object` to be a Series @@ -388,7 +339,7 @@ def from_native( native_object, pass_through=pass_through, eager_only=eager_only, - eager_or_interchange_only=eager_or_interchange_only, + eager_or_interchange_only=False, series_only=series_only, allow_series=allow_series, version=Version.MAIN, @@ -400,6 +351,7 @@ def _from_native_impl( # noqa: PLR0915 *, pass_through: bool = False, eager_only: bool = False, + # Interchange-level was removed after v1 eager_or_interchange_only: bool = False, series_only: bool = False, allow_series: bool | None = None, @@ -728,12 +680,12 @@ def _from_native_impl( # noqa: PLR0915 DuckDBLazyFrame( native_object, backend_version=backend_version, version=version ), - level="full", + level="lazy", ) # Ibis elif is_ibis_table(native_object): # pragma: no cover - from narwhals._ibis.dataframe import IbisInterchangeFrame + from narwhals._ibis.dataframe import IbisLazyFrame if eager_only or series_only: if not pass_through: @@ -746,11 +698,18 @@ def _from_native_impl( # noqa: PLR0915 import ibis # ignore-banned-import backend_version = parse_version(ibis.__version__) - return DataFrame( - IbisInterchangeFrame( - native_object, version=version, backend_version=backend_version + if version is Version.V1: + return DataFrame( + IbisLazyFrame( + native_object, backend_version=backend_version, version=version + ), + level="interchange", + ) + return LazyFrame( + IbisLazyFrame( + native_object, backend_version=backend_version, version=version ), - level="interchange", + level="lazy", ) # PySpark @@ -850,7 +809,6 @@ def narwhalify( strict: bool | None = None, pass_through: bool | None = None, eager_only: bool = False, - eager_or_interchange_only: bool = False, series_only: bool = False, allow_series: bool | None = True, ) -> Callable[..., Any]: @@ -883,16 +841,6 @@ def narwhalify( - `False` (default): don't require `native_object` to be eager - `True`: only convert to Narwhals if `native_object` is eager - eager_or_interchange_only: Whether to only allow eager objects or objects which - have interchange-level support in Narwhals: - - - `False` (default): don't require `native_object` to either be eager or to - have interchange-level support in Narwhals - - `True`: only convert to Narwhals if `native_object` is eager or has - interchange-level support in Narwhals - - See [interchange-only support](../extending.md/#interchange-only-support) - for more details. series_only: Whether to only allow Series: - `False` (default): don't require `native_object` to be a Series @@ -934,7 +882,6 @@ def wrapper(*args: Any, **kwargs: Any) -> Any: arg, pass_through=pass_through, eager_only=eager_only, - eager_or_interchange_only=eager_or_interchange_only, series_only=series_only, allow_series=allow_series, ) @@ -946,7 +893,6 @@ def wrapper(*args: Any, **kwargs: Any) -> Any: value, pass_through=pass_through, eager_only=eager_only, - eager_or_interchange_only=eager_or_interchange_only, series_only=series_only, allow_series=allow_series, ) diff --git a/pyproject.toml b/pyproject.toml index c461d70319..9efc72c963 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -191,7 +191,7 @@ omit = [ # the latest pyspark (3.5) doesn't officially support Python 3.12 and 3.13 'narwhals/_spark_like/*', # we don't run these in every environment - 'tests/spark_like_test.py', + 'tests/ibis_test.py', ] exclude_also = [ "if sys.version_info() <", diff --git a/tests/expr_and_series/dt/datetime_attributes_test.py b/tests/expr_and_series/dt/datetime_attributes_test.py index 3c8a16b7da..ad5f8dc3fe 100644 --- a/tests/expr_and_series/dt/datetime_attributes_test.py +++ b/tests/expr_and_series/dt/datetime_attributes_test.py @@ -118,7 +118,6 @@ def test_to_date(request: pytest.FixtureRequest, constructor: Constructor) -> No "pandas_nullable_constructor", "cudf", "modin_constructor", - "pyspark", ) ): request.applymarker(pytest.mark.xfail) diff --git a/tests/frame/join_test.py b/tests/frame/join_test.py index 68e2ed8b8d..d0e2766060 100644 --- a/tests/frame/join_test.py +++ b/tests/frame/join_test.py @@ -27,12 +27,12 @@ def test_inner_join_two_keys(constructor: Constructor) -> None: df = nw_main.from_native(constructor(data)) df_right = df result = df.join( - df_right, # type: ignore[arg-type] + df_right, left_on=["antananarivo", "bob"], right_on=["antananarivo", "bob"], how="inner", ) - result_on = df.join(df_right, on=["antananarivo", "bob"], how="inner") # type: ignore[arg-type] + result_on = df.join(df_right, on=["antananarivo", "bob"], how="inner") result = result.sort("idx").drop("idx_right") result_on = result_on.sort("idx").drop("idx_right") expected = { diff --git a/tests/ibis_test.py b/tests/ibis_test.py new file mode 100644 index 0000000000..d4014f2669 --- /dev/null +++ b/tests/ibis_test.py @@ -0,0 +1,30 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING +from typing import Any + +import polars as pl +import pytest + +import narwhals as nw + +if TYPE_CHECKING: + import ibis + + from tests.utils import Constructor + +ibis = pytest.importorskip("ibis") + + +@pytest.fixture +def ibis_constructor() -> Constructor: + def func(data: dict[str, Any]) -> ibis.Table: + df = pl.DataFrame(data) + return ibis.memtable(df) + + return func + + +def test_from_native(ibis_constructor: Constructor) -> None: + df = nw.from_native(ibis_constructor({"a": [1, 2, 3], "b": [4, 5, 6]})) + assert df.columns == ["a", "b"] diff --git a/tests/stable_api_test.py b/tests/stable_api_test.py index 6872dddc5e..cac0b0986d 100644 --- a/tests/stable_api_test.py +++ b/tests/stable_api_test.py @@ -40,16 +40,16 @@ def test_renamed_taxicab_norm( with pytest.raises(AttributeError): result = df.with_columns(b=nw.col("a")._l1_norm()) # type: ignore[attr-defined] - df = nw_v1.from_native(constructor({"a": [1, 2, 3, -4, 5]})) + df_v1 = nw_v1.from_native(constructor({"a": [1, 2, 3, -4, 5]})) # The newer `_taxicab_norm` can still work in the old API, no issue. # It's new, so it couldn't be backwards-incompatible. - result = df.with_columns(b=nw_v1.col("a")._taxicab_norm()) + result_v1 = df_v1.with_columns(b=nw_v1.col("a")._taxicab_norm()) expected = {"a": [1, 2, 3, -4, 5], "b": [15] * 5} - assert_equal_data(result, expected) + assert_equal_data(result_v1, expected) # The older `_l1_norm` still works in the stable api - result = df.with_columns(b=nw_v1.col("a")._l1_norm()) - assert_equal_data(result, expected) + result_v1 = df_v1.with_columns(b=nw_v1.col("a")._l1_norm()) + assert_equal_data(result_v1, expected) def test_renamed_taxicab_norm_dataframe( @@ -104,6 +104,10 @@ def test_stable_api_docstrings() -> None: for item in main_namespace_api: if getattr(nw, item).__doc__ is None: continue + if item in ("from_native", "narwhalify"): + # `eager_or_interchange` param was removed from main namespace, + # but is still present in v1 docstring. + continue v1_doc = remove_docstring_examples(getattr(nw_v1, item).__doc__) nw_doc = remove_docstring_examples(getattr(nw, item).__doc__) assert v1_doc == nw_doc, item diff --git a/tests/translate/from_native_test.py b/tests/translate/from_native_test.py index c992879517..78d896844a 100644 --- a/tests/translate/from_native_test.py +++ b/tests/translate/from_native_test.py @@ -240,7 +240,6 @@ def test_from_native_strict_false_typing() -> None: with pytest.deprecated_call(match="please use `pass_through` instead"): unstable_nw.from_native(df, strict=False) # type: ignore[call-overload] unstable_nw.from_native(df, strict=False, eager_only=True) # type: ignore[call-overload] - unstable_nw.from_native(df, strict=False, eager_or_interchange_only=True) # type: ignore[call-overload] def test_from_native_strict_false_invalid() -> None: