Skip to content

Commit e016751

Browse files
committed
Merge branch 'main' into shuowei-anywidget-sort-multi-column
# Conflicts: # bigframes/display/html.py
2 parents f7ad44e + 69fa7f4 commit e016751

File tree

13 files changed

+238
-123
lines changed

13 files changed

+238
-123
lines changed

bigframes/core/log_adapter.py

Lines changed: 62 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,8 @@ def wrapper(*args, **kwargs):
174174
full_method_name = f"{base_name.lower()}-{api_method_name}"
175175
# Track directly called methods
176176
if len(_call_stack) == 0:
177-
add_api_method(full_method_name)
177+
session = _find_session(*args, **kwargs)
178+
add_api_method(full_method_name, session=session)
178179

179180
_call_stack.append(full_method_name)
180181

@@ -220,7 +221,8 @@ def wrapped(*args, **kwargs):
220221
full_property_name = f"{class_name.lower()}-{property_name.lower()}"
221222

222223
if len(_call_stack) == 0:
223-
add_api_method(full_property_name)
224+
session = _find_session(*args, **kwargs)
225+
add_api_method(full_property_name, session=session)
224226

225227
_call_stack.append(full_property_name)
226228
try:
@@ -250,25 +252,41 @@ def wrapper(func):
250252
return wrapper
251253

252254

253-
def add_api_method(api_method_name):
255+
def add_api_method(api_method_name, session=None):
254256
global _lock
255257
global _api_methods
256-
with _lock:
257-
# Push the method to the front of the _api_methods list
258-
_api_methods.insert(0, api_method_name.replace("<", "").replace(">", ""))
259-
# Keep the list length within the maximum limit (adjust MAX_LABELS_COUNT as needed)
260-
_api_methods = _api_methods[:MAX_LABELS_COUNT]
261258

259+
clean_method_name = api_method_name.replace("<", "").replace(">", "")
260+
261+
if session is not None and _is_session_initialized(session):
262+
with session._api_methods_lock:
263+
session._api_methods.insert(0, clean_method_name)
264+
session._api_methods = session._api_methods[:MAX_LABELS_COUNT]
265+
else:
266+
with _lock:
267+
# Push the method to the front of the _api_methods list
268+
_api_methods.insert(0, clean_method_name)
269+
# Keep the list length within the maximum limit (adjust MAX_LABELS_COUNT as needed)
270+
_api_methods = _api_methods[:MAX_LABELS_COUNT]
262271

263-
def get_and_reset_api_methods(dry_run: bool = False):
272+
273+
def get_and_reset_api_methods(dry_run: bool = False, session=None):
264274
global _lock
275+
methods = []
276+
277+
if session is not None and _is_session_initialized(session):
278+
with session._api_methods_lock:
279+
methods.extend(session._api_methods)
280+
if not dry_run:
281+
session._api_methods.clear()
282+
265283
with _lock:
266-
previous_api_methods = list(_api_methods)
284+
methods.extend(_api_methods)
267285

268286
# dry_run might not make a job resource, so only reset the log on real queries.
269287
if not dry_run:
270288
_api_methods.clear()
271-
return previous_api_methods
289+
return methods
272290

273291

274292
def _get_bq_client(*args, **kwargs):
@@ -283,3 +301,36 @@ def _get_bq_client(*args, **kwargs):
283301
return kwargv._block.session.bqclient
284302

285303
return None
304+
305+
306+
def _is_session_initialized(session):
307+
"""Return True if fully initialized.
308+
309+
Because the method logger could get called before Session.__init__ has a
310+
chance to run, we use the globals in that case.
311+
"""
312+
return hasattr(session, "_api_methods_lock") and hasattr(session, "_api_methods")
313+
314+
315+
def _find_session(*args, **kwargs):
316+
# This function cannot import Session at the top level because Session
317+
# imports log_adapter.
318+
from bigframes.session import Session
319+
320+
session = args[0] if args else None
321+
if (
322+
session is not None
323+
and isinstance(session, Session)
324+
and _is_session_initialized(session)
325+
):
326+
return session
327+
328+
session = kwargs.get("session")
329+
if (
330+
session is not None
331+
and isinstance(session, Session)
332+
and _is_session_initialized(session)
333+
):
334+
return session
335+
336+
return None

bigframes/display/html.py

Lines changed: 35 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -48,60 +48,62 @@ def render_html(
4848
orderable_columns: list[str] | None = None,
4949
) -> str:
5050
"""Render a pandas DataFrame to HTML with specific styling."""
51-
classes = "dataframe table table-striped table-hover"
52-
table_html = [f'<table border="1" class="{classes}" id="{table_id}">']
53-
precision = options.display.precision
5451
orderable_columns = orderable_columns or []
52+
classes = "dataframe table table-striped table-hover"
53+
table_html_parts = [f'<table border="1" class="{classes}" id="{table_id}">']
54+
table_html_parts.append(_render_table_header(dataframe, orderable_columns))
55+
table_html_parts.append(_render_table_body(dataframe))
56+
table_html_parts.append("</table>")
57+
return "".join(table_html_parts)
5558

56-
# Render table head
57-
table_html.append(" <thead>")
58-
table_html.append(' <tr style="text-align: left;">')
59+
60+
def _render_table_header(dataframe: pd.DataFrame, orderable_columns: list[str]) -> str:
61+
"""Render the header of the HTML table."""
62+
header_parts = [" <thead>", " <tr>"]
5963
for col in dataframe.columns:
6064
th_classes = []
6165
if col in orderable_columns:
6266
th_classes.append("sortable")
6367
class_str = f'class="{" ".join(th_classes)}"' if th_classes else ""
64-
header_div = (
65-
'<div style="resize: horizontal; overflow: auto; '
66-
"box-sizing: border-box; width: 100%; height: 100%; "
67-
'padding: 0.5em;">'
68-
f"{html.escape(str(col))}"
69-
"</div>"
70-
)
71-
table_html.append(
72-
f' <th style="text-align: left;" {class_str}>{header_div}</th>'
68+
header_parts.append(
69+
f' <th {class_str}><div class="bf-header-content">'
70+
f"{html.escape(str(col))}</div></th>"
7371
)
74-
table_html.append(" </tr>")
75-
table_html.append(" </thead>")
72+
header_parts.extend([" </tr>", " </thead>"])
73+
return "\n".join(header_parts)
74+
75+
76+
def _render_table_body(dataframe: pd.DataFrame) -> str:
77+
"""Render the body of the HTML table."""
78+
body_parts = [" <tbody>"]
79+
precision = options.display.precision
7680

77-
# Render table body
78-
table_html.append(" <tbody>")
7981
for i in range(len(dataframe)):
80-
table_html.append(" <tr>")
82+
body_parts.append(" <tr>")
8183
row = dataframe.iloc[i]
8284
for col_name, value in row.items():
8385
dtype = dataframe.dtypes.loc[col_name] # type: ignore
8486
align = "right" if _is_dtype_numeric(dtype) else "left"
85-
table_html.append(
86-
' <td style="text-align: {}; padding: 0.5em;">'.format(align)
87-
)
8887

8988
# TODO(b/438181139): Consider semi-exploding ARRAY/STRUCT columns
9089
# into multiple rows/columns like the BQ UI does.
9190
if pandas.api.types.is_scalar(value) and pd.isna(value):
92-
table_html.append(' <em style="color: gray;">&lt;NA&gt;</em>')
91+
body_parts.append(
92+
f' <td class="cell-align-{align}">'
93+
'<em class="null-value">&lt;NA&gt;</em></td>'
94+
)
9395
else:
9496
if isinstance(value, float):
95-
formatted_value = f"{value:.{precision}f}"
96-
table_html.append(f" {html.escape(formatted_value)}")
97+
cell_content = f"{value:.{precision}f}"
9798
else:
98-
table_html.append(f" {html.escape(str(value))}")
99-
table_html.append(" </td>")
100-
table_html.append(" </tr>")
101-
table_html.append(" </tbody>")
102-
table_html.append("</table>")
103-
104-
return "\n".join(table_html)
99+
cell_content = str(value)
100+
body_parts.append(
101+
f' <td class="cell-align-{align}">'
102+
f"{html.escape(cell_content)}</td>"
103+
)
104+
body_parts.append(" </tr>")
105+
body_parts.append(" </tbody>")
106+
return "\n".join(body_parts)
105107

106108

107109
def _obj_ref_rt_to_html(obj_ref_rt: str) -> str:

0 commit comments

Comments
 (0)