Skip to content

Commit

Permalink
feat: remove eager_or_interchange from from_native in main namesp…
Browse files Browse the repository at this point in the history
…ace, switch Ibis' support from interchange to lazy in main namespace (but preserve status-quo in stable.v1) (#1829)
  • Loading branch information
MarcoGorelli authored Jan 20, 2025
1 parent a55ff89 commit 02544e4
Show file tree
Hide file tree
Showing 10 changed files with 79 additions and 82 deletions.
10 changes: 10 additions & 0 deletions docs/backcompat.md
Original file line number Diff line number Diff line change
Expand Up @@ -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'`.
Expand Down
13 changes: 10 additions & 3 deletions narwhals/_ibis/dataframe.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -18,7 +19,6 @@

from narwhals._ibis.series import IbisInterchangeSeries
from narwhals.dtypes import DType
from narwhals.utils import Version


@lru_cache(maxsize=16)
Expand Down Expand Up @@ -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__(
Expand All @@ -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:
Expand Down
2 changes: 2 additions & 0 deletions narwhals/_pandas_like/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down
84 changes: 15 additions & 69 deletions narwhals/translate.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]: ...
Expand All @@ -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]: ...
Expand All @@ -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]: ...
Expand All @@ -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: ...
Expand All @@ -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]: ...
Expand All @@ -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: ...
Expand All @@ -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]: ...
Expand All @@ -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]: ...
Expand All @@ -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]: ...
Expand All @@ -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]: ...
Expand All @@ -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]: ...
Expand All @@ -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]: ...
Expand All @@ -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: ...
Expand All @@ -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:
Expand Down Expand Up @@ -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
Expand All @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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:
Expand All @@ -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
Expand Down Expand Up @@ -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]:
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
)
Expand All @@ -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,
)
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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() <",
Expand Down
1 change: 0 additions & 1 deletion tests/expr_and_series/dt/datetime_attributes_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
4 changes: 2 additions & 2 deletions tests/frame/join_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
30 changes: 30 additions & 0 deletions tests/ibis_test.py
Original file line number Diff line number Diff line change
@@ -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"]
Loading

0 comments on commit 02544e4

Please sign in to comment.