diff --git a/narwhals/_duckdb/dataframe.py b/narwhals/_duckdb/dataframe.py index 555555d4a..5877ed51e 100644 --- a/narwhals/_duckdb/dataframe.py +++ b/narwhals/_duckdb/dataframe.py @@ -4,9 +4,12 @@ from typing import TYPE_CHECKING from typing import Any +from narwhals.dependencies import get_duckdb from narwhals.utils import parse_version if TYPE_CHECKING: + from types import ModuleType + import pandas as pd import pyarrow as pa from typing_extensions import Self @@ -68,6 +71,9 @@ def __init__(self, df: Any, dtypes: DTypes) -> None: def __narwhals_dataframe__(self) -> Any: return self + def __native_namespace__(self: Self) -> ModuleType: + return get_duckdb() # type: ignore[no-any-return] + def __getitem__(self, item: str) -> DuckDBInterchangeSeries: from narwhals._duckdb.series import DuckDBInterchangeSeries diff --git a/narwhals/_duckdb/series.py b/narwhals/_duckdb/series.py index a7dbdd549..e30b14ac9 100644 --- a/narwhals/_duckdb/series.py +++ b/narwhals/_duckdb/series.py @@ -4,8 +4,11 @@ from typing import Any from narwhals._duckdb.dataframe import map_duckdb_dtype_to_narwhals_dtype +from narwhals.dependencies import get_duckdb if TYPE_CHECKING: + from types import ModuleType + from narwhals.typing import DTypes @@ -17,6 +20,9 @@ def __init__(self, df: Any, dtypes: DTypes) -> None: def __narwhals_series__(self) -> Any: return self + def __native_namespace__(self) -> ModuleType: + return get_duckdb() # type: ignore[no-any-return] + def __getattr__(self, attr: str) -> Any: if attr == "dtype": return map_duckdb_dtype_to_narwhals_dtype( diff --git a/narwhals/_ibis/dataframe.py b/narwhals/_ibis/dataframe.py index 8ee01e78b..9d7ebefb0 100644 --- a/narwhals/_ibis/dataframe.py +++ b/narwhals/_ibis/dataframe.py @@ -3,7 +3,11 @@ from typing import TYPE_CHECKING from typing import Any +from narwhals.dependencies import get_ibis + if TYPE_CHECKING: + from types import ModuleType + import pandas as pd import pyarrow as pa from typing_extensions import Self @@ -59,6 +63,9 @@ def __init__(self, df: Any, dtypes: DTypes) -> None: def __narwhals_dataframe__(self) -> Any: return self + def __native_namespace__(self: Self) -> ModuleType: + return get_ibis() # type: ignore[no-any-return] + def __getitem__(self, item: str) -> IbisInterchangeSeries: from narwhals._ibis.series import IbisInterchangeSeries diff --git a/narwhals/_ibis/series.py b/narwhals/_ibis/series.py index 2f6cd6faa..e2c313690 100644 --- a/narwhals/_ibis/series.py +++ b/narwhals/_ibis/series.py @@ -4,8 +4,11 @@ from typing import Any from narwhals._ibis.dataframe import map_ibis_dtype_to_narwhals_dtype +from narwhals.dependencies import get_ibis if TYPE_CHECKING: + from types import ModuleType + from narwhals.typing import DTypes @@ -17,6 +20,9 @@ def __init__(self, df: Any, dtypes: DTypes) -> None: def __narwhals_series__(self) -> Any: return self + def __native_namespace__(self) -> ModuleType: + return get_ibis() # type: ignore[no-any-return] + def __getattr__(self, attr: str) -> Any: if attr == "dtype": return map_ibis_dtype_to_narwhals_dtype( diff --git a/narwhals/_interchange/dataframe.py b/narwhals/_interchange/dataframe.py index 1dc671dc7..4e8e542e7 100644 --- a/narwhals/_interchange/dataframe.py +++ b/narwhals/_interchange/dataframe.py @@ -82,6 +82,14 @@ def __init__(self, df: Any, dtypes: DTypes) -> None: def __narwhals_dataframe__(self) -> Any: return self + def __native_namespace__(self: Self) -> NoReturn: + msg = ( + "Cannot access native namespace for metadata-only dataframes with unknown backend." + "If you would like to see this kind of object supported in Narwhals, please " + "open a feature request at https://github.com/narwhals-dev/narwhals/issues." + ) + raise NotImplementedError(msg) + def __getitem__(self, item: str) -> InterchangeSeries: from narwhals._interchange.series import InterchangeSeries diff --git a/narwhals/_interchange/series.py b/narwhals/_interchange/series.py index 00426e6c0..a9cf41f07 100644 --- a/narwhals/_interchange/series.py +++ b/narwhals/_interchange/series.py @@ -2,6 +2,7 @@ from typing import TYPE_CHECKING from typing import Any +from typing import NoReturn from narwhals._interchange.dataframe import map_interchange_dtype_to_narwhals_dtype @@ -17,6 +18,14 @@ def __init__(self, df: Any, dtypes: DTypes) -> None: def __narwhals_series__(self) -> Any: return self + def __native_namespace__(self) -> NoReturn: + msg = ( + "Cannot access native namespace for metadata-only series with unknown backend. " + "If you would like to see this kind of object supported in Narwhals, please " + "open a feature request at https://github.com/narwhals-dev/narwhals/issues." + ) + raise NotImplementedError(msg) + def __getattr__(self, attr: str) -> Any: if attr == "dtype": return map_interchange_dtype_to_narwhals_dtype( diff --git a/tests/frame/interchange_native_namespace_test.py b/tests/frame/interchange_native_namespace_test.py new file mode 100644 index 000000000..8a67d07b8 --- /dev/null +++ b/tests/frame/interchange_native_namespace_test.py @@ -0,0 +1,52 @@ +import duckdb +import polars as pl +import pytest + +import narwhals.stable.v1 as nw + +data = {"a": [1, 2, 3], "b": [4.5, 6.7, 8.9], "z": ["x", "y", "w"]} + + +def test_interchange() -> None: + df_pl = pl.DataFrame(data) + df = nw.from_native(df_pl.__dataframe__(), eager_or_interchange_only=True) + series = df["a"] + + with pytest.raises( + NotImplementedError, + match="Cannot access native namespace for metadata-only dataframes with unknown backend", + ): + df.__native_namespace__() + + with pytest.raises( + NotImplementedError, + match="Cannot access native namespace for metadata-only series with unknown backend", + ): + series.__native_namespace__() + + +def test_ibis( + tmpdir: pytest.TempdirFactory, +) -> None: # pragma: no cover + ibis = pytest.importorskip("ibis") + df_pl = pl.DataFrame(data) + + filepath = str(tmpdir / "file.parquet") # type: ignore[operator] + df_pl.write_parquet(filepath) + tbl = ibis.read_parquet(filepath) + df = nw.from_native(tbl, eager_or_interchange_only=True) + series = df["a"] + + assert df.__native_namespace__() == ibis + assert series.__native_namespace__() == ibis + + +def test_duckdb() -> None: + df_pl = pl.DataFrame(data) # noqa: F841 + + rel = duckdb.sql("select * from df_pl") + df = nw.from_native(rel, eager_or_interchange_only=True) + series = df["a"] + + assert df.__native_namespace__() == duckdb + assert series.__native_namespace__() == duckdb