Repository: streamlit/streamlit

Issue:
Support ibis table expressions as input to st.dataframe
### Problem

[Ibis](https://ibis-project.org/) is a popular lightweight interface for data wrangling which has some nice synergy with upcoming `st.experimental_connection()`. If I'm using Ibis, it would be convenient if I could pass an ibis Table expression directly into st.dataframe / st.data_editor etc.

```python
t = conn.table('mytable')
t.filter(...)
  .group_by(...)
  .aggregate(...)
  .sort_by(...)
st.dataframe(t)
```

### Solution

Since ibis table expressions are lazily evaluated, it would probably make sense to force evaluation when this happens and get back a pandas dataframe or pyarrow table. This is supported with several methods already:

1. `table.to_pandas()` returns a pandas DataFrame
2. `table.to_pyarrow()` returns a pyarrow Table
3. `table.to_pyarrow_batches()` returns a pyarrow RecordBatchReader. The in-memory results are streamed back to the client one batch at a time.

### Additional context

Discussed this with @cpcloud and @ianmcook and wanted to document the request / discussion here to consider. Also in case anyone else has feedback or wants to upvote 😄 cc @LukasMasuch 

---

Community voting on feature requests enables the Streamlit team to understand which features are most important to our users.

**If you'd like the Streamlit team to prioritize this feature request, please use the 👍 (thumbs up emoji) reaction in response to the initial post.**



Discussion:
This looks pretty simple to implement.
```diff
-    if isinstance(data, pa.Table):
+    if hasattr(data, 'to_pandas'):
        return data.to_pandas()
```
https://github.com/streamlit/streamlit/blob/3e67555eed7eadd45238e699b87aa2bd76066529/lib/streamlit/type_util.py#L509-L510
In addition, we probably should move `if` block a little lower, so that more specialized implementations, e.g. Snowpark, are checked earlier. Snowpark also has `to_pandas` method. https://github.com/snowflakedb/snowpark-python/blob/6de4fa262046f5b7b950baf580110783e7c1dfbf/src/snowflake/snowpark/dataframe.py#L727

Resolving patch:
diff --git a/lib/streamlit/type_util.py b/lib/streamlit/type_util.py
--- a/lib/streamlit/type_util.py
+++ b/lib/streamlit/type_util.py
@@ -503,12 +503,6 @@ def convert_anything_to_df(
     pandas.DataFrame
 
     """
-    # This is inefficient as the data will be converted back to Arrow
-    # when marshalled to protobuf, but area/bar/line charts need
-    # DataFrame magic to generate the correct output.
-    if isinstance(data, pa.Table):
-        return data.to_pandas()
-
     if is_type(data, _PANDAS_DF_TYPE_STR):
         return data.copy() if ensure_copy else data
 
@@ -536,6 +530,12 @@ def convert_anything_to_df(
             )
         return data
 
+    # This is inefficient when data is a pyarrow.Table as it will be converted
+    # back to Arrow when marshalled to protobuf, but area/bar/line charts need
+    # DataFrame magic to generate the correct output.
+    if hasattr(data, "to_pandas"):
+        return data.to_pandas()
+
     # Try to convert to pandas.DataFrame. This will raise an error is df is not
     # compatible with the pandas.DataFrame constructor.
     try:


Test patch:
diff --git a/lib/tests/streamlit/type_util_test.py b/lib/tests/streamlit/type_util_test.py
--- a/lib/tests/streamlit/type_util_test.py
+++ b/lib/tests/streamlit/type_util_test.py
@@ -178,6 +178,15 @@ def test_convert_anything_to_df_supports_key_value_dicts(self):
         df = convert_anything_to_df(data)
         pd.testing.assert_frame_equal(df, pd.DataFrame.from_dict(data, orient="index"))
 
+    def test_convert_anything_to_df_calls_to_pandas_when_available(self):
+        class DataFrameIsh:
+            def to_pandas(self):
+                return pd.DataFrame([])
+
+        converted = convert_anything_to_df(DataFrameIsh())
+        assert isinstance(converted, pd.DataFrame)
+        assert converted.empty
+
     @parameterized.expand(
         [
             # Complex numbers: