From 34bff420e11485e8b327e249706fe652a8933629 Mon Sep 17 00:00:00 2001 From: Shuowei Li Date: Tue, 20 Jan 2026 23:40:28 +0000 Subject: [PATCH 1/9] feat: add deferred mode to TableWidget --- bigframes/display/anywidget.py | 17 +++- bigframes/display/html.py | 2 +- bigframes/display/table_widget.css | 26 +++++ bigframes/display/table_widget.js | 40 ++++++++ tests/js/table_widget_deferred.test.js | 129 +++++++++++++++++++++++++ tests/unit/display/test_anywidget.py | 42 ++++++++ 6 files changed, 253 insertions(+), 3 deletions(-) create mode 100644 tests/js/table_widget_deferred.test.js diff --git a/bigframes/display/anywidget.py b/bigframes/display/anywidget.py index be0d2b45d0..80703c3d69 100644 --- a/bigframes/display/anywidget.py +++ b/bigframes/display/anywidget.py @@ -76,12 +76,17 @@ class TableWidget(_WIDGET_BASE): _error_message = traitlets.Unicode(allow_none=True, default_value=None).tag( sync=True ) + start_execution = traitlets.Bool(False).tag(sync=True) + is_deferred_mode = traitlets.Bool(False).tag(sync=True) - def __init__(self, dataframe: bigframes.dataframe.DataFrame): + def __init__( + self, dataframe: bigframes.dataframe.DataFrame, deferred: bool = False + ): """Initialize the TableWidget. Args: dataframe: The Bigframes Dataframe to display in the widget. + deferred: Whether to defer the initial data load. """ if not _ANYWIDGET_INSTALLED: raise ImportError( @@ -90,6 +95,7 @@ def __init__(self, dataframe: bigframes.dataframe.DataFrame): ) self._dataframe = dataframe + self.is_deferred_mode = deferred super().__init__() @@ -122,12 +128,19 @@ def __init__(self, dataframe: bigframes.dataframe.DataFrame): else: self.orderable_columns = [] - self._initial_load() + if not self.is_deferred_mode: + self._initial_load() # Signals to the frontend that the initial data load is complete. # Also used as a guard to prevent observers from firing during initialization. self._initial_load_complete = True + @traitlets.observe("start_execution") + def _on_start_execution(self, change: dict[str, Any]): + if change["new"]: + self._initial_load() + self.is_deferred_mode = False + def _initial_load(self) -> None: """Get initial data and row count.""" # obtain the row counts diff --git a/bigframes/display/html.py b/bigframes/display/html.py index 6102d1512c..1676cfe691 100644 --- a/bigframes/display/html.py +++ b/bigframes/display/html.py @@ -267,7 +267,7 @@ def get_anywidget_bundle( else: df, blob_cols = obj._get_display_df_and_blob_cols() - widget = display.TableWidget(df) + widget = display.TableWidget(df, deferred=True) widget_repr_result = widget._repr_mimebundle_(include=include, exclude=exclude) if isinstance(widget_repr_result, tuple): diff --git a/bigframes/display/table_widget.css b/bigframes/display/table_widget.css index da0a701d69..d51eac70b7 100644 --- a/bigframes/display/table_widget.css +++ b/bigframes/display/table_widget.css @@ -242,6 +242,32 @@ body[data-theme='dark'] .bigframes-widget.bigframes-widget { color: var(--bf-null-fg); } +.bigframes-widget .deferred-message { + align-items: center; + background-color: var(--bf-bg); + border: 1px solid var(--bf-border-color); + display: flex; + flex-direction: column; + gap: 12px; + justify-content: center; + padding: 24px; + text-align: center; +} + +.bigframes-widget .deferred-message p { + margin: 0; +} + +.bigframes-widget .run-button { + background-color: var(--bf-bg); + border: 1px solid var(--bf-border-color); + padding: 6px 12px; +} + +.bigframes-widget .run-button:hover { + background-color: var(--bf-header-bg); +} + .bigframes-widget .debug-info { border-top: 1px solid var(--bf-border-color); } diff --git a/bigframes/display/table_widget.js b/bigframes/display/table_widget.js index 314bf771d0..967ea58bc2 100644 --- a/bigframes/display/table_widget.js +++ b/bigframes/display/table_widget.js @@ -23,6 +23,8 @@ const ModelProperty = { SORT_CONTEXT: 'sort_context', TABLE_HTML: 'table_html', MAX_COLUMNS: 'max_columns', + START_EXECUTION: 'start_execution', + IS_DEFERRED_MODE: 'is_deferred_mode', }; const Event = { @@ -41,6 +43,17 @@ function render({ model, el }) { const errorContainer = document.createElement('div'); errorContainer.classList.add('error-message'); + const deferredContainer = document.createElement('div'); + deferredContainer.classList.add('deferred-message'); + const deferredText = document.createElement('p'); + deferredText.textContent = + 'This is a preview of the widget. The SQL query has not been executed yet.'; + const runButton = document.createElement('button'); + runButton.textContent = 'Run Query and Display Widget'; + runButton.classList.add('run-button'); + deferredContainer.appendChild(deferredText); + deferredContainer.appendChild(runButton); + const tableContainer = document.createElement('div'); tableContainer.classList.add('table-container'); const footer = document.createElement('footer'); @@ -299,6 +312,30 @@ function render({ model, el }) { } } + function updateDeferredMode() { + const isDeferred = model.get(ModelProperty.IS_DEFERRED_MODE); + if (isDeferred) { + deferredContainer.style.display = 'flex'; + tableContainer.style.display = 'none'; + footer.style.display = 'none'; + } else { + deferredContainer.style.display = 'none'; + tableContainer.style.display = 'block'; + footer.style.display = 'flex'; + // Trigger a resize/layout update if needed when becoming visible + handleTableHTMLChange(); + } + } + + runButton.addEventListener(Event.CLICK, () => { + model.set(ModelProperty.START_EXECUTION, true); + model.save_changes(); + // Optimistically switch UI state or wait for model update? + // Wait for model update via observer for robustness, but could show loading here. + runButton.textContent = 'Running...'; + runButton.disabled = true; + }); + prevPage.addEventListener(Event.CLICK, () => handlePageChange(-1)); nextPage.addEventListener(Event.CLICK, () => handlePageChange(1)); pageSizeInput.addEventListener(Event.CHANGE, (e) => { @@ -321,6 +358,7 @@ function render({ model, el }) { if (val) updateButtonStates(); }); model.on(`change:${ModelProperty.PAGE}`, updateButtonStates); + model.on(`change:${ModelProperty.IS_DEFERRED_MODE}`, updateDeferredMode); paginationContainer.appendChild(prevPage); paginationContainer.appendChild(pageIndicator); @@ -340,11 +378,13 @@ function render({ model, el }) { footer.appendChild(settingsContainer); el.appendChild(errorContainer); + el.appendChild(deferredContainer); el.appendChild(tableContainer); el.appendChild(footer); handleTableHTMLChange(); handleErrorMessageChange(); + updateDeferredMode(); } export default { render }; diff --git a/tests/js/table_widget_deferred.test.js b/tests/js/table_widget_deferred.test.js new file mode 100644 index 0000000000..6649641eb6 --- /dev/null +++ b/tests/js/table_widget_deferred.test.js @@ -0,0 +1,129 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { jest } from '@jest/globals'; + +describe('TableWidget Deferred Mode', () => { + let model; + let el; + let render; + + beforeEach(async () => { + jest.resetModules(); + document.body.innerHTML = '
'; + el = document.body.querySelector('div'); + + const tableWidget = ( + await import('../../bigframes/display/table_widget.js') + ).default; + render = tableWidget.render; + + model = { + get: jest.fn(), + set: jest.fn(), + save_changes: jest.fn(), + on: jest.fn(), + }; + }); + + describe('Deferred Mode UI', () => { + it('should show deferred message and hide table when in deferred mode', () => { + // Mock deferred mode = true + model.get.mockImplementation((property) => { + if (property === 'is_deferred_mode') return true; + if (property === 'table_html') return '
'; + return null; + }); + + render({ model, el }); + + const deferredContainer = el.querySelector('.deferred-message'); + const tableContainer = el.querySelector('.table-container'); + const footer = el.querySelector('.footer'); + + expect(deferredContainer.style.display).toBe('flex'); + expect(tableContainer.style.display).toBe('none'); + expect(footer.style.display).toBe('none'); + expect(deferredContainer.textContent).toContain( + 'This is a preview of the widget', + ); + }); + + it('should show table and hide deferred message when not in deferred mode', () => { + // Mock deferred mode = false + model.get.mockImplementation((property) => { + if (property === 'is_deferred_mode') return false; + if (property === 'table_html') return '
'; + return null; + }); + + render({ model, el }); + + const deferredContainer = el.querySelector('.deferred-message'); + const tableContainer = el.querySelector('.table-container'); + const footer = el.querySelector('.footer'); + + expect(deferredContainer.style.display).toBe('none'); + expect(tableContainer.style.display).toBe('block'); + expect(footer.style.display).toBe('flex'); + }); + + it('should trigger start_execution when run button is clicked', () => { + model.get.mockImplementation((property) => { + if (property === 'is_deferred_mode') return true; + return null; + }); + + render({ model, el }); + + const runButton = el.querySelector('.run-button'); + runButton.click(); + + expect(model.set).toHaveBeenCalledWith('start_execution', true); + expect(model.save_changes).toHaveBeenCalled(); + expect(runButton.textContent).toBe('Running...'); + expect(runButton.disabled).toBe(true); + }); + + it('should update UI when is_deferred_mode changes', () => { + // Start in deferred mode + let isDeferred = true; + model.get.mockImplementation((property) => { + if (property === 'is_deferred_mode') return isDeferred; + if (property === 'table_html') return '
'; + return null; + }); + + render({ model, el }); + + const deferredContainer = el.querySelector('.deferred-message'); + const tableContainer = el.querySelector('.table-container'); + + expect(deferredContainer.style.display).toBe('flex'); + expect(tableContainer.style.display).toBe('none'); + + // Change to non-deferred mode + isDeferred = false; + const changeHandler = model.on.mock.calls.find( + (call) => call[0] === 'change:is_deferred_mode', + )[1]; + changeHandler(); + + expect(deferredContainer.style.display).toBe('none'); + expect(tableContainer.style.display).toBe('block'); + }); + }); +}); diff --git a/tests/unit/display/test_anywidget.py b/tests/unit/display/test_anywidget.py index 252ba8100e..9544ac092d 100644 --- a/tests/unit/display/test_anywidget.py +++ b/tests/unit/display/test_anywidget.py @@ -179,3 +179,45 @@ def test_page_size_change_resets_sort(mock_df): # to_pandas_batches called again (reset) assert mock_df.to_pandas_batches.call_count >= 2 + + +def test_deferred_mode_initialization(mock_df): + """Test that deferred mode does not load data initially.""" + from bigframes.display.anywidget import TableWidget + + with mock.patch.object(TableWidget, "_initial_load") as mock_load: + widget = TableWidget(mock_df, deferred=True) + + assert widget.is_deferred_mode is True + mock_load.assert_not_called() + + +def test_deferred_mode_execution(mock_df): + """Test that setting start_execution triggers load and disables deferred mode.""" + from bigframes.display.anywidget import TableWidget + + # specific mock for _initial_load to avoid real execution but allow tracking calls + # We need to make sure _initial_load exists on the class to patch it + with mock.patch.object(TableWidget, "_initial_load") as mock_load: + widget = TableWidget(mock_df, deferred=True) + + assert widget.is_deferred_mode is True + mock_load.assert_not_called() + + # Simulate user clicking "Run" + widget.start_execution = True + + # Verify load triggered + mock_load.assert_called_once() + assert widget.is_deferred_mode is False + + +def test_normal_mode_initialization(mock_df): + """Test that normal mode loads data initially.""" + from bigframes.display.anywidget import TableWidget + + with mock.patch.object(TableWidget, "_initial_load") as mock_load: + widget = TableWidget(mock_df, deferred=False) + + assert widget.is_deferred_mode is False + mock_load.assert_called_once() From 193aecf582cd6de95199e20906400f3ddc9e6eeb Mon Sep 17 00:00:00 2001 From: Shuowei Li Date: Tue, 20 Jan 2026 23:59:06 +0000 Subject: [PATCH 2/9] fix: prevent widget shaking by removing redundant render call --- bigframes/display/table_widget.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/bigframes/display/table_widget.js b/bigframes/display/table_widget.js index 967ea58bc2..8af57194ad 100644 --- a/bigframes/display/table_widget.js +++ b/bigframes/display/table_widget.js @@ -322,8 +322,6 @@ function render({ model, el }) { deferredContainer.style.display = 'none'; tableContainer.style.display = 'block'; footer.style.display = 'flex'; - // Trigger a resize/layout update if needed when becoming visible - handleTableHTMLChange(); } } From 7ccd383d617ec531358ee6832eb90222adfa0d01 Mon Sep 17 00:00:00 2001 From: Shuowei Li Date: Wed, 21 Jan 2026 00:15:35 +0000 Subject: [PATCH 3/9] fix: ensure widget height is initialized when exiting deferred mode --- bigframes/display/table_widget.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/bigframes/display/table_widget.js b/bigframes/display/table_widget.js index 8af57194ad..ab3b2ce786 100644 --- a/bigframes/display/table_widget.js +++ b/bigframes/display/table_widget.js @@ -185,9 +185,7 @@ function render({ model, el }) { let isHeightInitialized = false; - function handleTableHTMLChange() { - tableContainer.innerHTML = model.get(ModelProperty.TABLE_HTML); - + function initializeHeight() { // After the first render, dynamically set the container height to fit the // initial page (usually 10 rows) and then lock it. setTimeout(() => { @@ -203,6 +201,12 @@ function render({ model, el }) { } } }, 0); + } + + function handleTableHTMLChange() { + tableContainer.innerHTML = model.get(ModelProperty.TABLE_HTML); + + initializeHeight(); const sortableColumns = model.get(ModelProperty.ORDERABLE_COLUMNS); const currentSortContext = model.get(ModelProperty.SORT_CONTEXT) || []; @@ -322,6 +326,7 @@ function render({ model, el }) { deferredContainer.style.display = 'none'; tableContainer.style.display = 'block'; footer.style.display = 'flex'; + initializeHeight(); } } From 277d8bc5fd90a93671f2f3519c815db2f11d50aa Mon Sep 17 00:00:00 2001 From: Shuowei Li Date: Wed, 21 Jan 2026 00:44:50 +0000 Subject: [PATCH 4/9] fix(display): use min-height for table widget to prevent shaking --- bigframes/display/table_widget.js | 23 +++++++++++++++++++---- tests/js/table_widget.test.js | 8 +++++--- 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/bigframes/display/table_widget.js b/bigframes/display/table_widget.js index ab3b2ce786..f6d2a029cb 100644 --- a/bigframes/display/table_widget.js +++ b/bigframes/display/table_widget.js @@ -188,19 +188,34 @@ function render({ model, el }) { function initializeHeight() { // After the first render, dynamically set the container height to fit the // initial page (usually 10 rows) and then lock it. - setTimeout(() => { + const runUpdate = () => { if (!isHeightInitialized) { const table = tableContainer.querySelector('table'); if (table) { const tableHeight = table.offsetHeight; - // Add a small buffer(e.g. 2px) for borders to avoid scrollbars. if (tableHeight > 0) { - tableContainer.style.height = `${tableHeight + 2}px`; + const style = window.getComputedStyle(tableContainer); + let maxHeight = parseFloat(style.maxHeight); + if (isNaN(maxHeight)) maxHeight = 620; // Default fallback if parsing fails or none + + // Clamp the target height to the max height to avoid conflicting with CSS + const targetHeight = Math.min(tableHeight + 2, maxHeight); + + // Use min-height to allow expansion (e.g. horizontal scrollbars) + // but prevent shrinking (jumping controls). + tableContainer.style.minHeight = `${targetHeight}px`; + tableContainer.style.height = 'auto'; // Ensure height is not locked isHeightInitialized = true; } } } - }, 0); + }; + + if (window.requestAnimationFrame) { + window.requestAnimationFrame(runUpdate); + } else { + setTimeout(runUpdate, 0); + } } function handleTableHTMLChange() { diff --git a/tests/js/table_widget.test.js b/tests/js/table_widget.test.js index d701d8692e..7d1658852b 100644 --- a/tests/js/table_widget.test.js +++ b/tests/js/table_widget.test.js @@ -370,7 +370,8 @@ describe('TableWidget', () => { jest.runAllTimers(); // Height should be set to the mocked offsetHeight + 2px buffer - expect(tableContainer.style.height).toBe('152px'); + expect(tableContainer.style.minHeight).toBe('152px'); + expect(tableContainer.style.height).toBe('auto'); // --- Second render (e.g., page size change) --- // Simulate the new content being taller @@ -378,8 +379,9 @@ describe('TableWidget', () => { tableHtmlChangeHandler(); jest.runAllTimers(); - // Height should NOT change - expect(tableContainer.style.height).toBe('152px'); + // Min Height should NOT change (it's initialized once) + expect(tableContainer.style.minHeight).toBe('152px'); + expect(tableContainer.style.height).toBe('auto'); // Restore original implementation Object.defineProperty(HTMLElement.prototype, 'offsetHeight', { From 055760915035dd56512759ba25335ab0b283ca25 Mon Sep 17 00:00:00 2001 From: Shuowei Li Date: Wed, 21 Jan 2026 01:06:04 +0000 Subject: [PATCH 5/9] fix(display): prevent shaking by syncing HTML update when exiting deferred mode --- bigframes/display/table_widget.js | 1 + 1 file changed, 1 insertion(+) diff --git a/bigframes/display/table_widget.js b/bigframes/display/table_widget.js index f6d2a029cb..b50acdc8ef 100644 --- a/bigframes/display/table_widget.js +++ b/bigframes/display/table_widget.js @@ -341,6 +341,7 @@ function render({ model, el }) { deferredContainer.style.display = 'none'; tableContainer.style.display = 'block'; footer.style.display = 'flex'; + handleTableHTMLChange(); initializeHeight(); } } From af9bd2104e7c4ae3c0786fcf54592a375abda077 Mon Sep 17 00:00:00 2001 From: Shuowei Li Date: Wed, 21 Jan 2026 01:17:03 +0000 Subject: [PATCH 6/9] fix(display): remove redundant initializeHeight call --- bigframes/display/table_widget.js | 1 - 1 file changed, 1 deletion(-) diff --git a/bigframes/display/table_widget.js b/bigframes/display/table_widget.js index b50acdc8ef..e6bb4e19ba 100644 --- a/bigframes/display/table_widget.js +++ b/bigframes/display/table_widget.js @@ -342,7 +342,6 @@ function render({ model, el }) { tableContainer.style.display = 'block'; footer.style.display = 'flex'; handleTableHTMLChange(); - initializeHeight(); } } From 5855a9bbdf7fd451caa307f5041f2449a616390e Mon Sep 17 00:00:00 2001 From: Shuowei Li Date: Wed, 21 Jan 2026 04:19:12 +0000 Subject: [PATCH 7/9] feat: remove flashing widget --- bigframes/display/anywidget.py | 4 +- bigframes/display/html.py | 2 +- bigframes/display/table_widget.js | 38 +- notebooks/dataframes/anywidget_mode.ipynb | 365 +++++++----------- tests/js/table_widget.test.js | 8 +- tests/system/small/test_anywidget.py | 42 +- tests/unit/display/test_anywidget.py | 4 +- .../pandas/core/config_init.py | 6 + 8 files changed, 181 insertions(+), 288 deletions(-) diff --git a/bigframes/display/anywidget.py b/bigframes/display/anywidget.py index 80703c3d69..84185f5493 100644 --- a/bigframes/display/anywidget.py +++ b/bigframes/display/anywidget.py @@ -79,9 +79,7 @@ class TableWidget(_WIDGET_BASE): start_execution = traitlets.Bool(False).tag(sync=True) is_deferred_mode = traitlets.Bool(False).tag(sync=True) - def __init__( - self, dataframe: bigframes.dataframe.DataFrame, deferred: bool = False - ): + def __init__(self, dataframe: bigframes.dataframe.DataFrame, deferred: bool = True): """Initialize the TableWidget. Args: diff --git a/bigframes/display/html.py b/bigframes/display/html.py index 1676cfe691..3214d1db32 100644 --- a/bigframes/display/html.py +++ b/bigframes/display/html.py @@ -267,7 +267,7 @@ def get_anywidget_bundle( else: df, blob_cols = obj._get_display_df_and_blob_cols() - widget = display.TableWidget(df, deferred=True) + widget = display.TableWidget(df, deferred=options.display.anywidget_deferred) widget_repr_result = widget._repr_mimebundle_(include=include, exclude=exclude) if isinstance(widget_repr_result, tuple): diff --git a/bigframes/display/table_widget.js b/bigframes/display/table_widget.js index e6bb4e19ba..d9abab0eb4 100644 --- a/bigframes/display/table_widget.js +++ b/bigframes/display/table_widget.js @@ -184,44 +184,30 @@ function render({ model, el }) { } let isHeightInitialized = false; + let lastHTML = ''; + + function handleTableHTMLChange() { + const newHTML = model.get(ModelProperty.TABLE_HTML); + if (newHTML && newHTML !== lastHTML) { + tableContainer.innerHTML = newHTML; + lastHTML = newHTML; + } - function initializeHeight() { // After the first render, dynamically set the container height to fit the // initial page (usually 10 rows) and then lock it. - const runUpdate = () => { + setTimeout(() => { if (!isHeightInitialized) { const table = tableContainer.querySelector('table'); if (table) { const tableHeight = table.offsetHeight; + // Add a small buffer(e.g. 2px) for borders to avoid scrollbars. if (tableHeight > 0) { - const style = window.getComputedStyle(tableContainer); - let maxHeight = parseFloat(style.maxHeight); - if (isNaN(maxHeight)) maxHeight = 620; // Default fallback if parsing fails or none - - // Clamp the target height to the max height to avoid conflicting with CSS - const targetHeight = Math.min(tableHeight + 2, maxHeight); - - // Use min-height to allow expansion (e.g. horizontal scrollbars) - // but prevent shrinking (jumping controls). - tableContainer.style.minHeight = `${targetHeight}px`; - tableContainer.style.height = 'auto'; // Ensure height is not locked + tableContainer.style.height = `${tableHeight + 2}px`; isHeightInitialized = true; } } } - }; - - if (window.requestAnimationFrame) { - window.requestAnimationFrame(runUpdate); - } else { - setTimeout(runUpdate, 0); - } - } - - function handleTableHTMLChange() { - tableContainer.innerHTML = model.get(ModelProperty.TABLE_HTML); - - initializeHeight(); + }, 0); const sortableColumns = model.get(ModelProperty.ORDERABLE_COLUMNS); const currentSortContext = model.get(ModelProperty.SORT_CONTEXT) || []; diff --git a/notebooks/dataframes/anywidget_mode.ipynb b/notebooks/dataframes/anywidget_mode.ipynb index 5dd8af1c5f..79fdcac746 100644 --- a/notebooks/dataframes/anywidget_mode.ipynb +++ b/notebooks/dataframes/anywidget_mode.ipynb @@ -119,17 +119,17 @@ "name": "stdout", "output_type": "stream", "text": [ - "state gender year name number\n", - " AL F 1910 Annie 482\n", - " AL F 1910 Myrtle 104\n", - " AR F 1910 Lillian 56\n", - " CT F 1910 Anne 38\n", - " CT F 1910 Frances 45\n", - " FL F 1910 Margaret 53\n", - " GA F 1910 Mae 73\n", - " GA F 1910 Beatrice 96\n", - " GA F 1910 Lola 47\n", - " IA F 1910 Viola 49\n", + "state gender year name number\n", + " AL F 1910 Lillian 99\n", + " AL F 1910 Ruby 204\n", + " AL F 1910 Helen 76\n", + " AL F 1910 Eunice 41\n", + " AR F 1910 Dora 42\n", + " CA F 1910 Edna 62\n", + " CA F 1910 Helen 239\n", + " CO F 1910 Alice 46\n", + " FL F 1910 Willie 71\n", + " FL F 1910 Thelma 65\n", "...\n", "\n", "[5552452 rows x 5 columns]\n" @@ -143,45 +143,14 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 5, "id": "220340b0", "metadata": {}, "outputs": [ { "data": { "text/html": [ - "\n", - " Query started with request ID bigframes-dev:US.161c75bd-f9f8-4b21-8a45-1d7dfc659034.
SQL
SELECT\n",
-       "`state` AS `state`,\n",
-       "`gender` AS `gender`,\n",
-       "`year` AS `year`,\n",
-       "`name` AS `name`,\n",
-       "`number` AS `number`\n",
-       "FROM\n",
-       "(SELECT\n",
-       "  `t0`.`state`,\n",
-       "  `t0`.`gender`,\n",
-       "  `t0`.`year`,\n",
-       "  `t0`.`name`,\n",
-       "  `t0`.`number`,\n",
-       "  `t0`.`bfuid_col_2` AS `bfuid_col_15`\n",
-       "FROM `bigframes-dev._8b037bfb7316dddf9d92b12dcf93e008906bfe52._c58be946_1477_4c00_b699_0ae022f13563_bqdf_8e323719-899f-4da2-89cd-2dbb53ab1dfc` AS `t0`)\n",
-       "ORDER BY `bfuid_col_15` ASC NULLS LAST
\n", - " " - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "✅ Completed. \n", - " Query processed 215.9 MB in 7 seconds of slot time. [Job bigframes-dev:US.job_IuiJsjhfPtOrKuTIOqPIjnVLX820 details]\n", - " " + "✅ Completed. " ], "text/plain": [ "" @@ -193,7 +162,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "e68fbb9eb4d24bab837c77730d31c8a1", + "model_id": "a528c6dcf6e642fcae793cf24173784f", "version_major": 2, "version_minor": 1 }, @@ -229,148 +198,105 @@ " AL\n", " F\n", " 1910\n", - " Hazel\n", - " 51\n", + " Annie\n", + " 482\n", " \n", " \n", " 1\n", " AL\n", " F\n", " 1910\n", - " Lucy\n", - " 76\n", + " Myrtle\n", + " 104\n", " \n", " \n", " 2\n", " AR\n", " F\n", " 1910\n", - " Nellie\n", - " 39\n", + " Lillian\n", + " 56\n", " \n", " \n", " 3\n", - " AR\n", + " CT\n", " F\n", " 1910\n", - " Lena\n", - " 40\n", + " Anne\n", + " 38\n", " \n", " \n", " 4\n", - " CO\n", + " CT\n", " F\n", " 1910\n", - " Thelma\n", - " 36\n", + " Frances\n", + " 45\n", " \n", " \n", " 5\n", - " CO\n", + " FL\n", " F\n", " 1910\n", - " Ruth\n", - " 68\n", + " Margaret\n", + " 53\n", " \n", " \n", " 6\n", - " CT\n", + " GA\n", " F\n", " 1910\n", - " Elizabeth\n", - " 86\n", + " Mae\n", + " 73\n", " \n", " \n", " 7\n", - " DC\n", + " GA\n", " F\n", " 1910\n", - " Mary\n", - " 80\n", + " Beatrice\n", + " 96\n", " \n", " \n", " 8\n", - " FL\n", + " GA\n", " F\n", " 1910\n", - " Annie\n", - " 101\n", + " Lola\n", + " 47\n", " \n", " \n", " 9\n", - " FL\n", + " IA\n", " F\n", " 1910\n", - " Alma\n", - " 39\n", + " Viola\n", + " 49\n", " \n", " \n", "\n", "

10 rows × 5 columns

\n", - "[5552452 rows x 5 columns in total]" + "[None rows x 5 columns in total]" ], "text/plain": [ - "state gender year name number\n", - " AL F 1910 Hazel 51\n", - " AL F 1910 Lucy 76\n", - " AR F 1910 Nellie 39\n", - " AR F 1910 Lena 40\n", - " CO F 1910 Thelma 36\n", - " CO F 1910 Ruth 68\n", - " CT F 1910 Elizabeth 86\n", - " DC F 1910 Mary 80\n", - " FL F 1910 Annie 101\n", - " FL F 1910 Alma 39\n", - "...\n", + "state gender year name number\n", + " AL F 1910 Annie 482\n", + " AL F 1910 Myrtle 104\n", + " AR F 1910 Lillian 56\n", + " CT F 1910 Anne 38\n", + " CT F 1910 Frances 45\n", + " FL F 1910 Margaret 53\n", + " GA F 1910 Mae 73\n", + " GA F 1910 Beatrice 96\n", + " GA F 1910 Lola 47\n", + " IA F 1910 Viola 49\n", "\n", - "[5552452 rows x 5 columns]" + "[? rows x 5 columns]" ] }, - "execution_count": 13, + "execution_count": 5, "metadata": {}, "output_type": "execute_result" - }, - { - "data": { - "text/html": [ - "✅ Completed. \n", - " Query processed 215.9 MB in 9 seconds of slot time. [Job bigframes-dev:US.job_IEjIRaqt2w-_pAttPw1VAVuRPxA7 details]\n", - " " - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "✅ Completed. \n", - " Query processed 215.9 MB in 5 seconds of slot time. [Job bigframes-dev:US.job_Mi-3m2AkEC1iPgWi7hmcWa1M1oIA details]\n", - " " - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "✅ Completed. \n", - " Query processed 215.9 MB in 6 seconds of slot time. [Job bigframes-dev:US.job_j8pvY385WwIY7tGvhI7Yxc62aBwd details]\n", - " " - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" } ], "source": [ @@ -396,7 +322,7 @@ "data": { "text/html": [ "✅ Completed. \n", - " Query processed 171.4 MB in 30 seconds of slot time. [Job bigframes-dev:US.ff90d507-bec8-4d24-abc3-0209ac28e21f details]\n", + " Query processed 171.4 MB in 28 seconds of slot time. [Job bigframes-dev:US.607eeda9-9c07-4058-b294-64d4549fb8ef details]\n", " " ], "text/plain": [ @@ -477,21 +403,7 @@ "data": { "text/html": [ "✅ Completed. \n", - " Query processed 88.8 MB in 3 seconds of slot time. [Job bigframes-dev:US.job_517TdI--FMoURkV7QQNMltY_-dZ7 details]\n", - " " - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "✅ Completed. \n", - " Query processed 88.8 MB in 2 seconds of slot time. [Job bigframes-dev:US.job_rCeYkeBPqmTKNFWFgwXjz5Ed8uWI details]\n", + " Query processed 88.8 MB in 5 seconds of slot time. [Job bigframes-dev:US.job_MBmce3URh52gYDHBQf9ITN02vRDq details]\n", " " ], "text/plain": [ @@ -504,7 +416,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "3e630b1a56c740e781772ca5f5c7267a", + "model_id": "e9d172afc7794ead9546caf90684a16b", "version_major": 2, "version_minor": 1 }, @@ -518,7 +430,7 @@ "6 1910\n", "7 1910\n", "8 1910\n", - "9 1910

[5552452 rows]

" + "9 1910" ], "text/plain": [ "1910\n", @@ -531,10 +443,7 @@ "1910\n", "1910\n", "1910\n", - "Name: year, dtype: Int64\n", - "...\n", - "\n", - "[5552452 rows]" + "Name: year, dtype: Int64" ] }, "execution_count": 7, @@ -592,8 +501,16 @@ "id": "programmatic-header", "metadata": {}, "source": [ - "## 3. Programmatic Widget Control\n", - "You can also instantiate the `TableWidget` directly for more control, such as checking page counts or driving navigation programmatically." + "## 3. Deferred Execution Mode\n", + "By default, complex or potentially expensive queries are loaded in **Deferred Mode**. This displays a preview of the widget and a **Run Query and Display Widget** button, allowing you to confirm the operation before loading the data.\n", + "\n", + "This is particularly useful for generative AI tasks or queries involving large-scale data processing.\n", + "\n", + "### Disabling Deferred Mode\n", + "If you prefer to always execute queries and display results immediately, you can disable deferred mode globally:\n", + "```python\n", + "bpd.options.display.anywidget_deferred = False\n", + "```" ] }, { @@ -606,7 +523,7 @@ "data": { "text/html": [ "✅ Completed. \n", - " Query processed 215.9 MB in 11 seconds of slot time. [Job bigframes-dev:US.job_XwXTDb6gWVkuyIFMeWA0waE33bSg details]\n", + " Query processed 215.9 MB in 8 seconds of slot time. [Job bigframes-dev:US.job_qXihJmA-IhC0CE2GlpuY1IbAYoHL details]\n", " " ], "text/plain": [ @@ -620,7 +537,7 @@ "data": { "text/html": [ "✅ Completed. \n", - " Query processed 215.9 MB in 7 seconds of slot time. [Job bigframes-dev:US.job_bCW0LYK5_PzyyGPf9OAg4YfNMG1C details]\n", + " Query processed 215.9 MB in 7 seconds of slot time. [Job bigframes-dev:US.job_C2fT-XkJv5dx4AyDHhM6rxsb_Wa6 details]\n", " " ], "text/plain": [ @@ -640,12 +557,12 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "a6a2b19314b04283a5a66ca9d66eb771", + "model_id": "0ccc3653aa5847efb54de2d63f9346d9", "version_major": 2, "version_minor": 1 }, "text/plain": [ - "" + "" ] }, "execution_count": 8, @@ -658,7 +575,7 @@ "import math\n", " \n", "# Create widget programmatically \n", - "widget = TableWidget(df)\n", + "widget = TableWidget(df, deferred=False)\n", "print(f\"Total pages: {math.ceil(widget.row_count / widget.page_size)}\")\n", " \n", "# Display the widget\n", @@ -755,12 +672,12 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "beb362548a6b4fd4a163569edd6f1a90", + "model_id": "1d33a211706349209e8be56691d74abe", "version_major": 2, "version_minor": 1 }, "text/plain": [ - "" + "" ] }, "execution_count": 10, @@ -771,7 +688,7 @@ "source": [ "# Test with very small dataset\n", "small_df = df.sort_values([\"name\", \"year\", \"state\"]).head(5)\n", - "small_widget = TableWidget(small_df)\n", + "small_widget = TableWidget(small_df, deferred=False)\n", "print(f\"Small dataset pages: {math.ceil(small_widget.row_count / small_widget.page_size)}\")\n", "small_widget" ] @@ -804,7 +721,7 @@ "data": { "text/html": [ "✅ Completed. \n", - " Query processed 85.9 kB in 19 seconds of slot time.\n", + " Query processed 85.9 kB in 25 seconds of slot time.\n", " " ], "text/plain": [ @@ -836,18 +753,6 @@ "metadata": {}, "output_type": "display_data" }, - { - "data": { - "text/html": [ - "✅ Completed. " - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, { "name": "stderr", "output_type": "stream", @@ -865,7 +770,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "02a46cf499b442d4bfe03934195e67df", + "model_id": "8c6da45535ee4d72b16fb52feba0799f", "version_major": 2, "version_minor": 1 }, @@ -912,17 +817,17 @@ " gs://gcs-public-data--labeled-patents/espacene...\n", " EU\n", " DE\n", - " 03.10.2018\n", - " H01L 21/20\n", - " <NA>\n", - " 18166536.5\n", - " 16.02.2016\n", + " 29.08.018\n", + " E04H 6/12\n", " <NA>\n", - " Scheider, Sascha et al\n", - " EV Group E. Thallner GmbH\n", - " Kurz, Florian\n", - " VORRICHTUNG ZUM BONDEN VON SUBSTRATEN\n", - " EP 3 382 744 A1\n", + " 18157874.1\n", + " 21.02.2018\n", + " 22.02.2017\n", + " Liedtke & Partner Patentanw√§lte\n", + " SHB Hebezeugbau GmbH\n", + " VOLGER, Alexander\n", + " STEUERUNGSSYSTEM F√úR AUTOMATISCHE PARKH√ÑUSER\n", + " EP 3 366 869 A1\n", " \n", " \n", " 1\n", @@ -931,16 +836,16 @@ " EU\n", " DE\n", " 03.10.2018\n", - " A01K 31/00\n", + " H05B 6/12\n", " <NA>\n", - " 18171005.4\n", - " 05.02.2015\n", - " 05.02.2014\n", - " Stork Bamberger Patentanw√§lte\n", - " Linco Food Systems A/S\n", - " Thrane, Uffe\n", - " MASTH√ÑHNCHENCONTAINER ALS BESTANDTEIL EINER E...\n", - " EP 3 381 276 A1\n", + " 18165514.3\n", + " 03.04.2018\n", + " 30.03.2017\n", + " <NA>\n", + " BSH Hausger√§te GmbH\n", + " Acero Acero, Jesus\n", + " VORRICHTUNG ZUR INDUKTIVEN ENERGIE√úBERTRAGUNG\n", + " EP 3 383 141 A2\n", " \n", " \n", " 2\n", @@ -967,16 +872,16 @@ " EU\n", " DE\n", " 03.10.2018\n", - " H05B 6/12\n", + " H01L 21/20\n", " <NA>\n", - " 18165514.3\n", - " 03.04.2018\n", - " 30.03.2017\n", + " 18166536.5\n", + " 16.02.2016\n", " <NA>\n", - " BSH Hausger√§te GmbH\n", - " Acero Acero, Jesus\n", - " VORRICHTUNG ZUR INDUKTIVEN ENERGIE√úBERTRAGUNG\n", - " EP 3 383 141 A2\n", + " Scheider, Sascha et al\n", + " EV Group E. Thallner GmbH\n", + " Kurz, Florian\n", + " VORRICHTUNG ZUM BONDEN VON SUBSTRATEN\n", + " EP 3 382 744 A1\n", " \n", " \n", " 4\n", @@ -984,22 +889,22 @@ " gs://gcs-public-data--labeled-patents/espacene...\n", " EU\n", " DE\n", - " 29.08.018\n", - " E04H 6/12\n", + " 03.10.2018\n", + " A01K 31/00\n", " <NA>\n", - " 18157874.1\n", - " 21.02.2018\n", - " 22.02.2017\n", - " Liedtke & Partner Patentanw√§lte\n", - " SHB Hebezeugbau GmbH\n", - " VOLGER, Alexander\n", - " STEUERUNGSSYSTEM F√úR AUTOMATISCHE PARKH√ÑUSER\n", - " EP 3 366 869 A1\n", + " 18171005.4\n", + " 05.02.2015\n", + " 05.02.2014\n", + " Stork Bamberger Patentanw√§lte\n", + " Linco Food Systems A/S\n", + " Thrane, Uffe\n", + " MASTH√ÑHNCHENCONTAINER ALS BESTANDTEIL EINER E...\n", + " EP 3 381 276 A1\n", " \n", " \n", "\n", "

5 rows × 15 columns

\n", - "[5 rows x 15 columns in total]" + "[None rows x 15 columns in total]" ], "text/plain": [ " result \\\n", @@ -1017,34 +922,34 @@ "4 gs://gcs-public-data--labeled-patents/espacene... EU DE \n", "\n", " publication_date class_international class_us application_number \\\n", - "0 03.10.2018 H01L 21/20 18166536.5 \n", - "1 03.10.2018 A01K 31/00 18171005.4 \n", + "0 29.08.018 E04H 6/12 18157874.1 \n", + "1 03.10.2018 H05B 6/12 18165514.3 \n", "2 03.10.2018 G06F 11/30 18157347.8 \n", - "3 03.10.2018 H05B 6/12 18165514.3 \n", - "4 29.08.018 E04H 6/12 18157874.1 \n", + "3 03.10.2018 H01L 21/20 18166536.5 \n", + "4 03.10.2018 A01K 31/00 18171005.4 \n", "\n", " filing_date priority_date_eu representative_line_1_eu \\\n", - "0 16.02.2016 Scheider, Sascha et al \n", - "1 05.02.2015 05.02.2014 Stork Bamberger Patentanw√§lte \n", + "0 21.02.2018 22.02.2017 Liedtke & Partner Patentanw√§lte \n", + "1 03.04.2018 30.03.2017 \n", "2 19.02.2018 31.03.2017 Hoffmann Eitle \n", - "3 03.04.2018 30.03.2017 \n", - "4 21.02.2018 22.02.2017 Liedtke & Partner Patentanw√§lte \n", + "3 16.02.2016 Scheider, Sascha et al \n", + "4 05.02.2015 05.02.2014 Stork Bamberger Patentanw√§lte \n", "\n", " applicant_line_1 inventor_line_1 \\\n", - "0 EV Group E. Thallner GmbH Kurz, Florian \n", - "1 Linco Food Systems A/S Thrane, Uffe \n", + "0 SHB Hebezeugbau GmbH VOLGER, Alexander \n", + "1 BSH Hausger√§te GmbH Acero Acero, Jesus \n", "2 FUJITSU LIMITED Kukihara, Kensuke \n", - "3 BSH Hausger√§te GmbH Acero Acero, Jesus \n", - "4 SHB Hebezeugbau GmbH VOLGER, Alexander \n", + "3 EV Group E. Thallner GmbH Kurz, Florian \n", + "4 Linco Food Systems A/S Thrane, Uffe \n", "\n", " title_line_1 number \n", - "0 VORRICHTUNG ZUM BONDEN VON SUBSTRATEN EP 3 382 744 A1 \n", - "1 MASTH√ÑHNCHENCONTAINER ALS BESTANDTEIL EINER E... EP 3 381 276 A1 \n", + "0 STEUERUNGSSYSTEM F√úR AUTOMATISCHE PARKH√ÑUSER EP 3 366 869 A1 \n", + "1 VORRICHTUNG ZUR INDUKTIVEN ENERGIE√úBERTRAGUNG EP 3 383 141 A2 \n", "2 METHOD EXECUTED BY A COMPUTER, INFORMATION PRO... EP 3 382 553 A1 \n", - "3 VORRICHTUNG ZUR INDUKTIVEN ENERGIE√úBERTRAGUNG EP 3 383 141 A2 \n", - "4 STEUERUNGSSYSTEM F√úR AUTOMATISCHE PARKH√ÑUSER EP 3 366 869 A1 \n", + "3 VORRICHTUNG ZUM BONDEN VON SUBSTRATEN EP 3 382 744 A1 \n", + "4 MASTH√ÑHNCHENCONTAINER ALS BESTANDTEIL EINER E... EP 3 381 276 A1 \n", "\n", - "[5 rows x 15 columns]" + "[? rows x 15 columns]" ] }, "execution_count": 11, diff --git a/tests/js/table_widget.test.js b/tests/js/table_widget.test.js index 7d1658852b..aaccfc03ac 100644 --- a/tests/js/table_widget.test.js +++ b/tests/js/table_widget.test.js @@ -370,8 +370,7 @@ describe('TableWidget', () => { jest.runAllTimers(); // Height should be set to the mocked offsetHeight + 2px buffer - expect(tableContainer.style.minHeight).toBe('152px'); - expect(tableContainer.style.height).toBe('auto'); + expect(tableContainer.style.height).toBe('152px'); // --- Second render (e.g., page size change) --- // Simulate the new content being taller @@ -379,9 +378,8 @@ describe('TableWidget', () => { tableHtmlChangeHandler(); jest.runAllTimers(); - // Min Height should NOT change (it's initialized once) - expect(tableContainer.style.minHeight).toBe('152px'); - expect(tableContainer.style.height).toBe('auto'); + // Height should NOT change (it's initialized once) + expect(tableContainer.style.height).toBe('152px'); // Restore original implementation Object.defineProperty(HTMLElement.prototype, 'offsetHeight', { diff --git a/tests/system/small/test_anywidget.py b/tests/system/small/test_anywidget.py index fad8f5b2b5..3f4fddfb14 100644 --- a/tests/system/small/test_anywidget.py +++ b/tests/system/small/test_anywidget.py @@ -73,7 +73,7 @@ def table_widget(paginated_bf_df: bigframes.dataframe.DataFrame): "display.repr_mode", "anywidget", "display.max_rows", 2 ): # Delay context manager cleanup of `max_rows` until after tests finish. - yield TableWidget(paginated_bf_df) + yield TableWidget(paginated_bf_df, deferred=False) @pytest.fixture(scope="module") @@ -101,7 +101,7 @@ def small_widget(small_bf_df): from bigframes.display import TableWidget with bf.option_context("display.repr_mode", "anywidget", "display.max_rows", 5): - yield TableWidget(small_bf_df) + yield TableWidget(small_bf_df, deferred=False) @pytest.fixture @@ -127,7 +127,7 @@ def unknown_row_count_widget(session): batches_iterator, total_rows=None ) with bf.option_context("display.repr_mode", "anywidget", "display.max_rows", 2): - widget = TableWidget(bf_df) + widget = TableWidget(bf_df, deferred=False) yield widget @@ -208,7 +208,7 @@ def test_widget_initialization_should_calculate_total_row_count( with bigframes.option_context( "display.repr_mode", "anywidget", "display.max_rows", 2 ): - widget = TableWidget(paginated_bf_df) + widget = TableWidget(paginated_bf_df, deferred=False) assert widget.row_count == EXPECTED_ROW_COUNT @@ -320,7 +320,7 @@ def test_widget_pagination_should_work_with_custom_page_size( ): from bigframes.display import TableWidget - widget = TableWidget(paginated_bf_df) + widget = TableWidget(paginated_bf_df, deferred=False) assert widget.page_size == 3 expected_slice = paginated_pandas_df.iloc[start_row:end_row] @@ -374,7 +374,7 @@ def test_global_options_change_should_not_affect_existing_widget_page_size( ): from bigframes.display import TableWidget - widget = TableWidget(paginated_bf_df) + widget = TableWidget(paginated_bf_df, deferred=False) initial_page_size = widget.page_size assert initial_page_size == 2 widget.page = 1 # a non-default state @@ -398,7 +398,7 @@ def test_widget_with_empty_dataframe_should_have_zero_row_count( with bigframes.option_context("display.repr_mode", "anywidget"): from bigframes.display import TableWidget - widget = TableWidget(empty_bf_df) + widget = TableWidget(empty_bf_df, deferred=False) assert widget.row_count == 0 @@ -425,7 +425,7 @@ def test_widget_with_empty_dataframe_should_render_table_headers( from bigframes.display import TableWidget - widget = TableWidget(empty_bf_df) + widget = TableWidget(empty_bf_df, deferred=False) html = widget.table_html @@ -589,7 +589,7 @@ def test_widget_should_show_error_on_batch_failure( from bigframes.display import TableWidget # The widget should handle the faulty data from the mock without crashing. - widget = TableWidget(paginated_bf_df) + widget = TableWidget(paginated_bf_df, deferred=False) # The widget should have an error message and display it in the HTML. assert widget.row_count is None @@ -611,7 +611,7 @@ def test_widget_row_count_reflects_actual_data_available( with bigframes.option_context( "display.repr_mode", "anywidget", "display.max_rows", 2 ): - widget = TableWidget(paginated_bf_df) + widget = TableWidget(paginated_bf_df, deferred=False) # The widget should report the total rows in the DataFrame, # not limited by page_size (which only affects pagination) @@ -641,7 +641,7 @@ def test_widget_with_unknown_row_count_should_auto_navigate_to_last_page( with bigframes.option_context( "display.repr_mode", "anywidget", "display.max_rows", 2 ): - widget = TableWidget(bf_df) + widget = TableWidget(bf_df, deferred=False) # Manually set row_count to None to simulate unknown total widget.row_count = None @@ -683,7 +683,7 @@ def test_widget_with_unknown_row_count_should_set_none_state_for_frontend( with bigframes.option_context( "display.repr_mode", "anywidget", "display.max_rows", 2 ): - widget = TableWidget(bf_df) + widget = TableWidget(bf_df, deferred=False) # Set row_count to None widget.row_count = None @@ -720,7 +720,7 @@ def test_widget_with_unknown_row_count_should_allow_forward_navigation( with bigframes.option_context( "display.repr_mode", "anywidget", "display.max_rows", 2 ): - widget = TableWidget(bf_df) + widget = TableWidget(bf_df, deferred=False) widget.row_count = None # Navigate to page 1 @@ -756,7 +756,7 @@ def test_widget_with_unknown_row_count_empty_dataframe( bf_df = session.read_pandas(empty_data) with bigframes.option_context("display.repr_mode", "anywidget"): - widget = TableWidget(bf_df) + widget = TableWidget(bf_df, deferred=False) widget.row_count = None # Attempt to navigate to page 5 @@ -886,7 +886,7 @@ def test_table_widget_integer_columns_disables_sorting(integer_column_df): """ from bigframes.display import TableWidget - widget = TableWidget(integer_column_df) + widget = TableWidget(integer_column_df, deferred=False) assert widget.orderable_columns == [] @@ -897,7 +897,7 @@ def test_table_widget_multiindex_columns_disables_sorting(multiindex_column_df): """ from bigframes.display import TableWidget - widget = TableWidget(multiindex_column_df) + widget = TableWidget(multiindex_column_df, deferred=False) assert widget.orderable_columns == [] @@ -1052,7 +1052,7 @@ def test_widget_with_default_index_should_display_index_column_with_empty_header from bigframes.display.anywidget import TableWidget with bf.option_context("display.repr_mode", "anywidget", "display.max_rows", 2): - widget = TableWidget(paginated_bf_df) + widget = TableWidget(paginated_bf_df, deferred=False) html = widget.table_html # The header for the index should be present but empty, matching the @@ -1076,7 +1076,7 @@ def test_widget_with_custom_index_should_display_index_column( from bigframes.display.anywidget import TableWidget with bf.option_context("display.repr_mode", "anywidget", "display.max_rows", 2): - widget = TableWidget(custom_index_bf_df) + widget = TableWidget(custom_index_bf_df, deferred=False) html = widget.table_html assert "custom_idx" in html @@ -1096,7 +1096,7 @@ def test_widget_with_custom_index_pagination_preserves_index( from bigframes.display.anywidget import TableWidget with bf.option_context("display.repr_mode", "anywidget", "display.max_rows", 2): - widget = TableWidget(custom_index_bf_df) + widget = TableWidget(custom_index_bf_df, deferred=False) widget.page = 1 # Navigate to page 2 html = widget.table_html @@ -1117,7 +1117,7 @@ def test_widget_with_custom_index_matches_pandas_output( from bigframes.display.anywidget import TableWidget with bf.option_context("display.repr_mode", "anywidget", "display.max_rows", 3): - widget = TableWidget(custom_index_bf_df) + widget = TableWidget(custom_index_bf_df, deferred=False) html = widget.table_html assert "row_1" in html @@ -1165,5 +1165,5 @@ def test_series_different_data_types_anywidget(session: bf.Session): with bf.option_context("display.repr_mode", "anywidget"): for col_name in test_data.columns: series = bf_df[col_name] - widget = bigframes.display.TableWidget(series.to_frame()) + widget = bigframes.display.TableWidget(series.to_frame(), deferred=False) assert widget.row_count == 3 diff --git a/tests/unit/display/test_anywidget.py b/tests/unit/display/test_anywidget.py index 9544ac092d..21e94ca5f8 100644 --- a/tests/unit/display/test_anywidget.py +++ b/tests/unit/display/test_anywidget.py @@ -186,7 +186,7 @@ def test_deferred_mode_initialization(mock_df): from bigframes.display.anywidget import TableWidget with mock.patch.object(TableWidget, "_initial_load") as mock_load: - widget = TableWidget(mock_df, deferred=True) + widget = TableWidget(mock_df) assert widget.is_deferred_mode is True mock_load.assert_not_called() @@ -199,7 +199,7 @@ def test_deferred_mode_execution(mock_df): # specific mock for _initial_load to avoid real execution but allow tracking calls # We need to make sure _initial_load exists on the class to patch it with mock.patch.object(TableWidget, "_initial_load") as mock_load: - widget = TableWidget(mock_df, deferred=True) + widget = TableWidget(mock_df) assert widget.is_deferred_mode is True mock_load.assert_not_called() diff --git a/third_party/bigframes_vendored/pandas/core/config_init.py b/third_party/bigframes_vendored/pandas/core/config_init.py index 194ec4a8a7..13cccf5762 100644 --- a/third_party/bigframes_vendored/pandas/core/config_init.py +++ b/third_party/bigframes_vendored/pandas/core/config_init.py @@ -154,3 +154,9 @@ class DisplayOptions: """ Height in pixels that the blob constrained to. Default None.. """ + + anywidget_deferred: bool = True + """ + If True, the interactive table widget (anywidget mode) will load in + deferred mode, showing a "Run Query" button. Default True. + """ From 0bb942620d1e04e2cf7044c96343f2f6b5bf4d6d Mon Sep 17 00:00:00 2001 From: Shuowei Li Date: Wed, 21 Jan 2026 04:43:01 +0000 Subject: [PATCH 8/9] refactor: code refactor --- bigframes/display/anywidget.py | 5 +- bigframes/display/table_widget.js | 29 +-- notebooks/dataframes/anywidget_mode.ipynb | 170 ++++++++-------- tests/unit/display/test_anywidget.py | 229 +++++++++++----------- 4 files changed, 223 insertions(+), 210 deletions(-) diff --git a/bigframes/display/anywidget.py b/bigframes/display/anywidget.py index 84185f5493..f9c1fd95f6 100644 --- a/bigframes/display/anywidget.py +++ b/bigframes/display/anywidget.py @@ -136,7 +136,10 @@ def __init__(self, dataframe: bigframes.dataframe.DataFrame, deferred: bool = Tr @traitlets.observe("start_execution") def _on_start_execution(self, change: dict[str, Any]): if change["new"]: - self._initial_load() + try: + self._initial_load() + except Exception as e: + self._error_message = str(e) self.is_deferred_mode = False def _initial_load(self) -> None: diff --git a/bigframes/display/table_widget.js b/bigframes/display/table_widget.js index d9abab0eb4..6384854d93 100644 --- a/bigframes/display/table_widget.js +++ b/bigframes/display/table_widget.js @@ -43,16 +43,22 @@ function render({ model, el }) { const errorContainer = document.createElement('div'); errorContainer.classList.add('error-message'); - const deferredContainer = document.createElement('div'); - deferredContainer.classList.add('deferred-message'); - const deferredText = document.createElement('p'); - deferredText.textContent = - 'This is a preview of the widget. The SQL query has not been executed yet.'; - const runButton = document.createElement('button'); - runButton.textContent = 'Run Query and Display Widget'; - runButton.classList.add('run-button'); - deferredContainer.appendChild(deferredText); - deferredContainer.appendChild(runButton); + function createDeferredView() { + const container = document.createElement('div'); + container.classList.add('deferred-message'); + const text = document.createElement('p'); + text.textContent = + 'This is a preview of the widget. The SQL query has not been executed yet.'; + const button = document.createElement('button'); + button.textContent = 'Run Query and Display Widget'; + button.classList.add('run-button'); + container.appendChild(text); + container.appendChild(button); + return { container, button }; + } + + const { container: deferredContainer, button: runButton } = + createDeferredView(); const tableContainer = document.createElement('div'); tableContainer.classList.add('table-container'); @@ -334,8 +340,7 @@ function render({ model, el }) { runButton.addEventListener(Event.CLICK, () => { model.set(ModelProperty.START_EXECUTION, true); model.save_changes(); - // Optimistically switch UI state or wait for model update? - // Wait for model update via observer for robustness, but could show loading here. + // Update button state to indicate loading. runButton.textContent = 'Running...'; runButton.disabled = true; }); diff --git a/notebooks/dataframes/anywidget_mode.ipynb b/notebooks/dataframes/anywidget_mode.ipynb index 79fdcac746..1e2e7d1561 100644 --- a/notebooks/dataframes/anywidget_mode.ipynb +++ b/notebooks/dataframes/anywidget_mode.ipynb @@ -119,17 +119,17 @@ "name": "stdout", "output_type": "stream", "text": [ - "state gender year name number\n", - " AL F 1910 Lillian 99\n", - " AL F 1910 Ruby 204\n", - " AL F 1910 Helen 76\n", - " AL F 1910 Eunice 41\n", - " AR F 1910 Dora 42\n", - " CA F 1910 Edna 62\n", - " CA F 1910 Helen 239\n", - " CO F 1910 Alice 46\n", - " FL F 1910 Willie 71\n", - " FL F 1910 Thelma 65\n", + "state gender year name number\n", + " AL F 1910 Cora 61\n", + " AL F 1910 Anna 74\n", + " AR F 1910 Willie 132\n", + " CO F 1910 Anna 42\n", + " FL F 1910 Louise 70\n", + " GA F 1910 Catherine 57\n", + " IL F 1910 Jessie 43\n", + " IN F 1910 Anna 100\n", + " IN F 1910 Pauline 77\n", + " IN F 1910 Beulah 39\n", "...\n", "\n", "[5552452 rows x 5 columns]\n" @@ -162,7 +162,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "a528c6dcf6e642fcae793cf24173784f", + "model_id": "d5c26aab713546749ccecae4b1f7ab6b", "version_major": 2, "version_minor": 1 }, @@ -198,80 +198,80 @@ " AL\n", " F\n", " 1910\n", - " Annie\n", - " 482\n", + " Cora\n", + " 61\n", " \n", " \n", " 1\n", " AL\n", " F\n", " 1910\n", - " Myrtle\n", - " 104\n", + " Anna\n", + " 74\n", " \n", " \n", " 2\n", " AR\n", " F\n", " 1910\n", - " Lillian\n", - " 56\n", + " Willie\n", + " 132\n", " \n", " \n", " 3\n", - " CT\n", + " CO\n", " F\n", " 1910\n", - " Anne\n", - " 38\n", + " Anna\n", + " 42\n", " \n", " \n", " 4\n", - " CT\n", + " FL\n", " F\n", " 1910\n", - " Frances\n", - " 45\n", + " Louise\n", + " 70\n", " \n", " \n", " 5\n", - " FL\n", + " GA\n", " F\n", " 1910\n", - " Margaret\n", - " 53\n", + " Catherine\n", + " 57\n", " \n", " \n", " 6\n", - " GA\n", + " IL\n", " F\n", " 1910\n", - " Mae\n", - " 73\n", + " Jessie\n", + " 43\n", " \n", " \n", " 7\n", - " GA\n", + " IN\n", " F\n", " 1910\n", - " Beatrice\n", - " 96\n", + " Anna\n", + " 100\n", " \n", " \n", " 8\n", - " GA\n", + " IN\n", " F\n", " 1910\n", - " Lola\n", - " 47\n", + " Pauline\n", + " 77\n", " \n", " \n", " 9\n", - " IA\n", + " IN\n", " F\n", " 1910\n", - " Viola\n", - " 49\n", + " Beulah\n", + " 39\n", " \n", " \n", "\n", @@ -279,17 +279,17 @@ "[None rows x 5 columns in total]" ], "text/plain": [ - "state gender year name number\n", - " AL F 1910 Annie 482\n", - " AL F 1910 Myrtle 104\n", - " AR F 1910 Lillian 56\n", - " CT F 1910 Anne 38\n", - " CT F 1910 Frances 45\n", - " FL F 1910 Margaret 53\n", - " GA F 1910 Mae 73\n", - " GA F 1910 Beatrice 96\n", - " GA F 1910 Lola 47\n", - " IA F 1910 Viola 49\n", + "state gender year name number\n", + " AL F 1910 Cora 61\n", + " AL F 1910 Anna 74\n", + " AR F 1910 Willie 132\n", + " CO F 1910 Anna 42\n", + " FL F 1910 Louise 70\n", + " GA F 1910 Catherine 57\n", + " IL F 1910 Jessie 43\n", + " IN F 1910 Anna 100\n", + " IN F 1910 Pauline 77\n", + " IN F 1910 Beulah 39\n", "\n", "[? rows x 5 columns]" ] @@ -322,7 +322,7 @@ "data": { "text/html": [ "✅ Completed. \n", - " Query processed 171.4 MB in 28 seconds of slot time. [Job bigframes-dev:US.607eeda9-9c07-4058-b294-64d4549fb8ef details]\n", + " Query processed 171.4 MB in 37 seconds of slot time. [Job bigframes-dev:US.297a1af8-220f-4ba5-a1f1-00bd5ec865ca details]\n", " " ], "text/plain": [ @@ -403,7 +403,7 @@ "data": { "text/html": [ "✅ Completed. \n", - " Query processed 88.8 MB in 5 seconds of slot time. [Job bigframes-dev:US.job_MBmce3URh52gYDHBQf9ITN02vRDq details]\n", + " Query processed 88.8 MB in 3 seconds of slot time. [Job bigframes-dev:US.job_qTZ6JkuiS8onARXv7mGERudnMxxo details]\n", " " ], "text/plain": [ @@ -416,7 +416,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "e9d172afc7794ead9546caf90684a16b", + "model_id": "db3b010baeb74d218ffe92d1f88a116e", "version_major": 2, "version_minor": 1 }, @@ -523,7 +523,7 @@ "data": { "text/html": [ "✅ Completed. \n", - " Query processed 215.9 MB in 8 seconds of slot time. [Job bigframes-dev:US.job_qXihJmA-IhC0CE2GlpuY1IbAYoHL details]\n", + " Query processed 215.9 MB in 14 seconds of slot time. [Job bigframes-dev:US.job_gni9I-cvUzZ1770rv_xL_cnA6TEa details]\n", " " ], "text/plain": [ @@ -537,7 +537,7 @@ "data": { "text/html": [ "✅ Completed. \n", - " Query processed 215.9 MB in 7 seconds of slot time. [Job bigframes-dev:US.job_C2fT-XkJv5dx4AyDHhM6rxsb_Wa6 details]\n", + " Query processed 215.9 MB in 9 seconds of slot time. [Job bigframes-dev:US.job_tnEKUOAfF3k5mWUWX1_8aAfqhTlG details]\n", " " ], "text/plain": [ @@ -557,12 +557,12 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "0ccc3653aa5847efb54de2d63f9346d9", + "model_id": "b013cf02a6a240ea90e4f74d4c1127b7", "version_major": 2, "version_minor": 1 }, "text/plain": [ - "" + "" ] }, "execution_count": 8, @@ -672,12 +672,12 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "1d33a211706349209e8be56691d74abe", + "model_id": "728b001b7c934e8db44363fab398c13c", "version_major": 2, "version_minor": 1 }, "text/plain": [ - "" + "" ] }, "execution_count": 10, @@ -721,7 +721,7 @@ "data": { "text/html": [ "✅ Completed. \n", - " Query processed 85.9 kB in 25 seconds of slot time.\n", + " Query processed 85.9 kB in 15 seconds of slot time.\n", " " ], "text/plain": [ @@ -770,7 +770,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "8c6da45535ee4d72b16fb52feba0799f", + "model_id": "c10142e3049749aba74379962fe14fb1", "version_major": 2, "version_minor": 1 }, @@ -836,16 +836,16 @@ " EU\n", " DE\n", " 03.10.2018\n", - " H05B 6/12\n", + " H01L 21/20\n", " <NA>\n", - " 18165514.3\n", - " 03.04.2018\n", - " 30.03.2017\n", + " 18166536.5\n", + " 16.02.2016\n", " <NA>\n", - " BSH Hausger√§te GmbH\n", - " Acero Acero, Jesus\n", - " VORRICHTUNG ZUR INDUKTIVEN ENERGIE√úBERTRAGUNG\n", - " EP 3 383 141 A2\n", + " Scheider, Sascha et al\n", + " EV Group E. Thallner GmbH\n", + " Kurz, Florian\n", + " VORRICHTUNG ZUM BONDEN VON SUBSTRATEN\n", + " EP 3 382 744 A1\n", " \n", " \n", " 2\n", @@ -872,16 +872,16 @@ " EU\n", " DE\n", " 03.10.2018\n", - " H01L 21/20\n", + " H05B 6/12\n", " <NA>\n", - " 18166536.5\n", - " 16.02.2016\n", + " 18165514.3\n", + " 03.04.2018\n", + " 30.03.2017\n", " <NA>\n", - " Scheider, Sascha et al\n", - " EV Group E. Thallner GmbH\n", - " Kurz, Florian\n", - " VORRICHTUNG ZUM BONDEN VON SUBSTRATEN\n", - " EP 3 382 744 A1\n", + " BSH Hausger√§te GmbH\n", + " Acero Acero, Jesus\n", + " VORRICHTUNG ZUR INDUKTIVEN ENERGIE√úBERTRAGUNG\n", + " EP 3 383 141 A2\n", " \n", " \n", " 4\n", @@ -923,30 +923,30 @@ "\n", " publication_date class_international class_us application_number \\\n", "0 29.08.018 E04H 6/12 18157874.1 \n", - "1 03.10.2018 H05B 6/12 18165514.3 \n", + "1 03.10.2018 H01L 21/20 18166536.5 \n", "2 03.10.2018 G06F 11/30 18157347.8 \n", - "3 03.10.2018 H01L 21/20 18166536.5 \n", + "3 03.10.2018 H05B 6/12 18165514.3 \n", "4 03.10.2018 A01K 31/00 18171005.4 \n", "\n", " filing_date priority_date_eu representative_line_1_eu \\\n", "0 21.02.2018 22.02.2017 Liedtke & Partner Patentanw√§lte \n", - "1 03.04.2018 30.03.2017 \n", + "1 16.02.2016 Scheider, Sascha et al \n", "2 19.02.2018 31.03.2017 Hoffmann Eitle \n", - "3 16.02.2016 Scheider, Sascha et al \n", + "3 03.04.2018 30.03.2017 \n", "4 05.02.2015 05.02.2014 Stork Bamberger Patentanw√§lte \n", "\n", " applicant_line_1 inventor_line_1 \\\n", "0 SHB Hebezeugbau GmbH VOLGER, Alexander \n", - "1 BSH Hausger√§te GmbH Acero Acero, Jesus \n", + "1 EV Group E. Thallner GmbH Kurz, Florian \n", "2 FUJITSU LIMITED Kukihara, Kensuke \n", - "3 EV Group E. Thallner GmbH Kurz, Florian \n", + "3 BSH Hausger√§te GmbH Acero Acero, Jesus \n", "4 Linco Food Systems A/S Thrane, Uffe \n", "\n", " title_line_1 number \n", "0 STEUERUNGSSYSTEM F√úR AUTOMATISCHE PARKH√ÑUSER EP 3 366 869 A1 \n", - "1 VORRICHTUNG ZUR INDUKTIVEN ENERGIE√úBERTRAGUNG EP 3 383 141 A2 \n", + "1 VORRICHTUNG ZUM BONDEN VON SUBSTRATEN EP 3 382 744 A1 \n", "2 METHOD EXECUTED BY A COMPUTER, INFORMATION PRO... EP 3 382 553 A1 \n", - "3 VORRICHTUNG ZUM BONDEN VON SUBSTRATEN EP 3 382 744 A1 \n", + "3 VORRICHTUNG ZUR INDUKTIVEN ENERGIE√úBERTRAGUNG EP 3 383 141 A2 \n", "4 MASTH√ÑHNCHENCONTAINER ALS BESTANDTEIL EINER E... EP 3 381 276 A1 \n", "\n", "[? rows x 15 columns]" diff --git a/tests/unit/display/test_anywidget.py b/tests/unit/display/test_anywidget.py index 21e94ca5f8..9c3a2fc6aa 100644 --- a/tests/unit/display/test_anywidget.py +++ b/tests/unit/display/test_anywidget.py @@ -12,173 +12,161 @@ # See the License for the specific language governing permissions and # limitations under the License. -import signal import unittest.mock as mock import pandas as pd import pytest -import bigframes +import bigframes.dtypes -# Skip if anywidget/traitlets not installed, though they should be in the dev env -pytest.importorskip("anywidget") -pytest.importorskip("traitlets") +@pytest.fixture +def mock_df(): + df = mock.Mock() + # Mock behavior for caching check (shape) + df.shape = (100, 4) + df.columns = ["A", "B", "C", "D"] + # Use actual bigframes dtypes or compatible types that works with is_orderable + df.dtypes = { + "A": bigframes.dtypes.INT_DTYPE, + "B": bigframes.dtypes.STRING_DTYPE, + "C": bigframes.dtypes.FLOAT_DTYPE, + "D": bigframes.dtypes.BOOL_DTYPE, + } + + # Ensure to_pandas_batches returns an iterable + df.to_pandas_batches.return_value = iter( + [pd.DataFrame({"A": [1], "B": ["a"], "C": [1.0], "D": [True]})] + ) + + # Ensure sort_values returns the mock itself (so to_pandas_batches is still configured) + df.sort_values.return_value = df + return df -def test_navigation_to_invalid_page_resets_to_valid_page_without_deadlock(): - """ - Given a widget on a page beyond available data, when navigating, - then it should reset to the last valid page without deadlock. - """ - from bigframes.display.anywidget import TableWidget - mock_df = mock.create_autospec(bigframes.dataframe.DataFrame, instance=True) - mock_df.columns = ["col1"] - mock_df.dtypes = {"col1": "object"} +def test_init_raises_if_anywidget_not_installed(): + with mock.patch("bigframes.display.anywidget._ANYWIDGET_INSTALLED", False): + with pytest.raises(ImportError): + from bigframes.display.anywidget import TableWidget + + TableWidget(mock.Mock()) - mock_block = mock.Mock() - mock_block.has_index = False - mock_df._block = mock_block - # We mock _initial_load to avoid complex setup +def test_init_initializes_attributes(mock_df): + from bigframes.display.anywidget import TableWidget + + # Mock _initial_load to avoid execution with mock.patch.object(TableWidget, "_initial_load"): - with bigframes.option_context( - "display.repr_mode", "anywidget", "display.max_rows", 10 - ): - widget = TableWidget(mock_df) + widget = TableWidget(mock_df) - # Simulate "loaded data but unknown total rows" state - widget.page_size = 10 - widget.row_count = None - widget._all_data_loaded = True + assert widget._dataframe is mock_df + assert widget.page == 0 + assert widget.page_size > 0 + assert widget.orderable_columns == [ + "A", + "B", + "C", + "D", + ] # Int, String, Float, Bool are orderable - # Populate cache with 1 page of data (10 rows). Page 0 is valid, page 1+ are invalid. - widget._cached_batches = [pd.DataFrame({"col1": range(10)})] - # Mark initial load as complete so observers fire - widget._initial_load_complete = True +def test_init_calls_initial_load(mock_df): + from bigframes.display.anywidget import TableWidget + + with mock.patch.object(TableWidget, "_initial_load") as mock_load: + TableWidget(mock_df, deferred=False) + mock_load.assert_called_once() - # Setup timeout to fail fast if deadlock occurs - # signal.SIGALRM is not available on Windows - has_sigalrm = hasattr(signal, "SIGALRM") - if has_sigalrm: - def handler(signum, frame): - raise TimeoutError("Deadlock detected!") +def test_validate_page_clamping(mock_df): + from bigframes.display.anywidget import TableWidget - signal.signal(signal.SIGALRM, handler) - signal.alarm(2) # 2 seconds timeout + with mock.patch.object(TableWidget, "_initial_load"): + widget = TableWidget(mock_df) + widget.row_count = 100 + widget.page_size = 10 - try: - # Trigger navigation to page 5 (invalid), which should reset to page 0 + # Valid page widget.page = 5 + assert widget.page == 5 - assert widget.page == 0 + # Negative page + with pytest.raises(ValueError): + widget.page = -1 - finally: - if has_sigalrm: - signal.alarm(0) + # Page too high + widget.page = 100 + assert widget.page == 9 # Max page is 9 (0-9) -def test_css_contains_dark_mode_selectors(): - """Test that the CSS for dark mode is loaded with all required selectors.""" +def test_validate_page_size(mock_df): from bigframes.display.anywidget import TableWidget - mock_df = mock.create_autospec(bigframes.dataframe.DataFrame, instance=True) - # mock_df.columns and mock_df.dtypes are needed for __init__ - mock_df.columns = ["col1"] - mock_df.dtypes = {"col1": "object"} - - # Mock _block to avoid AttributeError during _set_table_html - mock_block = mock.Mock() - mock_block.has_index = False - mock_df._block = mock_block - with mock.patch.object(TableWidget, "_initial_load"): widget = TableWidget(mock_df) - css = widget._css - assert "@media (prefers-color-scheme: dark)" in css - assert 'html[theme="dark"]' in css - assert 'body[data-theme="dark"]' in css + # Valid page size + widget.page_size = 50 + assert widget.page_size == 50 -@pytest.fixture -def mock_df(): - """A mock DataFrame that can be used in multiple tests.""" - df = mock.create_autospec(bigframes.dataframe.DataFrame, instance=True) - df.columns = ["col1", "col2"] - df.dtypes = {"col1": "int64", "col2": "int64"} - - mock_block = mock.Mock() - mock_block.has_index = False - df._block = mock_block - - # Mock to_pandas_batches to return empty iterator or simple data - batch_df = pd.DataFrame({"col1": [1, 2], "col2": [3, 4]}) - batches = mock.MagicMock() - batches.__iter__.return_value = iter([batch_df]) - batches.total_rows = 2 - df.to_pandas_batches.return_value = batches - - # Mock sort_values to return self (for chaining) - df.sort_values.return_value = df + # Negative/Zero page size (should be ignored/reset to previous) + # Note: Traitlets validation returns the *value to set*. + # Our validator returns self.page_size if input <= 0. + original_size = widget.page_size + widget.page_size = -5 + assert widget.page_size == original_size - return df + # Too large page size + widget.page_size = 10000 + assert widget.page_size == 1000 -def test_sorting_single_column(mock_df): - """Test that the widget can be sorted by a single column.""" +def test_page_size_change_resets_page_and_sort(mock_df): from bigframes.display.anywidget import TableWidget - with bigframes.option_context("display.repr_mode", "anywidget"): + with mock.patch.object(TableWidget, "_initial_load"): widget = TableWidget(mock_df) + widget._initial_load_complete = True # Enable observers + widget.page = 5 + widget.sort_context = [{"column": "A", "ascending": True}] - # Verify initial state - assert widget.sort_context == [] - - # Apply sort - widget.sort_context = [{"column": "col1", "ascending": True}] - - # This should trigger _sort_changed -> _set_table_html - # which calls df.sort_values + # Change page size + widget.page_size = 20 - mock_df.sort_values.assert_called_with(by=["col1"], ascending=[True]) + assert widget.page == 0 + assert widget.sort_context == [] -def test_sorting_multi_column(mock_df): - """Test that the widget can be sorted by multiple columns.""" +def test_page_size_change_resets_batches(mock_df): from bigframes.display.anywidget import TableWidget - with bigframes.option_context("display.repr_mode", "anywidget"): + with mock.patch.object(TableWidget, "_initial_load"): widget = TableWidget(mock_df) + widget._initial_load_complete = True # Enable observers - # Apply multi-column sort - widget.sort_context = [ - {"column": "col1", "ascending": True}, - {"column": "col2", "ascending": False}, - ] + # Trigger page size change + widget.page_size = 50 - mock_df.sort_values.assert_called_with(by=["col1", "col2"], ascending=[True, False]) + # to_pandas_batches called in _reset_batches_for_new_page_size + mock_df.to_pandas_batches.assert_called() def test_page_size_change_resets_sort(mock_df): - """Test that changing the page size resets the sorting.""" from bigframes.display.anywidget import TableWidget - with bigframes.option_context("display.repr_mode", "anywidget"): + with mock.patch.object(TableWidget, "_initial_load"): widget = TableWidget(mock_df) + widget._initial_load_complete = True - # Set sort state - widget.sort_context = [{"column": "col1", "ascending": True}] - - # Change page size - widget.page_size = 50 + # Setup initial batches mock + mock_df.to_pandas_batches.reset_mock() - # Sort should be reset - assert widget.sort_context == [] + # Change sort + widget.sort_context = [{"column": "B", "ascending": False}] # to_pandas_batches called again (reset) - assert mock_df.to_pandas_batches.call_count >= 2 + # Note: _initial_load is mocked, so this is the first call in this test setup + assert mock_df.to_pandas_batches.call_count >= 1 def test_deferred_mode_initialization(mock_df): @@ -212,6 +200,23 @@ def test_deferred_mode_execution(mock_df): assert widget.is_deferred_mode is False +def test_deferred_mode_execution_error(mock_df): + """Test that error during deferred execution is handled.""" + from bigframes.display.anywidget import TableWidget + + with mock.patch.object(TableWidget, "_initial_load") as mock_load: + mock_load.side_effect = RuntimeError("Query Failed") + + widget = TableWidget(mock_df) + + # Simulate user clicking "Run" + widget.start_execution = True + + # Verify mode switched and error set + assert widget.is_deferred_mode is False + assert widget._error_message == "Query Failed" + + def test_normal_mode_initialization(mock_df): """Test that normal mode loads data initially.""" from bigframes.display.anywidget import TableWidget From 876f8ae9f5fe017b230ae852d7c4aaa34f219600 Mon Sep 17 00:00:00 2001 From: Shuowei Li Date: Wed, 21 Jan 2026 19:58:22 +0000 Subject: [PATCH 9/9] fix: Robust optional dependency handling for TableWidget and consolidated JS tests --- bigframes/display/anywidget.py | 72 ++++++++++++++ tests/js/table_widget.test.js | 88 +++++++++++++++++ tests/js/table_widget_deferred.test.js | 129 ------------------------- tests/unit/display/test_anywidget.py | 43 +++++---- 4 files changed, 183 insertions(+), 149 deletions(-) delete mode 100644 tests/js/table_widget_deferred.test.js diff --git a/bigframes/display/anywidget.py b/bigframes/display/anywidget.py index f9c1fd95f6..5c1da8c875 100644 --- a/bigframes/display/anywidget.py +++ b/bigframes/display/anywidget.py @@ -44,6 +44,78 @@ except Exception: _ANYWIDGET_INSTALLED = False + class _DummyTraitlet: + def __init__(self, default_value=None): + self.default_value = default_value + self.name = None + + def tag(self, **kwargs): + return self + + def __set_name__(self, owner, name): + self.name = name + + def __get__(self, instance, owner): + if instance is None: + return self + return instance.__dict__.get(self.name, self.default_value) + + def __set__(self, instance, value): + # Basic mimic of traitlets validation/observation + # Look for validators registered via @validate + for attr_name in dir(instance): + attr = getattr(type(instance), attr_name, None) + if ( + attr is not None + and hasattr(attr, "_validated_trait") + and attr._validated_trait == self.name + ): + value = getattr(instance, attr_name)({"value": value}) + + instance.__dict__[self.name] = value + + # Look for observers registered via @observe + for attr_name in dir(instance): + attr = getattr(type(instance), attr_name, None) + if ( + attr is not None + and hasattr(attr, "_observed_trait") + and attr._observed_trait == self.name + ): + getattr(instance, attr_name)({"new": value}) + + class _DummyTraitletsModule: + def Int(self, default_value=0, **kwargs): + return _DummyTraitlet(default_value) + + def Unicode(self, default_value="", **kwargs): + return _DummyTraitlet(default_value) + + def List(self, trait=None, default_value=None, **kwargs): + return _DummyTraitlet(default_value if default_value is not None else []) + + def Bool(self, default_value=False, **kwargs): + return _DummyTraitlet(default_value) + + def Dict(self, *args, **kwargs): + return _DummyTraitlet({}) + + def observe(self, trait_name, **kwargs): + def decorator(func): + func._observed_trait = trait_name + return func + + return decorator + + def validate(self, trait_name, **kwargs): + def decorator(func): + func._validated_trait = trait_name + return func + + return decorator + + traitlets = _DummyTraitletsModule() # type: ignore[assignment] + _WIDGET_BASE: type[Any] if _ANYWIDGET_INSTALLED: _WIDGET_BASE = anywidget.AnyWidget diff --git a/tests/js/table_widget.test.js b/tests/js/table_widget.test.js index aaccfc03ac..5435b6c0f4 100644 --- a/tests/js/table_widget.test.js +++ b/tests/js/table_widget.test.js @@ -528,4 +528,92 @@ describe('TableWidget', () => { expect(model.save_changes).toHaveBeenCalled(); }); }); + + describe('Deferred Mode UI', () => { + it('should show deferred message and hide table when in deferred mode', () => { + // Mock deferred mode = true + model.get.mockImplementation((property) => { + if (property === 'is_deferred_mode') return true; + if (property === 'table_html') return '
'; + return null; + }); + + render({ model, el }); + + const deferredContainer = el.querySelector('.deferred-message'); + const tableContainer = el.querySelector('.table-container'); + const footer = el.querySelector('.footer'); + + expect(deferredContainer.style.display).toBe('flex'); + expect(tableContainer.style.display).toBe('none'); + expect(footer.style.display).toBe('none'); + expect(deferredContainer.textContent).toContain( + 'This is a preview of the widget', + ); + }); + + it('should show table and hide deferred message when not in deferred mode', () => { + // Mock deferred mode = false + model.get.mockImplementation((property) => { + if (property === 'is_deferred_mode') return false; + if (property === 'table_html') return '
'; + return null; + }); + + render({ model, el }); + + const deferredContainer = el.querySelector('.deferred-message'); + const tableContainer = el.querySelector('.table-container'); + const footer = el.querySelector('.footer'); + + expect(deferredContainer.style.display).toBe('none'); + expect(tableContainer.style.display).toBe('block'); + expect(footer.style.display).toBe('flex'); + }); + + it('should trigger start_execution when run button is clicked', () => { + model.get.mockImplementation((property) => { + if (property === 'is_deferred_mode') return true; + return null; + }); + + render({ model, el }); + + const runButton = el.querySelector('.run-button'); + runButton.click(); + + expect(model.set).toHaveBeenCalledWith('start_execution', true); + expect(model.save_changes).toHaveBeenCalled(); + expect(runButton.textContent).toBe('Running...'); + expect(runButton.disabled).toBe(true); + }); + + it('should update UI when is_deferred_mode changes', () => { + // Start in deferred mode + let isDeferred = true; + model.get.mockImplementation((property) => { + if (property === 'is_deferred_mode') return isDeferred; + if (property === 'table_html') return '
'; + return null; + }); + + render({ model, el }); + + const deferredContainer = el.querySelector('.deferred-message'); + const tableContainer = el.querySelector('.table-container'); + + expect(deferredContainer.style.display).toBe('flex'); + expect(tableContainer.style.display).toBe('none'); + + // Change to non-deferred mode + isDeferred = false; + const changeHandler = model.on.mock.calls.find( + (call) => call[0] === 'change:is_deferred_mode', + )[1]; + changeHandler(); + + expect(deferredContainer.style.display).toBe('none'); + expect(tableContainer.style.display).toBe('block'); + }); + }); }); diff --git a/tests/js/table_widget_deferred.test.js b/tests/js/table_widget_deferred.test.js deleted file mode 100644 index 6649641eb6..0000000000 --- a/tests/js/table_widget_deferred.test.js +++ /dev/null @@ -1,129 +0,0 @@ -/* - * Copyright 2025 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { jest } from '@jest/globals'; - -describe('TableWidget Deferred Mode', () => { - let model; - let el; - let render; - - beforeEach(async () => { - jest.resetModules(); - document.body.innerHTML = '
'; - el = document.body.querySelector('div'); - - const tableWidget = ( - await import('../../bigframes/display/table_widget.js') - ).default; - render = tableWidget.render; - - model = { - get: jest.fn(), - set: jest.fn(), - save_changes: jest.fn(), - on: jest.fn(), - }; - }); - - describe('Deferred Mode UI', () => { - it('should show deferred message and hide table when in deferred mode', () => { - // Mock deferred mode = true - model.get.mockImplementation((property) => { - if (property === 'is_deferred_mode') return true; - if (property === 'table_html') return '
'; - return null; - }); - - render({ model, el }); - - const deferredContainer = el.querySelector('.deferred-message'); - const tableContainer = el.querySelector('.table-container'); - const footer = el.querySelector('.footer'); - - expect(deferredContainer.style.display).toBe('flex'); - expect(tableContainer.style.display).toBe('none'); - expect(footer.style.display).toBe('none'); - expect(deferredContainer.textContent).toContain( - 'This is a preview of the widget', - ); - }); - - it('should show table and hide deferred message when not in deferred mode', () => { - // Mock deferred mode = false - model.get.mockImplementation((property) => { - if (property === 'is_deferred_mode') return false; - if (property === 'table_html') return '
'; - return null; - }); - - render({ model, el }); - - const deferredContainer = el.querySelector('.deferred-message'); - const tableContainer = el.querySelector('.table-container'); - const footer = el.querySelector('.footer'); - - expect(deferredContainer.style.display).toBe('none'); - expect(tableContainer.style.display).toBe('block'); - expect(footer.style.display).toBe('flex'); - }); - - it('should trigger start_execution when run button is clicked', () => { - model.get.mockImplementation((property) => { - if (property === 'is_deferred_mode') return true; - return null; - }); - - render({ model, el }); - - const runButton = el.querySelector('.run-button'); - runButton.click(); - - expect(model.set).toHaveBeenCalledWith('start_execution', true); - expect(model.save_changes).toHaveBeenCalled(); - expect(runButton.textContent).toBe('Running...'); - expect(runButton.disabled).toBe(true); - }); - - it('should update UI when is_deferred_mode changes', () => { - // Start in deferred mode - let isDeferred = true; - model.get.mockImplementation((property) => { - if (property === 'is_deferred_mode') return isDeferred; - if (property === 'table_html') return '
'; - return null; - }); - - render({ model, el }); - - const deferredContainer = el.querySelector('.deferred-message'); - const tableContainer = el.querySelector('.table-container'); - - expect(deferredContainer.style.display).toBe('flex'); - expect(tableContainer.style.display).toBe('none'); - - // Change to non-deferred mode - isDeferred = false; - const changeHandler = model.on.mock.calls.find( - (call) => call[0] === 'change:is_deferred_mode', - )[1]; - changeHandler(); - - expect(deferredContainer.style.display).toBe('none'); - expect(tableContainer.style.display).toBe('block'); - }); - }); -}); diff --git a/tests/unit/display/test_anywidget.py b/tests/unit/display/test_anywidget.py index 9c3a2fc6aa..9be541aeed 100644 --- a/tests/unit/display/test_anywidget.py +++ b/tests/unit/display/test_anywidget.py @@ -22,26 +22,29 @@ @pytest.fixture def mock_df(): - df = mock.Mock() - # Mock behavior for caching check (shape) - df.shape = (100, 4) - df.columns = ["A", "B", "C", "D"] - # Use actual bigframes dtypes or compatible types that works with is_orderable - df.dtypes = { - "A": bigframes.dtypes.INT_DTYPE, - "B": bigframes.dtypes.STRING_DTYPE, - "C": bigframes.dtypes.FLOAT_DTYPE, - "D": bigframes.dtypes.BOOL_DTYPE, - } - - # Ensure to_pandas_batches returns an iterable - df.to_pandas_batches.return_value = iter( - [pd.DataFrame({"A": [1], "B": ["a"], "C": [1.0], "D": [True]})] - ) - - # Ensure sort_values returns the mock itself (so to_pandas_batches is still configured) - df.sort_values.return_value = df - return df + # Mock _ANYWIDGET_INSTALLED to True so we can test the class logic + # even when anywidget isn't installed in the test environment. + with mock.patch("bigframes.display.anywidget._ANYWIDGET_INSTALLED", True): + df = mock.Mock() + # Mock behavior for caching check (shape) + df.shape = (100, 4) + df.columns = ["A", "B", "C", "D"] + # Use actual bigframes dtypes or compatible types that works with is_orderable + df.dtypes = { + "A": bigframes.dtypes.INT_DTYPE, + "B": bigframes.dtypes.STRING_DTYPE, + "C": bigframes.dtypes.FLOAT_DTYPE, + "D": bigframes.dtypes.BOOL_DTYPE, + } + + # Ensure to_pandas_batches returns an iterable + df.to_pandas_batches.return_value = iter( + [pd.DataFrame({"A": [1], "B": ["a"], "C": [1.0], "D": [True]})] + ) + + # Ensure sort_values returns the mock itself (so to_pandas_batches is still configured) + df.sort_values.return_value = df + yield df def test_init_raises_if_anywidget_not_installed():