Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/narwhals/dependencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -590,28 +590,33 @@ def is_cudf_dtype(


def _is_native_series(obj: Any | IntoSeriesT) -> TypeIs[IntoSeriesT]:
from narwhals import plugins
from narwhals._utils import _hasattr_static

return (
_hasattr_static(obj, "__narwhals_series__")
or _is_polars_series(obj)
or _is_pyarrow_chunked_array(obj)
or _is_pandas_like_series(obj)
or plugins.is_native_series(obj)
)


def _is_native_dataframe(obj: Any | IntoDataFrameT) -> TypeIs[IntoDataFrameT]:
from narwhals import plugins
from narwhals._utils import _hasattr_static

return (
_hasattr_static(obj, "__narwhals_dataframe__")
or _is_polars_dataframe(obj)
or _is_pyarrow_table(obj)
or _is_pandas_like_dataframe(obj)
or plugins.is_native_dataframe(obj)
)


def _is_native_lazyframe(obj: Any | IntoLazyFrameT) -> TypeIs[IntoLazyFrameT]:
from narwhals import plugins
from narwhals._utils import _hasattr_static

return (
Expand All @@ -623,6 +628,7 @@ def _is_native_lazyframe(obj: Any | IntoLazyFrameT) -> TypeIs[IntoLazyFrameT]:
or _is_pyspark_dataframe(obj)
or _is_pyspark_connect_dataframe(obj)
or _is_sqlframe_dataframe(obj)
or plugins.is_native_lazyframe(obj)
)


Expand Down
21 changes: 21 additions & 0 deletions src/narwhals/plugins.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,27 @@ def from_native(native_object: Any, version: Version) -> CompliantAny | None:
return next(_iter_from_native(native_object, version), None)


def is_native_dataframe(native_object: Any) -> bool:
"""Check whether an installed plugin converts `native_object` to an eager DataFrame."""
from narwhals._utils import Version, is_compliant_dataframe

return is_compliant_dataframe(from_native(native_object, Version.MAIN))


def is_native_lazyframe(native_object: Any) -> bool:
"""Check whether an installed plugin converts `native_object` to a LazyFrame."""
from narwhals._utils import Version, is_compliant_lazyframe

return is_compliant_lazyframe(from_native(native_object, Version.MAIN))


def is_native_series(native_object: Any) -> bool:
"""Check whether an installed plugin converts `native_object` to a Series."""
from narwhals._utils import Version, is_compliant_series

return is_compliant_series(from_native(native_object, Version.MAIN))
Comment on lines +113 to +131

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Whether an object is eager or lazy is only knowable from the compliant object a plugin produces, so this calls the plugin's from_native and inspects the result.



def _show_suggestions(native_object_type: type) -> str | None:
if _might_be(native_object_type, "daft"): # pragma: no cover
return (
Expand Down
9 changes: 6 additions & 3 deletions tests/dependencies/is_into_lazyframe_test.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations

from importlib.util import find_spec
from typing import TYPE_CHECKING, Any

import pytest
Expand Down Expand Up @@ -76,9 +77,11 @@ def test_is_into_lazyframe_numpy() -> None:


def test_is_into_lazyframe_other(always_has_attr: AlwaysHasAttr) -> None:
assert not is_into_lazyframe(data)
assert not v1_is_into_lazyframe(data)
assert not v2_is_into_lazyframe(data)
# If `test_plugin` is installed, a plain `dict` is convertible to a LazyFrame.
expected = find_spec("test_plugin") is not None
assert is_into_lazyframe(data) is expected
assert v1_is_into_lazyframe(data) is expected
assert v2_is_into_lazyframe(data) is expected

assert is_into_lazyframe(DictLazyFrame(data))
assert v1_is_into_lazyframe(DictLazyFrame(data))
Expand Down
101 changes: 100 additions & 1 deletion tests/plugins_test.py

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Mocking everything I can here, #3753 adds much more features in test-plugin that can be used here

Original file line number Diff line number Diff line change
@@ -1,13 +1,71 @@
from __future__ import annotations

from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Any

import pytest

import narwhals as nw
import narwhals.stable.v1.dependencies as nw_v1_dependencies
import narwhals.stable.v2.dependencies as nw_v2_dependencies
from narwhals import dependencies as nw_dependencies

if TYPE_CHECKING:
from typing_extensions import Self

from narwhals.plugins import Plugin
from narwhals.utils import Version

DEPENDENCIES_MODULES = (nw_dependencies, nw_v1_dependencies, nw_v2_dependencies)


class FakeNative:
"""Native object of an imaginary plugin-backed library."""


class FakeCompliantDataFrame:
def __narwhals_dataframe__(self) -> Self: # pragma: no cover
return self


class FakeCompliantLazyFrame:
def __narwhals_lazyframe__(self) -> Self: # pragma: no cover
return self


class FakeCompliantSeries:
def __narwhals_series__(self) -> Self: # pragma: no cover
return self


class FakeNamespace:
def __init__(self, compliant_cls: type, version: Version) -> None:
self._compliant_cls = compliant_cls
self._version = version

def from_native(self, native_object: object) -> Any:
assert isinstance(native_object, FakeNative)
return self._compliant_cls()


class FakePlugin:
NATIVE_PACKAGE = "builtins"

def __init__(self, compliant_cls: type) -> None:
self._compliant_cls = compliant_cls

def is_native(self, native_object: object) -> bool:
return isinstance(native_object, FakeNative)

def __narwhals_namespace__(self, version: Version) -> FakeNamespace:
return FakeNamespace(self._compliant_cls, version)


class FakeEntryPoint:
def __init__(self, plugin: FakePlugin) -> None:
self._plugin = plugin

def load(self) -> FakePlugin:
return self._plugin


def test_plugin() -> None:
Expand All @@ -28,6 +86,47 @@ def test_not_implemented() -> None:
lf.select(nw.col("a").ewm_mean())


def test_is_into_lazyframe() -> None:
# https://github.com/narwhals-dev/narwhals/issues/3714
pytest.importorskip("test_plugin")
df_native = {"a": [1, 1, 2], "b": [4, 5, 6]}
for dependencies in DEPENDENCIES_MODULES:
assert dependencies.is_into_lazyframe(df_native)


def test_is_into_dataframe() -> None:
# `test_plugin` converts to a LazyFrame, so `is_into_dataframe` should not match.
pytest.importorskip("test_plugin")
df_native = {"a": [1, 1, 2], "b": [4, 5, 6]}
for dependencies in DEPENDENCIES_MODULES:
assert not dependencies.is_into_dataframe(df_native)


@pytest.mark.parametrize(
("compliant_cls", "expected_kind"),
[
(FakeCompliantDataFrame, "dataframe"),
(FakeCompliantLazyFrame, "lazyframe"),
(FakeCompliantSeries, "series"),
],
)
def test_is_into_mocked_plugin(
monkeypatch: pytest.MonkeyPatch, compliant_cls: type, expected_kind: str
) -> None:
from narwhals import plugins

monkeypatch.setattr(
plugins,
"_discover_entrypoints",
lambda: (FakeEntryPoint(FakePlugin(compliant_cls)),),
)
native = FakeNative()
for dependencies in DEPENDENCIES_MODULES:
assert dependencies.is_into_dataframe(native) is (expected_kind == "dataframe")
assert dependencies.is_into_lazyframe(native) is (expected_kind == "lazyframe")
assert dependencies.is_into_series(native) is (expected_kind == "series")


def test_typing() -> None:
pytest.importorskip("test_plugin")
import test_plugin
Expand Down
Loading