Skip to content

Commit 3531646

Browse files
authored
feat: support columns and select for InterchangeFrame if _df is present (#1283)
1 parent f349cb2 commit 3531646

File tree

3 files changed

+69
-23
lines changed

3 files changed

+69
-23
lines changed

narwhals/_interchange/dataframe.py

+37-15
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,6 @@ def map_interchange_dtype_to_narwhals_dtype(
7575

7676
class InterchangeFrame:
7777
def __init__(self, df: Any, dtypes: DTypes) -> None:
78-
self._native_frame = df
7978
self._interchange_frame = df.__dataframe__()
8079
self._dtypes = dtypes
8180

@@ -97,21 +96,11 @@ def __getitem__(self, item: str) -> InterchangeSeries:
9796
self._interchange_frame.get_column_by_name(item), dtypes=self._dtypes
9897
)
9998

100-
@property
101-
def schema(self) -> dict[str, DType]:
102-
return {
103-
column_name: map_interchange_dtype_to_narwhals_dtype(
104-
self._interchange_frame.get_column_by_name(column_name).dtype,
105-
self._dtypes,
106-
)
107-
for column_name in self._interchange_frame.column_names()
108-
}
109-
11099
def to_pandas(self: Self) -> pd.DataFrame:
111100
import pandas as pd # ignore-banned-import()
112101

113102
if parse_version(pd.__version__) >= parse_version("1.5.0"):
114-
return pd.api.interchange.from_dataframe(self._native_frame)
103+
return pd.api.interchange.from_dataframe(self._interchange_frame)
115104
else: # pragma: no cover
116105
msg = (
117106
"Conversion to pandas is achieved via interchange protocol which requires"
@@ -122,9 +111,19 @@ def to_pandas(self: Self) -> pd.DataFrame:
122111
def to_arrow(self: Self) -> pa.Table:
123112
from pyarrow.interchange import from_dataframe # ignore-banned-import()
124113

125-
return from_dataframe(self._native_frame)
126-
127-
def __getattr__(self, attr: str) -> NoReturn:
114+
return from_dataframe(self._interchange_frame)
115+
116+
def __getattr__(self, attr: str) -> Any:
117+
if attr == "schema":
118+
return {
119+
column_name: map_interchange_dtype_to_narwhals_dtype(
120+
self._interchange_frame.get_column_by_name(column_name).dtype,
121+
self._dtypes,
122+
)
123+
for column_name in self._interchange_frame.column_names()
124+
}
125+
elif attr == "columns":
126+
return list(self._interchange_frame.column_names())
128127
msg = (
129128
f"Attribute {attr} is not supported for metadata-only dataframes.\n\n"
130129
"Hint: you probably called `nw.from_native` on an object which isn't fully "
@@ -133,3 +132,26 @@ def __getattr__(self, attr: str) -> NoReturn:
133132
"at https://github.com/narwhals-dev/narwhals/issues."
134133
)
135134
raise NotImplementedError(msg)
135+
136+
def select(
137+
self: Self,
138+
*exprs: Any,
139+
**named_exprs: Any,
140+
) -> Self:
141+
if named_exprs or not all(isinstance(x, str) for x in exprs): # pragma: no cover
142+
msg = (
143+
"`select`-ing not by name is not supported for interchange-only level.\n\n"
144+
"If you would like to see this kind of object better supported in "
145+
"Narwhals, please open a feature request "
146+
"at https://github.com/narwhals-dev/narwhals/issues."
147+
)
148+
raise NotImplementedError(msg)
149+
150+
frame = self._interchange_frame.select_columns_by_name(exprs)
151+
if not hasattr(frame, "_df"): # pragma: no cover
152+
msg = (
153+
"Expected interchange object to implement `_df` property to allow for recovering original object.\n"
154+
"See https://github.com/data-apis/dataframe-api/issues/360."
155+
)
156+
raise NotImplementedError(frame)
157+
return self.__class__(frame._df, dtypes=self._dtypes)

tests/frame/interchange_schema_test.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,7 @@ def test_invalid() -> None:
227227
with pytest.raises(
228228
NotImplementedError, match="is not supported for metadata-only dataframes"
229229
):
230-
nw.from_native(df, eager_or_interchange_only=True).select("a")
230+
nw.from_native(df, eager_or_interchange_only=True).filter([True, False, True])
231231
with pytest.raises(TypeError, match="Cannot only use `series_only=True`"):
232232
nw.from_native(df, eager_only=True)
233233
with pytest.raises(ValueError, match="Invalid parameter combination"):

tests/frame/interchange_select_test.py

+31-7
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
from __future__ import annotations
22

3+
from typing import Any
4+
35
import duckdb
46
import polars as pl
57
import pytest
@@ -9,14 +11,36 @@
911
data = {"a": [1, 2, 3], "b": [4.0, 5.0, 6.1], "z": ["x", "y", "z"]}
1012

1113

14+
class InterchangeDataFrame:
15+
def __init__(self, df: CustomDataFrame) -> None:
16+
self._df = df
17+
18+
def __dataframe__(self) -> InterchangeDataFrame: # pragma: no cover
19+
return self
20+
21+
def column_names(self) -> list[str]:
22+
return list(self._df._data.keys())
23+
24+
def select_columns_by_name(self, columns: list[str]) -> InterchangeDataFrame:
25+
return InterchangeDataFrame(
26+
CustomDataFrame(
27+
{key: value for key, value in self._df._data.items() if key in columns}
28+
)
29+
)
30+
31+
32+
class CustomDataFrame:
33+
def __init__(self, data: dict[str, Any]) -> None:
34+
self._data = data
35+
36+
def __dataframe__(self, *, allow_copy: bool = True) -> InterchangeDataFrame:
37+
return InterchangeDataFrame(self)
38+
39+
1240
def test_interchange() -> None:
13-
df_pl = pl.DataFrame(data)
14-
df = nw.from_native(df_pl.__dataframe__(), eager_or_interchange_only=True)
15-
with pytest.raises(
16-
NotImplementedError,
17-
match="Attribute select is not supported for metadata-only dataframes",
18-
):
19-
df.select("a", "z")
41+
df = CustomDataFrame({"a": [1, 2, 3], "b": [4, 5, 6], "z": [1, 4, 2]})
42+
result = nw.from_native(df, eager_or_interchange_only=True).select("a", "z")
43+
assert result.columns == ["a", "z"]
2044

2145

2246
def test_interchange_ibis(

0 commit comments

Comments
 (0)