Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: remove eager_or_interchange from from_native in main namespace, switch Ibis' support from interchange to lazy in main namespace (but preserve status-quo in stable.v1) #1829

Merged
merged 12 commits into from
Jan 20, 2025
Merged
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],
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this param could be slowly deprecated...but tbh I'm pretty keen to simplify from_native (the first thing most people will see), and we're keeping this around in stable.v1 anyway

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
Loading