Skip to content

feat: raise informative error or warning when passing narwhals object to nw.dependencies.is_*_dataframe and nw.dependencies.is_*_series #1444

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

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
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
134 changes: 134 additions & 0 deletions narwhals/dependencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,16 @@ def get_ibis() -> Any:

def is_pandas_dataframe(df: Any) -> TypeGuard[pd.DataFrame]:
"""Check whether `df` is a pandas DataFrame without importing pandas."""
from narwhals.dataframe import DataFrame
from narwhals.dataframe import LazyFrame

if isinstance(df, (DataFrame, LazyFrame)):
msg = (
f"You passed a `{type(df)}` to `is_pandas_dataframe`.\n\n"
"Hint: Instead of e.g. `is_pandas_dataframe(df)`, "
"did you mean `is_pandas_dataframe(df.to_native())`?"
)
raise TypeError(msg)
return ((pd := get_pandas()) is not None and isinstance(df, pd.DataFrame)) or any(
(mod := sys.modules.get(module_name, None)) is not None
and isinstance(df, mod.pandas.DataFrame)
Expand All @@ -104,6 +114,15 @@ def is_pandas_dataframe(df: Any) -> TypeGuard[pd.DataFrame]:

def is_pandas_series(ser: Any) -> TypeGuard[pd.Series[Any]]:
"""Check whether `ser` is a pandas Series without importing pandas."""
from narwhals.series import Series

if isinstance(ser, Series):
msg = (
f"You passed a `{type(ser)}` to `is_pandas_series`.\n\n"
"Hint: Instead of e.g. `is_pandas_series(ser)`, "
"did you mean `is_pandas_series(ser.to_native())`?"
)
raise TypeError(msg)
return ((pd := get_pandas()) is not None and isinstance(ser, pd.Series)) or any(
(mod := sys.modules.get(module_name, None)) is not None
and isinstance(ser, mod.pandas.Series)
Expand All @@ -122,11 +141,30 @@ def is_pandas_index(index: Any) -> TypeGuard[pd.Index]:

def is_modin_dataframe(df: Any) -> TypeGuard[mpd.DataFrame]:
"""Check whether `df` is a modin DataFrame without importing modin."""
from narwhals.dataframe import DataFrame
from narwhals.dataframe import LazyFrame

if isinstance(df, (DataFrame, LazyFrame)):
msg = (
f"You passed a `{type(df)}` to `is_modin_dataframe`.\n\n"
"Hint: Instead of e.g. `is_modin_dataframe(df)`, "
"did you mean `is_modin_dataframe(df.to_native())`?"
)
raise TypeError(msg)
return (mpd := get_modin()) is not None and isinstance(df, mpd.DataFrame)


def is_modin_series(ser: Any) -> TypeGuard[mpd.Series]:
"""Check whether `ser` is a modin Series without importing modin."""
from narwhals.series import Series

if isinstance(ser, Series):
msg = (
f"You passed a `{type(ser)}` to `is_modin_series`.\n\n"
"Hint: Instead of e.g. `is_modin_series(ser)`, "
"did you mean `is_modin_series(ser.to_native())`?"
)
raise TypeError(msg)
return (mpd := get_modin()) is not None and isinstance(ser, mpd.Series)


Expand All @@ -139,11 +177,30 @@ def is_modin_index(index: Any) -> TypeGuard[mpd.Index]:

def is_cudf_dataframe(df: Any) -> TypeGuard[cudf.DataFrame]:
"""Check whether `df` is a cudf DataFrame without importing cudf."""
from narwhals.dataframe import DataFrame
from narwhals.dataframe import LazyFrame

if isinstance(df, (DataFrame, LazyFrame)):
msg = (
f"You passed a `{type(df)}` to `is_cudf_dataframe`.\n\n"
"Hint: Instead of e.g. `is_cudf_dataframe(df)`, "
"did you mean `is_cudf_dataframe(df.to_native())`?"
)
raise TypeError(msg)
return (cudf := get_cudf()) is not None and isinstance(df, cudf.DataFrame)


def is_cudf_series(ser: Any) -> TypeGuard[cudf.Series[Any]]:
"""Check whether `ser` is a cudf Series without importing cudf."""
from narwhals.series import Series

if isinstance(ser, Series):
msg = (
f"You passed a `{type(ser)}` to `is_cudf_series`.\n\n"
"Hint: Instead of e.g. `is_cudf_series(ser)`, "
"did you mean `is_cudf_series(ser.to_native())`?"
)
raise TypeError(msg)
return (cudf := get_cudf()) is not None and isinstance(ser, cudf.Series)


Expand All @@ -168,31 +225,89 @@ def is_duckdb_relation(df: Any) -> TypeGuard[duckdb.DuckDBPyRelation]:

def is_ibis_table(df: Any) -> TypeGuard[ibis.Table]:
"""Check whether `df` is a Ibis Table without importing Ibis."""
from narwhals.dataframe import DataFrame
from narwhals.dataframe import LazyFrame

if isinstance(df, (DataFrame, LazyFrame)):
msg = (
f"You passed a `{type(df)}` to `is_ibis_table`.\n\n"
"Hint: Instead of e.g. `is_ibis_table(df)`, "
"did you mean `is_ibis_table(df.to_native())`?"
)
raise TypeError(msg)
return (ibis := get_ibis()) is not None and isinstance(df, ibis.expr.types.Table)


def is_polars_dataframe(df: Any) -> TypeGuard[pl.DataFrame]:
"""Check whether `df` is a Polars DataFrame without importing Polars."""
from narwhals.dataframe import DataFrame
from narwhals.dataframe import LazyFrame

if isinstance(df, (DataFrame, LazyFrame)):
msg = (
f"You passed a `{type(df)}` to `is_polars_dataframe`.\n\n"
"Hint: Instead of e.g. `is_polars_dataframe(df)`, "
"did you mean `is_polars_dataframe(df.to_native())`?"
)
raise TypeError(msg)
return (pl := get_polars()) is not None and isinstance(df, pl.DataFrame)


def is_polars_lazyframe(df: Any) -> TypeGuard[pl.LazyFrame]:
"""Check whether `df` is a Polars LazyFrame without importing Polars."""
from narwhals.dataframe import DataFrame
from narwhals.dataframe import LazyFrame

if isinstance(df, (DataFrame, LazyFrame)):
msg = (
f"You passed a `{type(df)}` to `is_polars_lazyframe`.\n\n"
"Hint: Instead of e.g. `is_polars_lazyframe(df)`, "
"did you mean `is_polars_lazyframe(df.to_native())`?"
)
raise TypeError(msg)
return (pl := get_polars()) is not None and isinstance(df, pl.LazyFrame)


def is_polars_series(ser: Any) -> TypeGuard[pl.Series]:
"""Check whether `ser` is a Polars Series without importing Polars."""
from narwhals.series import Series

if isinstance(ser, Series):
msg = (
f"You passed a `{type(ser)}` to `is_polars_series`.\n\n"
"Hint: Instead of e.g. `is_polars_series(ser)`, "
"did you mean `is_polars_series(ser.to_native())`?"
)
raise TypeError(msg)
return (pl := get_polars()) is not None and isinstance(ser, pl.Series)


def is_pyarrow_chunked_array(ser: Any) -> TypeGuard[pa.ChunkedArray]:
"""Check whether `ser` is a PyArrow ChunkedArray without importing PyArrow."""
from narwhals.series import Series

if isinstance(ser, Series):
msg = (
f"You passed a `{type(ser)}` to `is_pyarrow_chunked_array`.\n\n"
"Hint: Instead of e.g. `is_pyarrow_chunked_array(ser)`, "
"did you mean `is_pyarrow_chunked_array(ser.to_native())`?"
)
raise TypeError(msg)
return (pa := get_pyarrow()) is not None and isinstance(ser, pa.ChunkedArray)


def is_pyarrow_table(df: Any) -> TypeGuard[pa.Table]:
"""Check whether `df` is a PyArrow Table without importing PyArrow."""
from narwhals.dataframe import DataFrame
from narwhals.dataframe import LazyFrame

if isinstance(df, (DataFrame, LazyFrame)):
msg = (
f"You passed a `{type(df)}` to `is_pyarrow_table`.\n\n"
"Hint: Instead of e.g. `is_pyarrow_table(df)`, "
"did you mean `is_pyarrow_table(df.to_native())`?"
)
raise TypeError(msg)
return (pa := get_pyarrow()) is not None and isinstance(df, pa.Table)


Expand All @@ -206,6 +321,16 @@ def is_pandas_like_dataframe(df: Any) -> bool:

By "pandas-like", we mean: pandas, Modin, cuDF.
"""
from narwhals.dataframe import DataFrame
from narwhals.dataframe import LazyFrame

if isinstance(df, (DataFrame, LazyFrame)):
msg = (
f"You passed a `{type(df)}` to `is_pandas_like_dataframe`.\n\n"
"Hint: Instead of e.g. `is_pandas_like_dataframe(df)`, "
"did you mean `is_pandas_like_dataframe(df.to_native())`?"
)
raise TypeError(msg)
return is_pandas_dataframe(df) or is_modin_dataframe(df) or is_cudf_dataframe(df)


Expand All @@ -214,6 +339,15 @@ def is_pandas_like_series(ser: Any) -> bool:

By "pandas-like", we mean: pandas, Modin, cuDF.
"""
from narwhals.series import Series

if isinstance(ser, Series):
msg = (
f"You passed a `{type(ser)}` to `is_pandas_like_series`.\n\n"
"Hint: Instead of e.g. `is_pandas_like_series(ser)`, "
"did you mean `is_pandas_like_series(ser.to_native())`?"
)
raise TypeError(msg)
return is_pandas_series(ser) or is_modin_series(ser) or is_cudf_series(ser)


Expand Down
29 changes: 16 additions & 13 deletions narwhals/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,21 +123,24 @@ def tupleify(arg: Any) -> Any:


def _is_iterable(arg: Any | Iterable[Any]) -> bool:
from narwhals.dataframe import DataFrame
from narwhals.dataframe import LazyFrame
from narwhals.series import Series

if is_pandas_dataframe(arg) or is_pandas_series(arg):
msg = f"Expected Narwhals class or scalar, got: {type(arg)}. Perhaps you forgot a `nw.from_native` somewhere?"
raise TypeError(msg)
if (pl := get_polars()) is not None and isinstance(
arg, (pl.Series, pl.Expr, pl.DataFrame, pl.LazyFrame)
):
msg = (
f"Expected Narwhals class or scalar, got: {type(arg)}.\n\n"
"Hint: Perhaps you\n"
"- forgot a `nw.from_native` somewhere?\n"
"- used `pl.col` instead of `nw.col`?"
)
raise TypeError(msg)
if not isinstance(arg, (Series, DataFrame, LazyFrame)):
if is_pandas_dataframe(arg) or is_pandas_series(arg):
msg = f"Expected Narwhals class or scalar, got: {type(arg)}. Perhaps you forgot a `nw.from_native` somewhere?"
raise TypeError(msg)
if (pl := get_polars()) is not None and isinstance(
arg, (pl.Series, pl.Expr, pl.DataFrame, pl.LazyFrame)
):
msg = (
f"Expected Narwhals class or scalar, got: {type(arg)}.\n\n"
"Hint: Perhaps you\n"
"- forgot a `nw.from_native` somewhere?\n"
"- used `pl.col` instead of `nw.col`?"
)
raise TypeError(msg)

return isinstance(arg, Iterable) and not isinstance(arg, (str, bytes, Series))

Expand Down
82 changes: 82 additions & 0 deletions tests/dependencies/is_native_dataframe_series_raise_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
from __future__ import annotations

import re
from typing import TYPE_CHECKING
from typing import Any
from typing import Callable

import pytest

import narwhals.stable.v1 as nw
from narwhals.stable.v1.dependencies import is_cudf_dataframe
from narwhals.stable.v1.dependencies import is_cudf_series
from narwhals.stable.v1.dependencies import is_ibis_table
from narwhals.stable.v1.dependencies import is_modin_dataframe
from narwhals.stable.v1.dependencies import is_modin_series
from narwhals.stable.v1.dependencies import is_pandas_dataframe
from narwhals.stable.v1.dependencies import is_pandas_like_dataframe
from narwhals.stable.v1.dependencies import is_pandas_like_series
from narwhals.stable.v1.dependencies import is_pandas_series
from narwhals.stable.v1.dependencies import is_polars_dataframe
from narwhals.stable.v1.dependencies import is_polars_lazyframe
from narwhals.stable.v1.dependencies import is_polars_series
from narwhals.stable.v1.dependencies import is_pyarrow_chunked_array
from narwhals.stable.v1.dependencies import is_pyarrow_table

if TYPE_CHECKING:
from tests.utils import Constructor
from tests.utils import ConstructorEager


@pytest.mark.parametrize(
"is_native_dataframe",
[
is_pandas_dataframe,
is_modin_dataframe,
is_polars_dataframe,
is_cudf_dataframe,
is_ibis_table,
is_polars_lazyframe,
is_pyarrow_table,
is_pandas_like_dataframe,
],
)
def test_is_native_dataframe(
constructor: Constructor, is_native_dataframe: Callable[[Any], Any]
) -> None:
data = {"a": [1, 2], "b": ["bar", "foo"]}
df = nw.from_native(constructor(data))
func_name = is_native_dataframe.__name__
msg = re.escape(
f"You passed a `{type(df)}` to `{func_name}`.\n\n"
f"Hint: Instead of e.g. `{func_name}(df)`, "
f"did you mean `{func_name}(df.to_native())`?"
)
with pytest.raises(TypeError, match=msg):
is_native_dataframe(df)


@pytest.mark.parametrize(
"is_native_series",
[
is_pandas_series,
is_modin_series,
is_polars_series,
is_cudf_series,
is_pyarrow_chunked_array,
is_pandas_like_series,
],
)
def test_is_native_series(
constructor_eager: ConstructorEager, is_native_series: Callable[[Any], Any]
) -> None:
data = {"a": [1, 2]}
ser = nw.from_native(constructor_eager(data))["a"]
func_name = is_native_series.__name__
msg = re.escape(
f"You passed a `{type(ser)}` to `{func_name}`.\n\n"
f"Hint: Instead of e.g. `{func_name}(ser)`, "
f"did you mean `{func_name}(ser.to_native())`?"
)
with pytest.raises(TypeError, match=msg):
is_native_series(ser)
Loading