Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
2eef28f
feat(repr): POC Jinja + Markup middle-ground for outer template
katosh Apr 20, 2026
fb29934
merge html_rep: pick up style commit + JS guard/test fix
katosh Apr 21, 2026
fa4402e
feat(repr): migrate section scaffolding to Jinja (phase A)
katosh Apr 21, 2026
a4f940e
feat(repr): migrate entry rendering to Jinja (phase B)
katosh Apr 21, 2026
9a7f273
feat(repr): wrap section HTML in Markup (phase C — sections.py)
katosh Apr 21, 2026
95ddde9
feat(repr): wrap formatter HTML in Markup (phase C — formatters.py)
katosh Apr 21, 2026
42f655f
refactor(repr): tighten component + FormattedOutput types to Markup (…
katosh Apr 21, 2026
10aed2e
refactor(repr): remove transitional str→Markup wrap (phase C finaliza…
katosh Apr 21, 2026
89d3c98
test(repr): wrap ecosystem-example HTML in Markup
katosh Apr 21, 2026
7b822e3
refactor(repr): rename *_html FormattedOutput fields to *_markup
katosh Apr 21, 2026
5dd1de7
refactor(repr): tighten remaining str→Markup boundaries (review batch A)
katosh Apr 21, 2026
00a3125
refactor(repr): tighten render_section entries to Markup (review batc…
katosh Apr 21, 2026
615fa91
refactor(repr): switch to Markup.format() idiom, drop escape_html at …
katosh Apr 21, 2026
d04a287
refactor(repr): unify cell rendering in _macros.j2 (C1)
katosh Apr 21, 2026
ce1abbf
refactor(repr): templatize orchestrator (header / footer / index / hi…
katosh Apr 21, 2026
018d155
refactor(repr): templatize unknown/error/raw sections (C2)
katosh Apr 21, 2026
ff0f7d1
test(repr): add Markup autoescape contract tests + update release note
katosh Apr 21, 2026
a69b067
refactor(repr): final nits from the review pass (batch E)
katosh Apr 21, 2026
2bb9153
security(repr): validate container_id, add render_html contract test,…
katosh Apr 21, 2026
d31f34c
cleanup(repr): remove escape_html, dedup get_macros, audit __all__ (F2)
katosh Apr 21, 2026
4062ce5
refactor(repr): decompose Markup wraps into macros (F1)
katosh Apr 21, 2026
4c7e1ee
docs(repr): correct docstring examples to use Markup.format() idiom; …
katosh Apr 21, 2026
ce8daa9
fix(visual-inspect): use Markup("\n").join in SpatialData demo
katosh Apr 21, 2026
177895d
Merge branch 'html_rep' into jinja-markup-poc-2
katosh Apr 21, 2026
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
2 changes: 1 addition & 1 deletion docs/release-notes/2236.feat.md
Original file line number Diff line number Diff line change
@@ -1 +1 @@
Add rich HTML representation for {class}`~anndata.AnnData` objects in Jupyter notebooks with foldable sections, search/filter, category color visualization, dark mode support, and configurable settings via {attr}`anndata.settings`
Add rich HTML representation for {class}`~anndata.AnnData` objects in Jupyter notebooks with foldable sections, search/filter, category color visualization, dark mode support, and configurable settings via {attr}`anndata.settings`. Rendering is built on {mod}`jinja2` and {mod}`markupsafe` with autoescape-by-default; extension packages register {class}`~anndata._repr.TypeFormatter` / {class}`~anndata._repr.SectionFormatter` subclasses that return {class}`~anndata._repr.FormattedOutput` with {class}`markupsafe.Markup`-typed HTML fragments (`preview_markup`, `type_markup`, `expanded_markup`).
2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ dependencies = [
"zarr >=3.1",
"typing-extensions; python_version<'3.13'",
"scverse-misc>=0.0.3",
"jinja2>=3.1",
"markupsafe>=3.0",
]
dynamic = [ "version" ]

Expand Down
50 changes: 32 additions & 18 deletions src/anndata/_repr/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@

Example - format by Python type::

from markupsafe import Markup

from anndata._repr import register_formatter, TypeFormatter, FormattedOutput


Expand All @@ -82,10 +84,27 @@ def format(self, obj, context):
return FormattedOutput(
type_name=f"MyArray {obj.shape}",
css_class="anndata-dtype--myarray",
# preview_html provides HTML for the preview column (rightmost)
preview_html=f'<span class="anndata-text--muted">({obj.n_items} items)</span>',
# ``preview_markup`` takes trusted HTML. Build it with
# ``Markup('<tag>{}</tag>').format(value)`` — MarkupSafe
# autoescapes each non-``Markup`` arg at that boundary.
preview_markup=Markup(
'<span class="mypackage-summary">{} items</span>'
).format(obj.n_items),
)

**Preview contract**: if no custom HTML is needed, prefer the plain-text
``preview`` field (autoescaped end-to-end). Use ``preview_markup`` when
you need custom structure. Three valid idioms:

- ``Markup('<tag>{}</tag>').format(value)`` — standard MarkupSafe pattern;
each non-``Markup`` arg is autoescaped.
- ``Markup(obj._repr_html_())`` — wrap trusted HTML from another package.
- ``get_macros().my_macro(value)`` — invoke a Jinja macro directly, which
also benefits from the engine's NUL-scrub finalize hook.

Never build HTML via ``Markup(f'...{value}...')`` — the f-string substitutes
``value`` before ``Markup`` sees it, bypassing autoescape.

**Error handling**: Formatters can signal errors in two ways:

1. **Raise an exception** - The registry catches it, emits a warning with the
Expand All @@ -97,10 +116,12 @@ def format(self, obj, context):
``FormattedOutput(error="reason")`` directly. The row will be highlighted
red and the error shown in the preview column.

When ``error`` is set, it takes precedence over ``preview`` and ``preview_html``.
When ``error`` is set, it takes precedence over ``preview`` and ``preview_markup``.

Example - format by embedded type hint (for tagged data in uns)::

from markupsafe import Markup

from anndata._repr import register_formatter, TypeFormatter, FormattedOutput
from anndata._repr import extract_uns_type_hint

Expand All @@ -118,7 +139,9 @@ def format(self, obj, context):
hint, data = extract_uns_type_hint(obj)
return FormattedOutput(
type_name="config",
preview_html="<span>Custom config preview</span>",
preview_markup=Markup(
'<span class="anndata-text--muted">{}</span>'
).format(data.get("name", "(unnamed)")),
)

Data structure for type hints (works in any section)::
Expand Down Expand Up @@ -277,7 +300,7 @@ def _repr_html_(self):
parts.append(
render_section(
"items",
"\\n".join(entries),
Markup("\\n").join(entries),
n_items=len(self.items),
)
)
Expand All @@ -290,12 +313,12 @@ def _repr_html_(self):

from anndata._repr import generate_repr_html, FormattedEntry, FormattedOutput

nested_html = generate_repr_html(adata, depth=1, max_depth=3)
# generate_repr_html already returns Markup; no Markup(...) wrap needed.
entry = FormattedEntry(
key="table",
output=FormattedOutput(
type_name=f"AnnData ({adata.n_obs} x {adata.n_vars})",
expanded_html=nested_html, # Collapsible content below the row
expanded_markup=generate_repr_html(adata, depth=1, max_depth=3),
),
)

Expand All @@ -321,7 +344,6 @@ def _repr_html_(self):
DEFAULT_PREVIEW_ITEMS,
DEFAULT_TYPE_WIDTH,
DEFAULT_UNIQUE_LIMIT,
NOT_SERIALIZABLE_MSG,
)

# Documentation base URL
Expand All @@ -346,10 +368,6 @@ def get_section_doc_url(section: str) -> str:
return f"{DOCS_BASE_URL}generated/anndata.AnnData.{section}.html"


# Import main functionality
# Inline styles for graceful degradation (from single source of truth)
from .._repr_constants import STYLE_HIDDEN # noqa: E402

# Building blocks for packages that want to create their own _repr_html_
# These allow reusing anndata's styling while building custom representations
from .components import ( # noqa: E402
Expand All @@ -361,6 +379,7 @@ def get_section_doc_url(section: str) -> str:
render_warning_icon,
)
from .css import get_css # noqa: E402
from .environment import get_macros # noqa: E402
from .html import ( # noqa: E402
generate_repr_html,
render_formatted_entry,
Expand All @@ -384,7 +403,6 @@ def get_section_doc_url(section: str) -> str:

# HTML rendering helpers for building custom sections
from .utils import ( # noqa: E402
escape_html,
format_memory_size,
format_number,
validate_key,
Expand All @@ -402,9 +420,6 @@ def get_section_doc_url(section: str) -> str:
"DEFAULT_UNIQUE_LIMIT",
"DEFAULT_MAX_FIELD_WIDTH",
"DEFAULT_TYPE_WIDTH",
"DOCS_BASE_URL",
"get_section_doc_url",
"NOT_SERIALIZABLE_MSG",
# CSS dtype constants for custom formatters
"CSS_DTYPE_NDARRAY",
"CSS_DTYPE_ANNDATA",
Expand All @@ -425,12 +440,11 @@ def get_section_doc_url(section: str) -> str:
# Building blocks for custom _repr_html_ implementations
"get_css",
"get_javascript",
"escape_html",
"get_macros",
"format_number",
"format_memory_size",
"render_section",
"render_formatted_entry",
"STYLE_HIDDEN",
# UI component helpers
"render_search_box",
"render_copy_button",
Expand Down
Loading
Loading