Skip to content

Commit 1b7952b

Browse files
committed
refactor: split repr_mimebundle logic, handle deferred mode in html, and fix mypy errors
1 parent 1a73628 commit 1b7952b

File tree

2 files changed

+90
-29
lines changed

2 files changed

+90
-29
lines changed

bigframes/display/html.py

Lines changed: 47 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import bigframes
3030
from bigframes._config import display_options, options
3131
from bigframes.display import plaintext
32+
import bigframes.formatting_helpers as formatter
3233

3334
if typing.TYPE_CHECKING:
3435
import bigframes.dataframe
@@ -114,16 +115,16 @@ def create_html_representation(
114115
from bigframes.series import Series
115116

116117
if isinstance(obj, Series):
118+
# Fallback to pandas string representation if the object is not a Series.
119+
# This protects against cases where obj might be something else unexpectedly,
120+
# or if the pandas Series implementation changes.
117121
pd_series = pandas_df.iloc[:, 0]
118122
try:
119123
html_string = pd_series._repr_html_()
120124
except AttributeError:
121125
html_string = f"<pre>{pd_series.to_string()}</pre>"
122126

123-
# Series doesn't typically show total rows/cols like DF in HTML repr here?
124-
# But let's check what it was doing.
125-
# Original code just returned _repr_html_ or wrapped to_string.
126-
# It didn't append row/col count string for Series (wait, Series usually has length in repr).
127+
html_string += f"[{total_rows} rows]"
127128
return html_string
128129
else:
129130
# It's a DataFrame
@@ -225,32 +226,21 @@ def get_anywidget_bundle(
225226
return widget_repr, widget_metadata
226227

227228

228-
def repr_mimebundle(
229+
def _repr_mimebundle_deferred(
229230
obj: Union[bigframes.dataframe.DataFrame, bigframes.series.Series],
230-
include=None,
231-
exclude=None,
232-
):
233-
"""
234-
Custom display method for IPython/Jupyter environments.
235-
"""
236-
# TODO(b/467647693): Anywidget integration has been tested in Jupyter, VS Code, and
237-
# BQ Studio, but there is a known compatibility issue with Marimo that needs to be addressed.
231+
) -> dict[str, str]:
232+
return {
233+
"text/plain": formatter.repr_query_job(obj._compute_dry_run()),
234+
"text/html": formatter.repr_query_job_html(obj._compute_dry_run()),
235+
}
236+
237+
238+
def _repr_mimebundle_head(
239+
obj: Union[bigframes.dataframe.DataFrame, bigframes.series.Series],
240+
) -> dict[str, str]:
238241
from bigframes.series import Series
239242

240243
opts = options.display
241-
if opts.repr_mode == "anywidget":
242-
try:
243-
return get_anywidget_bundle(obj, include=include, exclude=exclude)
244-
except ImportError:
245-
# Anywidget is an optional dependency, so warn rather than fail.
246-
# TODO(shuowei): When Anywidget becomes the default for all repr modes,
247-
# remove this warning.
248-
warnings.warn(
249-
"Anywidget mode is not available. "
250-
"Please `pip install anywidget traitlets` or `pip install 'bigframes[anywidget]'` to use interactive tables. "
251-
f"Falling back to static HTML. Error: {traceback.format_exc()}"
252-
)
253-
254244
blob_cols: list[str]
255245
if isinstance(obj, Series):
256246
pandas_df, row_count, query_job = obj._block.retrieve_repr_request_results(
@@ -275,3 +265,34 @@ def repr_mimebundle(
275265
)
276266

277267
return {"text/html": html_string, "text/plain": text_representation}
268+
269+
270+
def repr_mimebundle(
271+
obj: Union[bigframes.dataframe.DataFrame, bigframes.series.Series],
272+
include=None,
273+
exclude=None,
274+
):
275+
"""
276+
Custom display method for IPython/Jupyter environments.
277+
"""
278+
# TODO(b/467647693): Anywidget integration has been tested in Jupyter, VS Code, and
279+
# BQ Studio, but there is a known compatibility issue with Marimo that needs to be addressed.
280+
281+
opts = options.display
282+
if opts.repr_mode == "deferred":
283+
return _repr_mimebundle_deferred(obj)
284+
285+
if opts.repr_mode == "anywidget":
286+
try:
287+
return get_anywidget_bundle(obj, include=include, exclude=exclude)
288+
except ImportError:
289+
# Anywidget is an optional dependency, so warn rather than fail.
290+
# TODO(shuowei): When Anywidget becomes the default for all repr modes,
291+
# remove this warning.
292+
warnings.warn(
293+
"Anywidget mode is not available. "
294+
"Please `pip install anywidget traitlets` or `pip install 'bigframes[anywidget]'` to use interactive tables. "
295+
f"Falling back to static HTML. Error: {traceback.format_exc()}"
296+
)
297+
298+
return _repr_mimebundle_head(obj)

bigframes/formatting_helpers.py

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ def repr_query_job(query_job: Optional[bigquery.QueryJob]):
6868
query_job:
6969
The job representing the execution of the query on the server.
7070
Returns:
71-
Pywidget html table.
71+
Formatted string.
7272
"""
7373
if query_job is None:
7474
return "No job information available"
@@ -94,6 +94,46 @@ def repr_query_job(query_job: Optional[bigquery.QueryJob]):
9494
return res
9595

9696

97+
def repr_query_job_html(query_job: Optional[bigquery.QueryJob]):
98+
"""Return query job as a formatted html string.
99+
Args:
100+
query_job:
101+
The job representing the execution of the query on the server.
102+
Returns:
103+
Html string.
104+
"""
105+
if query_job is None:
106+
return "No job information available"
107+
if query_job.dry_run:
108+
return f"Computation deferred. Computation will process {get_formatted_bytes(query_job.total_bytes_processed)}"
109+
110+
# We can reuse the plaintext repr for now or make a nicer table.
111+
# For deferred mode consistency, let's just wrap the text in a pre block or similar,
112+
# but the request implies we want a distinct HTML representation if possible.
113+
# However, existing repr_query_job returns a simple string.
114+
# Let's format it as a simple table or list.
115+
116+
res = "<h3>Query Job Info</h3><ul>"
117+
for key, value in query_job_prop_pairs.items():
118+
job_val = getattr(query_job, value)
119+
if job_val is not None:
120+
if key == "Job Id": # add link to job
121+
url = get_job_url(
122+
project_id=query_job.project,
123+
location=query_job.location,
124+
job_id=query_job.job_id,
125+
)
126+
res += f'<li>Job: <a target="_blank" href="{url}">{query_job.job_id}</a></li>'
127+
elif key == "Slot Time":
128+
res += f"<li>{key}: {get_formatted_time(job_val)}</li>"
129+
elif key == "Bytes Processed":
130+
res += f"<li>{key}: {get_formatted_bytes(job_val)}</li>"
131+
else:
132+
res += f"<li>{key}: {job_val}</li>"
133+
res += "</ul>"
134+
return res
135+
136+
97137
current_display: Optional[display.HTML] = None
98138
current_display_id: Optional[str] = None
99139
previous_display_html: str = ""
@@ -296,7 +336,7 @@ def get_job_url(
296336
"""
297337
if project_id is None or location is None or job_id is None:
298338
return None
299-
return f"""https://console.cloud.google.com/bigquery?project={project_id}&j=bq:{location}:{job_id}&page=queryresults"""
339+
return f"""https://console.cloud. google.com/bigquery?project={project_id}&j=bq:{location}:{job_id}&page=queryresults"""
300340

301341

302342
def render_bqquery_sent_event_html(
@@ -508,7 +548,7 @@ def get_base_job_loading_html(job: GenericJob):
508548
Returns:
509549
Html string.
510550
"""
511-
return f"""{job.job_type.capitalize()} job {job.job_id} is {job.state}. <a target="_blank" href="{get_job_url(
551+
return f"""{job.job_type.capitalize()} job {job.job_id} is {job.state}. <a target=\"_blank\" href="{get_job_url(
512552
project_id=job.job_id,
513553
location=job.location,
514554
job_id=job.job_id,

0 commit comments

Comments
 (0)