Skip to content

Commit be0a3cf

Browse files
authored
feat: Add bpd.options.display.repr_mode = "anywidget" to create an interactive display of the results (#1820)
* add anywidget as extra python package * Add anywidget to bigframes * add the first testcase * fix mypy error * Show first page of results (to_pandas_batches()) is done * add the testcase * Add more testcase * change a import * change anywidget mode for plain text * fix noxfile * add anywidget for docx * ignore missing import * fix doctest * add unittest * add notebook test * fix unit-10.12 * change testcase * make anywidget addtional * remove anywidget_mode.ipynb in notebook session due to deferred mode * fix typo * fix failed testcase
1 parent 37666e4 commit be0a3cf

File tree

10 files changed

+211
-9
lines changed

10 files changed

+211
-9
lines changed

bigframes/_config/display_options.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ class DisplayOptions:
2929
max_columns: int = 20
3030
max_rows: int = 25
3131
progress_bar: Optional[str] = "auto"
32-
repr_mode: Literal["head", "deferred"] = "head"
32+
repr_mode: Literal["head", "deferred", "anywidget"] = "head"
3333

3434
max_info_columns: int = 100
3535
max_info_rows: Optional[int] = 200000

bigframes/core/indexes/base.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -251,7 +251,9 @@ def __repr__(self) -> str:
251251
# metadata, like we do with DataFrame.
252252
opts = bigframes.options.display
253253
max_results = opts.max_rows
254-
if opts.repr_mode == "deferred":
254+
# anywdiget mode uses the same display logic as the "deferred" mode
255+
# for faster execution
256+
if opts.repr_mode in ("deferred", "anywidget"):
255257
_, dry_run_query_job = self._block._compute_dry_run()
256258
return formatter.repr_query_job(dry_run_query_job)
257259

bigframes/dataframe.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -725,7 +725,9 @@ def __repr__(self) -> str:
725725

726726
opts = bigframes.options.display
727727
max_results = opts.max_rows
728-
if opts.repr_mode == "deferred":
728+
# anywdiget mode uses the same display logic as the "deferred" mode
729+
# for faster execution
730+
if opts.repr_mode in ("deferred", "anywidget"):
729731
return formatter.repr_query_job(self._compute_dry_run())
730732

731733
# TODO(swast): pass max_columns and get the true column count back. Maybe
@@ -774,6 +776,23 @@ def _repr_html_(self) -> str:
774776
if opts.repr_mode == "deferred":
775777
return formatter.repr_query_job(self._compute_dry_run())
776778

779+
if opts.repr_mode == "anywidget":
780+
import anywidget # type: ignore
781+
782+
# create an iterator for the data batches
783+
batches = self.to_pandas_batches()
784+
785+
# get the first page result
786+
try:
787+
first_page = next(iter(batches))
788+
except StopIteration:
789+
first_page = pandas.DataFrame(columns=self.columns)
790+
791+
# Instantiate and return the widget. The widget's frontend will
792+
# handle the display of the table and pagination
793+
return anywidget.AnyWidget(dataframe=first_page)
794+
795+
self._cached()
777796
df = self.copy()
778797
if bigframes.options.display.blob_display:
779798
blob_cols = [

bigframes/series.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -430,7 +430,9 @@ def __repr__(self) -> str:
430430
# metadata, like we do with DataFrame.
431431
opts = bigframes.options.display
432432
max_results = opts.max_rows
433-
if opts.repr_mode == "deferred":
433+
# anywdiget mode uses the same display logic as the "deferred" mode
434+
# for faster execution
435+
if opts.repr_mode in ("deferred", "anywidget"):
434436
return formatter.repr_query_job(self._compute_dry_run())
435437

436438
self._cached()

mypy.ini

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,6 @@ ignore_missing_imports = True
4141

4242
[mypy-google.cloud.bigtable]
4343
ignore_missing_imports = True
44+
45+
[mypy-anywidget]
46+
ignore_missing_imports = True
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
{
2+
"cells": [
3+
{
4+
"cell_type": "code",
5+
"execution_count": 1,
6+
"id": "d10bfca4",
7+
"metadata": {},
8+
"outputs": [],
9+
"source": [
10+
"# Copyright 2025 Google LLC\n",
11+
"#\n",
12+
"# Licensed under the Apache License, Version 2.0 (the \"License\");\n",
13+
"# you may not use this file except in compliance with the License.\n",
14+
"# You may obtain a copy of the License at\n",
15+
"#\n",
16+
"# https://www.apache.org/licenses/LICENSE-2.0\n",
17+
"#\n",
18+
"# Unless required by applicable law or agreed to in writing, software\n",
19+
"# distributed under the License is distributed on an \"AS IS\" BASIS,\n",
20+
"# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n",
21+
"# See the License for the specific language governing permissions and\n",
22+
"# limitations under the License."
23+
]
24+
},
25+
{
26+
"cell_type": "markdown",
27+
"id": "acca43ae",
28+
"metadata": {},
29+
"source": [
30+
"# Demo to Show Anywidget mode"
31+
]
32+
},
33+
{
34+
"cell_type": "code",
35+
"execution_count": 2,
36+
"id": "ca22f059",
37+
"metadata": {},
38+
"outputs": [],
39+
"source": [
40+
"import bigframes.pandas as bpd"
41+
]
42+
},
43+
{
44+
"cell_type": "markdown",
45+
"id": "04406a4d",
46+
"metadata": {},
47+
"source": [
48+
"Set the display option to use anywidget"
49+
]
50+
},
51+
{
52+
"cell_type": "code",
53+
"execution_count": 3,
54+
"id": "1bc5aaf3",
55+
"metadata": {},
56+
"outputs": [],
57+
"source": [
58+
"bpd.options.display.repr_mode = \"anywidget\""
59+
]
60+
},
61+
{
62+
"cell_type": "markdown",
63+
"id": "0a354c69",
64+
"metadata": {},
65+
"source": [
66+
"Display the dataframe in anywidget mode"
67+
]
68+
},
69+
{
70+
"cell_type": "code",
71+
"execution_count": 4,
72+
"id": "f289d250",
73+
"metadata": {},
74+
"outputs": [
75+
{
76+
"data": {
77+
"text/html": [
78+
"Query job 91997f19-1768-4360-afa7-4a431b3e2d22 is DONE. 0 Bytes processed. <a target=\"_blank\" href=\"https://console.cloud.google.com/bigquery?project=bigframes-dev&j=bq:US:91997f19-1768-4360-afa7-4a431b3e2d22&page=queryresults\">Open Job</a>"
79+
],
80+
"text/plain": [
81+
"<IPython.core.display.HTML object>"
82+
]
83+
},
84+
"metadata": {},
85+
"output_type": "display_data"
86+
},
87+
{
88+
"name": "stdout",
89+
"output_type": "stream",
90+
"text": [
91+
"Computation deferred. Computation will process 171.4 MB\n"
92+
]
93+
}
94+
],
95+
"source": [
96+
"df = bpd.read_gbq(\"bigquery-public-data.usa_names.usa_1910_2013\")\n",
97+
"print(df)"
98+
]
99+
},
100+
{
101+
"cell_type": "markdown",
102+
"id": "3a73e472",
103+
"metadata": {},
104+
"source": [
105+
"Display Series in anywidget mode"
106+
]
107+
},
108+
{
109+
"cell_type": "code",
110+
"execution_count": 5,
111+
"id": "42bb02ab",
112+
"metadata": {},
113+
"outputs": [
114+
{
115+
"name": "stdout",
116+
"output_type": "stream",
117+
"text": [
118+
"Computation deferred. Computation will process 171.4 MB\n"
119+
]
120+
}
121+
],
122+
"source": [
123+
"test_series = df[\"year\"]\n",
124+
"print(test_series)"
125+
]
126+
}
127+
],
128+
"metadata": {
129+
"kernelspec": {
130+
"display_name": "venv",
131+
"language": "python",
132+
"name": "python3"
133+
},
134+
"language_info": {
135+
"codemirror_mode": {
136+
"name": "ipython",
137+
"version": 3
138+
},
139+
"file_extension": ".py",
140+
"mimetype": "text/x-python",
141+
"name": "python",
142+
"nbconvert_exporter": "python",
143+
"pygments_lexer": "ipython3",
144+
"version": "3.10.15"
145+
}
146+
},
147+
"nbformat": 4,
148+
"nbformat_minor": 5
149+
}

noxfile.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -77,9 +77,9 @@
7777
]
7878
UNIT_TEST_LOCAL_DEPENDENCIES: List[str] = []
7979
UNIT_TEST_DEPENDENCIES: List[str] = []
80-
UNIT_TEST_EXTRAS: List[str] = ["tests"]
80+
UNIT_TEST_EXTRAS: List[str] = ["tests", "anywidget"]
8181
UNIT_TEST_EXTRAS_BY_PYTHON: Dict[str, List[str]] = {
82-
"3.12": ["tests", "polars", "scikit-learn"],
82+
"3.12": ["tests", "polars", "scikit-learn", "anywidget"],
8383
}
8484

8585
# 3.10 is needed for Windows tests as it is the only version installed in the
@@ -106,9 +106,9 @@
106106
SYSTEM_TEST_DEPENDENCIES: List[str] = []
107107
SYSTEM_TEST_EXTRAS: List[str] = []
108108
SYSTEM_TEST_EXTRAS_BY_PYTHON: Dict[str, List[str]] = {
109-
"3.9": ["tests"],
109+
"3.9": ["tests", "anywidget"],
110110
"3.10": ["tests"],
111-
"3.12": ["tests", "scikit-learn", "polars"],
111+
"3.12": ["tests", "scikit-learn", "polars", "anywidget"],
112112
"3.13": ["tests", "polars"],
113113
}
114114

@@ -276,6 +276,7 @@ def mypy(session):
276276
"types-setuptools",
277277
"types-tabulate",
278278
"polars",
279+
"anywidget",
279280
]
280281
)
281282
| set(SYSTEM_TEST_STANDARD_DEPENDENCIES)
@@ -518,6 +519,7 @@ def docs(session):
518519
SPHINX_VERSION,
519520
"alabaster",
520521
"recommonmark",
522+
"anywidget",
521523
)
522524

523525
shutil.rmtree(os.path.join("docs", "_build"), ignore_errors=True)
@@ -560,6 +562,7 @@ def docfx(session):
560562
"alabaster",
561563
"recommonmark",
562564
"gcp-sphinx-docfx-yaml==3.0.1",
565+
"anywidget",
563566
)
564567

565568
shutil.rmtree(os.path.join("docs", "_build"), ignore_errors=True)
@@ -763,6 +766,7 @@ def notebook(session: nox.Session):
763766
"google-cloud-aiplatform",
764767
"matplotlib",
765768
"seaborn",
769+
"anywidget",
766770
)
767771

768772
notebooks_list = list(pathlib.Path("notebooks/").glob("*/*.ipynb"))
@@ -805,6 +809,9 @@ def notebook(session: nox.Session):
805809
# continuously tested.
806810
"notebooks/apps/synthetic_data_generation.ipynb",
807811
"notebooks/multimodal/multimodal_dataframe.ipynb", # too slow
812+
# This anywidget notebook uses deferred execution, so it won't
813+
# produce metrics for the performance benchmark script.
814+
"notebooks/dataframes/anywidget_mode.ipynb",
808815
]
809816

810817
# TODO: remove exception for Python 3.13 cloud run adds a runtime for it (internal issue 333742751)

setup.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,10 @@
8686
"nox",
8787
"google-cloud-testutils",
8888
],
89+
# install anywidget for SQL
90+
"anywidget": [
91+
"anywidget>=0.9.18",
92+
],
8993
}
9094
extras["all"] = list(sorted(frozenset(itertools.chain.from_iterable(extras.values()))))
9195

tests/system/small/test_dataframe.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -810,7 +810,7 @@ def test_repr_html_w_all_rows(scalars_dfs, session):
810810
+ f"[{len(pandas_df.index)} rows x {len(pandas_df.columns)} columns in total]"
811811
)
812812
assert actual == expected
813-
assert (executions_post - executions_pre) <= 2
813+
assert (executions_post - executions_pre) <= 3
814814

815815

816816
def test_df_column_name_with_space(scalars_dfs):

tests/system/small/test_progress_bar.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
import numpy as np
1919
import pandas as pd
20+
import pytest
2021

2122
import bigframes as bf
2223
import bigframes.formatting_helpers as formatting_helpers
@@ -164,3 +165,18 @@ def test_query_job_dry_run_series(penguins_df_default_index: bf.dataframe.DataFr
164165
with bf.option_context("display.repr_mode", "deferred"):
165166
series_result = repr(penguins_df_default_index["body_mass_g"])
166167
assert EXPECTED_DRY_RUN_MESSAGE in series_result
168+
169+
170+
def test_repr_anywidget_dataframe(penguins_df_default_index: bf.dataframe.DataFrame):
171+
pytest.importorskip("anywidget")
172+
with bf.option_context("display.repr_mode", "anywidget"):
173+
actual_repr = repr(penguins_df_default_index)
174+
assert EXPECTED_DRY_RUN_MESSAGE in actual_repr
175+
176+
177+
def test_repr_anywidget_idex(penguins_df_default_index: bf.dataframe.DataFrame):
178+
pytest.importorskip("anywidget")
179+
with bf.option_context("display.repr_mode", "anywidget"):
180+
index = penguins_df_default_index.index
181+
actual_repr = repr(index)
182+
assert EXPECTED_DRY_RUN_MESSAGE in actual_repr

0 commit comments

Comments
 (0)