Skip to content

Commit 2e1b713

Browse files
authored
refactor: style loading logic in DataFrameHtmlFormatter (#1177)
1 parent 98f4773 commit 2e1b713

File tree

2 files changed

+63
-114
lines changed

2 files changed

+63
-114
lines changed

python/datafusion/dataframe_formatter.py

Lines changed: 40 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -135,9 +135,6 @@ class DataFrameHtmlFormatter:
135135
session
136136
"""
137137

138-
# Class variable to track if styles have been loaded in the notebook
139-
_styles_loaded = False
140-
141138
def __init__(
142139
self,
143140
max_cell_length: int = 25,
@@ -260,23 +257,6 @@ def set_custom_header_builder(self, builder: Callable[[Any], str]) -> None:
260257
"""
261258
self._custom_header_builder = builder
262259

263-
@classmethod
264-
def is_styles_loaded(cls) -> bool:
265-
"""Check if HTML styles have been loaded in the current session.
266-
267-
This method is primarily intended for debugging UI rendering issues
268-
related to style loading.
269-
270-
Returns:
271-
True if styles have been loaded, False otherwise
272-
273-
Example:
274-
>>> from datafusion.dataframe_formatter import DataFrameHtmlFormatter
275-
>>> DataFrameHtmlFormatter.is_styles_loaded()
276-
False
277-
"""
278-
return cls._styles_loaded
279-
280260
def format_html(
281261
self,
282262
batches: list,
@@ -315,18 +295,7 @@ def format_html(
315295
# Build HTML components
316296
html = []
317297

318-
# Only include styles and scripts if:
319-
# 1. Not using shared styles, OR
320-
# 2. Using shared styles but they haven't been loaded yet
321-
include_styles = (
322-
not self.use_shared_styles or not DataFrameHtmlFormatter._styles_loaded
323-
)
324-
325-
if include_styles:
326-
html.extend(self._build_html_header())
327-
# If we're using shared styles, mark them as loaded
328-
if self.use_shared_styles:
329-
DataFrameHtmlFormatter._styles_loaded = True
298+
html.extend(self._build_html_header())
330299

331300
html.extend(self._build_table_container_start())
332301

@@ -338,7 +307,7 @@ def format_html(
338307
html.append("</div>")
339308

340309
# Add footer (JavaScript and messages)
341-
if include_styles and self.enable_cell_expansion:
310+
if self.enable_cell_expansion:
342311
html.append(self._get_javascript())
343312

344313
# Always add truncation message if needed (independent of styles)
@@ -375,14 +344,20 @@ def format_str(
375344

376345
def _build_html_header(self) -> list[str]:
377346
"""Build the HTML header with CSS styles."""
378-
html = []
379-
html.append("<style>")
380-
# Only include expandable CSS if cell expansion is enabled
381-
if self.enable_cell_expansion:
382-
html.append(self._get_default_css())
347+
default_css = self._get_default_css() if self.enable_cell_expansion else ""
348+
script = f"""
349+
<script>
350+
if (!document.getElementById('df-styles')) {{
351+
const style = document.createElement('style');
352+
style.id = 'df-styles';
353+
style.textContent = `{default_css}`;
354+
document.head.appendChild(style);
355+
}}
356+
</script>
357+
"""
358+
html = [script]
383359
if self.custom_css:
384-
html.append(self.custom_css)
385-
html.append("</style>")
360+
html.append(f"<style>{self.custom_css}</style>")
386361
return html
387362

388363
def _build_table_container_start(self) -> list[str]:
@@ -570,28 +545,31 @@ def _get_default_css(self) -> str:
570545
def _get_javascript(self) -> str:
571546
"""Get JavaScript code for interactive elements."""
572547
return """
573-
<script>
574-
function toggleDataFrameCellText(table_uuid, row, col) {
575-
var shortText = document.getElementById(
576-
table_uuid + "-min-text-" + row + "-" + col
577-
);
578-
var fullText = document.getElementById(
579-
table_uuid + "-full-text-" + row + "-" + col
580-
);
581-
var button = event.target;
582-
583-
if (fullText.style.display === "none") {
584-
shortText.style.display = "none";
585-
fullText.style.display = "inline";
586-
button.textContent = "(less)";
587-
} else {
588-
shortText.style.display = "inline";
589-
fullText.style.display = "none";
590-
button.textContent = "...";
591-
}
592-
}
593-
</script>
594-
"""
548+
<script>
549+
if (!window.__df_formatter_js_loaded__) {
550+
window.__df_formatter_js_loaded__ = true;
551+
window.toggleDataFrameCellText = function (table_uuid, row, col) {
552+
var shortText = document.getElementById(
553+
table_uuid + "-min-text-" + row + "-" + col
554+
);
555+
var fullText = document.getElementById(
556+
table_uuid + "-full-text-" + row + "-" + col
557+
);
558+
var button = event.target;
559+
560+
if (fullText.style.display === "none") {
561+
shortText.style.display = "none";
562+
fullText.style.display = "inline";
563+
button.textContent = "(less)";
564+
} else {
565+
shortText.style.display = "inline";
566+
fullText.style.display = "none";
567+
button.textContent = "...";
568+
}
569+
};
570+
}
571+
</script>
572+
"""
595573

596574

597575
class FormatterManager:
@@ -712,24 +690,9 @@ def reset_formatter() -> None:
712690
>>> reset_formatter() # Reset formatter to default settings
713691
"""
714692
formatter = DataFrameHtmlFormatter()
715-
# Reset the styles_loaded flag to ensure styles will be reloaded
716-
DataFrameHtmlFormatter._styles_loaded = False
717693
set_formatter(formatter)
718694

719695

720-
def reset_styles_loaded_state() -> None:
721-
"""Reset the styles loaded state to force reloading of styles.
722-
723-
This can be useful when switching between notebook sessions or
724-
when styles need to be refreshed.
725-
726-
Example:
727-
>>> from datafusion.html_formatter import reset_styles_loaded_state
728-
>>> reset_styles_loaded_state() # Force styles to reload in next render
729-
"""
730-
DataFrameHtmlFormatter._styles_loaded = False
731-
732-
733696
def _refresh_formatter_reference() -> None:
734697
"""Refresh formatter reference in any modules using it.
735698

python/tests/test_dataframe.py

Lines changed: 23 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,6 @@
4242
configure_formatter,
4343
get_formatter,
4444
reset_formatter,
45-
reset_styles_loaded_state,
4645
)
4746
from datafusion.expr import Window
4847
from pyarrow.csv import write_csv
@@ -2177,27 +2176,15 @@ def test_html_formatter_shared_styles(df, clean_formatter_state):
21772176
# First, ensure we're using shared styles
21782177
configure_formatter(use_shared_styles=True)
21792178

2180-
# Get HTML output for first table - should include styles
21812179
html_first = df._repr_html_()
2182-
2183-
# Verify styles are included in first render
2184-
assert "<style>" in html_first
2185-
assert ".expandable-container" in html_first
2186-
2187-
# Get HTML output for second table - should NOT include styles
21882180
html_second = df._repr_html_()
21892181

2190-
# Verify styles are NOT included in second render
2182+
assert "<script>" in html_first
2183+
assert "df-styles" in html_first
2184+
assert "<script>" in html_second
2185+
assert "df-styles" in html_second
2186+
assert "<style>" not in html_first
21912187
assert "<style>" not in html_second
2192-
assert ".expandable-container" not in html_second
2193-
2194-
# Reset the styles loaded state and verify styles are included again
2195-
reset_styles_loaded_state()
2196-
html_after_reset = df._repr_html_()
2197-
2198-
# Verify styles are included after reset
2199-
assert "<style>" in html_after_reset
2200-
assert ".expandable-container" in html_after_reset
22012188

22022189

22032190
def test_html_formatter_no_shared_styles(df, clean_formatter_state):
@@ -2206,15 +2193,15 @@ def test_html_formatter_no_shared_styles(df, clean_formatter_state):
22062193
# Configure formatter to NOT use shared styles
22072194
configure_formatter(use_shared_styles=False)
22082195

2209-
# Generate HTML multiple times
22102196
html_first = df._repr_html_()
22112197
html_second = df._repr_html_()
22122198

2213-
# Verify styles are included in both renders
2214-
assert "<style>" in html_first
2215-
assert "<style>" in html_second
2216-
assert ".expandable-container" in html_first
2217-
assert ".expandable-container" in html_second
2199+
assert "<script>" in html_first
2200+
assert "<script>" in html_second
2201+
assert "df-styles" in html_first
2202+
assert "df-styles" in html_second
2203+
assert "<style>" not in html_first
2204+
assert "<style>" not in html_second
22182205

22192206

22202207
def test_html_formatter_manual_format_html(clean_formatter_state):
@@ -2228,20 +2215,15 @@ def test_html_formatter_manual_format_html(clean_formatter_state):
22282215

22292216
formatter = get_formatter()
22302217

2231-
# First call should include styles
22322218
html_first = formatter.format_html([batch], batch.schema)
2233-
assert "<style>" in html_first
2234-
2235-
# Second call should not include styles (using shared styles by default)
22362219
html_second = formatter.format_html([batch], batch.schema)
2237-
assert "<style>" not in html_second
22382220

2239-
# Reset loaded state
2240-
reset_styles_loaded_state()
2241-
2242-
# After reset, styles should be included again
2243-
html_reset = formatter.format_html([batch], batch.schema)
2244-
assert "<style>" in html_reset
2221+
assert "<script>" in html_first
2222+
assert "<script>" in html_second
2223+
assert "df-styles" in html_first
2224+
assert "df-styles" in html_second
2225+
assert "<style>" not in html_first
2226+
assert "<style>" not in html_second
22452227

22462228
# Create a new formatter with shared_styles=False
22472229
local_formatter = DataFrameHtmlFormatter(use_shared_styles=False)
@@ -2250,8 +2232,12 @@ def test_html_formatter_manual_format_html(clean_formatter_state):
22502232
local_html_1 = local_formatter.format_html([batch], batch.schema)
22512233
local_html_2 = local_formatter.format_html([batch], batch.schema)
22522234

2253-
assert "<style>" in local_html_1
2254-
assert "<style>" in local_html_2
2235+
assert "<script>" in local_html_1
2236+
assert "<script>" in local_html_2
2237+
assert "df-styles" in local_html_1
2238+
assert "df-styles" in local_html_2
2239+
assert "<style>" not in local_html_1
2240+
assert "<style>" not in local_html_2
22552241

22562242

22572243
def test_fill_null_basic(null_df):

0 commit comments

Comments
 (0)