Skip to content

Commit 3ce375d

Browse files
committed
test: add more test cases
1 parent 5030067 commit 3ce375d

File tree

8 files changed

+692
-970
lines changed

8 files changed

+692
-970
lines changed

bigframes/display/anywidget.py

Lines changed: 62 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
from importlib import resources
2121
import functools
2222
import math
23-
from typing import Any, Dict, Iterator, List, Optional, Type
23+
from typing import Any, Iterator
2424
import uuid
2525

2626
import pandas as pd
@@ -43,7 +43,7 @@
4343
except Exception:
4444
ANYWIDGET_INSTALLED = False
4545

46-
WIDGET_BASE: Type[Any]
46+
WIDGET_BASE: type[Any]
4747
if ANYWIDGET_INSTALLED:
4848
WIDGET_BASE = anywidget.AnyWidget
4949
else:
@@ -65,17 +65,13 @@ class TableWidget(WIDGET_BASE):
6565

6666
page = traitlets.Int(0).tag(sync=True)
6767
page_size = traitlets.Int(0).tag(sync=True)
68-
row_count = traitlets.Union(
69-
[traitlets.Int(), traitlets.Instance(type(None))],
70-
default_value=None,
71-
allow_none=True,
72-
).tag(sync=True)
68+
row_count = traitlets.Int(allow_none=True, default_value=None).tag(sync=True)
7369
table_html = traitlets.Unicode().tag(sync=True)
7470
sort_column = traitlets.Unicode("").tag(sync=True)
7571
sort_ascending = traitlets.Bool(True).tag(sync=True)
7672
orderable_columns = traitlets.List(traitlets.Unicode(), []).tag(sync=True)
7773
_initial_load_complete = traitlets.Bool(False).tag(sync=True)
78-
_batches: Optional[blocks.PandasBatches] = None
74+
_batches: blocks.PandasBatches | None = None
7975
_error_message = traitlets.Unicode(allow_none=True, default_value=None).tag(
8076
sync=True
8177
)
@@ -88,7 +84,8 @@ def __init__(self, dataframe: bigframes.dataframe.DataFrame):
8884
"""
8985
if not ANYWIDGET_INSTALLED:
9086
raise ImportError(
91-
"Please `pip install anywidget traitlets` or `pip install 'bigframes[anywidget]'` to use TableWidget."
87+
"Please `pip install anywidget traitlets` or "
88+
"`pip install 'bigframes[anywidget]'` to use TableWidget."
9289
)
9390

9491
self._dataframe = dataframe
@@ -98,9 +95,9 @@ def __init__(self, dataframe: bigframes.dataframe.DataFrame):
9895
# Initialize attributes that might be needed by observers first
9996
self._table_id = str(uuid.uuid4())
10097
self._all_data_loaded = False
101-
self._batch_iter: Optional[Iterator[pd.DataFrame]] = None
102-
self._cached_batches: List[pd.DataFrame] = []
103-
self._last_sort_state: Optional[_SortState] = None
98+
self._batch_iter: Iterator[pd.DataFrame] | None = None
99+
self._cached_batches: list[pd.DataFrame] = []
100+
self._last_sort_state: _SortState | None = None
104101

105102
# respect display options for initial page size
106103
initial_page_size = bigframes.options.display.max_rows
@@ -124,7 +121,10 @@ def __init__(self, dataframe: bigframes.dataframe.DataFrame):
124121
self._reset_batches_for_new_page_size()
125122

126123
if self._batches is None:
127-
self._error_message = "Could not retrieve data batches. Data might be unavailable or an error occurred."
124+
self._error_message = (
125+
"Could not retrieve data batches. Data might be unavailable or "
126+
"an error occurred."
127+
)
128128
self.row_count = None
129129
elif self._batches.total_rows is None:
130130
# Total rows is unknown, this is an expected state.
@@ -143,7 +143,7 @@ def __init__(self, dataframe: bigframes.dataframe.DataFrame):
143143
self._initial_load_complete = True
144144

145145
@traitlets.observe("_initial_load_complete")
146-
def _on_initial_load_complete(self, change: Dict[str, Any]):
146+
def _on_initial_load_complete(self, change: dict[str, Any]):
147147
if change["new"]:
148148
self._set_table_html()
149149

@@ -158,7 +158,7 @@ def _css(self):
158158
return resources.read_text(bigframes.display, "table_widget.css")
159159

160160
@traitlets.validate("page")
161-
def _validate_page(self, proposal: Dict[str, Any]) -> int:
161+
def _validate_page(self, proposal: dict[str, Any]) -> int:
162162
"""Validate and clamp the page number to a valid range.
163163
164164
Args:
@@ -191,7 +191,7 @@ def _validate_page(self, proposal: Dict[str, Any]) -> int:
191191
return max(0, min(value, max_page))
192192

193193
@traitlets.validate("page_size")
194-
def _validate_page_size(self, proposal: Dict[str, Any]) -> int:
194+
def _validate_page_size(self, proposal: dict[str, Any]) -> int:
195195
"""Validate page size to ensure it's positive and reasonable.
196196
197197
Args:
@@ -229,10 +229,6 @@ def _get_next_batch(self) -> bool:
229229
except StopIteration:
230230
self._all_data_loaded = True
231231
return False
232-
except Exception as e:
233-
# Handle other potential errors
234-
self._error_message = f"Error loading data: {str(e)}"
235-
return False
236232

237233
@property
238234
def _batch_iterator(self) -> Iterator[pd.DataFrame]:
@@ -272,7 +268,8 @@ def _set_table_html(self) -> None:
272268
try:
273269
if self._error_message:
274270
self.table_html = (
275-
f"<div class='bigframes-error-message'>{self._error_message}</div>"
271+
f"<div class='bigframes-error-message'>"
272+
f"{self._error_message}</div>"
276273
)
277274
return
278275

@@ -297,48 +294,53 @@ def _set_table_html(self) -> None:
297294
)
298295
self.page = 0 # Reset to first page
299296

300-
start = self.page * self.page_size
301-
end = start + self.page_size
302-
303-
# fetch more data if the requested page is outside our cache
304-
cached_data = self._cached_data
305-
while len(cached_data) < end and not self._all_data_loaded:
306-
if self._get_next_batch():
307-
cached_data = self._cached_data
308-
else:
309-
break
297+
page_data = pd.DataFrame()
298+
# This loop is to handle auto-correction of page number when row count is unknown
299+
while True:
300+
start = self.page * self.page_size
301+
end = start + self.page_size
302+
303+
# fetch more data if the requested page is outside our cache
304+
cached_data = self._cached_data
305+
while len(cached_data) < end and not self._all_data_loaded:
306+
if self._get_next_batch():
307+
cached_data = self._cached_data
308+
else:
309+
break
310+
311+
# Get the data for the current page
312+
page_data = cached_data.iloc[start:end].copy()
313+
314+
# Handle case where user navigated beyond available data with unknown row count
315+
is_unknown_count = self.row_count is None
316+
is_beyond_data = (
317+
self._all_data_loaded and len(page_data) == 0 and self.page > 0
318+
)
319+
if is_unknown_count and is_beyond_data:
320+
# Calculate the last valid page (zero-indexed)
321+
total_rows = len(cached_data)
322+
last_valid_page = max(0, math.ceil(total_rows / self.page_size) - 1)
323+
# Navigate back to the last valid page
324+
self.page = last_valid_page
325+
# Continue the loop to re-calculate page data
326+
continue
310327

311-
# Get the data for the current page
312-
page_data = cached_data.iloc[start:end].copy()
328+
# If page is valid, break out of the loop.
329+
break
313330

314331
# Handle index display
315-
# TODO(b/438181139): Add tests for custom multiindex
316332
if self._dataframe._block.has_index:
317-
index_name = page_data.index.name
318-
page_data.insert(
319-
0, index_name if index_name is not None else "", page_data.index
333+
is_unnamed_single_index = (
334+
page_data.index.name is None
335+
and not isinstance(page_data.index, pd.MultiIndex)
320336
)
321-
else:
322-
# Default index - include as "Row" column
337+
page_data = page_data.reset_index()
338+
if is_unnamed_single_index and "index" in page_data.columns:
339+
page_data.rename(columns={"index": ""}, inplace=True)
340+
341+
# Default index - include as "Row" column if no index was present originally
342+
if not self._dataframe._block.has_index:
323343
page_data.insert(0, "Row", range(start + 1, start + len(page_data) + 1))
324-
# Handle case where user navigated beyond available data with unknown row count
325-
is_unknown_count = self.row_count is None
326-
is_beyond_data = (
327-
self._all_data_loaded and len(page_data) == 0 and self.page > 0
328-
)
329-
if is_unknown_count and is_beyond_data:
330-
# Calculate the last valid page (zero-indexed)
331-
total_rows = len(cached_data)
332-
if total_rows > 0:
333-
last_valid_page = max(0, math.ceil(total_rows / self.page_size) - 1)
334-
# Navigate back to the last valid page
335-
self.page = last_valid_page
336-
# Recursively call to display the correct page
337-
return self._set_table_html()
338-
else:
339-
# If no data at all, stay on page 0 with empty display
340-
self.page = 0
341-
return self._set_table_html()
342344

343345
# Generate HTML table
344346
self.table_html = bigframes.display.html.render_html(
@@ -349,19 +351,19 @@ def _set_table_html(self) -> None:
349351
delattr(self, "_setting_html")
350352

351353
@traitlets.observe("sort_column", "sort_ascending")
352-
def _sort_changed(self, _change: Dict[str, Any]):
354+
def _sort_changed(self, _change: dict[str, Any]):
353355
"""Handler for when sorting parameters change from the frontend."""
354356
self._set_table_html()
355357

356358
@traitlets.observe("page")
357-
def _page_changed(self, _change: Dict[str, Any]) -> None:
359+
def _page_changed(self, _change: dict[str, Any]) -> None:
358360
"""Handler for when the page number is changed from the frontend."""
359361
if not self._initial_load_complete:
360362
return
361363
self._set_table_html()
362364

363365
@traitlets.observe("page_size")
364-
def _page_size_changed(self, _change: Dict[str, Any]) -> None:
366+
def _page_size_changed(self, _change: dict[str, Any]) -> None:
365367
"""Handler for when the page size is changed from the frontend."""
366368
if not self._initial_load_complete:
367369
return

0 commit comments

Comments
 (0)