Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
9ad45ba
feat: Use `_repr_html_` when native supports it
dangotbanned Jul 3, 2025
04e1f51
Merge branch 'main' into df-repr-html
dangotbanned Jul 3, 2025
bf7e87a
Merge branch 'main' into df-repr-html
dangotbanned Jul 3, 2025
edb7ae4
Merge remote-tracking branch 'upstream/main' into df-repr-html
dangotbanned Jul 9, 2025
3769326
feat: Support `pl.LazyFrame._repr_html_`
dangotbanned Jul 9, 2025
3559cf8
Merge remote-tracking branch 'upstream/main' into df-repr-html
dangotbanned Jul 9, 2025
ea42e83
chore: add note on testing
dangotbanned Jul 9, 2025
3748f23
feat: Support `pl.Series._repr_html_`
dangotbanned Jul 9, 2025
5bb73d8
Merge branch 'main' into df-repr-html
dangotbanned Jul 11, 2025
e77c8de
Merge branch 'main' into df-repr-html
dangotbanned Jul 12, 2025
d8f09a4
Merge branch 'main' into df-repr-html
dangotbanned Jul 14, 2025
4227f10
Merge branch 'main' into df-repr-html
dangotbanned Jul 14, 2025
690e6e4
Merge branch 'main' into df-repr-html
dangotbanned Jul 15, 2025
6f2ab6b
Merge branch 'main' into df-repr-html
dangotbanned Jul 18, 2025
61497d6
Merge branch 'main' into df-repr-html
dangotbanned Jul 18, 2025
c4dca25
Merge remote-tracking branch 'upstream/main' into df-repr-html
dangotbanned Jul 25, 2025
f73d564
Merge branch 'main' into df-repr-html
dangotbanned Jul 28, 2025
e32e139
Merge remote-tracking branch 'upstream/main' into df-repr-html
dangotbanned Aug 3, 2025
57e333d
rename `header` -> `caption_text`
dangotbanned Aug 3, 2025
7758265
change `caption-side` to `top`
dangotbanned Aug 3, 2025
dfb5f5e
Merge remote-tracking branch 'upstream/main' into df-repr-html
dangotbanned Aug 8, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions narwhals/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -1544,6 +1544,49 @@ def generate_repr(header: str, native_repr: str) -> str:
)


# NOTE: Unsure on how to test this reliably
def generate_repr_html(
caption_text: Literal["Narwhals DataFrame", "Narwhals LazyFrame", "Narwhals Series"],
/,
native_html: str,
) -> str | None: # pragma: no cover
if caption_text == "Narwhals LazyFrame" and "LazyFrame" in native_html:
html = native_html.replace("LazyFrame", "LazyFrame.to_native()")
return f"{html}<p><b>{caption_text}</b></p>"
import io
import xml.etree.ElementTree as ET

style_css = (
".dataframe caption { "
"caption-side: top; "
"text-align: center; "
"font-weight: bold; "
"padding-top: 8px;"
"}"
)
try:
tree = ET.parse(io.StringIO(native_html.replace("<style scoped>", "<style>"))) # noqa: S314
except (SyntaxError, TypeError):
return None
table = tree.find("table")
if table is None:
return None
caption = ET.Element("caption")
caption.text = caption_text
table.insert(0, caption)
style = tree.find("style")
if style is not None:
text_existing = style.text or ""
style.text = f"{text_existing}\n{style_css}"
else:
style = ET.Element("style")
style.text = style_css
tree.getroot().insert(0, style)
buf = io.BytesIO()
tree.write(buf, "utf-8", method="html")
return buf.getvalue().decode()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Everything else in this function is a new language to me - I am not very helpful

Copy link
Member Author

@dangotbanned dangotbanned Aug 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yeah xml.etree.elementtree is a bit of a strange one

I had to learn a bit of lxml once to fix a particularly broken file.
The API of that is based on this stdlib module, but was more ergonoic than this mess πŸ˜„

To simplify this:

  • Element: Is a HTML Element
  • Tree: Refers to a document/webpage, but in this case it is just a table

So I'm essentially doing a fancy find/replace, but trying to preserve the structure of the document



def check_columns_exist(
subset: Collection[str], /, *, available: Collection[str]
) -> ColumnNotFoundError | None:
Expand Down
14 changes: 14 additions & 0 deletions narwhals/dataframe.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,11 @@
from narwhals._utils import (
Implementation,
Version,
_hasattr_static,
check_columns_exist,
flatten,
generate_repr,
generate_repr_html,
is_compliant_dataframe,
is_compliant_lazyframe,
is_eager_allowed,
Expand Down Expand Up @@ -698,6 +700,12 @@ def __array__(self, dtype: Any = None, copy: bool | None = None) -> _2DArray: #
def __repr__(self) -> str: # pragma: no cover
return generate_repr("Narwhals DataFrame", self.to_native().__repr__())

def _repr_html_(self) -> str | None: # pragma: no cover
native: Any = self.to_native()
if _hasattr_static(native, "_repr_html_") and (html := native._repr_html_()):
return generate_repr_html("Narwhals DataFrame", html)
return None

def __arrow_c_stream__(self, requested_schema: object | None = None) -> object:
"""Export a DataFrame via the Arrow PyCapsule Interface.

Expand Down Expand Up @@ -2446,6 +2454,12 @@ def __init__(self, df: Any, *, level: Literal["full", "lazy", "interchange"]) ->
def __repr__(self) -> str: # pragma: no cover
return generate_repr("Narwhals LazyFrame", self.to_native().__repr__())

def _repr_html_(self) -> str | None: # pragma: no cover
native: Any = self.to_native()
if _hasattr_static(native, "_repr_html_") and (html := native._repr_html_()):
return generate_repr_html("Narwhals LazyFrame", html)
return None

@property
def implementation(self) -> Implementation:
"""Return implementation of native frame.
Expand Down
8 changes: 8 additions & 0 deletions narwhals/series.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@
from narwhals._utils import (
Implementation,
Version,
_hasattr_static,
_validate_rolling_arguments,
ensure_type,
generate_repr,
generate_repr_html,
is_compliant_series,
is_eager_allowed,
is_index_selector,
Expand Down Expand Up @@ -411,6 +413,12 @@ def pipe(self, function: Callable[[Any], Self], *args: Any, **kwargs: Any) -> Se
def __repr__(self) -> str: # pragma: no cover
return generate_repr("Narwhals Series", self.to_native().__repr__())

def _repr_html_(self) -> str | None: # pragma: no cover
native: Any = self.to_native()
if _hasattr_static(native, "_repr_html_") and (html := native._repr_html_()):
return generate_repr_html("Narwhals Series", html)
return None

def __len__(self) -> int:
return len(self._compliant_series)

Expand Down
Loading