Skip to content

Commit 4d22992

Browse files
committed
feat: Refactor anywidget display to use _ipython_display_
1 parent 12e04d5 commit 4d22992

File tree

3 files changed

+75
-84
lines changed

3 files changed

+75
-84
lines changed

README.rst

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,19 @@ For details, see the `third_party
110110
directory.
111111

112112

113+
Display Enhancements
114+
--------------------
115+
116+
**DataFrame `_repr_html_()` and `_ipython_display_()` Integration**
117+
118+
The `bigframes.pandas.DataFrame._repr_html_()` method has been updated. When `bpd.options.display.repr_mode` is set to “anywidget”, it will:
119+
120+
* Wrap the import of `anywidget` in a `try...except ImportError` block. If the dependency is not found, it will issue a `warnings.warn` message and fall back to returning the deferred representation.
121+
* If the import is successful, it instantiates a new `TableWidget`, passing the DataFrame's data.
122+
* Return the widget instance, which Jupyter automatically renders. A new widget instance is created for each `_repr_html_()` call to ensure cell outputs are isolated.
123+
124+
A new `_ipython_display_()` method has been introduced to handle the actual widget rendering, separating concerns from `_repr_html_()`.
125+
113126
Contact Us
114127
----------
115128

bigframes/dataframe.py

Lines changed: 53 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -831,10 +831,12 @@ def _repr_html_(self) -> str:
831831
"""
832832
opts = bigframes.options.display
833833
max_results = opts.max_rows
834-
if opts.repr_mode == "deferred":
834+
# For anywidget mode, return deferred representation
835+
# The actual widget display is handled by _ipython_display_()
836+
if opts.repr_mode in ("deferred", "anywidget"):
835837
return formatter.repr_query_job(self._compute_dry_run())
836838

837-
# Process blob columns first, regardless of display mode
839+
# Process blob columns first for non-deferred modes
838840
self._cached()
839841
df = self.copy()
840842
if bigframes.options.display.blob_display:
@@ -849,29 +851,6 @@ def _repr_html_(self) -> str:
849851
else:
850852
blob_cols = []
851853

852-
if opts.repr_mode == "anywidget":
853-
try:
854-
from IPython.display import display as ipython_display
855-
856-
from bigframes import display
857-
858-
# Always create a new widget instance for each display call
859-
# This ensures that each cell gets its own widget and prevents
860-
# unintended sharing between cells
861-
widget = display.TableWidget(df.copy())
862-
863-
ipython_display(widget)
864-
return "" # Return empty string since we used display()
865-
866-
except (AttributeError, ValueError, ImportError):
867-
# Fallback if anywidget is not available
868-
warnings.warn(
869-
"Anywidget mode is not available. "
870-
"Please `pip install anywidget traitlets` or `pip install 'bigframes[anywidget]'` to use interactive tables. "
871-
f"Falling back to deferred mode. Error: {traceback.format_exc()}"
872-
)
873-
return formatter.repr_query_job(self._compute_dry_run())
874-
875854
# Continue with regular HTML rendering for non-anywidget modes
876855
# TODO(swast): pass max_columns and get the true column count back. Maybe
877856
# get 1 more column than we have requested so that pandas can add the
@@ -931,6 +910,55 @@ def obj_ref_rt_to_html(obj_ref_rt) -> str:
931910
html_string += f"[{row_count} rows x {column_count} columns in total]"
932911
return html_string
933912

913+
def _ipython_display_(self):
914+
"""
915+
Custom display method for IPython/Jupyter environments.
916+
This is called by IPython's display system when the object is displayed.
917+
"""
918+
opts = bigframes.options.display
919+
920+
# Only handle widget display in anywidget mode
921+
if opts.repr_mode == "anywidget":
922+
try:
923+
from bigframes import display
924+
925+
# Process blob columns if needed
926+
self._cached()
927+
df = self.copy()
928+
if bigframes.options.display.blob_display:
929+
blob_cols = [
930+
series_name
931+
for series_name, series in df.items()
932+
if series.dtype == bigframes.dtypes.OBJ_REF_DTYPE
933+
]
934+
for col in blob_cols:
935+
df[col] = df[col].blob._get_runtime(
936+
mode="R", with_metadata=True
937+
)
938+
939+
# Create and display the widget
940+
widget = display.TableWidget(df)
941+
942+
# IPython will automatically display the widget
943+
# since we're returning it from _ipython_display_()
944+
from IPython.display import display as ipython_display
945+
946+
ipython_display(widget)
947+
return # Important: return None to signal we handled display
948+
949+
except (AttributeError, ValueError, ImportError):
950+
# Fallback: let IPython use _repr_html_() instead
951+
warnings.warn(
952+
"Anywidget mode is not available. "
953+
"Please `pip install anywidget traitlets` or `pip install 'bigframes[anywidget]'` to use interactive tables. "
954+
f"Falling back to deferred mode. Error: {traceback.format_exc()}"
955+
)
956+
# Don't return anything - let IPython fall back to _repr_html_()
957+
return
958+
959+
# For other modes, don't handle display - let IPython use _repr_html_()
960+
return
961+
934962
def __delitem__(self, key: str):
935963
df = self.drop(columns=[key])
936964
self._set_block(df._get_block())

notebooks/dataframes/anywidget_mode.ipynb

Lines changed: 9 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -35,16 +35,7 @@
3535
"execution_count": 2,
3636
"id": "ca22f059",
3737
"metadata": {},
38-
"outputs": [
39-
{
40-
"name": "stderr",
41-
"output_type": "stream",
42-
"text": [
43-
"/usr/local/google/home/shuowei/src/python-bigquery-dataframes/venv/lib/python3.10/site-packages/google/api_core/_python_version_support.py:266: FutureWarning: You are using a Python version (3.10.15) which Google will stop supporting in new releases of google.api_core once it reaches its end of life (2026-10-04). Please upgrade to the latest Python version, or at least Python 3.11, to continue receiving updates for google.api_core past that date.\n",
44-
" warnings.warn(message, FutureWarning)\n"
45-
]
46-
}
47-
],
38+
"outputs": [],
4839
"source": [
4940
"import bigframes.pandas as bpd"
5041
]
@@ -151,7 +142,7 @@
151142
{
152143
"data": {
153144
"application/vnd.jupyter.widget-view+json": {
154-
"model_id": "47795eaa10f149aeb99574232c0936eb",
145+
"model_id": "4cc789c49be246bb94967e625986900a",
155146
"version_major": 2,
156147
"version_minor": 1
157148
},
@@ -161,17 +152,6 @@
161152
},
162153
"metadata": {},
163154
"output_type": "display_data"
164-
},
165-
{
166-
"data": {
167-
"text/html": [],
168-
"text/plain": [
169-
"Computation deferred. Computation will process 171.4 MB"
170-
]
171-
},
172-
"execution_count": 6,
173-
"metadata": {},
174-
"output_type": "execute_result"
175155
}
176156
],
177157
"source": [
@@ -214,7 +194,7 @@
214194
{
215195
"data": {
216196
"application/vnd.jupyter.widget-view+json": {
217-
"model_id": "8354ce0f82d3495a9b630dfc362f73ee",
197+
"model_id": "b1089664ce03440d81ef206da1210229",
218198
"version_major": 2,
219199
"version_minor": 1
220200
},
@@ -293,27 +273,8 @@
293273
{
294274
"data": {
295275
"text/html": [
296-
"\n",
297-
" Query started with request ID bigframes-dev:US.c45952fb-01b4-409c-9da4-f7c5bfc0d47d.<details><summary>SQL</summary><pre>SELECT\n",
298-
"`state` AS `state`,\n",
299-
"`gender` AS `gender`,\n",
300-
"`year` AS `year`,\n",
301-
"`name` AS `name`,\n",
302-
"`number` AS `number`\n",
303-
"FROM\n",
304-
"(SELECT\n",
305-
" *\n",
306-
"FROM (\n",
307-
" SELECT\n",
308-
" `state`,\n",
309-
" `gender`,\n",
310-
" `year`,\n",
311-
" `name`,\n",
312-
" `number`\n",
313-
" FROM `bigquery-public-data.usa_names.usa_1910_2013` FOR SYSTEM_TIME AS OF TIMESTAMP(&#x27;2025-10-30T21:48:48.979701+00:00&#x27;)\n",
314-
") AS `t0`)\n",
315-
"ORDER BY `name` ASC NULLS LAST ,`year` ASC NULLS LAST ,`state` ASC NULLS LAST\n",
316-
"LIMIT 5</pre></details>\n",
276+
"✅ Completed. \n",
277+
" Query processed 171.4 MB in a moment of slot time.\n",
317278
" "
318279
],
319280
"text/plain": [
@@ -333,7 +294,7 @@
333294
{
334295
"data": {
335296
"application/vnd.jupyter.widget-view+json": {
336-
"model_id": "59461286a17d4a42b6be6d9d9c7bf7e3",
297+
"model_id": "f18925fc13304fb2ae34056f2cb1c68b",
337298
"version_major": 2,
338299
"version_minor": 1
339300
},
@@ -373,7 +334,7 @@
373334
"data": {
374335
"text/html": [
375336
"✅ Completed. \n",
376-
" Query processed 85.9 kB in 14 seconds of slot time.\n",
337+
" Query processed 85.9 kB in 15 seconds of slot time.\n",
377338
" "
378339
],
379340
"text/plain": [
@@ -387,7 +348,7 @@
387348
"name": "stderr",
388349
"output_type": "stream",
389350
"text": [
390-
"/usr/local/google/home/shuowei/src/python-bigquery-dataframes/bigframes/dtypes.py:969: JSONDtypeWarning: JSON columns will be represented as pandas.ArrowDtype(pyarrow.json_())\n",
351+
"/usr/local/google/home/shuowei/src/python-bigquery-dataframes/bigframes/dtypes.py:987: JSONDtypeWarning: JSON columns will be represented as pandas.ArrowDtype(pyarrow.json_())\n",
391352
"instead of using `db_dtypes` in the future when available in pandas\n",
392353
"(https://github.com/pandas-dev/pandas/issues/60958) and pyarrow.\n",
393354
" warnings.warn(msg, bigframes.exceptions.JSONDtypeWarning)\n"
@@ -408,7 +369,7 @@
408369
{
409370
"data": {
410371
"application/vnd.jupyter.widget-view+json": {
411-
"model_id": "d1794b42579542a8980bd158e521bd3e",
372+
"model_id": "2335d3161b704a6da85165dbebf5ca0f",
412373
"version_major": 2,
413374
"version_minor": 1
414375
},
@@ -418,17 +379,6 @@
418379
},
419380
"metadata": {},
420381
"output_type": "display_data"
421-
},
422-
{
423-
"data": {
424-
"text/html": [],
425-
"text/plain": [
426-
"Computation deferred. Computation will process 0 Bytes"
427-
]
428-
},
429-
"execution_count": 10,
430-
"metadata": {},
431-
"output_type": "execute_result"
432382
}
433383
],
434384
"source": [

0 commit comments

Comments
 (0)