From 9a72f990dda879f7239437e786279bd89ee3bcba Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Tue, 12 May 2026 00:26:33 +0200 Subject: [PATCH 01/19] feat: Add is_into_lazyframe --- narwhals/dependencies.py | 39 ++++++++++++++++ narwhals/stable/v1/dependencies.py | 2 + tests/dependencies/is_into_lazyframe_test.py | 49 ++++++++++++++++++++ 3 files changed, 90 insertions(+) create mode 100644 tests/dependencies/is_into_lazyframe_test.py diff --git a/narwhals/dependencies.py b/narwhals/dependencies.py index bbc49e4f71..04bcad19b0 100644 --- a/narwhals/dependencies.py +++ b/narwhals/dependencies.py @@ -550,6 +550,44 @@ def is_into_dataframe(native_dataframe: Any | IntoDataFrameT) -> TypeIs[IntoData ) +def is_into_lazyframe(native_lazyframe: Any | IntoLazyFrameT) -> TypeIs[IntoLazyFrameT]: + """Check whether `native_lazyframe` can be converted to a Narwhals LazyFrame. + + Arguments: + native_lazyframe: The object to check. + + Examples: + >>> import pandas as pd + >>> import polars as pl + >>> import numpy as np + >>> from narwhals.dependencies import is_into_lazyframe + + >>> df_pd = pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]}) + >>> lf_pl = pl.LazyFrame({"a": [1, 2, 3], "b": [4, 5, 6]}) + >>> np_arr = np.array([[1, 4], [2, 5], [3, 6]]) + + >>> is_into_lazyframe(df_pd) + False + >>> is_into_lazyframe(lf_pl) + True + >>> is_into_lazyframe(np_arr) + False + """ + from narwhals.dataframe import LazyFrame + + return ( + isinstance(native_lazyframe, LazyFrame) + or hasattr(native_lazyframe, "__narwhals_lazyframe__") + or is_polars_lazyframe(native_lazyframe) + or is_dask_dataframe(native_lazyframe) + or is_duckdb_relation(native_lazyframe) + or is_ibis_table(native_lazyframe) + or is_pyspark_dataframe(native_lazyframe) + or is_pyspark_connect_dataframe(native_lazyframe) + or is_sqlframe_dataframe(native_lazyframe) + ) + + def is_narwhals_dataframe( df: DataFrame[IntoDataFrameT] | Any, ) -> TypeIs[DataFrame[IntoDataFrameT]]: @@ -613,6 +651,7 @@ def is_narwhals_series_bool( "is_dask_dataframe", "is_ibis_table", "is_into_dataframe", + "is_into_lazyframe", "is_into_series", "is_modin_dataframe", "is_modin_series", diff --git a/narwhals/stable/v1/dependencies.py b/narwhals/stable/v1/dependencies.py index 2674f2890f..f2bd4bc117 100644 --- a/narwhals/stable/v1/dependencies.py +++ b/narwhals/stable/v1/dependencies.py @@ -25,6 +25,7 @@ get_polars, get_pyarrow, is_into_dataframe, + is_into_lazyframe, is_into_series, is_narwhals_dataframe, is_narwhals_lazyframe, @@ -136,6 +137,7 @@ def is_pandas_like_series(ser: Any) -> bool: "is_dask_dataframe", "is_ibis_table", "is_into_dataframe", + "is_into_lazyframe", "is_into_series", "is_modin_dataframe", "is_modin_series", diff --git a/tests/dependencies/is_into_lazyframe_test.py b/tests/dependencies/is_into_lazyframe_test.py new file mode 100644 index 0000000000..fc43dee769 --- /dev/null +++ b/tests/dependencies/is_into_lazyframe_test.py @@ -0,0 +1,49 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Any + +import pytest + +from narwhals.stable.v1.dependencies import is_into_lazyframe + +if TYPE_CHECKING: + from collections.abc import Mapping, Sequence + + from typing_extensions import Self + + from tests.utils import Constructor, ConstructorEager + +EAGER_CONSTRUCTOR_NAMES = ("pandas", "modin", "cudf", "polars_eager", "pyarrow") + +data: dict[str, Any] = {"a": [1, 2, 3], "b": [4, 5, 6]} + + +class LazyDictDataFrame: + def __init__(self, data: Mapping[str, Sequence[Any]]) -> None: + self._data = data + + def __len__(self) -> int: # pragma: no cover + return len(next(iter(self._data.values()))) + + def __narwhals_lazyframe__(self) -> Self: # pragma: no cover + return self + + +def test_is_into_lazyframe_lazy(constructor: Constructor) -> None: + if any(x in str(constructor) for x in EAGER_CONSTRUCTOR_NAMES): + assert not is_into_lazyframe(constructor(data)) + else: + assert is_into_lazyframe(constructor(data)) + + +def test_is_into_lazyframe_eager(constructor_eager: ConstructorEager) -> None: + assert not is_into_lazyframe(constructor_eager(data)) + + +def test_is_into_lazyframe_other() -> None: + pytest.importorskip("numpy") + import numpy as np + + assert is_into_lazyframe(LazyDictDataFrame(data)) # pyrefly: ignore[bad-specialization] + assert not is_into_lazyframe(np.array([[1, 4], [2, 5], [3, 6]])) + assert not is_into_lazyframe(data) From 22e979ab91064c9dc4bc70578f927e32ace258da Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Tue, 12 May 2026 00:37:18 +0200 Subject: [PATCH 02/19] fixup warnings --- narwhals/dependencies.py | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/narwhals/dependencies.py b/narwhals/dependencies.py index 04bcad19b0..c3eccc2711 100644 --- a/narwhals/dependencies.py +++ b/narwhals/dependencies.py @@ -128,22 +128,28 @@ def get_sqlframe() -> Any: return sys.modules.get("sqlframe", None) -def _warn_if_narwhals_df_or_lf(df: Any) -> None: - if is_narwhals_dataframe(df) or is_narwhals_lazyframe(df): +def _warn_if_narwhals_df_or_lf(frame: Any, /) -> None: + if is_narwhals_dataframe(frame) or is_narwhals_lazyframe(frame): + from narwhals._utils import qualified_type_name + + caller = sys._getframe(1).f_code.co_name 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())`?" + f"You passed a `{qualified_type_name(frame)}` to `{caller}`.\n\n" + f"Hint: Instead of e.g. `{caller}(frame)`, " + f"did you mean `{caller}(frame.to_native())`?" ) issue_warning(msg, UserWarning) -def _warn_if_narwhals_series(ser: Any) -> None: +def _warn_if_narwhals_series(ser: Any, /) -> None: if is_narwhals_series(ser): + from narwhals._utils import qualified_type_name + + caller = sys._getframe(1).f_code.co_name 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())`?" + f"You passed a `{qualified_type_name(ser)}` to `{caller}`.\n\n" + f"Hint: Instead of e.g. `{caller}(ser)`, " + f"did you mean `{caller}(ser.to_native())`?" ) issue_warning(msg, UserWarning) From b613345f80842162482019070c5978bf767ed769 Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Tue, 12 May 2026 01:01:05 +0200 Subject: [PATCH 03/19] main and v1 dependencies fixes --- narwhals/dependencies.py | 14 ++++ narwhals/stable/v1/dependencies.py | 122 ++++++++++++++++++++++++++++- 2 files changed, 133 insertions(+), 3 deletions(-) diff --git a/narwhals/dependencies.py b/narwhals/dependencies.py index c3eccc2711..dc2c564832 100644 --- a/narwhals/dependencies.py +++ b/narwhals/dependencies.py @@ -646,20 +646,30 @@ def is_narwhals_series_bool( __all__ = [ "get_cudf", + "get_dask", + "get_dask_dataframe", + "get_duckdb", "get_ibis", "get_modin", "get_numpy", "get_pandas", "get_polars", "get_pyarrow", + "get_pyspark", + "get_pyspark_connect", + "get_pyspark_sql", + "get_sqlframe", "is_cudf_dataframe", + "is_cudf_index", "is_cudf_series", "is_dask_dataframe", + "is_duckdb_relation", "is_ibis_table", "is_into_dataframe", "is_into_lazyframe", "is_into_series", "is_modin_dataframe", + "is_modin_index", "is_modin_series", "is_narwhals_dataframe", "is_narwhals_lazyframe", @@ -668,6 +678,7 @@ def is_narwhals_series_bool( "is_pandas_dataframe", "is_pandas_index", "is_pandas_like_dataframe", + "is_pandas_like_index", "is_pandas_like_series", "is_pandas_series", "is_polars_dataframe", @@ -675,4 +686,7 @@ def is_narwhals_series_bool( "is_polars_series", "is_pyarrow_chunked_array", "is_pyarrow_table", + "is_pyspark_connect_dataframe", + "is_pyspark_dataframe", + "is_sqlframe_dataframe", ] diff --git a/narwhals/stable/v1/dependencies.py b/narwhals/stable/v1/dependencies.py index f2bd4bc117..23daa6db75 100644 --- a/narwhals/stable/v1/dependencies.py +++ b/narwhals/stable/v1/dependencies.py @@ -6,32 +6,44 @@ if TYPE_CHECKING: import cudf import dask.dataframe as dd + import duckdb import ibis import modin.pandas as mpd import pandas as pd import polars as pl import pyarrow as pa + import pyspark.sql as pyspark_sql + from pyspark.sql.connect.dataframe import DataFrame as PySparkConnectDataFrame from typing_extensions import TypeIs + from narwhals._spark_like.dataframe import SQLFrameDataFrame + from narwhals.stable.v1.typing import IntoDataFrameT, IntoLazyFrameT, IntoSeriesT + from narwhals.dependencies import ( IMPORT_HOOKS, get_cudf, + get_dask, get_dask_dataframe, + get_duckdb, get_ibis, get_modin, get_numpy, get_pandas, get_polars, get_pyarrow, - is_into_dataframe, - is_into_lazyframe, - is_into_series, + get_pyspark, + get_pyspark_connect, + get_pyspark_sql, + get_sqlframe, + is_cudf_index, + is_modin_index, is_narwhals_dataframe, is_narwhals_lazyframe, is_narwhals_series, is_numpy_array, is_pandas_index, + is_pandas_like_index, ) @@ -78,6 +90,13 @@ def is_dask_dataframe(df: Any) -> TypeIs[dd.DataFrame]: return (dd := get_dask_dataframe()) is not None and isinstance(df, dd.DataFrame) +def is_duckdb_relation(df: Any) -> TypeIs[duckdb.DuckDBPyRelation]: + """Check whether `df` is a DuckDB Relation without importing DuckDB.""" + return (duckdb := get_duckdb()) is not None and isinstance( + df, duckdb.DuckDBPyRelation + ) + + def is_ibis_table(df: Any) -> TypeIs[ibis.Table]: """Check whether `df` is a Ibis Table without importing Ibis.""" return (ibis := get_ibis()) is not None and isinstance(df, ibis.expr.types.Table) @@ -124,22 +143,115 @@ def is_pandas_like_series(ser: Any) -> bool: return is_pandas_series(ser) or is_modin_series(ser) or is_cudf_series(ser) +def is_pyspark_dataframe(df: Any) -> TypeIs[pyspark_sql.DataFrame]: + """Check whether `df` is a PySpark DataFrame without importing PySpark. + + Warning: + This method cannot be called on a Narwhals DataFrame/LazyFrame. + """ + return bool( + (pyspark_sql := get_pyspark_sql()) is not None + and isinstance(df, pyspark_sql.DataFrame) + ) + + +def is_pyspark_connect_dataframe(df: Any) -> TypeIs[PySparkConnectDataFrame]: + """Check whether `df` is a PySpark Connect DataFrame without importing PySpark. + + Warning: + This method cannot be called on a Narwhals DataFrame/LazyFrame. + """ + if get_pyspark_connect() is not None: # pragma: no cover + try: + from pyspark.sql.connect.dataframe import DataFrame + except ImportError: + return False + return isinstance(df, DataFrame) + return False + + +def is_sqlframe_dataframe(df: Any) -> TypeIs[SQLFrameDataFrame]: + """Check whether `df` is a SQLFrame DataFrame without importing SQLFrame. + + Warning: + This method cannot be called on a Narwhals DataFrame/LazyFrame. + """ + if get_sqlframe() is not None: + from sqlframe.base.dataframe import BaseDataFrame + + return isinstance(df, BaseDataFrame) + return False # pragma: no cover + + +def is_into_dataframe(native_dataframe: Any | IntoDataFrameT) -> TypeIs[IntoDataFrameT]: + """Check whether `native_dataframe` can be converted to a narwhals.stable.v1.DataFrame.""" + from narwhals.stable.v1 import DataFrame + + return ( + isinstance(native_dataframe, DataFrame) + or hasattr(native_dataframe, "__narwhals_dataframe__") + or is_polars_dataframe(native_dataframe) + or is_pyarrow_table(native_dataframe) + or is_pandas_like_dataframe(native_dataframe) + or is_ibis_table(native_dataframe) + or is_duckdb_relation(native_dataframe) + ) + + +def is_into_lazyframe(native_lazyframe: Any | IntoLazyFrameT) -> TypeIs[IntoLazyFrameT]: + """Check whether `native_lazyframe` can be converted to a narwhals.stable.v1.LazyFrame.""" + from narwhals.dataframe import LazyFrame + + return ( + isinstance(native_lazyframe, LazyFrame) + or hasattr(native_lazyframe, "__narwhals_lazyframe__") + or is_polars_lazyframe(native_lazyframe) + or is_dask_dataframe(native_lazyframe) + or is_pyspark_dataframe(native_lazyframe) + or is_pyspark_connect_dataframe(native_lazyframe) + or is_sqlframe_dataframe(native_lazyframe) + ) + + +def is_into_series(native_series: Any | IntoSeriesT) -> TypeIs[IntoSeriesT]: + """Check whether `native_series` can be converted to a narwhals.stable.v1.Series.""" + from narwhals.stable.v1 import Series + + return ( + isinstance(native_series, Series) + or hasattr(native_series, "__narwhals_series__") + or is_polars_series(native_series) + or is_pyarrow_chunked_array(native_series) + or is_pandas_like_series(native_series) + ) + + __all__ = [ "get_cudf", + "get_dask", + "get_dask_dataframe", + "get_duckdb", "get_ibis", "get_modin", "get_numpy", "get_pandas", "get_polars", "get_pyarrow", + "get_pyspark", + "get_pyspark_connect", + "get_pyspark_sql", + "get_sqlframe", "is_cudf_dataframe", + "is_cudf_index", "is_cudf_series", "is_dask_dataframe", + "is_duckdb_relation", "is_ibis_table", "is_into_dataframe", "is_into_lazyframe", "is_into_series", "is_modin_dataframe", + "is_modin_index", "is_modin_series", "is_narwhals_dataframe", "is_narwhals_lazyframe", @@ -148,6 +260,7 @@ def is_pandas_like_series(ser: Any) -> bool: "is_pandas_dataframe", "is_pandas_index", "is_pandas_like_dataframe", + "is_pandas_like_index", "is_pandas_like_series", "is_pandas_series", "is_polars_dataframe", @@ -155,4 +268,7 @@ def is_pandas_like_series(ser: Any) -> bool: "is_polars_series", "is_pyarrow_chunked_array", "is_pyarrow_table", + "is_pyspark_connect_dataframe", + "is_pyspark_dataframe", + "is_sqlframe_dataframe", ] From db4aa279aa81462468716d5bdcd88ebf88f2a256 Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Tue, 12 May 2026 01:14:40 +0200 Subject: [PATCH 04/19] unit tests and sqlframe issue --- narwhals/dependencies.py | 2 +- narwhals/stable/v1/dependencies.py | 2 +- tests/dependencies/is_into_dataframe_test.py | 48 +++++++++++--------- tests/dependencies/is_into_lazyframe_test.py | 14 +++++- 4 files changed, 41 insertions(+), 25 deletions(-) diff --git a/narwhals/dependencies.py b/narwhals/dependencies.py index dc2c564832..e0d7cc4bf4 100644 --- a/narwhals/dependencies.py +++ b/narwhals/dependencies.py @@ -553,7 +553,7 @@ def is_into_dataframe(native_dataframe: Any | IntoDataFrameT) -> TypeIs[IntoData or is_polars_dataframe(native_dataframe) or is_pyarrow_table(native_dataframe) or is_pandas_like_dataframe(native_dataframe) - ) + ) and not is_sqlframe_dataframe(native_dataframe) def is_into_lazyframe(native_lazyframe: Any | IntoLazyFrameT) -> TypeIs[IntoLazyFrameT]: diff --git a/narwhals/stable/v1/dependencies.py b/narwhals/stable/v1/dependencies.py index 23daa6db75..e8b95b1b8b 100644 --- a/narwhals/stable/v1/dependencies.py +++ b/narwhals/stable/v1/dependencies.py @@ -195,7 +195,7 @@ def is_into_dataframe(native_dataframe: Any | IntoDataFrameT) -> TypeIs[IntoData or is_pandas_like_dataframe(native_dataframe) or is_ibis_table(native_dataframe) or is_duckdb_relation(native_dataframe) - ) + ) and not is_sqlframe_dataframe(native_dataframe) def is_into_lazyframe(native_lazyframe: Any | IntoLazyFrameT) -> TypeIs[IntoLazyFrameT]: diff --git a/tests/dependencies/is_into_dataframe_test.py b/tests/dependencies/is_into_dataframe_test.py index 32b2251ad6..1dc622faff 100644 --- a/tests/dependencies/is_into_dataframe_test.py +++ b/tests/dependencies/is_into_dataframe_test.py @@ -4,15 +4,20 @@ import pytest -import narwhals as nw -from narwhals.stable.v1.dependencies import is_into_dataframe +from narwhals.dependencies import is_into_dataframe +from narwhals.stable.v1.dependencies import is_into_dataframe as v1_is_into_dataframe if TYPE_CHECKING: from collections.abc import Mapping from typing_extensions import Self -DATA: dict[str, Any] = {"a": [1, 2, 3], "b": [4, 5, 6]} + from tests.utils import Constructor, ConstructorEager + +EAGER_CONSTRUCTOR_NAMES = ("pandas", "modin", "cudf", "polars_eager", "pyarrow") +V1_INTO_DATAFRAMES = (*EAGER_CONSTRUCTOR_NAMES, "duckdb", "ibis") + +data: dict[str, Any] = {"a": [1, 2, 3], "b": [4, 5, 6]} class DictDataFrame: @@ -26,32 +31,31 @@ def __narwhals_dataframe__(self) -> Self: # pragma: no cover return self -def test_is_into_dataframe_pyarrow() -> None: - pytest.importorskip("pyarrow") - import pyarrow as pa - - assert is_into_dataframe(pa.table(DATA)) - +def test_is_into_dataframe_lazy(constructor: Constructor) -> None: + if any(x in str(constructor) for x in EAGER_CONSTRUCTOR_NAMES): + assert is_into_dataframe(constructor(data)) + else: + assert not is_into_dataframe(constructor(data)) -def test_is_into_dataframe_polars() -> None: - pytest.importorskip("polars") - import polars as pl + if any(x in str(constructor) for x in V1_INTO_DATAFRAMES): + assert v1_is_into_dataframe(constructor(data)) + else: + assert not v1_is_into_dataframe(constructor(data)) - assert is_into_dataframe(pl.DataFrame(DATA)) - -def test_is_into_dataframe_pandas() -> None: - pytest.importorskip("pandas") - import pandas as pd - - assert is_into_dataframe(pd.DataFrame(DATA)) - assert is_into_dataframe(nw.from_native(pd.DataFrame(DATA))) +def test_is_into_dataframe_eager(constructor_eager: ConstructorEager) -> None: + assert is_into_dataframe(constructor_eager(data)) + assert v1_is_into_dataframe(constructor_eager(data)) def test_is_into_dataframe_other() -> None: pytest.importorskip("numpy") import numpy as np - assert is_into_dataframe(DictDataFrame(DATA)) # pyrefly: ignore[bad-specialization] + assert is_into_dataframe(DictDataFrame(data)) # pyrefly: ignore[bad-specialization] assert not is_into_dataframe(np.array([[1, 4], [2, 5], [3, 6]])) - assert not is_into_dataframe(DATA) + assert not is_into_dataframe(data) + + assert v1_is_into_dataframe(DictDataFrame(data)) # pyrefly: ignore[bad-specialization] + assert not v1_is_into_dataframe(np.array([[1, 4], [2, 5], [3, 6]])) + assert not v1_is_into_dataframe(data) diff --git a/tests/dependencies/is_into_lazyframe_test.py b/tests/dependencies/is_into_lazyframe_test.py index fc43dee769..977e2c766c 100644 --- a/tests/dependencies/is_into_lazyframe_test.py +++ b/tests/dependencies/is_into_lazyframe_test.py @@ -4,7 +4,8 @@ import pytest -from narwhals.stable.v1.dependencies import is_into_lazyframe +from narwhals.dependencies import is_into_lazyframe +from narwhals.stable.v1.dependencies import is_into_lazyframe as v1_is_into_lazyframe if TYPE_CHECKING: from collections.abc import Mapping, Sequence @@ -14,6 +15,7 @@ from tests.utils import Constructor, ConstructorEager EAGER_CONSTRUCTOR_NAMES = ("pandas", "modin", "cudf", "polars_eager", "pyarrow") +V1_INTO_DATAFRAMES = (*EAGER_CONSTRUCTOR_NAMES, "duckdb", "ibis") data: dict[str, Any] = {"a": [1, 2, 3], "b": [4, 5, 6]} @@ -35,9 +37,15 @@ def test_is_into_lazyframe_lazy(constructor: Constructor) -> None: else: assert is_into_lazyframe(constructor(data)) + if any(x in str(constructor) for x in V1_INTO_DATAFRAMES): + assert not v1_is_into_lazyframe(constructor(data)) + else: + assert v1_is_into_lazyframe(constructor(data)) + def test_is_into_lazyframe_eager(constructor_eager: ConstructorEager) -> None: assert not is_into_lazyframe(constructor_eager(data)) + assert not v1_is_into_lazyframe(constructor_eager(data)) def test_is_into_lazyframe_other() -> None: @@ -47,3 +55,7 @@ def test_is_into_lazyframe_other() -> None: assert is_into_lazyframe(LazyDictDataFrame(data)) # pyrefly: ignore[bad-specialization] assert not is_into_lazyframe(np.array([[1, 4], [2, 5], [3, 6]])) assert not is_into_lazyframe(data) + + assert v1_is_into_lazyframe(LazyDictDataFrame(data)) # pyrefly: ignore[bad-specialization] + assert not v1_is_into_lazyframe(np.array([[1, 4], [2, 5], [3, 6]])) + assert not v1_is_into_lazyframe(data) From fd75a8b2e5b4e147fc157935e0459ea2d15d64a6 Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Tue, 12 May 2026 01:43:41 +0200 Subject: [PATCH 05/19] fix import, fix unit tests --- narwhals/stable/v1/dependencies.py | 2 +- tests/dependencies/is_into_dataframe_test.py | 29 +++++++------ tests/dependencies/is_into_lazyframe_test.py | 39 +++++++++-------- tests/dependencies/is_into_series_test.py | 44 +++++++++++--------- 4 files changed, 62 insertions(+), 52 deletions(-) diff --git a/narwhals/stable/v1/dependencies.py b/narwhals/stable/v1/dependencies.py index e8b95b1b8b..b5926ee971 100644 --- a/narwhals/stable/v1/dependencies.py +++ b/narwhals/stable/v1/dependencies.py @@ -200,7 +200,7 @@ def is_into_dataframe(native_dataframe: Any | IntoDataFrameT) -> TypeIs[IntoData def is_into_lazyframe(native_lazyframe: Any | IntoLazyFrameT) -> TypeIs[IntoLazyFrameT]: """Check whether `native_lazyframe` can be converted to a narwhals.stable.v1.LazyFrame.""" - from narwhals.dataframe import LazyFrame + from narwhals.stable.v1 import LazyFrame return ( isinstance(native_lazyframe, LazyFrame) diff --git a/tests/dependencies/is_into_dataframe_test.py b/tests/dependencies/is_into_dataframe_test.py index 1dc622faff..c9805d74d6 100644 --- a/tests/dependencies/is_into_dataframe_test.py +++ b/tests/dependencies/is_into_dataframe_test.py @@ -4,6 +4,8 @@ import pytest +import narwhals as nw +import narwhals.stable.v1 as nw_v1 from narwhals.dependencies import is_into_dataframe from narwhals.stable.v1.dependencies import is_into_dataframe as v1_is_into_dataframe @@ -12,7 +14,7 @@ from typing_extensions import Self - from tests.utils import Constructor, ConstructorEager + from tests.utils import Constructor EAGER_CONSTRUCTOR_NAMES = ("pandas", "modin", "cudf", "polars_eager", "pyarrow") V1_INTO_DATAFRAMES = (*EAGER_CONSTRUCTOR_NAMES, "duckdb", "ibis") @@ -31,21 +33,22 @@ def __narwhals_dataframe__(self) -> Self: # pragma: no cover return self -def test_is_into_dataframe_lazy(constructor: Constructor) -> None: - if any(x in str(constructor) for x in EAGER_CONSTRUCTOR_NAMES): - assert is_into_dataframe(constructor(data)) - else: - assert not is_into_dataframe(constructor(data)) +@pytest.mark.filterwarnings("ignore:.*You passed a.*:UserWarning") +def test_is_into_dataframe(constructor: Constructor) -> None: + native_frame = constructor(data) + nw_frame = nw.from_native(native_frame) + nw_v1_frame = nw_v1.from_native(native_frame) - if any(x in str(constructor) for x in V1_INTO_DATAFRAMES): - assert v1_is_into_dataframe(constructor(data)) - else: - assert not v1_is_into_dataframe(constructor(data)) + result = any(x in str(constructor) for x in EAGER_CONSTRUCTOR_NAMES) + assert is_into_dataframe(native_frame) == result + assert is_into_dataframe(nw_frame) == result + result_v1 = any(x in str(constructor) for x in V1_INTO_DATAFRAMES) + assert v1_is_into_dataframe(native_frame) == result_v1 + assert v1_is_into_dataframe(nw_v1_frame) == result_v1 -def test_is_into_dataframe_eager(constructor_eager: ConstructorEager) -> None: - assert is_into_dataframe(constructor_eager(data)) - assert v1_is_into_dataframe(constructor_eager(data)) + assert is_into_dataframe(nw_v1_frame) == result_v1 + assert not v1_is_into_dataframe(nw_frame) def test_is_into_dataframe_other() -> None: diff --git a/tests/dependencies/is_into_lazyframe_test.py b/tests/dependencies/is_into_lazyframe_test.py index 977e2c766c..2302b74270 100644 --- a/tests/dependencies/is_into_lazyframe_test.py +++ b/tests/dependencies/is_into_lazyframe_test.py @@ -4,15 +4,17 @@ import pytest +import narwhals as nw +import narwhals.stable.v1 as nw_v1 from narwhals.dependencies import is_into_lazyframe from narwhals.stable.v1.dependencies import is_into_lazyframe as v1_is_into_lazyframe if TYPE_CHECKING: - from collections.abc import Mapping, Sequence + from collections.abc import Mapping from typing_extensions import Self - from tests.utils import Constructor, ConstructorEager + from tests.utils import Constructor EAGER_CONSTRUCTOR_NAMES = ("pandas", "modin", "cudf", "polars_eager", "pyarrow") V1_INTO_DATAFRAMES = (*EAGER_CONSTRUCTOR_NAMES, "duckdb", "ibis") @@ -20,8 +22,8 @@ data: dict[str, Any] = {"a": [1, 2, 3], "b": [4, 5, 6]} -class LazyDictDataFrame: - def __init__(self, data: Mapping[str, Sequence[Any]]) -> None: +class DictDataFrame: + def __init__(self, data: Mapping[str, Any]) -> None: self._data = data def __len__(self) -> int: # pragma: no cover @@ -31,31 +33,32 @@ def __narwhals_lazyframe__(self) -> Self: # pragma: no cover return self -def test_is_into_lazyframe_lazy(constructor: Constructor) -> None: - if any(x in str(constructor) for x in EAGER_CONSTRUCTOR_NAMES): - assert not is_into_lazyframe(constructor(data)) - else: - assert is_into_lazyframe(constructor(data)) +@pytest.mark.filterwarnings("ignore:.*You passed a.*:UserWarning") +def test_is_into_lazyframe(constructor: Constructor) -> None: + native_frame = constructor(data) + nw_frame = nw.from_native(native_frame) + nw_v1_frame = nw_v1.from_native(native_frame) - if any(x in str(constructor) for x in V1_INTO_DATAFRAMES): - assert not v1_is_into_lazyframe(constructor(data)) - else: - assert v1_is_into_lazyframe(constructor(data)) + result = not any(x in str(constructor) for x in EAGER_CONSTRUCTOR_NAMES) + assert is_into_lazyframe(native_frame) == result + assert is_into_lazyframe(nw_frame) == result + result_v1 = not any(x in str(constructor) for x in V1_INTO_DATAFRAMES) + assert v1_is_into_lazyframe(native_frame) == result_v1 + assert v1_is_into_lazyframe(nw_v1_frame) == result_v1 -def test_is_into_lazyframe_eager(constructor_eager: ConstructorEager) -> None: - assert not is_into_lazyframe(constructor_eager(data)) - assert not v1_is_into_lazyframe(constructor_eager(data)) + assert is_into_lazyframe(nw_v1_frame) == result_v1 + assert not v1_is_into_lazyframe(nw_frame) def test_is_into_lazyframe_other() -> None: pytest.importorskip("numpy") import numpy as np - assert is_into_lazyframe(LazyDictDataFrame(data)) # pyrefly: ignore[bad-specialization] + assert is_into_lazyframe(DictDataFrame(data)) # pyrefly: ignore[bad-specialization] assert not is_into_lazyframe(np.array([[1, 4], [2, 5], [3, 6]])) assert not is_into_lazyframe(data) - assert v1_is_into_lazyframe(LazyDictDataFrame(data)) # pyrefly: ignore[bad-specialization] + assert v1_is_into_lazyframe(DictDataFrame(data)) # pyrefly: ignore[bad-specialization] assert not v1_is_into_lazyframe(np.array([[1, 4], [2, 5], [3, 6]])) assert not v1_is_into_lazyframe(data) diff --git a/tests/dependencies/is_into_series_test.py b/tests/dependencies/is_into_series_test.py index 8aa895343b..72ba972b0f 100644 --- a/tests/dependencies/is_into_series_test.py +++ b/tests/dependencies/is_into_series_test.py @@ -5,11 +5,17 @@ import pytest import narwhals as nw -from narwhals.stable.v1.dependencies import is_into_series +import narwhals.stable.v1 as nw_v1 +from narwhals.dependencies import is_into_series +from narwhals.stable.v1.dependencies import is_into_series as v1_is_into_series if TYPE_CHECKING: from typing_extensions import Self + from tests.utils import ConstructorEager + +data: dict[str, Any] = {"a": [1, 2, 3], "b": [4, 5, 6]} + class ListBackedSeries: def __init__(self, name: str, data: list[Any]) -> None: @@ -23,32 +29,30 @@ def __narwhals_series__(self) -> Self: # pragma: no cover return self -def test_is_into_series_pyarrow() -> None: - pytest.importorskip("pyarrow") - import pyarrow as pa - - assert is_into_series(pa.chunked_array([["a", "b"]])) - +@pytest.mark.filterwarnings("ignore:.*You passed a.*:UserWarning") +def test_is_into_series(constructor_eager: ConstructorEager) -> None: + native_frame = constructor_eager(data) + nw_series = nw.from_native(native_frame)["a"] + nw_v1_series = nw_v1.from_native(native_frame)["a"] + native_series = nw_series.to_native() -def test_is_into_series_polars() -> None: - pytest.importorskip("polars") - import polars as pl + assert is_into_series(native_series) + assert is_into_series(nw_series) + assert is_into_series(nw_v1_series) - assert is_into_series(pl.Series([1, 2, 3])) + assert v1_is_into_series(native_series) + assert not v1_is_into_series(nw_series) + assert v1_is_into_series(nw_v1_series) -def test_is_into_series_pandas() -> None: - pytest.importorskip("pandas") - import pandas as pd - - assert is_into_series(pd.Series([1, 2, 3])) - assert is_into_series(nw.from_native(pd.Series([1, 2, 3]), series_only=True)) - - -def test_is_into_series() -> None: +def test_is_into_series_other() -> None: pytest.importorskip("numpy") import numpy as np assert is_into_series(ListBackedSeries("a", [1, 4, 2])) # pyrefly: ignore[bad-specialization] assert not is_into_series(np.array([1, 2, 3])) assert not is_into_series([1, 2, 3]) + + assert v1_is_into_series(ListBackedSeries("a", [1, 4, 2])) # pyrefly: ignore[bad-specialization] + assert not v1_is_into_series(np.array([1, 2, 3])) + assert not v1_is_into_series([1, 2, 3]) From 74e216447f44ab95521c18f9209344b20079300a Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Tue, 12 May 2026 23:04:33 +0200 Subject: [PATCH 06/19] Align v2 behaviour, resolve comment: https://github.com/narwhals-dev/narwhals/pull/3613\#discussion_r3229405470 --- narwhals/stable/v2/dependencies.py | 142 ++++++++++++++++++- tests/dependencies/is_into_dataframe_test.py | 15 ++ tests/dependencies/is_into_lazyframe_test.py | 16 ++- tests/dependencies/is_into_series_test.py | 14 ++ 4 files changed, 185 insertions(+), 2 deletions(-) diff --git a/narwhals/stable/v2/dependencies.py b/narwhals/stable/v2/dependencies.py index 09dac96892..c71cb32492 100644 --- a/narwhals/stable/v2/dependencies.py +++ b/narwhals/stable/v2/dependencies.py @@ -1,3 +1,143 @@ from __future__ import annotations -from narwhals.dependencies import * # noqa: F403 +from typing import TYPE_CHECKING, Any + +from narwhals.dependencies import ( + get_cudf, + get_dask, + get_dask_dataframe, + get_duckdb, + get_ibis, + get_modin, + get_numpy, + get_pandas, + get_polars, + get_pyarrow, + get_pyspark, + get_pyspark_connect, + get_pyspark_sql, + get_sqlframe, + is_cudf_dataframe, + is_cudf_index, + is_cudf_series, + is_dask_dataframe, + is_duckdb_relation, + is_ibis_table, + is_modin_dataframe, + is_modin_index, + is_modin_series, + is_narwhals_dataframe, + is_narwhals_lazyframe, + is_narwhals_series, + is_numpy_array, + is_pandas_dataframe, + is_pandas_index, + is_pandas_like_dataframe, + is_pandas_like_index, + is_pandas_like_series, + is_pandas_series, + is_polars_dataframe, + is_polars_lazyframe, + is_polars_series, + is_pyarrow_chunked_array, + is_pyarrow_table, + is_pyspark_connect_dataframe, + is_pyspark_dataframe, + is_sqlframe_dataframe, +) + +if TYPE_CHECKING: + from typing_extensions import TypeIs + + from narwhals.stable.v2.typing import IntoDataFrameT, IntoLazyFrameT, IntoSeriesT + + +def is_into_dataframe(native_dataframe: Any | IntoDataFrameT) -> TypeIs[IntoDataFrameT]: + """Check whether `native_dataframe` can be converted to a narwhals.stable.v2.DataFrame.""" + from narwhals.stable.v2 import DataFrame + + return ( + isinstance(native_dataframe, DataFrame) + or hasattr(native_dataframe, "__narwhals_dataframe__") + or is_polars_dataframe(native_dataframe) + or is_pyarrow_table(native_dataframe) + or is_pandas_like_dataframe(native_dataframe) + ) and not is_sqlframe_dataframe(native_dataframe) + + +def is_into_lazyframe(native_lazyframe: Any | IntoLazyFrameT) -> TypeIs[IntoLazyFrameT]: + """Check whether `native_lazyframe` can be converted to a narwhals.stable.v2.LazyFrame.""" + from narwhals.stable.v2 import LazyFrame + + return ( + isinstance(native_lazyframe, LazyFrame) + or hasattr(native_lazyframe, "__narwhals_lazyframe__") + or is_polars_lazyframe(native_lazyframe) + or is_dask_dataframe(native_lazyframe) + or is_duckdb_relation(native_lazyframe) + or is_ibis_table(native_lazyframe) + or is_pyspark_dataframe(native_lazyframe) + or is_pyspark_connect_dataframe(native_lazyframe) + or is_sqlframe_dataframe(native_lazyframe) + ) + + +def is_into_series(native_series: Any | IntoSeriesT) -> TypeIs[IntoSeriesT]: + """Check whether `native_series` can be converted to a narwhals.stable.v2.Series.""" + from narwhals.stable.v2 import Series + + return ( + isinstance(native_series, Series) + or hasattr(native_series, "__narwhals_series__") + or is_polars_series(native_series) + or is_pyarrow_chunked_array(native_series) + or is_pandas_like_series(native_series) + ) + + +__all__ = [ + "get_cudf", + "get_dask", + "get_dask_dataframe", + "get_duckdb", + "get_ibis", + "get_modin", + "get_numpy", + "get_pandas", + "get_polars", + "get_pyarrow", + "get_pyspark", + "get_pyspark_connect", + "get_pyspark_sql", + "get_sqlframe", + "is_cudf_dataframe", + "is_cudf_index", + "is_cudf_series", + "is_dask_dataframe", + "is_duckdb_relation", + "is_ibis_table", + "is_into_dataframe", + "is_into_lazyframe", + "is_into_series", + "is_modin_dataframe", + "is_modin_index", + "is_modin_series", + "is_narwhals_dataframe", + "is_narwhals_lazyframe", + "is_narwhals_series", + "is_numpy_array", + "is_pandas_dataframe", + "is_pandas_index", + "is_pandas_like_dataframe", + "is_pandas_like_index", + "is_pandas_like_series", + "is_pandas_series", + "is_polars_dataframe", + "is_polars_lazyframe", + "is_polars_series", + "is_pyarrow_chunked_array", + "is_pyarrow_table", + "is_pyspark_connect_dataframe", + "is_pyspark_dataframe", + "is_sqlframe_dataframe", +] diff --git a/tests/dependencies/is_into_dataframe_test.py b/tests/dependencies/is_into_dataframe_test.py index c9805d74d6..2e4f98e0a7 100644 --- a/tests/dependencies/is_into_dataframe_test.py +++ b/tests/dependencies/is_into_dataframe_test.py @@ -6,8 +6,10 @@ import narwhals as nw import narwhals.stable.v1 as nw_v1 +import narwhals.stable.v2 as nw_v2 from narwhals.dependencies import is_into_dataframe from narwhals.stable.v1.dependencies import is_into_dataframe as v1_is_into_dataframe +from narwhals.stable.v2.dependencies import is_into_dataframe as v2_is_into_dataframe if TYPE_CHECKING: from collections.abc import Mapping @@ -38,6 +40,7 @@ def test_is_into_dataframe(constructor: Constructor) -> None: native_frame = constructor(data) nw_frame = nw.from_native(native_frame) nw_v1_frame = nw_v1.from_native(native_frame) + nw_v2_frame = nw_v2.from_native(native_frame) result = any(x in str(constructor) for x in EAGER_CONSTRUCTOR_NAMES) assert is_into_dataframe(native_frame) == result @@ -46,9 +49,17 @@ def test_is_into_dataframe(constructor: Constructor) -> None: result_v1 = any(x in str(constructor) for x in V1_INTO_DATAFRAMES) assert v1_is_into_dataframe(native_frame) == result_v1 assert v1_is_into_dataframe(nw_v1_frame) == result_v1 + assert v1_is_into_dataframe(nw_v2_frame) is False + + result_v2 = any(x in str(constructor) for x in EAGER_CONSTRUCTOR_NAMES) + assert v2_is_into_dataframe(native_frame) == result_v2 + assert v2_is_into_dataframe(nw_v2_frame) == result_v2 + assert v2_is_into_dataframe(nw_v1_frame) is False assert is_into_dataframe(nw_v1_frame) == result_v1 + assert is_into_dataframe(nw_v2_frame) == result_v2 assert not v1_is_into_dataframe(nw_frame) + assert not v2_is_into_dataframe(nw_frame) def test_is_into_dataframe_other() -> None: @@ -62,3 +73,7 @@ def test_is_into_dataframe_other() -> None: assert v1_is_into_dataframe(DictDataFrame(data)) # pyrefly: ignore[bad-specialization] assert not v1_is_into_dataframe(np.array([[1, 4], [2, 5], [3, 6]])) assert not v1_is_into_dataframe(data) + + assert v2_is_into_dataframe(DictDataFrame(data)) # pyrefly: ignore[bad-specialization] + assert not v2_is_into_dataframe(np.array([[1, 4], [2, 5], [3, 6]])) + assert not v2_is_into_dataframe(data) diff --git a/tests/dependencies/is_into_lazyframe_test.py b/tests/dependencies/is_into_lazyframe_test.py index 2302b74270..17cc29db00 100644 --- a/tests/dependencies/is_into_lazyframe_test.py +++ b/tests/dependencies/is_into_lazyframe_test.py @@ -6,8 +6,10 @@ import narwhals as nw import narwhals.stable.v1 as nw_v1 +import narwhals.stable.v2 as nw_v2 from narwhals.dependencies import is_into_lazyframe from narwhals.stable.v1.dependencies import is_into_lazyframe as v1_is_into_lazyframe +from narwhals.stable.v2.dependencies import is_into_lazyframe as v2_is_into_lazyframe if TYPE_CHECKING: from collections.abc import Mapping @@ -38,6 +40,7 @@ def test_is_into_lazyframe(constructor: Constructor) -> None: native_frame = constructor(data) nw_frame = nw.from_native(native_frame) nw_v1_frame = nw_v1.from_native(native_frame) + nw_v2_frame = nw_v2.from_native(native_frame) result = not any(x in str(constructor) for x in EAGER_CONSTRUCTOR_NAMES) assert is_into_lazyframe(native_frame) == result @@ -45,10 +48,17 @@ def test_is_into_lazyframe(constructor: Constructor) -> None: result_v1 = not any(x in str(constructor) for x in V1_INTO_DATAFRAMES) assert v1_is_into_lazyframe(native_frame) == result_v1 - assert v1_is_into_lazyframe(nw_v1_frame) == result_v1 + assert v1_is_into_lazyframe(nw_v2_frame) is False + + result_v2 = not any(x in str(constructor) for x in EAGER_CONSTRUCTOR_NAMES) + assert v2_is_into_lazyframe(native_frame) == result_v2 + assert v2_is_into_lazyframe(nw_v2_frame) == result_v2 + assert v2_is_into_lazyframe(nw_v1_frame) is False assert is_into_lazyframe(nw_v1_frame) == result_v1 + assert is_into_lazyframe(nw_v2_frame) == result_v2 assert not v1_is_into_lazyframe(nw_frame) + assert not v2_is_into_lazyframe(nw_frame) def test_is_into_lazyframe_other() -> None: @@ -62,3 +72,7 @@ def test_is_into_lazyframe_other() -> None: assert v1_is_into_lazyframe(DictDataFrame(data)) # pyrefly: ignore[bad-specialization] assert not v1_is_into_lazyframe(np.array([[1, 4], [2, 5], [3, 6]])) assert not v1_is_into_lazyframe(data) + + assert v2_is_into_lazyframe(DictDataFrame(data)) # pyrefly: ignore[bad-specialization] + assert not v2_is_into_lazyframe(np.array([[1, 4], [2, 5], [3, 6]])) + assert not v2_is_into_lazyframe(data) diff --git a/tests/dependencies/is_into_series_test.py b/tests/dependencies/is_into_series_test.py index 72ba972b0f..315f829fb5 100644 --- a/tests/dependencies/is_into_series_test.py +++ b/tests/dependencies/is_into_series_test.py @@ -6,8 +6,10 @@ import narwhals as nw import narwhals.stable.v1 as nw_v1 +import narwhals.stable.v2 as nw_v2 from narwhals.dependencies import is_into_series from narwhals.stable.v1.dependencies import is_into_series as v1_is_into_series +from narwhals.stable.v2.dependencies import is_into_series as v2_is_into_series if TYPE_CHECKING: from typing_extensions import Self @@ -34,15 +36,23 @@ def test_is_into_series(constructor_eager: ConstructorEager) -> None: native_frame = constructor_eager(data) nw_series = nw.from_native(native_frame)["a"] nw_v1_series = nw_v1.from_native(native_frame)["a"] + nw_v2_series = nw_v2.from_native(native_frame)["a"] native_series = nw_series.to_native() assert is_into_series(native_series) assert is_into_series(nw_series) assert is_into_series(nw_v1_series) + assert is_into_series(nw_v2_series) assert v1_is_into_series(native_series) assert not v1_is_into_series(nw_series) assert v1_is_into_series(nw_v1_series) + assert not v1_is_into_series(nw_v2_series) + + assert v2_is_into_series(native_series) + assert not v2_is_into_series(nw_series) + assert not v2_is_into_series(nw_v1_series) + assert v2_is_into_series(nw_v2_series) def test_is_into_series_other() -> None: @@ -56,3 +66,7 @@ def test_is_into_series_other() -> None: assert v1_is_into_series(ListBackedSeries("a", [1, 4, 2])) # pyrefly: ignore[bad-specialization] assert not v1_is_into_series(np.array([1, 2, 3])) assert not v1_is_into_series([1, 2, 3]) + + assert v2_is_into_series(ListBackedSeries("a", [1, 4, 2])) # pyrefly: ignore[bad-specialization] + assert not v2_is_into_series(np.array([1, 2, 3])) + assert not v2_is_into_series([1, 2, 3]) From e1aeff51684c2a6dccd48903c3c40e70014f53c8 Mon Sep 17 00:00:00 2001 From: dangotbanned <125183946+dangotbanned@users.noreply.github.com> Date: Wed, 13 May 2026 22:01:22 +0000 Subject: [PATCH 07/19] fix: Avoid `hasattr` false-positives related https://github.com/narwhals-dev/narwhals/pull/3613#discussion_r3222746562 --- narwhals/dependencies.py | 5 +++-- narwhals/stable/v1/dependencies.py | 5 +++-- narwhals/stable/v2/dependencies.py | 5 +++-- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/narwhals/dependencies.py b/narwhals/dependencies.py index e0d7cc4bf4..a141397511 100644 --- a/narwhals/dependencies.py +++ b/narwhals/dependencies.py @@ -545,15 +545,16 @@ def is_into_dataframe(native_dataframe: Any | IntoDataFrameT) -> TypeIs[IntoData >>> is_into_dataframe(np_arr) False """ + from narwhals._utils import _hasattr_static from narwhals.dataframe import DataFrame return ( isinstance(native_dataframe, DataFrame) - or hasattr(native_dataframe, "__narwhals_dataframe__") + or _hasattr_static(native_dataframe, "__narwhals_dataframe__") or is_polars_dataframe(native_dataframe) or is_pyarrow_table(native_dataframe) or is_pandas_like_dataframe(native_dataframe) - ) and not is_sqlframe_dataframe(native_dataframe) + ) def is_into_lazyframe(native_lazyframe: Any | IntoLazyFrameT) -> TypeIs[IntoLazyFrameT]: diff --git a/narwhals/stable/v1/dependencies.py b/narwhals/stable/v1/dependencies.py index b5926ee971..5d296d7a24 100644 --- a/narwhals/stable/v1/dependencies.py +++ b/narwhals/stable/v1/dependencies.py @@ -185,17 +185,18 @@ def is_sqlframe_dataframe(df: Any) -> TypeIs[SQLFrameDataFrame]: def is_into_dataframe(native_dataframe: Any | IntoDataFrameT) -> TypeIs[IntoDataFrameT]: """Check whether `native_dataframe` can be converted to a narwhals.stable.v1.DataFrame.""" + from narwhals._utils import _hasattr_static from narwhals.stable.v1 import DataFrame return ( isinstance(native_dataframe, DataFrame) - or hasattr(native_dataframe, "__narwhals_dataframe__") + or _hasattr_static(native_dataframe, "__narwhals_dataframe__") or is_polars_dataframe(native_dataframe) or is_pyarrow_table(native_dataframe) or is_pandas_like_dataframe(native_dataframe) or is_ibis_table(native_dataframe) or is_duckdb_relation(native_dataframe) - ) and not is_sqlframe_dataframe(native_dataframe) + ) def is_into_lazyframe(native_lazyframe: Any | IntoLazyFrameT) -> TypeIs[IntoLazyFrameT]: diff --git a/narwhals/stable/v2/dependencies.py b/narwhals/stable/v2/dependencies.py index c71cb32492..e2b38097e7 100644 --- a/narwhals/stable/v2/dependencies.py +++ b/narwhals/stable/v2/dependencies.py @@ -54,15 +54,16 @@ def is_into_dataframe(native_dataframe: Any | IntoDataFrameT) -> TypeIs[IntoDataFrameT]: """Check whether `native_dataframe` can be converted to a narwhals.stable.v2.DataFrame.""" + from narwhals._utils import _hasattr_static from narwhals.stable.v2 import DataFrame return ( isinstance(native_dataframe, DataFrame) - or hasattr(native_dataframe, "__narwhals_dataframe__") + or _hasattr_static(native_dataframe, "__narwhals_dataframe__") or is_polars_dataframe(native_dataframe) or is_pyarrow_table(native_dataframe) or is_pandas_like_dataframe(native_dataframe) - ) and not is_sqlframe_dataframe(native_dataframe) + ) def is_into_lazyframe(native_lazyframe: Any | IntoLazyFrameT) -> TypeIs[IntoLazyFrameT]: From 95287e6a9f5624f9e01bcc4f07ca85cd8fe5bfdc Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Thu, 14 May 2026 09:27:24 +0200 Subject: [PATCH 08/19] Use _hasattr_static in all is_into_* --- narwhals/dependencies.py | 6 ++++-- narwhals/stable/v1/dependencies.py | 6 ++++-- narwhals/stable/v2/dependencies.py | 6 ++++-- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/narwhals/dependencies.py b/narwhals/dependencies.py index a141397511..7a921046d0 100644 --- a/narwhals/dependencies.py +++ b/narwhals/dependencies.py @@ -511,11 +511,12 @@ def is_into_series(native_series: Any | IntoSeriesT) -> TypeIs[IntoSeriesT]: >>> nw.dependencies.is_into_series(np_arr) False """ + from narwhals._utils import _hasattr_static from narwhals.series import Series return ( isinstance(native_series, Series) - or hasattr(native_series, "__narwhals_series__") + or _hasattr_static(native_series, "__narwhals_series__") or is_polars_series(native_series) or is_pyarrow_chunked_array(native_series) or is_pandas_like_series(native_series) @@ -580,11 +581,12 @@ def is_into_lazyframe(native_lazyframe: Any | IntoLazyFrameT) -> TypeIs[IntoLazy >>> is_into_lazyframe(np_arr) False """ + from narwhals._utils import _hasattr_static from narwhals.dataframe import LazyFrame return ( isinstance(native_lazyframe, LazyFrame) - or hasattr(native_lazyframe, "__narwhals_lazyframe__") + or _hasattr_static(native_lazyframe, "__narwhals_lazyframe__") or is_polars_lazyframe(native_lazyframe) or is_dask_dataframe(native_lazyframe) or is_duckdb_relation(native_lazyframe) diff --git a/narwhals/stable/v1/dependencies.py b/narwhals/stable/v1/dependencies.py index 5d296d7a24..9bcf1a715a 100644 --- a/narwhals/stable/v1/dependencies.py +++ b/narwhals/stable/v1/dependencies.py @@ -201,11 +201,12 @@ def is_into_dataframe(native_dataframe: Any | IntoDataFrameT) -> TypeIs[IntoData def is_into_lazyframe(native_lazyframe: Any | IntoLazyFrameT) -> TypeIs[IntoLazyFrameT]: """Check whether `native_lazyframe` can be converted to a narwhals.stable.v1.LazyFrame.""" + from narwhals._utils import _hasattr_static from narwhals.stable.v1 import LazyFrame return ( isinstance(native_lazyframe, LazyFrame) - or hasattr(native_lazyframe, "__narwhals_lazyframe__") + or _hasattr_static(native_lazyframe, "__narwhals_lazyframe__") or is_polars_lazyframe(native_lazyframe) or is_dask_dataframe(native_lazyframe) or is_pyspark_dataframe(native_lazyframe) @@ -216,11 +217,12 @@ def is_into_lazyframe(native_lazyframe: Any | IntoLazyFrameT) -> TypeIs[IntoLazy def is_into_series(native_series: Any | IntoSeriesT) -> TypeIs[IntoSeriesT]: """Check whether `native_series` can be converted to a narwhals.stable.v1.Series.""" + from narwhals._utils import _hasattr_static from narwhals.stable.v1 import Series return ( isinstance(native_series, Series) - or hasattr(native_series, "__narwhals_series__") + or _hasattr_static(native_series, "__narwhals_series__") or is_polars_series(native_series) or is_pyarrow_chunked_array(native_series) or is_pandas_like_series(native_series) diff --git a/narwhals/stable/v2/dependencies.py b/narwhals/stable/v2/dependencies.py index e2b38097e7..354ddcb54e 100644 --- a/narwhals/stable/v2/dependencies.py +++ b/narwhals/stable/v2/dependencies.py @@ -68,11 +68,12 @@ def is_into_dataframe(native_dataframe: Any | IntoDataFrameT) -> TypeIs[IntoData def is_into_lazyframe(native_lazyframe: Any | IntoLazyFrameT) -> TypeIs[IntoLazyFrameT]: """Check whether `native_lazyframe` can be converted to a narwhals.stable.v2.LazyFrame.""" + from narwhals._utils import _hasattr_static from narwhals.stable.v2 import LazyFrame return ( isinstance(native_lazyframe, LazyFrame) - or hasattr(native_lazyframe, "__narwhals_lazyframe__") + or _hasattr_static(native_lazyframe, "__narwhals_lazyframe__") or is_polars_lazyframe(native_lazyframe) or is_dask_dataframe(native_lazyframe) or is_duckdb_relation(native_lazyframe) @@ -85,11 +86,12 @@ def is_into_lazyframe(native_lazyframe: Any | IntoLazyFrameT) -> TypeIs[IntoLazy def is_into_series(native_series: Any | IntoSeriesT) -> TypeIs[IntoSeriesT]: """Check whether `native_series` can be converted to a narwhals.stable.v2.Series.""" + from narwhals._utils import _hasattr_static from narwhals.stable.v2 import Series return ( isinstance(native_series, Series) - or hasattr(native_series, "__narwhals_series__") + or _hasattr_static(native_series, "__narwhals_series__") or is_polars_series(native_series) or is_pyarrow_chunked_array(native_series) or is_pandas_like_series(native_series) From 35d0b88f2658a091970238ab936663c26b743ed0 Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Thu, 14 May 2026 10:11:14 +0200 Subject: [PATCH 09/19] refactor(perf): Do not check isinstance per each nested call --- narwhals/dependencies.py | 220 ++++++++++++++++++++--------- narwhals/stable/v1/dependencies.py | 30 ++-- narwhals/stable/v2/dependencies.py | 41 ++---- 3 files changed, 178 insertions(+), 113 deletions(-) diff --git a/narwhals/dependencies.py b/narwhals/dependencies.py index 7a921046d0..88524b7eab 100644 --- a/narwhals/dependencies.py +++ b/narwhals/dependencies.py @@ -154,6 +154,14 @@ def _warn_if_narwhals_series(ser: Any, /) -> None: issue_warning(msg, UserWarning) +def _is_pandas_dataframe(df: Any) -> bool: + 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) + for module_name in IMPORT_HOOKS + ) + + def is_pandas_dataframe(df: Any) -> TypeIs[pd.DataFrame]: """Check whether `df` is a pandas DataFrame without importing pandas. @@ -161,9 +169,13 @@ def is_pandas_dataframe(df: Any) -> TypeIs[pd.DataFrame]: This method cannot be called on a Narwhals DataFrame/LazyFrame. """ _warn_if_narwhals_df_or_lf(df) - return ((pd := get_pandas()) is not None and isinstance(df, pd.DataFrame)) or any( + return _is_pandas_dataframe(df) + + +def _is_pandas_series(ser: Any) -> bool: + 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(df, mod.pandas.DataFrame) + and isinstance(ser, mod.pandas.Series) for module_name in IMPORT_HOOKS ) @@ -175,11 +187,7 @@ def is_pandas_series(ser: Any) -> TypeIs[pd.Series[Any]]: This method cannot be called on Narwhals Series. """ _warn_if_narwhals_series(ser) - 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) - for module_name in IMPORT_HOOKS - ) + return _is_pandas_series(ser) def is_pandas_index(index: Any) -> TypeIs[pd.Index[Any]]: @@ -191,6 +199,10 @@ def is_pandas_index(index: Any) -> TypeIs[pd.Index[Any]]: ) +def _is_modin_dataframe(df: Any) -> bool: + return (mpd := get_modin()) is not None and isinstance(df, mpd.DataFrame) + + def is_modin_dataframe(df: Any) -> TypeIs[mpd.DataFrame]: """Check whether `df` is a modin DataFrame without importing modin. @@ -198,7 +210,11 @@ def is_modin_dataframe(df: Any) -> TypeIs[mpd.DataFrame]: This method cannot be called on a Narwhals DataFrame/LazyFrame. """ _warn_if_narwhals_df_or_lf(df) - return (mpd := get_modin()) is not None and isinstance(df, mpd.DataFrame) + return _is_modin_dataframe(df) + + +def _is_modin_series(ser: Any) -> bool: + return (mpd := get_modin()) is not None and isinstance(ser, mpd.Series) def is_modin_series(ser: Any) -> TypeIs[mpd.Series]: @@ -208,7 +224,7 @@ def is_modin_series(ser: Any) -> TypeIs[mpd.Series]: This method cannot be called on Narwhals Series. """ _warn_if_narwhals_series(ser) - return (mpd := get_modin()) is not None and isinstance(ser, mpd.Series) + return _is_modin_series(ser) def is_modin_index(index: Any) -> TypeIs[mpd.Index[Any]]: # pragma: no cover @@ -216,6 +232,10 @@ def is_modin_index(index: Any) -> TypeIs[mpd.Index[Any]]: # pragma: no cover return (mpd := get_modin()) is not None and isinstance(index, mpd.Index) +def _is_cudf_dataframe(df: Any) -> bool: + return (cudf := get_cudf()) is not None and isinstance(df, cudf.DataFrame) + + def is_cudf_dataframe(df: Any) -> TypeIs[cudf.DataFrame]: """Check whether `df` is a cudf DataFrame without importing cudf. @@ -223,7 +243,11 @@ def is_cudf_dataframe(df: Any) -> TypeIs[cudf.DataFrame]: This method cannot be called on a Narwhals DataFrame/LazyFrame. """ _warn_if_narwhals_df_or_lf(df) - return (cudf := get_cudf()) is not None and isinstance(df, cudf.DataFrame) + return _is_cudf_dataframe(df) + + +def _is_cudf_series(ser: Any) -> bool: + return (cudf := get_cudf()) is not None and isinstance(ser, cudf.Series) def is_cudf_series(ser: Any) -> TypeIs[cudf.Series[Any]]: @@ -233,7 +257,7 @@ def is_cudf_series(ser: Any) -> TypeIs[cudf.Series[Any]]: This method cannot be called on Narwhals Series. """ _warn_if_narwhals_series(ser) - return (cudf := get_cudf()) is not None and isinstance(ser, cudf.Series) + return _is_cudf_series(ser) def is_cudf_index(index: Any) -> TypeIs[cudf.Index]: @@ -251,6 +275,10 @@ def is_cupy_scalar(obj: Any) -> bool: ) # pragma: no cover +def _is_dask_dataframe(df: Any) -> bool: + return (dd := get_dask_dataframe()) is not None and isinstance(df, dd.DataFrame) + + def is_dask_dataframe(df: Any) -> TypeIs[dd.DataFrame]: """Check whether `df` is a Dask DataFrame without importing Dask. @@ -258,7 +286,13 @@ def is_dask_dataframe(df: Any) -> TypeIs[dd.DataFrame]: This method cannot be called on a Narwhals DataFrame/LazyFrame. """ _warn_if_narwhals_df_or_lf(df) - return (dd := get_dask_dataframe()) is not None and isinstance(df, dd.DataFrame) + return _is_dask_dataframe(df) + + +def _is_duckdb_relation(df: Any) -> bool: + return (duckdb := get_duckdb()) is not None and isinstance( + df, duckdb.DuckDBPyRelation + ) def is_duckdb_relation(df: Any) -> TypeIs[duckdb.DuckDBPyRelation]: @@ -268,9 +302,11 @@ def is_duckdb_relation(df: Any) -> TypeIs[duckdb.DuckDBPyRelation]: This method cannot be called on Narwhals DataFrame/LazyFrame. """ _warn_if_narwhals_df_or_lf(df) - return (duckdb := get_duckdb()) is not None and isinstance( - df, duckdb.DuckDBPyRelation - ) + return _is_duckdb_relation(df) + + +def _is_ibis_table(df: Any) -> bool: + return (ibis := get_ibis()) is not None and isinstance(df, ibis.expr.types.Table) def is_ibis_table(df: Any) -> TypeIs[ibis.Table]: @@ -280,7 +316,11 @@ def is_ibis_table(df: Any) -> TypeIs[ibis.Table]: This method cannot be called on Narwhals DataFrame/LazyFrame. """ _warn_if_narwhals_df_or_lf(df) - return (ibis := get_ibis()) is not None and isinstance(df, ibis.expr.types.Table) + return _is_ibis_table(df) + + +def _is_polars_dataframe(df: Any) -> bool: + return (pl := get_polars()) is not None and isinstance(df, pl.DataFrame) def is_polars_dataframe(df: Any) -> TypeIs[pl.DataFrame]: @@ -290,7 +330,11 @@ def is_polars_dataframe(df: Any) -> TypeIs[pl.DataFrame]: This method cannot be called on a Narwhals DataFrame/LazyFrame. """ _warn_if_narwhals_df_or_lf(df) - return (pl := get_polars()) is not None and isinstance(df, pl.DataFrame) + return _is_polars_dataframe(df) + + +def _is_polars_lazyframe(df: Any) -> bool: + return (pl := get_polars()) is not None and isinstance(df, pl.LazyFrame) def is_polars_lazyframe(df: Any) -> TypeIs[pl.LazyFrame]: @@ -300,7 +344,11 @@ def is_polars_lazyframe(df: Any) -> TypeIs[pl.LazyFrame]: This method cannot be called on Narwhals DataFrame/LazyFrame. """ _warn_if_narwhals_df_or_lf(df) - return (pl := get_polars()) is not None and isinstance(df, pl.LazyFrame) + return _is_polars_lazyframe(df) + + +def _is_polars_series(ser: Any) -> bool: + return (pl := get_polars()) is not None and isinstance(ser, pl.Series) def is_polars_series(ser: Any) -> TypeIs[pl.Series]: @@ -310,7 +358,7 @@ def is_polars_series(ser: Any) -> TypeIs[pl.Series]: This method cannot be called on Narwhals Series. """ _warn_if_narwhals_series(ser) - return (pl := get_polars()) is not None and isinstance(ser, pl.Series) + return _is_polars_series(ser) def is_polars_schema(obj: Any) -> TypeIs[pl.Schema]: @@ -324,6 +372,10 @@ def is_polars_data_type(obj: Any) -> TypeIs[pl.DataType]: return bool(pl := get_polars()) and isinstance(obj, pl.DataType) +def _is_pyarrow_chunked_array(ser: Any) -> bool: + return (pa := get_pyarrow()) is not None and isinstance(ser, pa.ChunkedArray) + + def is_pyarrow_chunked_array(ser: Any) -> TypeIs[pa.ChunkedArray[Any]]: """Check whether `ser` is a PyArrow ChunkedArray without importing PyArrow. @@ -331,17 +383,21 @@ def is_pyarrow_chunked_array(ser: Any) -> TypeIs[pa.ChunkedArray[Any]]: This method cannot be called on Narwhals Series. """ _warn_if_narwhals_series(ser) - return (pa := get_pyarrow()) is not None and isinstance(ser, pa.ChunkedArray) + return _is_pyarrow_chunked_array(ser) + + +def _is_pyarrow_table(df: Any) -> bool: + return (pa := get_pyarrow()) is not None and isinstance(df, pa.Table) def is_pyarrow_table(df: Any) -> TypeIs[pa.Table]: """Check whether `df` is a PyArrow Table without importing PyArrow. Warning: - This method cannot be called on Narwhals DataFrame/LazyFrame. + This method cannot be called on a Narwhals DataFrame/LazyFrame. """ _warn_if_narwhals_df_or_lf(df) - return (pa := get_pyarrow()) is not None and isinstance(df, pa.Table) + return _is_pyarrow_table(df) def is_pyarrow_scalar(obj: Any) -> TypeIs[pa.Scalar[Any]]: @@ -356,26 +412,24 @@ def is_pyarrow_data_type(obj: Any) -> TypeIs[pa.DataType]: return bool(pa := get_pyarrow()) and isinstance(obj, pa.DataType) -def is_pyspark_dataframe(df: Any) -> TypeIs[pyspark_sql.DataFrame]: - """Check whether `df` is a PySpark DataFrame without importing PySpark. - - Warning: - This method cannot be called on a Narwhals DataFrame/LazyFrame. - """ - _warn_if_narwhals_df_or_lf(df) +def _is_pyspark_dataframe(df: Any) -> bool: return bool( (pyspark_sql := get_pyspark_sql()) is not None and isinstance(df, pyspark_sql.DataFrame) ) -def is_pyspark_connect_dataframe(df: Any) -> TypeIs[PySparkConnectDataFrame]: - """Check whether `df` is a PySpark Connect DataFrame without importing PySpark. +def is_pyspark_dataframe(df: Any) -> TypeIs[pyspark_sql.DataFrame]: + """Check whether `df` is a PySpark DataFrame without importing PySpark. Warning: This method cannot be called on a Narwhals DataFrame/LazyFrame. """ _warn_if_narwhals_df_or_lf(df) + return _is_pyspark_dataframe(df) + + +def _is_pyspark_connect_dataframe(df: Any) -> bool: if get_pyspark_connect() is not None: # pragma: no cover try: from pyspark.sql.connect.dataframe import DataFrame @@ -385,13 +439,17 @@ def is_pyspark_connect_dataframe(df: Any) -> TypeIs[PySparkConnectDataFrame]: return False -def is_sqlframe_dataframe(df: Any) -> TypeIs[SQLFrameDataFrame]: - """Check whether `df` is a SQLFrame DataFrame without importing SQLFrame. +def is_pyspark_connect_dataframe(df: Any) -> TypeIs[PySparkConnectDataFrame]: + """Check whether `df` is a PySpark Connect DataFrame without importing PySpark. Warning: This method cannot be called on a Narwhals DataFrame/LazyFrame. """ _warn_if_narwhals_df_or_lf(df) + return _is_pyspark_connect_dataframe(df) + + +def _is_sqlframe_dataframe(df: Any) -> bool: if get_sqlframe() is not None: from sqlframe.base.dataframe import BaseDataFrame @@ -399,6 +457,16 @@ def is_sqlframe_dataframe(df: Any) -> TypeIs[SQLFrameDataFrame]: return False # pragma: no cover +def is_sqlframe_dataframe(df: Any) -> TypeIs[SQLFrameDataFrame]: + """Check whether `df` is a SQLFrame DataFrame without importing SQLFrame. + + Warning: + This method cannot be called on a Narwhals DataFrame/LazyFrame. + """ + _warn_if_narwhals_df_or_lf(df) + return _is_sqlframe_dataframe(df) + + def is_numpy_array(arr: Any | _NDArray[_ShapeT]) -> TypeIs[_NDArray[_ShapeT]]: """Check whether `arr` is a NumPy Array without importing NumPy.""" return (np := get_numpy()) is not None and isinstance(arr, np.ndarray) @@ -438,6 +506,10 @@ def is_numpy_scalar(scalar: Any) -> TypeGuard[_NumpyScalar]: return (np := get_numpy()) is not None and isinstance(scalar, np.generic) +def _is_pandas_like_dataframe(df: Any) -> bool: + return _is_pandas_dataframe(df) or _is_modin_dataframe(df) or _is_cudf_dataframe(df) + + def is_pandas_like_dataframe(df: Any) -> bool: """Check whether `df` is a pandas-like DataFrame without doing any imports. @@ -447,7 +519,11 @@ def is_pandas_like_dataframe(df: Any) -> bool: This method cannot be called on a Narwhals DataFrame/LazyFrame. """ _warn_if_narwhals_df_or_lf(df) - return is_pandas_dataframe(df) or is_modin_dataframe(df) or is_cudf_dataframe(df) + return _is_pandas_like_dataframe(df) + + +def _is_pandas_like_series(ser: Any) -> bool: + return _is_pandas_series(ser) or _is_modin_series(ser) or _is_cudf_series(ser) def is_pandas_like_series(ser: Any) -> bool: @@ -459,7 +535,7 @@ def is_pandas_like_series(ser: Any) -> bool: This method cannot be called on Narwhals Series. """ _warn_if_narwhals_series(ser) - return is_pandas_series(ser) or is_modin_series(ser) or is_cudf_series(ser) + return _is_pandas_like_series(ser) def is_pandas_like_index(index: Any) -> bool: @@ -488,6 +564,43 @@ def is_cudf_dtype( ) +def _is_into_native_series(obj: Any) -> bool: + 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) + ) + + +def _is_into_native_dataframe(obj: Any) -> bool: + 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) + ) + + +def _is_into_native_lazyframe(obj: Any) -> bool: + from narwhals._utils import _hasattr_static + + return ( + _hasattr_static(obj, "__narwhals_lazyframe__") + or _is_polars_lazyframe(obj) + or _is_dask_dataframe(obj) + or _is_duckdb_relation(obj) + or _is_ibis_table(obj) + or _is_pyspark_dataframe(obj) + or _is_pyspark_connect_dataframe(obj) + or _is_sqlframe_dataframe(obj) + ) + + def is_into_series(native_series: Any | IntoSeriesT) -> TypeIs[IntoSeriesT]: """Check whether `native_series` can be converted to a Narwhals Series. @@ -511,16 +624,9 @@ def is_into_series(native_series: Any | IntoSeriesT) -> TypeIs[IntoSeriesT]: >>> nw.dependencies.is_into_series(np_arr) False """ - from narwhals._utils import _hasattr_static from narwhals.series import Series - return ( - isinstance(native_series, Series) - or _hasattr_static(native_series, "__narwhals_series__") - or is_polars_series(native_series) - or is_pyarrow_chunked_array(native_series) - or is_pandas_like_series(native_series) - ) + return isinstance(native_series, Series) or _is_into_native_series(native_series) def is_into_dataframe(native_dataframe: Any | IntoDataFrameT) -> TypeIs[IntoDataFrameT]: @@ -546,16 +652,12 @@ def is_into_dataframe(native_dataframe: Any | IntoDataFrameT) -> TypeIs[IntoData >>> is_into_dataframe(np_arr) False """ - from narwhals._utils import _hasattr_static from narwhals.dataframe import DataFrame - return ( - isinstance(native_dataframe, DataFrame) - or _hasattr_static(native_dataframe, "__narwhals_dataframe__") - or is_polars_dataframe(native_dataframe) - or is_pyarrow_table(native_dataframe) - or is_pandas_like_dataframe(native_dataframe) - ) + if isinstance(native_dataframe, DataFrame): + return True + _warn_if_narwhals_df_or_lf(native_dataframe) + return _is_into_native_dataframe(native_dataframe) def is_into_lazyframe(native_lazyframe: Any | IntoLazyFrameT) -> TypeIs[IntoLazyFrameT]: @@ -581,20 +683,12 @@ def is_into_lazyframe(native_lazyframe: Any | IntoLazyFrameT) -> TypeIs[IntoLazy >>> is_into_lazyframe(np_arr) False """ - from narwhals._utils import _hasattr_static from narwhals.dataframe import LazyFrame - return ( - isinstance(native_lazyframe, LazyFrame) - or _hasattr_static(native_lazyframe, "__narwhals_lazyframe__") - or is_polars_lazyframe(native_lazyframe) - or is_dask_dataframe(native_lazyframe) - or is_duckdb_relation(native_lazyframe) - or is_ibis_table(native_lazyframe) - or is_pyspark_dataframe(native_lazyframe) - or is_pyspark_connect_dataframe(native_lazyframe) - or is_sqlframe_dataframe(native_lazyframe) - ) + if isinstance(native_lazyframe, LazyFrame): + return True + _warn_if_narwhals_df_or_lf(native_lazyframe) + return _is_into_native_lazyframe(native_lazyframe) def is_narwhals_dataframe( diff --git a/narwhals/stable/v1/dependencies.py b/narwhals/stable/v1/dependencies.py index 9bcf1a715a..315e2c5b83 100644 --- a/narwhals/stable/v1/dependencies.py +++ b/narwhals/stable/v1/dependencies.py @@ -22,6 +22,9 @@ from narwhals.dependencies import ( IMPORT_HOOKS, + _is_into_native_dataframe, + _is_into_native_lazyframe, + _is_into_native_series, get_cudf, get_dask, get_dask_dataframe, @@ -185,15 +188,11 @@ def is_sqlframe_dataframe(df: Any) -> TypeIs[SQLFrameDataFrame]: def is_into_dataframe(native_dataframe: Any | IntoDataFrameT) -> TypeIs[IntoDataFrameT]: """Check whether `native_dataframe` can be converted to a narwhals.stable.v1.DataFrame.""" - from narwhals._utils import _hasattr_static from narwhals.stable.v1 import DataFrame return ( isinstance(native_dataframe, DataFrame) - or _hasattr_static(native_dataframe, "__narwhals_dataframe__") - or is_polars_dataframe(native_dataframe) - or is_pyarrow_table(native_dataframe) - or is_pandas_like_dataframe(native_dataframe) + or _is_into_native_dataframe(native_dataframe) or is_ibis_table(native_dataframe) or is_duckdb_relation(native_dataframe) ) @@ -201,32 +200,19 @@ def is_into_dataframe(native_dataframe: Any | IntoDataFrameT) -> TypeIs[IntoData def is_into_lazyframe(native_lazyframe: Any | IntoLazyFrameT) -> TypeIs[IntoLazyFrameT]: """Check whether `native_lazyframe` can be converted to a narwhals.stable.v1.LazyFrame.""" - from narwhals._utils import _hasattr_static from narwhals.stable.v1 import LazyFrame - return ( - isinstance(native_lazyframe, LazyFrame) - or _hasattr_static(native_lazyframe, "__narwhals_lazyframe__") - or is_polars_lazyframe(native_lazyframe) - or is_dask_dataframe(native_lazyframe) - or is_pyspark_dataframe(native_lazyframe) - or is_pyspark_connect_dataframe(native_lazyframe) - or is_sqlframe_dataframe(native_lazyframe) + return isinstance(native_lazyframe, LazyFrame) or ( + _is_into_native_lazyframe(native_lazyframe) + and not (is_ibis_table(native_lazyframe) or is_duckdb_relation(native_lazyframe)) ) def is_into_series(native_series: Any | IntoSeriesT) -> TypeIs[IntoSeriesT]: """Check whether `native_series` can be converted to a narwhals.stable.v1.Series.""" - from narwhals._utils import _hasattr_static from narwhals.stable.v1 import Series - return ( - isinstance(native_series, Series) - or _hasattr_static(native_series, "__narwhals_series__") - or is_polars_series(native_series) - or is_pyarrow_chunked_array(native_series) - or is_pandas_like_series(native_series) - ) + return isinstance(native_series, Series) or _is_into_native_series(native_series) __all__ = [ diff --git a/narwhals/stable/v2/dependencies.py b/narwhals/stable/v2/dependencies.py index 354ddcb54e..787f16422c 100644 --- a/narwhals/stable/v2/dependencies.py +++ b/narwhals/stable/v2/dependencies.py @@ -3,6 +3,10 @@ from typing import TYPE_CHECKING, Any from narwhals.dependencies import ( + _is_into_native_dataframe, + _is_into_native_lazyframe, + _is_into_native_series, + _warn_if_narwhals_df_or_lf, get_cudf, get_dask, get_dask_dataframe, @@ -54,48 +58,29 @@ def is_into_dataframe(native_dataframe: Any | IntoDataFrameT) -> TypeIs[IntoDataFrameT]: """Check whether `native_dataframe` can be converted to a narwhals.stable.v2.DataFrame.""" - from narwhals._utils import _hasattr_static from narwhals.stable.v2 import DataFrame - return ( - isinstance(native_dataframe, DataFrame) - or _hasattr_static(native_dataframe, "__narwhals_dataframe__") - or is_polars_dataframe(native_dataframe) - or is_pyarrow_table(native_dataframe) - or is_pandas_like_dataframe(native_dataframe) - ) + if isinstance(native_dataframe, DataFrame): + return True + _warn_if_narwhals_df_or_lf(native_dataframe) + return _is_into_native_dataframe(native_dataframe) def is_into_lazyframe(native_lazyframe: Any | IntoLazyFrameT) -> TypeIs[IntoLazyFrameT]: """Check whether `native_lazyframe` can be converted to a narwhals.stable.v2.LazyFrame.""" - from narwhals._utils import _hasattr_static from narwhals.stable.v2 import LazyFrame - return ( - isinstance(native_lazyframe, LazyFrame) - or _hasattr_static(native_lazyframe, "__narwhals_lazyframe__") - or is_polars_lazyframe(native_lazyframe) - or is_dask_dataframe(native_lazyframe) - or is_duckdb_relation(native_lazyframe) - or is_ibis_table(native_lazyframe) - or is_pyspark_dataframe(native_lazyframe) - or is_pyspark_connect_dataframe(native_lazyframe) - or is_sqlframe_dataframe(native_lazyframe) - ) + if isinstance(native_lazyframe, LazyFrame): + return True + _warn_if_narwhals_df_or_lf(native_lazyframe) + return _is_into_native_lazyframe(native_lazyframe) def is_into_series(native_series: Any | IntoSeriesT) -> TypeIs[IntoSeriesT]: """Check whether `native_series` can be converted to a narwhals.stable.v2.Series.""" - from narwhals._utils import _hasattr_static from narwhals.stable.v2 import Series - return ( - isinstance(native_series, Series) - or _hasattr_static(native_series, "__narwhals_series__") - or is_polars_series(native_series) - or is_pyarrow_chunked_array(native_series) - or is_pandas_like_series(native_series) - ) + return isinstance(native_series, Series) or _is_into_native_series(native_series) __all__ = [ From 98b4c8ccb21953a695272e8af69de90b39e06d0e Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Thu, 14 May 2026 10:34:17 +0200 Subject: [PATCH 10/19] simplify by aliasing --- narwhals/dependencies.py | 65 +++++++---- narwhals/stable/v1/dependencies.py | 168 ++++------------------------- 2 files changed, 64 insertions(+), 169 deletions(-) diff --git a/narwhals/dependencies.py b/narwhals/dependencies.py index 88524b7eab..cac8ad0340 100644 --- a/narwhals/dependencies.py +++ b/narwhals/dependencies.py @@ -154,7 +154,8 @@ def _warn_if_narwhals_series(ser: Any, /) -> None: issue_warning(msg, UserWarning) -def _is_pandas_dataframe(df: Any) -> bool: +def _is_pandas_dataframe(df: Any) -> TypeIs[pd.DataFrame]: + """Check whether `df` is a pandas DataFrame without importing pandas.""" 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) @@ -172,7 +173,8 @@ def is_pandas_dataframe(df: Any) -> TypeIs[pd.DataFrame]: return _is_pandas_dataframe(df) -def _is_pandas_series(ser: Any) -> bool: +def _is_pandas_series(ser: Any) -> TypeIs[pd.Series[Any]]: + """Check whether `ser` is a pandas Series without importing pandas.""" 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) @@ -199,7 +201,8 @@ def is_pandas_index(index: Any) -> TypeIs[pd.Index[Any]]: ) -def _is_modin_dataframe(df: Any) -> bool: +def _is_modin_dataframe(df: Any) -> TypeIs[mpd.DataFrame]: + """Check whether `df` is a modin DataFrame without importing modin.""" return (mpd := get_modin()) is not None and isinstance(df, mpd.DataFrame) @@ -213,7 +216,8 @@ def is_modin_dataframe(df: Any) -> TypeIs[mpd.DataFrame]: return _is_modin_dataframe(df) -def _is_modin_series(ser: Any) -> bool: +def _is_modin_series(ser: Any) -> TypeIs[mpd.Series]: + """Check whether `ser` is a modin Series without importing modin.""" return (mpd := get_modin()) is not None and isinstance(ser, mpd.Series) @@ -232,7 +236,8 @@ def is_modin_index(index: Any) -> TypeIs[mpd.Index[Any]]: # pragma: no cover return (mpd := get_modin()) is not None and isinstance(index, mpd.Index) -def _is_cudf_dataframe(df: Any) -> bool: +def _is_cudf_dataframe(df: Any) -> TypeIs[cudf.DataFrame]: + """Check whether `df` is a cudf DataFrame without importing cudf.""" return (cudf := get_cudf()) is not None and isinstance(df, cudf.DataFrame) @@ -246,7 +251,8 @@ def is_cudf_dataframe(df: Any) -> TypeIs[cudf.DataFrame]: return _is_cudf_dataframe(df) -def _is_cudf_series(ser: Any) -> bool: +def _is_cudf_series(ser: Any) -> TypeIs[cudf.Series[Any]]: + """Check whether `ser` is a cudf Series without importing cudf.""" return (cudf := get_cudf()) is not None and isinstance(ser, cudf.Series) @@ -275,7 +281,8 @@ def is_cupy_scalar(obj: Any) -> bool: ) # pragma: no cover -def _is_dask_dataframe(df: Any) -> bool: +def _is_dask_dataframe(df: Any) -> TypeIs[dd.DataFrame]: + """Check whether `df` is a Dask DataFrame without importing Dask.""" return (dd := get_dask_dataframe()) is not None and isinstance(df, dd.DataFrame) @@ -289,7 +296,8 @@ def is_dask_dataframe(df: Any) -> TypeIs[dd.DataFrame]: return _is_dask_dataframe(df) -def _is_duckdb_relation(df: Any) -> bool: +def _is_duckdb_relation(df: Any) -> TypeIs[duckdb.DuckDBPyRelation]: + """Check whether `df` is a DuckDB Relation without importing DuckDB.""" return (duckdb := get_duckdb()) is not None and isinstance( df, duckdb.DuckDBPyRelation ) @@ -305,7 +313,8 @@ def is_duckdb_relation(df: Any) -> TypeIs[duckdb.DuckDBPyRelation]: return _is_duckdb_relation(df) -def _is_ibis_table(df: Any) -> bool: +def _is_ibis_table(df: Any) -> TypeIs[ibis.Table]: + """Check whether `df` is a Ibis Table without importing Ibis.""" return (ibis := get_ibis()) is not None and isinstance(df, ibis.expr.types.Table) @@ -319,7 +328,8 @@ def is_ibis_table(df: Any) -> TypeIs[ibis.Table]: return _is_ibis_table(df) -def _is_polars_dataframe(df: Any) -> bool: +def _is_polars_dataframe(df: Any) -> TypeIs[pl.DataFrame]: + """Check whether `df` is a Polars DataFrame without importing Polars.""" return (pl := get_polars()) is not None and isinstance(df, pl.DataFrame) @@ -333,7 +343,8 @@ def is_polars_dataframe(df: Any) -> TypeIs[pl.DataFrame]: return _is_polars_dataframe(df) -def _is_polars_lazyframe(df: Any) -> bool: +def _is_polars_lazyframe(df: Any) -> TypeIs[pl.LazyFrame]: + """Check whether `df` is a Polars LazyFrame without importing Polars.""" return (pl := get_polars()) is not None and isinstance(df, pl.LazyFrame) @@ -347,7 +358,8 @@ def is_polars_lazyframe(df: Any) -> TypeIs[pl.LazyFrame]: return _is_polars_lazyframe(df) -def _is_polars_series(ser: Any) -> bool: +def _is_polars_series(ser: Any) -> TypeIs[pl.Series]: + """Check whether `ser` is a Polars Series without importing Polars.""" return (pl := get_polars()) is not None and isinstance(ser, pl.Series) @@ -372,7 +384,8 @@ def is_polars_data_type(obj: Any) -> TypeIs[pl.DataType]: return bool(pl := get_polars()) and isinstance(obj, pl.DataType) -def _is_pyarrow_chunked_array(ser: Any) -> bool: +def _is_pyarrow_chunked_array(ser: Any) -> TypeIs[pa.ChunkedArray[Any]]: + """Check whether `ser` is a PyArrow ChunkedArray without importing PyArrow.""" return (pa := get_pyarrow()) is not None and isinstance(ser, pa.ChunkedArray) @@ -386,7 +399,8 @@ def is_pyarrow_chunked_array(ser: Any) -> TypeIs[pa.ChunkedArray[Any]]: return _is_pyarrow_chunked_array(ser) -def _is_pyarrow_table(df: Any) -> bool: +def _is_pyarrow_table(df: Any) -> TypeIs[pa.Table]: + """Check whether `df` is a PyArrow Table without importing PyArrow.""" return (pa := get_pyarrow()) is not None and isinstance(df, pa.Table) @@ -412,7 +426,8 @@ def is_pyarrow_data_type(obj: Any) -> TypeIs[pa.DataType]: return bool(pa := get_pyarrow()) and isinstance(obj, pa.DataType) -def _is_pyspark_dataframe(df: Any) -> bool: +def _is_pyspark_dataframe(df: Any) -> TypeIs[pyspark_sql.DataFrame]: + """Check whether `df` is a PySpark DataFrame without importing PySpark.""" return bool( (pyspark_sql := get_pyspark_sql()) is not None and isinstance(df, pyspark_sql.DataFrame) @@ -429,7 +444,8 @@ def is_pyspark_dataframe(df: Any) -> TypeIs[pyspark_sql.DataFrame]: return _is_pyspark_dataframe(df) -def _is_pyspark_connect_dataframe(df: Any) -> bool: +def _is_pyspark_connect_dataframe(df: Any) -> TypeIs[PySparkConnectDataFrame]: + """Check whether `df` is a PySpark Connect DataFrame without importing PySpark.""" if get_pyspark_connect() is not None: # pragma: no cover try: from pyspark.sql.connect.dataframe import DataFrame @@ -449,7 +465,8 @@ def is_pyspark_connect_dataframe(df: Any) -> TypeIs[PySparkConnectDataFrame]: return _is_pyspark_connect_dataframe(df) -def _is_sqlframe_dataframe(df: Any) -> bool: +def _is_sqlframe_dataframe(df: Any) -> TypeIs[SQLFrameDataFrame]: + """Check whether `df` is a SQLFrame DataFrame without importing SQLFrame.""" if get_sqlframe() is not None: from sqlframe.base.dataframe import BaseDataFrame @@ -507,6 +524,10 @@ def is_numpy_scalar(scalar: Any) -> TypeGuard[_NumpyScalar]: def _is_pandas_like_dataframe(df: Any) -> bool: + """Check whether `df` is a pandas-like DataFrame without doing any imports. + + By "pandas-like", we mean: pandas, Modin, cuDF. + """ return _is_pandas_dataframe(df) or _is_modin_dataframe(df) or _is_cudf_dataframe(df) @@ -523,6 +544,10 @@ def is_pandas_like_dataframe(df: Any) -> bool: def _is_pandas_like_series(ser: Any) -> bool: + """Check whether `ser` is a pandas-like Series without doing any imports. + + By "pandas-like", we mean: pandas, Modin, cuDF. + """ return _is_pandas_series(ser) or _is_modin_series(ser) or _is_cudf_series(ser) @@ -564,7 +589,7 @@ def is_cudf_dtype( ) -def _is_into_native_series(obj: Any) -> bool: +def _is_into_native_series(obj: Any | IntoSeriesT) -> TypeIs[IntoSeriesT]: from narwhals._utils import _hasattr_static return ( @@ -575,7 +600,7 @@ def _is_into_native_series(obj: Any) -> bool: ) -def _is_into_native_dataframe(obj: Any) -> bool: +def _is_into_native_dataframe(obj: Any | IntoDataFrameT) -> TypeIs[IntoDataFrameT]: from narwhals._utils import _hasattr_static return ( @@ -586,7 +611,7 @@ def _is_into_native_dataframe(obj: Any) -> bool: ) -def _is_into_native_lazyframe(obj: Any) -> bool: +def _is_into_native_lazyframe(obj: Any | IntoLazyFrameT) -> TypeIs[IntoLazyFrameT]: from narwhals._utils import _hasattr_static return ( diff --git a/narwhals/stable/v1/dependencies.py b/narwhals/stable/v1/dependencies.py index 315e2c5b83..d35b110259 100644 --- a/narwhals/stable/v1/dependencies.py +++ b/narwhals/stable/v1/dependencies.py @@ -1,30 +1,36 @@ from __future__ import annotations -import sys from typing import TYPE_CHECKING, Any if TYPE_CHECKING: - import cudf - import dask.dataframe as dd - import duckdb - import ibis - import modin.pandas as mpd - import pandas as pd - import polars as pl - import pyarrow as pa - import pyspark.sql as pyspark_sql - from pyspark.sql.connect.dataframe import DataFrame as PySparkConnectDataFrame from typing_extensions import TypeIs - from narwhals._spark_like.dataframe import SQLFrameDataFrame from narwhals.stable.v1.typing import IntoDataFrameT, IntoLazyFrameT, IntoSeriesT from narwhals.dependencies import ( - IMPORT_HOOKS, + _is_cudf_dataframe as is_cudf_dataframe, + _is_cudf_series as is_cudf_series, + _is_dask_dataframe as is_dask_dataframe, + _is_duckdb_relation as is_duckdb_relation, + _is_ibis_table as is_ibis_table, _is_into_native_dataframe, _is_into_native_lazyframe, _is_into_native_series, + _is_modin_dataframe as is_modin_dataframe, + _is_modin_series as is_modin_series, + _is_pandas_dataframe as is_pandas_dataframe, + _is_pandas_like_dataframe as is_pandas_like_dataframe, + _is_pandas_like_series as is_pandas_like_series, + _is_pandas_series as is_pandas_series, + _is_polars_dataframe as is_polars_dataframe, + _is_polars_lazyframe as is_polars_lazyframe, + _is_polars_series as is_polars_series, + _is_pyarrow_chunked_array as is_pyarrow_chunked_array, + _is_pyarrow_table as is_pyarrow_table, + _is_pyspark_connect_dataframe as is_pyspark_connect_dataframe, + _is_pyspark_dataframe as is_pyspark_dataframe, + _is_sqlframe_dataframe as is_sqlframe_dataframe, get_cudf, get_dask, get_dask_dataframe, @@ -50,142 +56,6 @@ ) -def is_pandas_dataframe(df: Any) -> TypeIs[pd.DataFrame]: - """Check whether `df` is a pandas DataFrame without importing pandas.""" - 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) - for module_name in IMPORT_HOOKS - ) - - -def is_pandas_series(ser: Any) -> TypeIs[pd.Series[Any]]: - """Check whether `ser` is a pandas Series without importing pandas.""" - 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) - for module_name in IMPORT_HOOKS - ) - - -def is_modin_dataframe(df: Any) -> TypeIs[mpd.DataFrame]: - """Check whether `df` is a modin DataFrame without importing modin.""" - return (mpd := get_modin()) is not None and isinstance(df, mpd.DataFrame) - - -def is_modin_series(ser: Any) -> TypeIs[mpd.Series]: - """Check whether `ser` is a modin Series without importing modin.""" - return (mpd := get_modin()) is not None and isinstance(ser, mpd.Series) - - -def is_cudf_dataframe(df: Any) -> TypeIs[cudf.DataFrame]: - """Check whether `df` is a cudf DataFrame without importing cudf.""" - return (cudf := get_cudf()) is not None and isinstance(df, cudf.DataFrame) - - -def is_cudf_series(ser: Any) -> TypeIs[cudf.Series[Any]]: - """Check whether `ser` is a cudf Series without importing cudf.""" - return (cudf := get_cudf()) is not None and isinstance(ser, cudf.Series) - - -def is_dask_dataframe(df: Any) -> TypeIs[dd.DataFrame]: - """Check whether `df` is a Dask DataFrame without importing Dask.""" - return (dd := get_dask_dataframe()) is not None and isinstance(df, dd.DataFrame) - - -def is_duckdb_relation(df: Any) -> TypeIs[duckdb.DuckDBPyRelation]: - """Check whether `df` is a DuckDB Relation without importing DuckDB.""" - return (duckdb := get_duckdb()) is not None and isinstance( - df, duckdb.DuckDBPyRelation - ) - - -def is_ibis_table(df: Any) -> TypeIs[ibis.Table]: - """Check whether `df` is a Ibis Table without importing Ibis.""" - return (ibis := get_ibis()) is not None and isinstance(df, ibis.expr.types.Table) - - -def is_polars_dataframe(df: Any) -> TypeIs[pl.DataFrame]: - """Check whether `df` is a Polars DataFrame without importing Polars.""" - return (pl := get_polars()) is not None and isinstance(df, pl.DataFrame) - - -def is_polars_lazyframe(df: Any) -> TypeIs[pl.LazyFrame]: - """Check whether `df` is a Polars LazyFrame without importing Polars.""" - return (pl := get_polars()) is not None and isinstance(df, pl.LazyFrame) - - -def is_polars_series(ser: Any) -> TypeIs[pl.Series]: - """Check whether `ser` is a Polars Series without importing Polars.""" - return (pl := get_polars()) is not None and isinstance(ser, pl.Series) - - -def is_pyarrow_chunked_array(ser: Any) -> TypeIs[pa.ChunkedArray[Any]]: - """Check whether `ser` is a PyArrow ChunkedArray without importing PyArrow.""" - return (pa := get_pyarrow()) is not None and isinstance(ser, pa.ChunkedArray) - - -def is_pyarrow_table(df: Any) -> TypeIs[pa.Table]: - """Check whether `df` is a PyArrow Table without importing PyArrow.""" - return (pa := get_pyarrow()) is not None and isinstance(df, pa.Table) - - -def is_pandas_like_dataframe(df: Any) -> bool: - """Check whether `df` is a pandas-like DataFrame without doing any imports. - - By "pandas-like", we mean: pandas, Modin, cuDF. - """ - return is_pandas_dataframe(df) or is_modin_dataframe(df) or is_cudf_dataframe(df) - - -def is_pandas_like_series(ser: Any) -> bool: - """Check whether `ser` is a pandas-like Series without doing any imports. - - By "pandas-like", we mean: pandas, Modin, cuDF. - """ - return is_pandas_series(ser) or is_modin_series(ser) or is_cudf_series(ser) - - -def is_pyspark_dataframe(df: Any) -> TypeIs[pyspark_sql.DataFrame]: - """Check whether `df` is a PySpark DataFrame without importing PySpark. - - Warning: - This method cannot be called on a Narwhals DataFrame/LazyFrame. - """ - return bool( - (pyspark_sql := get_pyspark_sql()) is not None - and isinstance(df, pyspark_sql.DataFrame) - ) - - -def is_pyspark_connect_dataframe(df: Any) -> TypeIs[PySparkConnectDataFrame]: - """Check whether `df` is a PySpark Connect DataFrame without importing PySpark. - - Warning: - This method cannot be called on a Narwhals DataFrame/LazyFrame. - """ - if get_pyspark_connect() is not None: # pragma: no cover - try: - from pyspark.sql.connect.dataframe import DataFrame - except ImportError: - return False - return isinstance(df, DataFrame) - return False - - -def is_sqlframe_dataframe(df: Any) -> TypeIs[SQLFrameDataFrame]: - """Check whether `df` is a SQLFrame DataFrame without importing SQLFrame. - - Warning: - This method cannot be called on a Narwhals DataFrame/LazyFrame. - """ - if get_sqlframe() is not None: - from sqlframe.base.dataframe import BaseDataFrame - - return isinstance(df, BaseDataFrame) - return False # pragma: no cover - - def is_into_dataframe(native_dataframe: Any | IntoDataFrameT) -> TypeIs[IntoDataFrameT]: """Check whether `native_dataframe` can be converted to a narwhals.stable.v1.DataFrame.""" from narwhals.stable.v1 import DataFrame From 76254055507bb6b1e18e7bed0c42c04a64431dc3 Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Sat, 16 May 2026 09:55:46 +0200 Subject: [PATCH 11/19] fix up is_into_*_test typing --- narwhals/_interchange/dataframe.py | 4 ++-- tests/dependencies/is_into_dataframe_test.py | 19 +++++++++++-------- tests/dependencies/is_into_lazyframe_test.py | 19 +++++++++++-------- tests/dependencies/is_into_series_test.py | 19 +++++++++++-------- 4 files changed, 35 insertions(+), 26 deletions(-) diff --git a/narwhals/_interchange/dataframe.py b/narwhals/_interchange/dataframe.py index a07f1fad4c..ad42e0d535 100644 --- a/narwhals/_interchange/dataframe.py +++ b/narwhals/_interchange/dataframe.py @@ -3,7 +3,7 @@ import enum from typing import TYPE_CHECKING, Any, NoReturn -from narwhals._utils import Version, parse_version +from narwhals._utils import Version, _hasattr_static, parse_version if TYPE_CHECKING: import pandas as pd @@ -156,4 +156,4 @@ def select(self, *exprs: str) -> Self: # pragma: no cover def supports_dataframe_interchange(obj: Any) -> TypeIs[DataFrameLike]: - return hasattr(obj, "__dataframe__") + return _hasattr_static(obj, "__dataframe__") diff --git a/tests/dependencies/is_into_dataframe_test.py b/tests/dependencies/is_into_dataframe_test.py index 2e4f98e0a7..0862c57751 100644 --- a/tests/dependencies/is_into_dataframe_test.py +++ b/tests/dependencies/is_into_dataframe_test.py @@ -24,16 +24,19 @@ data: dict[str, Any] = {"a": [1, 2, 3], "b": [4, 5, 6]} -class DictDataFrame: +class DictDataFrame: # pragma: no cover def __init__(self, data: Mapping[str, Any]) -> None: self._data = data - def __len__(self) -> int: # pragma: no cover - return len(next(iter(self._data.values()))) - - def __narwhals_dataframe__(self) -> Self: # pragma: no cover + def __narwhals_dataframe__(self) -> Self: return self + def __len__(self) -> int: ... + @property + def columns(self) -> Any: ... + def drop(self, *args: Any, **kwargs: Any) -> Any: ... + def join(self, *args: Any, **kwargs: Any) -> Any: ... + @pytest.mark.filterwarnings("ignore:.*You passed a.*:UserWarning") def test_is_into_dataframe(constructor: Constructor) -> None: @@ -66,14 +69,14 @@ def test_is_into_dataframe_other() -> None: pytest.importorskip("numpy") import numpy as np - assert is_into_dataframe(DictDataFrame(data)) # pyrefly: ignore[bad-specialization] + assert is_into_dataframe(DictDataFrame(data)) assert not is_into_dataframe(np.array([[1, 4], [2, 5], [3, 6]])) assert not is_into_dataframe(data) - assert v1_is_into_dataframe(DictDataFrame(data)) # pyrefly: ignore[bad-specialization] + assert v1_is_into_dataframe(DictDataFrame(data)) assert not v1_is_into_dataframe(np.array([[1, 4], [2, 5], [3, 6]])) assert not v1_is_into_dataframe(data) - assert v2_is_into_dataframe(DictDataFrame(data)) # pyrefly: ignore[bad-specialization] + assert v2_is_into_dataframe(DictDataFrame(data)) assert not v2_is_into_dataframe(np.array([[1, 4], [2, 5], [3, 6]])) assert not v2_is_into_dataframe(data) diff --git a/tests/dependencies/is_into_lazyframe_test.py b/tests/dependencies/is_into_lazyframe_test.py index 17cc29db00..0cfe57774b 100644 --- a/tests/dependencies/is_into_lazyframe_test.py +++ b/tests/dependencies/is_into_lazyframe_test.py @@ -24,16 +24,19 @@ data: dict[str, Any] = {"a": [1, 2, 3], "b": [4, 5, 6]} -class DictDataFrame: +class DictLazyFrame: # pragma: no cover def __init__(self, data: Mapping[str, Any]) -> None: self._data = data - def __len__(self) -> int: # pragma: no cover - return len(next(iter(self._data.values()))) - - def __narwhals_lazyframe__(self) -> Self: # pragma: no cover + def __narwhals_lazyframe__(self) -> Self: return self + @property + def columns(self) -> Any: ... + def drop(self, *args: Any, **kwargs: Any) -> Any: ... + def explain(self, *args: Any, **kwargs: Any) -> Any: ... + def join(self, *args: Any, **kwargs: Any) -> Any: ... + @pytest.mark.filterwarnings("ignore:.*You passed a.*:UserWarning") def test_is_into_lazyframe(constructor: Constructor) -> None: @@ -65,14 +68,14 @@ def test_is_into_lazyframe_other() -> None: pytest.importorskip("numpy") import numpy as np - assert is_into_lazyframe(DictDataFrame(data)) # pyrefly: ignore[bad-specialization] + assert is_into_lazyframe(DictLazyFrame(data)) assert not is_into_lazyframe(np.array([[1, 4], [2, 5], [3, 6]])) assert not is_into_lazyframe(data) - assert v1_is_into_lazyframe(DictDataFrame(data)) # pyrefly: ignore[bad-specialization] + assert v1_is_into_lazyframe(DictLazyFrame(data)) assert not v1_is_into_lazyframe(np.array([[1, 4], [2, 5], [3, 6]])) assert not v1_is_into_lazyframe(data) - assert v2_is_into_lazyframe(DictDataFrame(data)) # pyrefly: ignore[bad-specialization] + assert v2_is_into_lazyframe(DictLazyFrame(data)) assert not v2_is_into_lazyframe(np.array([[1, 4], [2, 5], [3, 6]])) assert not v2_is_into_lazyframe(data) diff --git a/tests/dependencies/is_into_series_test.py b/tests/dependencies/is_into_series_test.py index 315f829fb5..23449a200e 100644 --- a/tests/dependencies/is_into_series_test.py +++ b/tests/dependencies/is_into_series_test.py @@ -19,17 +19,20 @@ data: dict[str, Any] = {"a": [1, 2, 3], "b": [4, 5, 6]} -class ListBackedSeries: +class ListBackedSeries: # pragma: no cover def __init__(self, name: str, data: list[Any]) -> None: self._data = data self._name = name - def __len__(self) -> int: # pragma: no cover - return len(self._data) - - def __narwhals_series__(self) -> Self: # pragma: no cover + def __narwhals_series__(self) -> Self: return self + def __len__(self) -> int: ... + def __iter__(self) -> Any: ... + def filter(self, *args: Any, **kwargs: Any) -> Any: ... + def value_counts(self, *args: Any, **kwargs: Any) -> Any: ... + def unique(self, *args: Any, **kwargs: Any) -> Any: ... + @pytest.mark.filterwarnings("ignore:.*You passed a.*:UserWarning") def test_is_into_series(constructor_eager: ConstructorEager) -> None: @@ -59,14 +62,14 @@ def test_is_into_series_other() -> None: pytest.importorskip("numpy") import numpy as np - assert is_into_series(ListBackedSeries("a", [1, 4, 2])) # pyrefly: ignore[bad-specialization] + assert is_into_series(ListBackedSeries("a", [1, 4, 2])) assert not is_into_series(np.array([1, 2, 3])) assert not is_into_series([1, 2, 3]) - assert v1_is_into_series(ListBackedSeries("a", [1, 4, 2])) # pyrefly: ignore[bad-specialization] + assert v1_is_into_series(ListBackedSeries("a", [1, 4, 2])) assert not v1_is_into_series(np.array([1, 2, 3])) assert not v1_is_into_series([1, 2, 3]) - assert v2_is_into_series(ListBackedSeries("a", [1, 4, 2])) # pyrefly: ignore[bad-specialization] + assert v2_is_into_series(ListBackedSeries("a", [1, 4, 2])) assert not v2_is_into_series(np.array([1, 2, 3])) assert not v2_is_into_series([1, 2, 3]) From c6b4d6fb57600f94364a174abc343cda4ba21556 Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Sat, 16 May 2026 10:15:34 +0200 Subject: [PATCH 12/19] Add DynamicAttrOnly tests --- tests/dependencies/conftest.py | 33 +++++++++++++++++++ tests/dependencies/is_into_dataframe_test.py | 28 ++++++++++------ tests/dependencies/is_into_lazyframe_test.py | 24 +++++++++----- tests/dependencies/is_into_series_test.py | 34 +++++++++++++------- 4 files changed, 91 insertions(+), 28 deletions(-) create mode 100644 tests/dependencies/conftest.py diff --git a/tests/dependencies/conftest.py b/tests/dependencies/conftest.py new file mode 100644 index 0000000000..a2112e9c38 --- /dev/null +++ b/tests/dependencies/conftest.py @@ -0,0 +1,33 @@ +from __future__ import annotations + +from typing import Any + +import pytest + + +class DynamicAttrOnly: + """An object whose attributes only exist dynamically, never statically. + + Every attribute access is fabricated on the fly via `__getattr__`, so + `hasattr` reports `True` for any name while [`inspect.getattr_static`] + (and therefore `_hasattr_static`) correctly reports `False`: + + obj = DynamicAttrOnly() + hasattr(obj, "i_dont_exist") # True (false positive) + _hasattr_static(obj, "i_dont_exist") # False (correct) + + Use it in tests to prove that code under test relies on `_hasattr_static` + rather than `hasattr` when probing unknown objects: any check that accepts + a `DynamicAttrOnly` instance is, by construction, using the unsafe path. + + [`inspect.getattr_static`]: https://docs.python.org/3/library/inspect.html#inspect.getattr_static + """ + + def __getattr__(self, name: str) -> Any: + return self + + +@pytest.fixture(scope="session") +def dynamic_attr_only() -> DynamicAttrOnly: + """An object that triggers `hasattr` false positives for every attribute name.""" + return DynamicAttrOnly() diff --git a/tests/dependencies/is_into_dataframe_test.py b/tests/dependencies/is_into_dataframe_test.py index 0862c57751..f20b3d7860 100644 --- a/tests/dependencies/is_into_dataframe_test.py +++ b/tests/dependencies/is_into_dataframe_test.py @@ -16,6 +16,7 @@ from typing_extensions import Self + from tests.dependencies.conftest import DynamicAttrOnly from tests.utils import Constructor EAGER_CONSTRUCTOR_NAMES = ("pandas", "modin", "cudf", "polars_eager", "pyarrow") @@ -31,7 +32,9 @@ def __init__(self, data: Mapping[str, Any]) -> None: def __narwhals_dataframe__(self) -> Self: return self - def __len__(self) -> int: ... + def __len__(self) -> int: + return len(next(iter(self._data.values()))) + @property def columns(self) -> Any: ... def drop(self, *args: Any, **kwargs: Any) -> Any: ... @@ -65,18 +68,25 @@ def test_is_into_dataframe(constructor: Constructor) -> None: assert not v2_is_into_dataframe(nw_frame) -def test_is_into_dataframe_other() -> None: +def test_is_into_dataframe_numpy() -> None: pytest.importorskip("numpy") import numpy as np - assert is_into_dataframe(DictDataFrame(data)) - assert not is_into_dataframe(np.array([[1, 4], [2, 5], [3, 6]])) - assert not is_into_dataframe(data) + arr = np.array([[1, 4], [2, 5], [3, 6]]) + assert not is_into_dataframe(arr) + assert not v1_is_into_dataframe(arr) + assert not v2_is_into_dataframe(arr) - assert v1_is_into_dataframe(DictDataFrame(data)) - assert not v1_is_into_dataframe(np.array([[1, 4], [2, 5], [3, 6]])) + +def test_is_into_dataframe_other(dynamic_attr_only: DynamicAttrOnly) -> None: + assert not is_into_dataframe(data) assert not v1_is_into_dataframe(data) + assert not v2_is_into_dataframe(data) + assert is_into_dataframe(DictDataFrame(data)) + assert v1_is_into_dataframe(DictDataFrame(data)) assert v2_is_into_dataframe(DictDataFrame(data)) - assert not v2_is_into_dataframe(np.array([[1, 4], [2, 5], [3, 6]])) - assert not v2_is_into_dataframe(data) + + assert not is_into_dataframe(dynamic_attr_only) + assert not v1_is_into_dataframe(dynamic_attr_only) + assert not v2_is_into_dataframe(dynamic_attr_only) diff --git a/tests/dependencies/is_into_lazyframe_test.py b/tests/dependencies/is_into_lazyframe_test.py index 0cfe57774b..44d9c7ff30 100644 --- a/tests/dependencies/is_into_lazyframe_test.py +++ b/tests/dependencies/is_into_lazyframe_test.py @@ -16,6 +16,7 @@ from typing_extensions import Self + from tests.dependencies.conftest import DynamicAttrOnly from tests.utils import Constructor EAGER_CONSTRUCTOR_NAMES = ("pandas", "modin", "cudf", "polars_eager", "pyarrow") @@ -64,18 +65,25 @@ def test_is_into_lazyframe(constructor: Constructor) -> None: assert not v2_is_into_lazyframe(nw_frame) -def test_is_into_lazyframe_other() -> None: +def test_is_into_lazyframe_numpy() -> None: pytest.importorskip("numpy") import numpy as np - assert is_into_lazyframe(DictLazyFrame(data)) - assert not is_into_lazyframe(np.array([[1, 4], [2, 5], [3, 6]])) - assert not is_into_lazyframe(data) + arr = np.array([[1, 4], [2, 5], [3, 6]]) + assert not is_into_lazyframe(arr) + assert not v1_is_into_lazyframe(arr) + assert not v2_is_into_lazyframe(arr) - assert v1_is_into_lazyframe(DictLazyFrame(data)) - assert not v1_is_into_lazyframe(np.array([[1, 4], [2, 5], [3, 6]])) + +def test_is_into_lazyframe_other(dynamic_attr_only: DynamicAttrOnly) -> None: + assert not is_into_lazyframe(data) assert not v1_is_into_lazyframe(data) + assert not v2_is_into_lazyframe(data) + assert is_into_lazyframe(DictLazyFrame(data)) + assert v1_is_into_lazyframe(DictLazyFrame(data)) assert v2_is_into_lazyframe(DictLazyFrame(data)) - assert not v2_is_into_lazyframe(np.array([[1, 4], [2, 5], [3, 6]])) - assert not v2_is_into_lazyframe(data) + + assert not is_into_lazyframe(dynamic_attr_only) + assert not v1_is_into_lazyframe(dynamic_attr_only) + assert not v2_is_into_lazyframe(dynamic_attr_only) diff --git a/tests/dependencies/is_into_series_test.py b/tests/dependencies/is_into_series_test.py index 23449a200e..cc3fb98e01 100644 --- a/tests/dependencies/is_into_series_test.py +++ b/tests/dependencies/is_into_series_test.py @@ -14,6 +14,7 @@ if TYPE_CHECKING: from typing_extensions import Self + from tests.dependencies.conftest import DynamicAttrOnly from tests.utils import ConstructorEager data: dict[str, Any] = {"a": [1, 2, 3], "b": [4, 5, 6]} @@ -27,7 +28,9 @@ def __init__(self, name: str, data: list[Any]) -> None: def __narwhals_series__(self) -> Self: return self - def __len__(self) -> int: ... + def __len__(self) -> int: + return len(self._data) + def __iter__(self) -> Any: ... def filter(self, *args: Any, **kwargs: Any) -> Any: ... def value_counts(self, *args: Any, **kwargs: Any) -> Any: ... @@ -58,18 +61,27 @@ def test_is_into_series(constructor_eager: ConstructorEager) -> None: assert v2_is_into_series(nw_v2_series) -def test_is_into_series_other() -> None: +def test_is_into_series_numpy() -> None: pytest.importorskip("numpy") import numpy as np - assert is_into_series(ListBackedSeries("a", [1, 4, 2])) - assert not is_into_series(np.array([1, 2, 3])) - assert not is_into_series([1, 2, 3]) + arr = np.array([1, 2, 3]) + assert not is_into_series(arr) + assert not v1_is_into_series(arr) + assert not v2_is_into_series(arr) + + +def test_is_into_series_other(dynamic_attr_only: DynamicAttrOnly) -> None: + values = [1, 4, 2] + + assert not is_into_series(values) + assert not v1_is_into_series(values) + assert not v2_is_into_series(values) - assert v1_is_into_series(ListBackedSeries("a", [1, 4, 2])) - assert not v1_is_into_series(np.array([1, 2, 3])) - assert not v1_is_into_series([1, 2, 3]) + assert is_into_series(ListBackedSeries("a", values)) + assert v1_is_into_series(ListBackedSeries("a", values)) + assert v2_is_into_series(ListBackedSeries("a", values)) - assert v2_is_into_series(ListBackedSeries("a", [1, 4, 2])) - assert not v2_is_into_series(np.array([1, 2, 3])) - assert not v2_is_into_series([1, 2, 3]) + assert is_into_series(dynamic_attr_only) + assert v1_is_into_series(dynamic_attr_only) + assert v2_is_into_series(dynamic_attr_only) From 3247238c6f1da32dd3ea8bedbe15044d986b68bf Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Sat, 16 May 2026 10:46:33 +0200 Subject: [PATCH 13/19] **not** is_into_series(dynamic_attr_only) --- tests/dependencies/is_into_series_test.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/dependencies/is_into_series_test.py b/tests/dependencies/is_into_series_test.py index cc3fb98e01..5decc8bdda 100644 --- a/tests/dependencies/is_into_series_test.py +++ b/tests/dependencies/is_into_series_test.py @@ -82,6 +82,6 @@ def test_is_into_series_other(dynamic_attr_only: DynamicAttrOnly) -> None: assert v1_is_into_series(ListBackedSeries("a", values)) assert v2_is_into_series(ListBackedSeries("a", values)) - assert is_into_series(dynamic_attr_only) - assert v1_is_into_series(dynamic_attr_only) - assert v2_is_into_series(dynamic_attr_only) + assert not is_into_series(dynamic_attr_only) + assert not v1_is_into_series(dynamic_attr_only) + assert not v2_is_into_series(dynamic_attr_only) From 8495ffdf296c0b2112766686cb837fe8361924d8 Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Sat, 16 May 2026 10:56:23 +0200 Subject: [PATCH 14/19] no cover DynamicAttrOnly.__getattr__ --- tests/dependencies/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/dependencies/conftest.py b/tests/dependencies/conftest.py index a2112e9c38..bd9ee2027d 100644 --- a/tests/dependencies/conftest.py +++ b/tests/dependencies/conftest.py @@ -23,7 +23,7 @@ class DynamicAttrOnly: [`inspect.getattr_static`]: https://docs.python.org/3/library/inspect.html#inspect.getattr_static """ - def __getattr__(self, name: str) -> Any: + def __getattr__(self, name: str) -> Any: # pragma: no cover return self From efaa06b3759ab251c26ee1be1a9f8709a237a313 Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Sat, 16 May 2026 22:42:35 +0200 Subject: [PATCH 15/19] rename: DynamicAttrOnly -> AlwaysHasAttr --- tests/dependencies/conftest.py | 10 +++++----- tests/dependencies/is_into_dataframe_test.py | 10 +++++----- tests/dependencies/is_into_lazyframe_test.py | 10 +++++----- tests/dependencies/is_into_series_test.py | 10 +++++----- 4 files changed, 20 insertions(+), 20 deletions(-) diff --git a/tests/dependencies/conftest.py b/tests/dependencies/conftest.py index bd9ee2027d..7448b04303 100644 --- a/tests/dependencies/conftest.py +++ b/tests/dependencies/conftest.py @@ -5,20 +5,20 @@ import pytest -class DynamicAttrOnly: +class AlwaysHasAttr: """An object whose attributes only exist dynamically, never statically. Every attribute access is fabricated on the fly via `__getattr__`, so `hasattr` reports `True` for any name while [`inspect.getattr_static`] (and therefore `_hasattr_static`) correctly reports `False`: - obj = DynamicAttrOnly() + obj = AlwaysHasAttr() hasattr(obj, "i_dont_exist") # True (false positive) _hasattr_static(obj, "i_dont_exist") # False (correct) Use it in tests to prove that code under test relies on `_hasattr_static` rather than `hasattr` when probing unknown objects: any check that accepts - a `DynamicAttrOnly` instance is, by construction, using the unsafe path. + a `AlwaysHasAttr` instance is, by construction, using the unsafe path. [`inspect.getattr_static`]: https://docs.python.org/3/library/inspect.html#inspect.getattr_static """ @@ -28,6 +28,6 @@ def __getattr__(self, name: str) -> Any: # pragma: no cover @pytest.fixture(scope="session") -def dynamic_attr_only() -> DynamicAttrOnly: +def always_has_attr() -> AlwaysHasAttr: """An object that triggers `hasattr` false positives for every attribute name.""" - return DynamicAttrOnly() + return AlwaysHasAttr() diff --git a/tests/dependencies/is_into_dataframe_test.py b/tests/dependencies/is_into_dataframe_test.py index f20b3d7860..6cd14d9058 100644 --- a/tests/dependencies/is_into_dataframe_test.py +++ b/tests/dependencies/is_into_dataframe_test.py @@ -16,7 +16,7 @@ from typing_extensions import Self - from tests.dependencies.conftest import DynamicAttrOnly + from tests.dependencies.conftest import AlwaysHasAttr from tests.utils import Constructor EAGER_CONSTRUCTOR_NAMES = ("pandas", "modin", "cudf", "polars_eager", "pyarrow") @@ -78,7 +78,7 @@ def test_is_into_dataframe_numpy() -> None: assert not v2_is_into_dataframe(arr) -def test_is_into_dataframe_other(dynamic_attr_only: DynamicAttrOnly) -> None: +def test_is_into_dataframe_other(always_has_attr: AlwaysHasAttr) -> None: assert not is_into_dataframe(data) assert not v1_is_into_dataframe(data) assert not v2_is_into_dataframe(data) @@ -87,6 +87,6 @@ def test_is_into_dataframe_other(dynamic_attr_only: DynamicAttrOnly) -> None: assert v1_is_into_dataframe(DictDataFrame(data)) assert v2_is_into_dataframe(DictDataFrame(data)) - assert not is_into_dataframe(dynamic_attr_only) - assert not v1_is_into_dataframe(dynamic_attr_only) - assert not v2_is_into_dataframe(dynamic_attr_only) + assert not is_into_dataframe(always_has_attr) + assert not v1_is_into_dataframe(always_has_attr) + assert not v2_is_into_dataframe(always_has_attr) diff --git a/tests/dependencies/is_into_lazyframe_test.py b/tests/dependencies/is_into_lazyframe_test.py index 44d9c7ff30..68495cca23 100644 --- a/tests/dependencies/is_into_lazyframe_test.py +++ b/tests/dependencies/is_into_lazyframe_test.py @@ -16,7 +16,7 @@ from typing_extensions import Self - from tests.dependencies.conftest import DynamicAttrOnly + from tests.dependencies.conftest import AlwaysHasAttr from tests.utils import Constructor EAGER_CONSTRUCTOR_NAMES = ("pandas", "modin", "cudf", "polars_eager", "pyarrow") @@ -75,7 +75,7 @@ def test_is_into_lazyframe_numpy() -> None: assert not v2_is_into_lazyframe(arr) -def test_is_into_lazyframe_other(dynamic_attr_only: DynamicAttrOnly) -> 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) @@ -84,6 +84,6 @@ def test_is_into_lazyframe_other(dynamic_attr_only: DynamicAttrOnly) -> None: assert v1_is_into_lazyframe(DictLazyFrame(data)) assert v2_is_into_lazyframe(DictLazyFrame(data)) - assert not is_into_lazyframe(dynamic_attr_only) - assert not v1_is_into_lazyframe(dynamic_attr_only) - assert not v2_is_into_lazyframe(dynamic_attr_only) + assert not is_into_lazyframe(always_has_attr) + assert not v1_is_into_lazyframe(always_has_attr) + assert not v2_is_into_lazyframe(always_has_attr) diff --git a/tests/dependencies/is_into_series_test.py b/tests/dependencies/is_into_series_test.py index 5decc8bdda..1cb8b0a1c1 100644 --- a/tests/dependencies/is_into_series_test.py +++ b/tests/dependencies/is_into_series_test.py @@ -14,7 +14,7 @@ if TYPE_CHECKING: from typing_extensions import Self - from tests.dependencies.conftest import DynamicAttrOnly + from tests.dependencies.conftest import AlwaysHasAttr from tests.utils import ConstructorEager data: dict[str, Any] = {"a": [1, 2, 3], "b": [4, 5, 6]} @@ -71,7 +71,7 @@ def test_is_into_series_numpy() -> None: assert not v2_is_into_series(arr) -def test_is_into_series_other(dynamic_attr_only: DynamicAttrOnly) -> None: +def test_is_into_series_other(always_has_attr: AlwaysHasAttr) -> None: values = [1, 4, 2] assert not is_into_series(values) @@ -82,6 +82,6 @@ def test_is_into_series_other(dynamic_attr_only: DynamicAttrOnly) -> None: assert v1_is_into_series(ListBackedSeries("a", values)) assert v2_is_into_series(ListBackedSeries("a", values)) - assert not is_into_series(dynamic_attr_only) - assert not v1_is_into_series(dynamic_attr_only) - assert not v2_is_into_series(dynamic_attr_only) + assert not is_into_series(always_has_attr) + assert not v1_is_into_series(always_has_attr) + assert not v2_is_into_series(always_has_attr) From cc294dd477b6137389cd94da4c45cf187a9de566 Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Thu, 21 May 2026 15:21:43 +0200 Subject: [PATCH 16/19] Discussion summarizing note --- src/narwhals/stable/v1/dependencies.py | 43 +++++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/src/narwhals/stable/v1/dependencies.py b/src/narwhals/stable/v1/dependencies.py index d35b110259..739ccd1060 100644 --- a/src/narwhals/stable/v1/dependencies.py +++ b/src/narwhals/stable/v1/dependencies.py @@ -57,7 +57,48 @@ def is_into_dataframe(native_dataframe: Any | IntoDataFrameT) -> TypeIs[IntoDataFrameT]: - """Check whether `native_dataframe` can be converted to a narwhals.stable.v1.DataFrame.""" + """Check whether `native_dataframe` can be converted to a [narwhals.stable.v1.DataFrame][]. + + Arguments: + native_dataframe: The object to check. + + Notes: + This guard intentionally diverges from its counterpart in the main namespace + ([narwhals.dependencies.is_into_dataframe][]) to preserve `v1` semantics: + `ibis` tables and `duckdb` relations are treated as DataFrames here, since + `v1.from_native(..., eager_or_interchange_only=True)` returns a + [narwhals.stable.v1.DataFrame][] for them, whereas in the main namespace + they are LazyFrames. + + The runtime check is narrower than the `v1.typing.IntoDataFrame` type alias. + In particular, arbitrary objects implementing the `__dataframe__` interchange + protocol ([DataFrameLike][narwhals.stable.v1.typing.DataFrameLike]) are accepted + by `v1.from_native(..., eager_or_interchange_only=True)` but are **not** + recognised by this function. If you need to dispatch on the interchange + protocol, check `__dataframe__` explicitly (preferably via + `inspect.getattr_static` to avoid false positives from dynamic attribute + lookup, e.g. a column literally named `"__dataframe__"`). + + For new code, prefer `narwhals.stable.v2` where these inconsistencies have + been resolved. + + Examples: + >>> import pandas as pd + >>> import polars as pl + >>> import numpy as np + >>> import narwhals.stable.v1 as nw_v1 + + >>> df_pd = pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]}) + >>> df_pl = pl.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]}) + >>> np_arr = np.array([[1, 4], [2, 5], [3, 6]]) + + >>> nw_v1.dependencies.is_into_dataframe(df_pd) + True + >>> nw_v1.dependencies.is_into_dataframe(df_pl) + True + >>> nw_v1.dependencies.is_into_dataframe(np_arr) + False + """ from narwhals.stable.v1 import DataFrame return ( From a7de8688b19813eadb10d4432b8318d73859e040 Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Fri, 22 May 2026 10:13:07 +0200 Subject: [PATCH 17/19] docs: Remove links --- src/narwhals/stable/v1/dependencies.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/narwhals/stable/v1/dependencies.py b/src/narwhals/stable/v1/dependencies.py index 739ccd1060..c17c46ac92 100644 --- a/src/narwhals/stable/v1/dependencies.py +++ b/src/narwhals/stable/v1/dependencies.py @@ -57,22 +57,22 @@ def is_into_dataframe(native_dataframe: Any | IntoDataFrameT) -> TypeIs[IntoDataFrameT]: - """Check whether `native_dataframe` can be converted to a [narwhals.stable.v1.DataFrame][]. + """Check whether `native_dataframe` can be converted to a narwhals.stable.v1.DataFrame. Arguments: native_dataframe: The object to check. - Notes: + Note: This guard intentionally diverges from its counterpart in the main namespace - ([narwhals.dependencies.is_into_dataframe][]) to preserve `v1` semantics: + `narwhals.dependencies.is_into_dataframe` to preserve `v1` semantics: `ibis` tables and `duckdb` relations are treated as DataFrames here, since `v1.from_native(..., eager_or_interchange_only=True)` returns a - [narwhals.stable.v1.DataFrame][] for them, whereas in the main namespace + `narwhals.stable.v1.DataFrame` for them, whereas in the main namespace they are LazyFrames. The runtime check is narrower than the `v1.typing.IntoDataFrame` type alias. In particular, arbitrary objects implementing the `__dataframe__` interchange - protocol ([DataFrameLike][narwhals.stable.v1.typing.DataFrameLike]) are accepted + protocol `narwhals.stable.v1.typing.DataFrameLike` are accepted by `v1.from_native(..., eager_or_interchange_only=True)` but are **not** recognised by this function. If you need to dispatch on the interchange protocol, check `__dataframe__` explicitly (preferably via From aa049802e6f19f6ad14270073686114928e468b5 Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Fri, 22 May 2026 12:02:17 +0200 Subject: [PATCH 18/19] rm warning in 'is_into_' --- src/narwhals/dependencies.py | 14 ++++++-------- src/narwhals/stable/v2/dependencies.py | 15 ++++++--------- tests/dependencies/is_into_dataframe_test.py | 1 - tests/dependencies/is_into_lazyframe_test.py | 1 - tests/dependencies/is_into_series_test.py | 1 - 5 files changed, 12 insertions(+), 20 deletions(-) diff --git a/src/narwhals/dependencies.py b/src/narwhals/dependencies.py index cac8ad0340..0fbaaded13 100644 --- a/src/narwhals/dependencies.py +++ b/src/narwhals/dependencies.py @@ -679,10 +679,9 @@ def is_into_dataframe(native_dataframe: Any | IntoDataFrameT) -> TypeIs[IntoData """ from narwhals.dataframe import DataFrame - if isinstance(native_dataframe, DataFrame): - return True - _warn_if_narwhals_df_or_lf(native_dataframe) - return _is_into_native_dataframe(native_dataframe) + return isinstance(native_dataframe, DataFrame) or _is_into_native_dataframe( + native_dataframe + ) def is_into_lazyframe(native_lazyframe: Any | IntoLazyFrameT) -> TypeIs[IntoLazyFrameT]: @@ -710,10 +709,9 @@ def is_into_lazyframe(native_lazyframe: Any | IntoLazyFrameT) -> TypeIs[IntoLazy """ from narwhals.dataframe import LazyFrame - if isinstance(native_lazyframe, LazyFrame): - return True - _warn_if_narwhals_df_or_lf(native_lazyframe) - return _is_into_native_lazyframe(native_lazyframe) + return isinstance(native_lazyframe, LazyFrame) or _is_into_native_lazyframe( + native_lazyframe + ) def is_narwhals_dataframe( diff --git a/src/narwhals/stable/v2/dependencies.py b/src/narwhals/stable/v2/dependencies.py index 787f16422c..06f273a752 100644 --- a/src/narwhals/stable/v2/dependencies.py +++ b/src/narwhals/stable/v2/dependencies.py @@ -6,7 +6,6 @@ _is_into_native_dataframe, _is_into_native_lazyframe, _is_into_native_series, - _warn_if_narwhals_df_or_lf, get_cudf, get_dask, get_dask_dataframe, @@ -60,20 +59,18 @@ def is_into_dataframe(native_dataframe: Any | IntoDataFrameT) -> TypeIs[IntoData """Check whether `native_dataframe` can be converted to a narwhals.stable.v2.DataFrame.""" from narwhals.stable.v2 import DataFrame - if isinstance(native_dataframe, DataFrame): - return True - _warn_if_narwhals_df_or_lf(native_dataframe) - return _is_into_native_dataframe(native_dataframe) + return isinstance(native_dataframe, DataFrame) or _is_into_native_dataframe( + native_dataframe + ) def is_into_lazyframe(native_lazyframe: Any | IntoLazyFrameT) -> TypeIs[IntoLazyFrameT]: """Check whether `native_lazyframe` can be converted to a narwhals.stable.v2.LazyFrame.""" from narwhals.stable.v2 import LazyFrame - if isinstance(native_lazyframe, LazyFrame): - return True - _warn_if_narwhals_df_or_lf(native_lazyframe) - return _is_into_native_lazyframe(native_lazyframe) + return isinstance(native_lazyframe, LazyFrame) or _is_into_native_lazyframe( + native_lazyframe + ) def is_into_series(native_series: Any | IntoSeriesT) -> TypeIs[IntoSeriesT]: diff --git a/tests/dependencies/is_into_dataframe_test.py b/tests/dependencies/is_into_dataframe_test.py index 6cd14d9058..ad3a86dc74 100644 --- a/tests/dependencies/is_into_dataframe_test.py +++ b/tests/dependencies/is_into_dataframe_test.py @@ -41,7 +41,6 @@ def drop(self, *args: Any, **kwargs: Any) -> Any: ... def join(self, *args: Any, **kwargs: Any) -> Any: ... -@pytest.mark.filterwarnings("ignore:.*You passed a.*:UserWarning") def test_is_into_dataframe(constructor: Constructor) -> None: native_frame = constructor(data) nw_frame = nw.from_native(native_frame) diff --git a/tests/dependencies/is_into_lazyframe_test.py b/tests/dependencies/is_into_lazyframe_test.py index 68495cca23..9ab13a6aa1 100644 --- a/tests/dependencies/is_into_lazyframe_test.py +++ b/tests/dependencies/is_into_lazyframe_test.py @@ -39,7 +39,6 @@ def explain(self, *args: Any, **kwargs: Any) -> Any: ... def join(self, *args: Any, **kwargs: Any) -> Any: ... -@pytest.mark.filterwarnings("ignore:.*You passed a.*:UserWarning") def test_is_into_lazyframe(constructor: Constructor) -> None: native_frame = constructor(data) nw_frame = nw.from_native(native_frame) diff --git a/tests/dependencies/is_into_series_test.py b/tests/dependencies/is_into_series_test.py index 1cb8b0a1c1..06dc1fb8b1 100644 --- a/tests/dependencies/is_into_series_test.py +++ b/tests/dependencies/is_into_series_test.py @@ -37,7 +37,6 @@ def value_counts(self, *args: Any, **kwargs: Any) -> Any: ... def unique(self, *args: Any, **kwargs: Any) -> Any: ... -@pytest.mark.filterwarnings("ignore:.*You passed a.*:UserWarning") def test_is_into_series(constructor_eager: ConstructorEager) -> None: native_frame = constructor_eager(data) nw_series = nw.from_native(native_frame)["a"] From 79b0aa905cbe67b371c4da321f95607072d78d40 Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Fri, 22 May 2026 17:59:10 +0200 Subject: [PATCH 19/19] Rollback v1 changes --- src/narwhals/stable/v1/dependencies.py | 60 ++++---------------- tests/dependencies/is_into_dataframe_test.py | 22 +++---- tests/dependencies/is_into_lazyframe_test.py | 23 ++++---- 3 files changed, 33 insertions(+), 72 deletions(-) diff --git a/src/narwhals/stable/v1/dependencies.py b/src/narwhals/stable/v1/dependencies.py index c17c46ac92..d479d4674b 100644 --- a/src/narwhals/stable/v1/dependencies.py +++ b/src/narwhals/stable/v1/dependencies.py @@ -56,56 +56,17 @@ ) +# TODO(Unassigned): For duckdb and ibis backends: +# * is_into_dataframe(native_frame) returns False +# * nw_v1.from_native(native_frame) returns a narwhals.stable.v1.DataFrame +# * Therefore is_into_dataframe(nw_v1.from_native(native_frame)) returns True +# See discussion https://github.com/narwhals-dev/narwhals/pull/3613#discussion_r3288440039 def is_into_dataframe(native_dataframe: Any | IntoDataFrameT) -> TypeIs[IntoDataFrameT]: - """Check whether `native_dataframe` can be converted to a narwhals.stable.v1.DataFrame. - - Arguments: - native_dataframe: The object to check. - - Note: - This guard intentionally diverges from its counterpart in the main namespace - `narwhals.dependencies.is_into_dataframe` to preserve `v1` semantics: - `ibis` tables and `duckdb` relations are treated as DataFrames here, since - `v1.from_native(..., eager_or_interchange_only=True)` returns a - `narwhals.stable.v1.DataFrame` for them, whereas in the main namespace - they are LazyFrames. - - The runtime check is narrower than the `v1.typing.IntoDataFrame` type alias. - In particular, arbitrary objects implementing the `__dataframe__` interchange - protocol `narwhals.stable.v1.typing.DataFrameLike` are accepted - by `v1.from_native(..., eager_or_interchange_only=True)` but are **not** - recognised by this function. If you need to dispatch on the interchange - protocol, check `__dataframe__` explicitly (preferably via - `inspect.getattr_static` to avoid false positives from dynamic attribute - lookup, e.g. a column literally named `"__dataframe__"`). - - For new code, prefer `narwhals.stable.v2` where these inconsistencies have - been resolved. - - Examples: - >>> import pandas as pd - >>> import polars as pl - >>> import numpy as np - >>> import narwhals.stable.v1 as nw_v1 - - >>> df_pd = pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]}) - >>> df_pl = pl.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]}) - >>> np_arr = np.array([[1, 4], [2, 5], [3, 6]]) - - >>> nw_v1.dependencies.is_into_dataframe(df_pd) - True - >>> nw_v1.dependencies.is_into_dataframe(df_pl) - True - >>> nw_v1.dependencies.is_into_dataframe(np_arr) - False - """ + """Check whether `native_dataframe` can be converted to a narwhals.stable.v1.DataFrame.""" from narwhals.stable.v1 import DataFrame - return ( - isinstance(native_dataframe, DataFrame) - or _is_into_native_dataframe(native_dataframe) - or is_ibis_table(native_dataframe) - or is_duckdb_relation(native_dataframe) + return isinstance(native_dataframe, DataFrame) or _is_into_native_dataframe( + native_dataframe ) @@ -113,9 +74,8 @@ def is_into_lazyframe(native_lazyframe: Any | IntoLazyFrameT) -> TypeIs[IntoLazy """Check whether `native_lazyframe` can be converted to a narwhals.stable.v1.LazyFrame.""" from narwhals.stable.v1 import LazyFrame - return isinstance(native_lazyframe, LazyFrame) or ( - _is_into_native_lazyframe(native_lazyframe) - and not (is_ibis_table(native_lazyframe) or is_duckdb_relation(native_lazyframe)) + return isinstance(native_lazyframe, LazyFrame) or _is_into_native_lazyframe( + native_lazyframe ) diff --git a/tests/dependencies/is_into_dataframe_test.py b/tests/dependencies/is_into_dataframe_test.py index ad3a86dc74..4e31b71afd 100644 --- a/tests/dependencies/is_into_dataframe_test.py +++ b/tests/dependencies/is_into_dataframe_test.py @@ -48,23 +48,23 @@ def test_is_into_dataframe(constructor: Constructor) -> None: nw_v2_frame = nw_v2.from_native(native_frame) result = any(x in str(constructor) for x in EAGER_CONSTRUCTOR_NAMES) + result_v1 = any(x in str(constructor) for x in V1_INTO_DATAFRAMES) + assert is_into_dataframe(native_frame) == result + assert v1_is_into_dataframe(native_frame) == result + assert v2_is_into_dataframe(native_frame) == result + assert is_into_dataframe(nw_frame) == result + assert not v1_is_into_dataframe(nw_frame) + assert not v2_is_into_dataframe(nw_frame) - result_v1 = any(x in str(constructor) for x in V1_INTO_DATAFRAMES) - assert v1_is_into_dataframe(native_frame) == result_v1 + assert is_into_dataframe(nw_v1_frame) == result_v1 assert v1_is_into_dataframe(nw_v1_frame) == result_v1 - assert v1_is_into_dataframe(nw_v2_frame) is False - - result_v2 = any(x in str(constructor) for x in EAGER_CONSTRUCTOR_NAMES) - assert v2_is_into_dataframe(native_frame) == result_v2 - assert v2_is_into_dataframe(nw_v2_frame) == result_v2 assert v2_is_into_dataframe(nw_v1_frame) is False - assert is_into_dataframe(nw_v1_frame) == result_v1 - assert is_into_dataframe(nw_v2_frame) == result_v2 - assert not v1_is_into_dataframe(nw_frame) - assert not v2_is_into_dataframe(nw_frame) + assert is_into_dataframe(nw_v2_frame) == result + assert v2_is_into_dataframe(nw_v2_frame) == result + assert v1_is_into_dataframe(nw_v2_frame) is False def test_is_into_dataframe_numpy() -> None: diff --git a/tests/dependencies/is_into_lazyframe_test.py b/tests/dependencies/is_into_lazyframe_test.py index 9ab13a6aa1..b9cabdea83 100644 --- a/tests/dependencies/is_into_lazyframe_test.py +++ b/tests/dependencies/is_into_lazyframe_test.py @@ -46,23 +46,24 @@ def test_is_into_lazyframe(constructor: Constructor) -> None: nw_v2_frame = nw_v2.from_native(native_frame) result = not any(x in str(constructor) for x in EAGER_CONSTRUCTOR_NAMES) - assert is_into_lazyframe(native_frame) == result - assert is_into_lazyframe(nw_frame) == result - result_v1 = not any(x in str(constructor) for x in V1_INTO_DATAFRAMES) - assert v1_is_into_lazyframe(native_frame) == result_v1 - assert v1_is_into_lazyframe(nw_v2_frame) is False - result_v2 = not any(x in str(constructor) for x in EAGER_CONSTRUCTOR_NAMES) - assert v2_is_into_lazyframe(native_frame) == result_v2 - assert v2_is_into_lazyframe(nw_v2_frame) == result_v2 - assert v2_is_into_lazyframe(nw_v1_frame) is False + assert is_into_lazyframe(native_frame) == result + assert v1_is_into_lazyframe(native_frame) == result + assert v2_is_into_lazyframe(native_frame) == result - assert is_into_lazyframe(nw_v1_frame) == result_v1 - assert is_into_lazyframe(nw_v2_frame) == result_v2 + assert is_into_lazyframe(nw_frame) == result assert not v1_is_into_lazyframe(nw_frame) assert not v2_is_into_lazyframe(nw_frame) + assert is_into_lazyframe(nw_v1_frame) == result_v1 + assert v1_is_into_lazyframe(nw_v1_frame) == result_v1 + assert v2_is_into_lazyframe(nw_v1_frame) is False + + assert is_into_lazyframe(nw_v2_frame) == result + assert v2_is_into_lazyframe(nw_v2_frame) == result + assert v1_is_into_lazyframe(nw_v2_frame) is False + def test_is_into_lazyframe_numpy() -> None: pytest.importorskip("numpy")