2020from importlib import resources
2121import functools
2222import math
23- from typing import Any , Dict , Iterator , List , Optional , Type
23+ from typing import Any , Iterator
2424import uuid
2525
2626import pandas as pd
4343except Exception :
4444 ANYWIDGET_INSTALLED = False
4545
46- WIDGET_BASE : Type [Any ]
46+ WIDGET_BASE : type [Any ]
4747if ANYWIDGET_INSTALLED :
4848 WIDGET_BASE = anywidget .AnyWidget
4949else :
@@ -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