Repository: modin-project/modin

Issue:
Add __dataframe__ method to the protocol dataframe
```python
import modin.pandas as pd
from modin.pandas.utils import from_dataframe
df = pd.DataFrame({"a": [1, 2, 3]})
dfx = df.__dataframe__()
from_dataframe(dfx)
ValueError: `df` does not support DataFrame exchange protocol, i.e. `__dataframe__` method
```


Discussion:

Resolving patch:
diff --git a/modin/core/dataframe/base/exchange/dataframe_protocol/dataframe.py b/modin/core/dataframe/base/exchange/dataframe_protocol/dataframe.py
--- a/modin/core/dataframe/base/exchange/dataframe_protocol/dataframe.py
+++ b/modin/core/dataframe/base/exchange/dataframe_protocol/dataframe.py
@@ -376,6 +376,33 @@ class ProtocolDataframe(ABC):
     to the dataframe interchange protocol specification.
     """
 
+    @abstractmethod
+    def __dataframe__(self, nan_as_null: bool = False, allow_copy: bool = True):
+        """
+        Get a new dataframe exchange object.
+
+        See more about the protocol in https://data-apis.org/dataframe-protocol/latest/index.html.
+
+        Parameters
+        ----------
+        nan_as_null : bool, default: False
+            A keyword intended for the consumer to tell the producer
+            to overwrite null values in the data with ``NaN`` (or ``NaT``).
+            This currently has no effect; once support for nullable extension
+            dtypes is added, this value should be propagated to columns.
+        allow_copy : bool, default: True
+            A keyword that defines whether or not the library is allowed
+            to make a copy of the data. For example, copying data would be necessary
+            if a library supports strided buffers, given that this protocol
+            specifies contiguous buffers. Currently, if the flag is set to ``False``
+            and a copy is needed, a ``RuntimeError`` will be raised.
+
+        Returns
+        -------
+        ProtocolDataframe
+        """
+        pass
+
     @property
     @abstractmethod
     def metadata(self):
diff --git a/modin/core/dataframe/pandas/exchange/dataframe_protocol/dataframe.py b/modin/core/dataframe/pandas/exchange/dataframe_protocol/dataframe.py
--- a/modin/core/dataframe/pandas/exchange/dataframe_protocol/dataframe.py
+++ b/modin/core/dataframe/pandas/exchange/dataframe_protocol/dataframe.py
@@ -79,6 +79,11 @@ def __init__(
         self._nan_as_null = nan_as_null
         self._allow_copy = allow_copy
 
+    def __dataframe__(self, nan_as_null: bool = False, allow_copy: bool = True):
+        return PandasProtocolDataframe(
+            self._df, nan_as_null=nan_as_null, allow_copy=allow_copy
+        )
+
     @property
     def metadata(self) -> Dict[str, Any]:
         return {"modin.index": self._df.index}
diff --git a/modin/experimental/core/execution/native/implementations/omnisci_on_native/exchange/dataframe_protocol/dataframe.py b/modin/experimental/core/execution/native/implementations/omnisci_on_native/exchange/dataframe_protocol/dataframe.py
--- a/modin/experimental/core/execution/native/implementations/omnisci_on_native/exchange/dataframe_protocol/dataframe.py
+++ b/modin/experimental/core/execution/native/implementations/omnisci_on_native/exchange/dataframe_protocol/dataframe.py
@@ -67,6 +67,11 @@ def __init__(
         self._nan_as_null = nan_as_null
         self._allow_copy = allow_copy
 
+    def __dataframe__(self, nan_as_null: bool = False, allow_copy: bool = True):
+        return OmnisciProtocolDataframe(
+            self._df, nan_as_null=nan_as_null, allow_copy=allow_copy
+        )
+
     @property
     @raise_copy_alert_if_materialize
     def metadata(self) -> Dict[str, Any]:


Test patch:
diff --git a/modin/test/exchange/dataframe_protocol/omnisci/test_protocol.py b/modin/test/exchange/dataframe_protocol/omnisci/test_protocol.py
--- a/modin/test/exchange/dataframe_protocol/omnisci/test_protocol.py
+++ b/modin/test/exchange/dataframe_protocol/omnisci/test_protocol.py
@@ -202,11 +202,13 @@ def test_simple_import(data_has_nulls):
     """Test that ``modin.pandas.utils.from_dataframe`` works properly."""
     data = get_data_of_all_types(data_has_nulls)
 
-    md_df_producer = pd.DataFrame(data)
+    modin_df_producer = pd.DataFrame(data)
+    internal_modin_df_producer = modin_df_producer.__dataframe__()
     # Our configuration in pytest.ini requires that we explicitly catch all
     # instances of defaulting to pandas, this one raises a warning on `.from_dataframe`
     with warns_that_defaulting_to_pandas():
-        md_df_consumer = from_dataframe(md_df_producer)
+        modin_df_consumer = from_dataframe(modin_df_producer)
+        internal_modin_df_consumer = from_dataframe(internal_modin_df_producer)
 
     # TODO: the following assertions verify that `from_dataframe` doesn't return
     # the same object untouched due to optimization branching, it actually should
@@ -214,13 +216,15 @@ def test_simple_import(data_has_nulls):
     # for now. It's required to replace the producer's type with a different one
     # to consumer when we have some other implementation of the protocol as the
     # assertions may start failing shortly.
-    assert md_df_producer is not md_df_consumer
+    assert modin_df_producer is not modin_df_consumer
+    assert internal_modin_df_producer is not internal_modin_df_consumer
     assert (
-        md_df_producer._query_compiler._modin_frame
-        is not md_df_consumer._query_compiler._modin_frame
+        modin_df_producer._query_compiler._modin_frame
+        is not modin_df_consumer._query_compiler._modin_frame
     )
 
-    df_equals(md_df_producer, md_df_consumer)
+    df_equals(modin_df_producer, modin_df_consumer)
+    df_equals(modin_df_producer, internal_modin_df_consumer)
 
 
 @pytest.mark.parametrize("data_has_nulls", [True, False])
diff --git a/modin/test/exchange/dataframe_protocol/pandas/test_protocol.py b/modin/test/exchange/dataframe_protocol/pandas/test_protocol.py
--- a/modin/test/exchange/dataframe_protocol/pandas/test_protocol.py
+++ b/modin/test/exchange/dataframe_protocol/pandas/test_protocol.py
@@ -21,10 +21,12 @@
 
 def test_simple_import():
     modin_df_producer = pd.DataFrame(test_data["int_data"])
+    internal_modin_df_producer = modin_df_producer.__dataframe__()
     # Our configuration in pytest.ini requires that we explicitly catch all
     # instances of defaulting to pandas, this one raises a warning on `.from_dataframe`
     with warns_that_defaulting_to_pandas():
         modin_df_consumer = from_dataframe(modin_df_producer)
+        internal_modin_df_consumer = from_dataframe(internal_modin_df_producer)
 
     # TODO: the following assertions verify that `from_dataframe` doesn't return
     # the same object untouched due to optimization branching, it actually should
@@ -33,9 +35,11 @@ def test_simple_import():
     # to consumer when we have some other implementation of the protocol as the
     # assertions may start failing shortly.
     assert modin_df_producer is not modin_df_consumer
+    assert internal_modin_df_producer is not internal_modin_df_consumer
     assert (
         modin_df_producer._query_compiler._modin_frame
         is not modin_df_consumer._query_compiler._modin_frame
     )
 
     df_equals(modin_df_producer, modin_df_consumer)
+    df_equals(modin_df_producer, internal_modin_df_consumer)