diff --git a/src/safeds/data/tabular/containers/_table.py b/src/safeds/data/tabular/containers/_table.py index 98a8265d5..a8e28451d 100644 --- a/src/safeds/data/tabular/containers/_table.py +++ b/src/safeds/data/tabular/containers/_table.py @@ -1190,3 +1190,40 @@ def _ipython_display_(self) -> DisplayHandle: with pd.option_context("display.max_rows", tmp.shape[0], "display.max_columns", tmp.shape[1]): return display(tmp) + + # ------------------------------------------------------------------------------------------------------------------ + # Dataframe interchange protocol + # ------------------------------------------------------------------------------------------------------------------ + + def __dataframe__(self, nan_as_null: bool = False, allow_copy: bool = True): # type: ignore[no-untyped-def] + """ + Return a DataFrame exchange object that conforms to the dataframe interchange protocol. + + Generally, there is no reason to call this method directly. The dataframe interchange protocol is designed to + allow libraries to consume tabular data from different sources, such as `pandas` or `polars`. If you still + decide to call this method, you should not rely on any capabilities of the returned object beyond the dataframe + interchange protocol. + + Parameters + ---------- + nan_as_null : bool + Whether to replace missing values in the data with `NaN`. + allow_copy : bool + Whether memory may be copied to create the DataFrame exchange object. + + Returns + ------- + dataframe + A DataFrame object that conforms to the dataframe interchange protocol. + + Notes + ----- + The specification of the dataframe interchange protocol can be found at + https://github.com/data-apis/dataframe-api. + """ + if not allow_copy: + raise NotImplementedError("For the moment we need to copy the data, so `allow_copy` must be True.") + + data_copy = self._data.copy() + data_copy.columns = self.get_column_names() + return data_copy.__dataframe__(nan_as_null, allow_copy) diff --git a/tests/safeds/data/tabular/containers/test_table.py b/tests/safeds/data/tabular/containers/test_table.py index 5d1b52ab7..e99922056 100644 --- a/tests/safeds/data/tabular/containers/test_table.py +++ b/tests/safeds/data/tabular/containers/test_table.py @@ -1,6 +1,7 @@ from typing import Any import pytest +from pandas.core.interchange.from_dataframe import from_dataframe from safeds.data.tabular.containers import Table from safeds.data.tabular.exceptions import ColumnLengthMismatchError from safeds.data.tabular.typing import Integer, Schema @@ -50,3 +51,27 @@ class TestToDict: ) def test_should_return_dict_for_table(self, table: Table, expected: dict[str, Any]) -> None: assert table.to_dict() == expected + + +class TestDataframe: + @pytest.mark.parametrize( + "table", + [ + Table([]), + Table.from_dict({"a": [1, 2], "b": [3, 4]}), + ], + ids=[ + "empty", + "non-empty", + ], + ) + def test_can_restore_table_from_exchange_object(self, table: Table) -> None: + exchange_object = table.__dataframe__() + restored = Table(from_dataframe(exchange_object)) + + assert restored == table + + def test_should_raise_if_allow_copy_is_false(self) -> None: + table = Table.from_dict({}) + with pytest.raises(NotImplementedError, match="`allow_copy` must be True"): + table.__dataframe__(allow_copy=False)