Skip to content

Conversation

@katosh
Copy link
Collaborator

@katosh katosh commented Dec 3, 2025

Export HTML Repr Building Blocks for External Packages

Modifies #2236 (Rich HTML representation for AnnData)

Summary

This PR exports building blocks from anndata._repr that allow external packages like SpatialData to build their own _repr_html_ while reusing anndata's CSS, JavaScript, and interactive features.

This is an alternative to the ObjectFormatter approach explored in settylab/anndata#2. Instead of adding new extension points to anndata, this approach lets SpatialData implement its own _repr_html_ while reusing anndata's styling infrastructure.

Exported Building Blocks

from anndata._repr import (
    # CSS and JavaScript
    get_css,              # Returns anndata's CSS styles
    get_javascript,       # Returns anndata's JS (fold/expand, search, copy buttons)

    # Utilities
    escape_html,          # Safe HTML escaping
    format_number,        # Number formatting (1000 → "1,000")
    format_memory_size,   # Memory formatting (1024 → "1.0 KB")

    # Section rendering
    render_section,       # Renders a complete section with header
    render_formatted_entry,  # Renders a single entry row
    FormattedEntry,       # Data class for entry configuration
    FormattedOutput,      # Data class for output configuration
    FormatterContext,     # Context passed to formatters

    # UI component helpers (avoid hardcoding CSS classes/styles)
    render_search_box,    # Search input with filter indicator
    render_fold_icon,     # Fold/expand icon for section headers
    render_copy_button,   # Copy-to-clipboard button
    render_badge,         # Badge (pill-shaped label)
    render_header_badges, # Standard view/backed badges
    render_warning_icon,  # Warning/error icon with tooltip

    # Validation helpers (for consistent serialization warnings)
    check_column_name,    # Validate key/column names → (is_valid, reason, is_hard_error)
    NOT_SERIALIZABLE_MSG, # Standard "Not serializable to H5AD/Zarr" message

    # Extensibility (for SpatialData to offer same pattern to its users)
    FormatterRegistry,    # Registry class for managing formatters
    TypeFormatter,        # Base class for value formatters
    SectionFormatter,     # Base class for section formatters

    # For graceful degradation (no-JS fallback)
    STYLE_HIDDEN,         # "display:none;" for JS-enabled elements

    # For embedding nested AnnData
    generate_repr_html,   # Generate HTML for an AnnData object
)

Live Demo

Live interactive demo (see test cases 20, 23 for SpatialData and serialization warnings) | Gist source

SpatialData Example

The visual test includes a complete MockSpatialData example (~280 lines) demonstrating:

  • Custom header (no shape since SpatialData has no central X matrix)
  • Custom index preview (coordinate_systems: instead of obs_names/var_names)
  • Custom sections (images, labels, points, shapes, tables)
  • Expandable nested AnnData in the tables section
  • Custom footer with spatialdata version
  • Full reuse of anndata's CSS, JS, dark mode, search, fold/expand
class MockSpatialData:
    def _repr_html_(self) -> str:
        parts = []
        parts.append(get_css())
        parts.append(f'<div class="anndata-repr" id="{container_id}" ...>')
        parts.append(self._render_header())
        parts.append(self._render_coordinate_systems_preview())
        parts.append('<div class="adata-sections">')
        parts.append(self._render_images_section())  # uses render_section()
        # ... more sections
        parts.append("</div>")
        parts.append(self._render_footer())
        parts.append("</div>")
        parts.append(get_javascript(container_id))
        return "\n".join(parts)

Comparison: Building Blocks vs ObjectFormatter

Aspect Building Blocks (this PR) ObjectFormatter (settylab#2)
Where code lives SpatialData package anndata (formatters registered by SpatialData)
Control Full - SpatialData owns entire _repr_html_ Partial - must fit into anndata's rendering pipeline
Maintenance burden on anndata Low - just stable exports Higher - must maintain ObjectFormatter API, HeaderConfig, IndexPreviewConfig
Implementation effort for SpatialData Higher (~280 lines of render methods) Lower (implement formatter class with config objects)
Flexibility for SpatialData Maximum - can do anything Constrained by ObjectFormatter's extension points
Consistency across scverse Via shared CSS/JS Via shared rendering pipeline
Breaking change risk Lower - SpatialData controls versioning Higher - anndata API changes affect all formatters
Extensibility for SpatialData's types Can reuse FormatterRegistry class Inherited from anndata's system

What SpatialData Can Do

With the building blocks approach, SpatialData has two main implementation options:

Option A: Full Custom (Maximum Flexibility)

Build entire _repr_html_ from scratch using only get_css() and get_javascript():

def _repr_html_(self):
    return f"{get_css()}<div class='anndata-repr'>...custom HTML...</div>{get_javascript()}"

Option B: Use Helpers (Recommended)

Use render_section() and render_formatted_entry() for consistent section structure:

def _render_images_section(self):
    rows = [render_formatted_entry(FormattedEntry(key=k, output=...)) for k in self.images]
    return render_section("images", "\n".join(rows), n_items=len(self.images), ...)

UI Component Helpers

Use helpers instead of hardcoding CSS classes and inline styles:

def _build_header(self, container_id):
    parts = ['<div class="anndata-hdr">']
    parts.append('<span class="adata-type">SpatialData</span>')

    # Use render_badge() instead of hardcoded HTML
    parts.append(render_badge("Zarr", "adata-badge-backed", "Backed by Zarr storage"))

    # Use render_search_box() instead of hardcoded input/indicator
    parts.append('<span style="flex-grow:1;"></span>')
    parts.append(render_search_box(container_id))
    parts.append("</div>")
    return "\n".join(parts)

This makes the code more readable and future-proof - if CSS classes change, only anndata needs updating.

Embedding Nested AnnData

Both options can use generate_repr_html() for nested AnnData with full interactivity:

nested_html = generate_repr_html(adata, depth=1, max_depth=3)
entry = FormattedEntry(key="table", output=FormattedOutput(
    type_name="AnnData (150 × 30)",
    html_content=nested_html,  # Expandable content (nested views)
    is_expandable=True,
))

FormattedOutput Columns

FormattedOutput has two optional HTML fields for different columns:

  • html_content - Expandable content (nested AnnData, SVG previews, etc.)
  • meta_content - Meta column (rightmost) for data previews, dimensions, etc.
FormattedOutput(
    type_name="DataArray (100, 512, 512) float32",  # Type column (middle)
    meta_content="<span>[c, y, x]</span>",          # Meta column (rightmost)
    html_content=nested_repr,                       # Expandable content
    is_expandable=True,
)

Extensibility via Registry

SpatialData can reuse anndata's FormatterRegistry class to enable the same extensibility pattern for their own types. This supports both:

  1. TypeFormatter - Custom rendering for specific value types (e.g., xarray DataTree)
  2. SectionFormatter - Adding entirely new sections (e.g., "transforms")
from anndata._repr import (
    FormatterRegistry, TypeFormatter, SectionFormatter,
    FormattedOutput, FormattedEntry, FormatterContext,
)

# Create SpatialData's own registry (in spatialdata package)
spatialdata_registry = FormatterRegistry()

# Example 1: TypeFormatter for custom value rendering
class DataTreeFormatter(TypeFormatter):
    def can_format(self, obj):
        return type(obj).__name__ == "DataTree"

    def format(self, obj, context):
        return FormattedOutput(
            type_name=f"DataTree {obj.sizes}",
            html_content=obj._repr_html_(),
            is_expandable=True,
        )

spatialdata_registry.register_type_formatter(DataTreeFormatter())

# Example 2: SectionFormatter for adding new sections
class TransformsSectionFormatter(SectionFormatter):
    section_name = "transforms"

    def should_show(self, obj) -> bool:
        return len(obj.coordinate_systems) > 1

    def get_entries(self, obj, context) -> list[FormattedEntry]:
        # Return entries for coordinate transforms
        ...

spatialdata_registry.register_section_formatter(TransformsSectionFormatter())

SpatialData then renders these in _repr_html_:

def _render_custom_sections(self):
    parts = []
    for section_name in spatialdata_registry.get_registered_sections():
        formatter = spatialdata_registry.get_section_formatter(section_name)
        if formatter.should_show(self):
            entries = formatter.get_entries(self, context)
            rows = [render_formatted_entry(e) for e in entries]
            parts.append(render_section(section_name, "\n".join(rows), ...))
    return "\n".join(parts)

This allows the same extension pattern that anndata uses (third-party packages registering formatters) to work for SpatialData's element types AND custom sections.

Maintenance Burden on anndata

Minimal. The exported API is:

  1. CSS/JS functions - get_css(), get_javascript() - stable, internal changes don't affect API
  2. Utility functions - escape_html(), format_number(), format_memory_size() - simple, stable
  3. Rendering helpers - render_section(), render_formatted_entry() - used internally, must remain stable anyway
  4. Data classes - FormattedEntry, FormattedOutput, FormatterContext - already public for TypeFormatter/SectionFormatter
  5. Registry classes - FormatterRegistry, TypeFormatter, SectionFormatter - already public for anndata's own extensibility
  6. Validation helpers - check_column_name(), render_warning_icon() - for consistent serialization warnings

The key difference from ObjectFormatter: we're not adding new extension points or configuration classes (HeaderConfig, IndexPreviewConfig). We're exposing existing classes that anndata already uses and must maintain.

Note: generate_repr_html() serves dual purpose - it's both production code for AnnData and a reference implementation that external packages can study or adapt.

Style Consistency Across scverse

Both approaches achieve visual consistency through shared CSS/JS:

  • Building blocks: SpatialData includes get_css() and get_javascript() → same styling
  • ObjectFormatter: Rendering happens in anndata → same styling

With the building blocks approach:

  1. Each package can adopt at their own pace
  2. Packages aren't blocked waiting for anndata API updates
  3. CSS/JS updates propagate automatically (no code changes needed in SpatialData)

Test Coverage

  • 286 tests for HTML repr functionality
  • Includes tests for structural helpers and serialization warnings
  • Visual test with 23 examples including SpatialData mock and serialization edge cases

Trade-offs

Building blocks wins:

  • Lower anndata maintenance burden
  • Maximum SpatialData flexibility
  • SpatialData controls their own release cycle
  • No new abstractions to learn/maintain
  • Natural evolution path (start simple, add helpers as needed)

ObjectFormatter wins:

  • Single source of truth for rendering logic
  • Easier to enforce consistency across all formatters
  • Less code duplication if many packages need similar customization
  • Type-safe configuration via HeaderConfig/IndexPreviewConfig
  • Lower implementation burden on SpatialData - just implement a formatter class vs ~280 lines of render methods

Recommendation

The building blocks approach shifts implementation effort to SpatialData but reduces anndata's maintenance burden and gives SpatialData maximum flexibility. This may be preferable if SpatialData's needs diverge significantly from AnnData's structure.

@settylab settylab deleted a comment from coderabbitai bot Dec 3, 2025
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (4)
src/anndata/_repr/__init__.py (1)

154-171: Remove unused noqa directives.

Static analysis indicates E402 is not enabled, so these # noqa: E402 comments are unnecessary and add noise.

Apply this diff:

 # Building blocks for packages that want to create their own _repr_html_
 # These allow reusing anndata's styling while building custom representations
-from anndata._repr.css import get_css  # noqa: E402
-from anndata._repr.javascript import get_javascript  # noqa: E402
-from anndata._repr.utils import (  # noqa: E402
+from anndata._repr.css import get_css
+from anndata._repr.javascript import get_javascript
+from anndata._repr.utils import (
     escape_html,
     format_memory_size,
     format_number,
 )

 # HTML rendering helpers for building custom sections
-from anndata._repr.html import (  # noqa: E402
+from anndata._repr.html import (
     render_formatted_entry,
     render_section,
 )

 # Inline styles for graceful degradation (from single source of truth)
-from anndata._repr.constants import STYLE_HIDDEN  # noqa: E402
+from anndata._repr.constants import STYLE_HIDDEN
tests/visual_inspect_repr_html.py (2)

434-458: Unused container_id parameter is acceptable for demonstration code.

The static analysis flags container_id as unused. However, this is demonstration code showing the API pattern - a real implementation might use it for scoping. Consider adding a brief comment if clarity is desired.


1066-1066: Remove unused noqa directive.

Static analysis indicates PLR0915 and PLR0912 are not enabled.

Apply this diff:

-def main():  # noqa: PLR0915, PLR0912
+def main():
src/anndata/_repr/html.py (1)

1547-1638: Well-designed public API with comprehensive documentation.

The render_section function provides a clean, flexible interface for building sections. The parameters cover common use cases (custom count strings, collapse behavior, section IDs) while maintaining sensible defaults.

Note: Static analysis flags the noqa: PLR0913 as unused. Consider removing it:

-def render_section(  # noqa: PLR0913
+def render_section(
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 302a81c and d0d009e.

📒 Files selected for processing (5)
  • src/anndata/_repr/__init__.py (2 hunks)
  • src/anndata/_repr/constants.py (1 hunks)
  • src/anndata/_repr/html.py (9 hunks)
  • src/anndata/_repr/registry.py (1 hunks)
  • tests/visual_inspect_repr_html.py (5 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
src/anndata/_repr/__init__.py (4)
src/anndata/_repr/css.py (1)
  • get_css (14-18)
src/anndata/_repr/javascript.py (1)
  • get_javascript (17-41)
src/anndata/_repr/utils.py (3)
  • escape_html (270-272)
  • format_memory_size (293-305)
  • format_number (308-315)
src/anndata/_repr/html.py (2)
  • render_formatted_entry (406-518)
  • render_section (1547-1638)
tests/visual_inspect_repr_html.py (5)
src/anndata/_repr/registry.py (12)
  • FormatterContext (105-144)
  • doc_url (264-266)
  • FormattedEntry (91-101)
  • FormattedOutput (56-87)
  • FormatterRegistry (342-420)
  • SectionFormatter (205-280)
  • TypeFormatter (147-202)
  • tooltip (269-271)
  • section_name (244-246)
  • get_section_formatter (414-416)
  • register_type_formatter (358-366)
  • register_section_formatter (368-370)
src/anndata/_repr/css.py (1)
  • get_css (14-18)
src/anndata/_repr/javascript.py (1)
  • get_javascript (17-41)
src/anndata/_repr/html.py (3)
  • render_formatted_entry (406-518)
  • render_section (1547-1638)
  • generate_repr_html (124-242)
src/anndata/_repr/formatters.py (12)
  • can_format (51-52)
  • can_format (88-89)
  • can_format (127-149)
  • can_format (228-229)
  • can_format (295-298)
  • can_format (321-324)
  • format (54-80)
  • format (91-107)
  • format (151-204)
  • format (231-287)
  • format (300-313)
  • format (326-342)
src/anndata/_repr/html.py (2)
src/anndata/_repr/registry.py (1)
  • FormattedEntry (91-101)
src/anndata/_repr/utils.py (1)
  • escape_html (270-272)
🪛 Ruff (0.14.7)
src/anndata/_repr/__init__.py

156-156: Unused noqa directive (non-enabled: E402)

Remove unused noqa directive

(RUF100)


157-157: Unused noqa directive (non-enabled: E402)

Remove unused noqa directive

(RUF100)


158-158: Unused noqa directive (non-enabled: E402)

Remove unused noqa directive

(RUF100)


165-165: Unused noqa directive (non-enabled: E402)

Remove unused noqa directive

(RUF100)


171-171: Unused noqa directive (non-enabled: E402)

Remove unused noqa directive

(RUF100)

tests/visual_inspect_repr_html.py

434-434: Unused method argument: container_id

(ARG002)


525-525: String contains ambiguous × (MULTIPLICATION SIGN). Did you mean x (LATIN SMALL LETTER X)?

(RUF001)


584-584: String contains ambiguous × (MULTIPLICATION SIGN). Did you mean x (LATIN SMALL LETTER X)?

(RUF001)


647-647: Unused method argument: context

(ARG002)


664-664: Unused method argument: context

(ARG002)


671-671: String contains ambiguous × (MULTIPLICATION SIGN). Did you mean x (LATIN SMALL LETTER X)?

(RUF001)


1066-1066: Unused noqa directive (non-enabled: PLR0915, PLR0912)

Remove unused noqa directive

(RUF100)

src/anndata/_repr/html.py

406-406: Unused function argument: section

(ARG001)


452-452: Docstring contains ambiguous × (MULTIPLICATION SIGN). Did you mean x (LATIN SMALL LETTER X)?

(RUF002)


1547-1547: Unused noqa directive (non-enabled: PLR0913)

Remove unused noqa directive

(RUF100)

🔇 Additional comments (13)
src/anndata/_repr/registry.py (1)

77-81: LGTM - Clean addition of meta_content field.

The new optional field with None default maintains backward compatibility. The docstring clearly describes its purpose for the meta column.

src/anndata/_repr/constants.py (1)

23-27: LGTM - Well-organized style constants.

Centralizing these inline styles provides a single source of truth and enables consistent graceful degradation across rendering components.

src/anndata/_repr/__init__.py (1)

200-208: LGTM - Public API surface is well-organized.

The new exports are logically grouped under "Building blocks for custom repr_html implementations" and provide the necessary components for downstream packages.

tests/visual_inspect_repr_html.py (4)

272-274: LGTM - Good addition of doc_url property.

Adding documentation URL to the MuData section formatter improves the help icon functionality.


362-432: Well-structured demonstration of the building blocks API.

The MockSpatialData class effectively demonstrates how downstream packages can build custom _repr_html_ implementations using anndata's exported utilities. The pattern of:

  1. get_css() for styling
  2. Container with unique ID
  3. Custom header
  4. render_section() + render_formatted_entry() for sections
  5. get_javascript() for interactivity

provides a clear template for other packages.


562-597: Good demonstration of nested AnnData embedding.

The tables section effectively shows how to use generate_repr_html() with is_expandable=True to embed fully interactive nested AnnData objects.


1616-1637: LGTM - Test case properly exercises the new public API.

The SpatialData test case validates the full integration of building blocks: CSS/JS reuse, section rendering, entry formatting, nested AnnData, and custom sections via FormatterRegistry.

src/anndata/_repr/html.py (6)

32-36: LGTM - Centralized style constant imports.

Importing from constants.py establishes a single source of truth for inline styles used in graceful degradation.


386-403: Good refactor to use render_section for consistency.

The custom section rendering now delegates to the public render_section function, ensuring consistent structure and behavior across all sections.


406-459: Well-documented public API with clear examples.

The render_formatted_entry function provides a clean interface for external packages. The section parameter is appropriately documented as "for future use" - this is a reasonable forward-compatibility pattern.

Note: Static analysis flags section as unused, but this is intentional per the docstring.


483-488: Minor: Prefer using list() constructor for clarity.

The existing code works correctly, but using list() makes the intent clearer when copying a list.

-        warnings_list = list(output.warnings)
+        warnings_list = output.warnings.copy() if isinstance(output.warnings, list) else list(output.warnings)

Actually, the current code is fine since output.warnings is typed as list[str] - list() creates a shallow copy. No change needed.


502-506: LGTM - Clean meta column rendering.

The meta column now renders output.meta_content when available, supporting the new FormattedOutput.meta_content field.


740-757: Good consolidation using render_section.

The dataframe section renderer now collects rows and delegates to render_section, eliminating duplicate section structure code.

@katosh
Copy link
Collaborator Author

katosh commented Dec 7, 2025

Feeling like this PR might be the version we want to adopt, I started implementing other features like case sensitive search, regular expression, and better search of nested anndatas only here.

coderabbitai[bot]

This comment was marked as off-topic.

@settylab settylab deleted a comment from coderabbitai bot Dec 15, 2025
@settylab settylab deleted a comment from coderabbitai bot Dec 15, 2025
@settylab settylab deleted a comment from coderabbitai bot Dec 15, 2025
@settylab settylab deleted a comment from coderabbitai bot Dec 15, 2025
@coderabbitai
Copy link

coderabbitai bot commented Dec 15, 2025

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Walkthrough

This PR expands AnnData's HTML representation system by introducing a comprehensive public API for rendering components and utilities. Changes include improved exception handling with logging, new CSS style constants, enhanced search functionality with regex/case sensitivity, support for multi-section formatters, and defensive attribute checks for raw objects.

Changes

Cohort / File(s) Summary
Exception Handling & Logging
src/anndata/_core/anndata.py
Enhanced _repr_html_ to capture and log exceptions with details before falling back to text representation. Now emits a UserWarning when HTML generation fails.
Public Representation API Expansion
src/anndata/_repr/__init__.py
Expanded public exports to include CSS/JS helpers (get_css, get_javascript), HTML rendering components (render_section, render_formatted_entry), UI helpers (render_search_box, render_fold_icon, render_copy_button, render_badge, render_header_badges), styling constants, and utilities for formatting and HTML escaping.
Style Constants
src/anndata/_repr/constants.py
Added three new inline style constants: STYLE_HIDDEN, STYLE_SECTION_CONTENT, STYLE_SECTION_TABLE.
CSS Search Box Refactoring
src/anndata/_repr/css.py
Replaced standalone .adata-search-input styling with new .adata-search-box container (inline-flex, center-aligned). Added nested .adata-search-input child, toggle buttons (.adata-search-toggle), regex-error state styling, and adjusted focus/hover interactions.
HTML Rendering Helpers & Error Handling
src/anndata/_repr/html.py
Introduced public rendering functions (render_section, render_formatted_entry, UI helpers), internal error-handling helpers (_render_error_entry, _safe_get_attr), unknown section detection, and support for nested/raw data rendering with try/except wrappers around section generation.
Enhanced Search with Regex & Case Sensitivity
src/anndata/_repr/javascript.py
Added case-sensitivity (.adata-toggle-case) and regex (.adata-toggle-regex) toggle buttons with event handlers. Introduced matchesQuery function with regex error handling, refactored filtering to use raw values and new matching logic, expanded nested content on matches, and reset visibility for nested X entries based on query state.
Multi-Section Formatter Support
src/anndata/_repr/registry.py
Added section_names property to SectionFormatter (returns tuple of section names), updated register_section_formatter to register formatters for all names in section_names. Added meta_content field to FormattedOutput for carrying meta information alongside HTML content.
Defensive Attribute Checks
src/anndata/_repr/utils.py
Added early-exit guards in get_matching_column_colors and check_color_category_mismatch to check for .uns, .obs, .var attributes before accessing, supporting objects like Raw that lack these attributes.
Comprehensive Test Suite Expansion
tests/visual_inspect_repr_html.py
Added MuDataInternalSectionsFormatter to suppress internal MuData mappings. Introduced MockSpatialData example demonstrating reuse of anndata rendering primitives (get_css, get_javascript, render_section, render_formatted_entry), complete with formatters (DataTreeFormatter, TransformsSectionFormatter) and test data factory. Expanded test suite to include SpatialData, TreeData, Raw section variants, and error-handling scenarios with failing property access mocks.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Key areas requiring careful attention:

  • html.py public API design: Verify that new render_* functions are complete, correctly parameterized, and will meet third-party extensibility needs; check consistency of return types and HTML escaping.
  • javascript.py matching logic: Review matchesQuery function for correctness with regex edge cases, case-sensitivity handling, and error recovery; verify nested content expansion logic works correctly across all nesting levels.
  • registry.py multi-section support: Confirm section_names property design and register_section_formatter changes don't break backward compatibility or existing formatter registration patterns.
  • Test file comprehensiveness: Validate that new test formatters (MuDataInternalSectionsFormatter, SpatialData examples) properly demonstrate the public API and test coverage includes error paths.
  • Defensive checks in utils.py: Ensure guard clauses for optional attributes don't mask real issues and align with intended Raw/other object support.

Poem

🐰 From search box dance to section sway,
New helpers render bright the day!
Formatters bloom in multi-fold,
Extensions shine, a tale retold.
CSS twirls, JavaScript gleams—
Extensibility now dreams!

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The PR title 'Expose _repr_html_ Building Blocks' directly and clearly summarizes the main objective: exporting building blocks from anndata._repr for external packages to reuse, which aligns with the primary change across all modified files.
Docstring Coverage ✅ Passed Docstring coverage is 81.69% which is sufficient. The required threshold is 80.00%.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (8)
src/anndata/_repr/utils.py (1)

259-275: Missing defensive checks for .obs and .var in the iteration loop.

While line 259-261 adds a guard for missing .uns, the iteration on line 269 still accesses adata.obs and adata.var directly. For consistency with get_matching_column_colors, consider adding hasattr checks here as well.

Apply this diff to add defensive checks:

     color_key = f"{column_name}_colors"
     if color_key not in adata.uns:
         return None
 
     colors = adata.uns[color_key]
 
-    for df in (adata.obs, adata.var):
+    dfs = []
+    if hasattr(adata, "obs"):
+        dfs.append(adata.obs)
+    if hasattr(adata, "var"):
+        dfs.append(adata.var)
+
+    for df in dfs:
         if column_name in df.columns and hasattr(df[column_name], "cat"):
             n_cats = len(df[column_name].cat.categories)
             if len(colors) != n_cats:
                 return f"Color mismatch: {len(colors)} colors for {n_cats} categories"
src/anndata/_repr/css.py (1)

439-446: Consider using CSS variables for error colors in dark mode.

The hardcoded #dc3545 for regex error states doesn't adapt to dark mode. For consistency with the rest of the theme system, consider using the existing --anndata-error-color variable.

 /* Regex error indicator */
 .anndata-repr .adata-search-box.regex-error {
-    border-color: #dc3545;
+    border-color: var(--anndata-error-color);
 }
 
 .anndata-repr .adata-search-box.regex-error .adata-search-input {
-    background: rgba(220, 53, 69, 0.05);
+    background: var(--anndata-error-bg);
 }
src/anndata/_repr/__init__.py (1)

139-172: Remove unused noqa directives flagged by static analysis.

The # noqa: E402 comments on lines 139, 143, 147, 157, and 172 are flagged as unnecessary by Ruff. These were likely added when E402 was enabled, but the rule appears to be disabled now.

-from anndata._repr.constants import STYLE_HIDDEN  # noqa: E402
+from anndata._repr.constants import STYLE_HIDDEN

 # Building blocks for packages that want to create their own _repr_html_
 # These allow reusing anndata's styling while building custom representations
-from anndata._repr.css import get_css  # noqa: E402
+from anndata._repr.css import get_css

 # HTML rendering helpers for building custom sections
 # UI component helpers (search box, fold icon, badges, etc.)
-from anndata._repr.html import (  # noqa: E402  # noqa: E402
+from anndata._repr.html import (
     generate_repr_html,
     render_badge,
     render_copy_button,
     render_fold_icon,
     render_formatted_entry,
     render_header_badges,
     render_search_box,
     render_section,
 )
-from anndata._repr.javascript import get_javascript  # noqa: E402
+from anndata._repr.javascript import get_javascript
 from anndata._repr.registry import (  # noqa: E402
     ...
 )
-from anndata._repr.utils import (  # noqa: E402
+from anndata._repr.utils import (
     escape_html,
     format_memory_size,
     format_number,
 )
tests/visual_inspect_repr_html.py (1)

1163-1163: Remove unused noqa directive.

The # noqa: PLR0915, PLR0912 directive is unnecessary as these rules are not enabled in the current Ruff configuration.

-def main():  # noqa: PLR0915, PLR0912
+def main():
src/anndata/_repr/html.py (4)

106-112: Consider logging suppressed exceptions for debugging.

The try-except-pass pattern silently swallows exceptions. While this is intentional to avoid breaking the repr on corrupted data, logging at DEBUG level would aid troubleshooting without impacting users.

         try:
             mapping = getattr(adata, attr, None)
             if mapping is not None:
                 all_names.extend(mapping.keys())
         except Exception:  # noqa: BLE001
-            # Skip sections that fail to access (will show error during rendering)
-            pass
+            # Skip sections that fail to access (will show error during rendering)
+            import logging
+            logging.getLogger(__name__).debug(
+                "Failed to access %s for field width calculation", attr, exc_info=True
+            )

Alternatively, if logging is not desired, keeping the pass is acceptable since errors will surface during actual rendering.


425-478: Unused section parameter in public API.

The section parameter is documented as "for future use" but is currently unused. For a public API, consider either:

  1. Removing it until needed (simpler API, can add later with default)
  2. Prefixing with _ to indicate it's reserved: _section
  3. Adding a TODO comment explaining the planned use case

Since this is a new public API, removing unused parameters keeps the surface area minimal.

-def render_formatted_entry(entry: FormattedEntry, section: str = "") -> str:
+def render_formatted_entry(entry: FormattedEntry) -> str:
     """
     Render a FormattedEntry as a table row.
     ...
     Parameters
     ----------
     entry
         A FormattedEntry containing the key and FormattedOutput
-    section
-        Optional section name (for future use)
     ...
     """

If keeping for future compatibility, add _ prefix:

-def render_formatted_entry(entry: FormattedEntry, section: str = "") -> str:
+def render_formatted_entry(entry: FormattedEntry, _section: str = "") -> str:

1636-1640: Callable check may not work as intended.

The condition not callable(val) checks if the value itself is callable, but val is already the result of getattr(adata, attr). If attr is a method, val would be a bound method (callable). However, methods are typically excluded by the callable(getattr(adata, attr)) check conceptually, but the current code first gets the value then checks if it's mapping-like.

The logic works because methods don't typically have keys() and __getitem__, but the not callable(val) seems redundant here since you're checking for mapping-like properties.

Consider simplifying:

             if isinstance(val, Mapping) or (
                 hasattr(val, "keys")
                 and hasattr(val, "__getitem__")
-                and not callable(val)
             ):

Or clarify the intent with a comment if the check is needed for edge cases.


2023-2033: Remove unused noqa directive from render_section.

The # noqa: PLR0913 directive is flagged as unused since this rule is not enabled. The function has appropriate parameters for its public API purpose.

-def render_section(  # noqa: PLR0913
+def render_section(
     name: str,
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7c7058b and 2fbd1db.

📒 Files selected for processing (9)
  • src/anndata/_core/anndata.py (1 hunks)
  • src/anndata/_repr/__init__.py (3 hunks)
  • src/anndata/_repr/constants.py (1 hunks)
  • src/anndata/_repr/css.py (2 hunks)
  • src/anndata/_repr/html.py (22 hunks)
  • src/anndata/_repr/javascript.py (6 hunks)
  • src/anndata/_repr/registry.py (4 hunks)
  • src/anndata/_repr/utils.py (3 hunks)
  • tests/visual_inspect_repr_html.py (6 hunks)
🧰 Additional context used
🧬 Code graph analysis (4)
src/anndata/_core/anndata.py (1)
src/anndata/_warnings.py (1)
  • warn (52-65)
src/anndata/_repr/utils.py (3)
tests/test_repr_html.py (1)
  • adata (69-80)
src/anndata/_core/anndata.py (6)
  • obs (889-891)
  • obs (894-895)
  • obs (898-899)
  • var (912-914)
  • var (917-918)
  • var (921-922)
src/anndata/_core/xarray.py (1)
  • columns (254-266)
src/anndata/_repr/__init__.py (4)
src/anndata/_repr/css.py (1)
  • get_css (14-18)
src/anndata/_repr/html.py (8)
  • generate_repr_html (128-246)
  • render_badge (642-677)
  • render_copy_button (611-639)
  • render_fold_icon (590-608)
  • render_formatted_entry (425-537)
  • render_header_badges (680-722)
  • render_search_box (548-587)
  • render_section (2023-2114)
src/anndata/_repr/javascript.py (1)
  • get_javascript (17-41)
src/anndata/_repr/utils.py (3)
  • escape_html (278-280)
  • format_memory_size (301-313)
  • format_number (316-323)
src/anndata/_repr/html.py (2)
src/anndata/_repr/registry.py (2)
  • FormattedEntry (91-101)
  • FormatterContext (105-144)
src/anndata/_repr/utils.py (2)
  • escape_html (278-280)
  • format_number (316-323)
🪛 Ruff (0.14.8)
tests/visual_inspect_repr_html.py

275-275: Unused method argument: obj

(ARG002)


278-278: Unused method argument: obj

(ARG002)


278-278: Unused method argument: context

(ARG002)


598-598: String contains ambiguous × (MULTIPLICATION SIGN). Did you mean x (LATIN SMALL LETTER X)?

(RUF001)


657-657: String contains ambiguous × (MULTIPLICATION SIGN). Did you mean x (LATIN SMALL LETTER X)?

(RUF001)


724-724: Unused method argument: context

(ARG002)


743-743: Unused method argument: context

(ARG002)


748-748: String contains ambiguous × (MULTIPLICATION SIGN). Did you mean x (LATIN SMALL LETTER X)?

(RUF001)


1163-1163: Unused noqa directive (non-enabled: PLR0915, PLR0912)

Remove unused noqa directive

(RUF100)

src/anndata/_repr/__init__.py

139-139: Unused noqa directive (non-enabled: E402)

Remove unused noqa directive

(RUF100)


143-143: Unused noqa directive (non-enabled: E402)

Remove unused noqa directive

(RUF100)


147-147: Unused noqa directive (non-enabled: E402)

Remove unused noqa directive

(RUF100)


157-157: Unused noqa directive (non-enabled: E402)

Remove unused noqa directive

(RUF100)


172-172: Unused noqa directive (non-enabled: E402)

Remove unused noqa directive

(RUF100)

src/anndata/_repr/html.py

110-112: try-except-pass detected, consider logging the exception

(S110)


425-425: Unused function argument: section

(ARG001)


471-471: Docstring contains ambiguous × (MULTIPLICATION SIGN). Did you mean x (LATIN SMALL LETTER X)?

(RUF002)


1713-1713: Consider moving this statement to an else block

(TRY300)


1724-1725: try-except-pass detected, consider logging the exception

(S110)


1729-1730: try-except-pass detected, consider logging the exception

(S110)


1778-1778: String contains ambiguous × (MULTIPLICATION SIGN). Did you mean x (LATIN SMALL LETTER X)?

(RUF001)


1868-1868: String contains ambiguous × (MULTIPLICATION SIGN). Did you mean x (LATIN SMALL LETTER X)?

(RUF001)


2023-2023: Unused noqa directive (non-enabled: PLR0913)

Remove unused noqa directive

(RUF100)

🔇 Additional comments (25)
src/anndata/_repr/utils.py (2)

210-212: LGTM: Defensive check for objects without .uns.

Good addition to handle Raw objects and other AnnData-like objects that may not have the .uns attribute.


222-225: LGTM: Defensive attribute checks for .obs and .var.

These guards correctly prevent AttributeError when processing objects that don't implement standard AnnData attributes.

src/anndata/_core/anndata.py (1)

602-609: LGTM: Improved error handling with informative warning.

The change from silent fallback to emitting a UserWarning with exception details is a good improvement for debugging HTML rendering issues while still maintaining graceful degradation.

src/anndata/_repr/constants.py (1)

23-27: LGTM: Centralized inline style constants.

Good addition of style constants for graceful no-JS degradation. Centralizing these in the constants module is the right approach for maintainability.

src/anndata/_repr/css.py (1)

360-388: LGTM: Well-structured search box container refactor.

The move from a single input to a flex container with input and toggles is cleanly implemented. The focus-within pseudo-class correctly highlights the container when any child has focus.

src/anndata/_repr/javascript.py (4)

55-63: LGTM: Updated visibility initialization for new search UI elements.

Properly shows the search box container and toggle buttons when JavaScript is enabled.


116-165: LGTM: Well-implemented search state management with accessibility.

The toggle buttons correctly update ARIA pressed states and the debounced search trigger prevents excessive filtering. The stopPropagation() calls appropriately prevent button clicks from bubbling to parent handlers.


168-190: LGTM: Robust regex matching with graceful error handling.

The matchesQuery helper properly handles:

  • Empty queries (returns true)
  • Regex mode with invalid patterns (catches exception, shows error state, returns false instead of crashing)
  • Case sensitivity for both regex and plain text modes

The error class management on searchBox provides good visual feedback.


236-285: LGTM: Ancestor visibility and X-entry management.

The nested entry ancestor reveal logic has appropriate safety bounds (maxIterations = 20) to prevent runaway loops. The X-entry visibility logic correctly hides orphaned entries when filtering is active and resets them when the query is cleared.

src/anndata/_repr/registry.py (3)

77-81: LGTM: Enhanced FormattedOutput with meta_content field.

The addition of meta_content for the meta column and the clarified html_content docstring appropriately extend the formatting capabilities.


268-276: LGTM: Well-designed multi-section support.

The section_names property with a sensible default maintains backward compatibility while enabling a single formatter to handle multiple sections. This is a clean extension point.


398-401: LGTM: Registration iterates over all section names.

Correctly registers the formatter for each name in section_names, enabling the multi-section pattern.

src/anndata/_repr/__init__.py (2)

138-176: LGTM: Well-organized public API surface for custom HTML representations.

The imports are properly grouped by purpose with helpful comments explaining their use case. This enables external packages like SpatialData to build custom _repr_html_ implementations while reusing anndata's styling and interactivity.


205-219: LGTM: Comprehensive all exports for the building blocks API.

The new exports are well-organized with clear category comments and provide everything needed for external packages to build custom representations.

tests/visual_inspect_repr_html.py (4)

391-465: Well-structured demonstration of the public API pattern.

The MockSpatialData._repr_html_ method clearly documents the 7-step pattern for building custom HTML representations. This serves as excellent reference documentation for external packages.

The × characters flagged by static analysis (RUF001) are intentional multiplication signs for dimension display (e.g., "100 × 50") and are correct for visual representation.


743-751: Consider edge case in transform entry generation.

The loop range(len(cs) - 1) works correctly when len(cs) >= 2 (due to should_show check), but if coordinate_systems contains only 1 item, should_show returns False so this is safe. The unused context parameter is required by the SectionFormatter.get_entries interface.


1885-1898: FailingMapping test helper is well-designed for error simulation.

The FailingMapping class correctly simulates I/O errors by raising on keys() and __iter__ while returning 1 from __len__ to trigger rendering attempts. This effectively tests the error handling paths in section rendering.


263-279: MuDataInternalSectionsFormatter has correct suppression logic.

The section_names tuple approach for multi-section suppression is clean. The unused obj and context parameters are required by the SectionFormatter interface, so the ARG002 static analysis hints are false positives. The registry explicitly iterates over all section_names during registration (see register_section_formatter() in src/anndata/_repr/registry.py), so all three names—obsmap, varmap, axis—are properly registered to the formatter.

src/anndata/_repr/html.py (7)

548-587: Well-designed public API for search box rendering.

The render_search_box function properly handles:

  • Unique ID generation for label association
  • Hidden by default with JS-enabled show
  • Accessibility attributes (aria-label, aria-pressed)
  • Filter indicator span for visual feedback

Good use of the STYLE_HIDDEN constant for graceful degradation.


642-678: render_badge variant documentation is helpful.

The docstring clearly documents the built-in variants with their semantic meanings. The implementation correctly handles the base class concatenation and optional tooltip.


1709-1731: Safe attribute access patterns are appropriate for error resilience.

The _safe_get_attr helper and _get_raw_meta_parts use try-except patterns to handle corrupted or inaccessible data gracefully. This is important for the repr to remain functional even with problematic data.

The static analysis suggestion (TRY300) to move line 1713 to an else block is a minor style improvement:

 def _safe_get_attr(obj, attr: str, default="?"):
     """Safely get an attribute with fallback."""
     try:
         val = getattr(obj, attr, None)
-        return val if val is not None else default
     except Exception:  # noqa: BLE001
         return default
+    else:
+        return val if val is not None else default

However, the current form is readable and functionally correct.


1734-1810: Raw section rendering is well-structured with defensive checks.

The implementation properly:

  • Safely accesses raw with getattr fallback
  • Uses _safe_get_attr for dimensions to handle corrupted data
  • Checks can_expand before generating nested content
  • Builds meta info with _get_raw_meta_parts that handles failures gracefully

The × character (RUF001) is intentional for visual dimension display.


2023-2113: Public render_section API is well-designed.

The function provides a clean interface for external packages with:

  • Sensible parameter defaults
  • Clear documentation with examples
  • Consistent use of internal helpers (_render_section_header, _render_empty_section)
  • Proper HTML escaping throughout

The parameter count is appropriate for the flexibility this API provides.


323-341: Good error handling wrapper for section rendering.

The try-except wrapper around section rendering ensures the HTML repr remains functional even when individual sections fail (e.g., due to corrupted data or I/O errors). The _render_error_entry provides user-visible feedback about what failed.


1879-1897: No compatibility issue detected. The utility functions already handle Raw objects gracefully.

Both get_matching_column_colors() and check_color_category_mismatch() explicitly check if not hasattr(adata, "uns") and return None for objects without .uns (with code comments noting "Handle objects without .uns (e.g., Raw)"). The code will function correctly with Raw objects passed to _render_dataframe_section().

Likely an incorrect or invalid review comment.

@settylab settylab deleted a comment from coderabbitai bot Dec 15, 2025
@katosh katosh merged commit 2597d99 into html_rep Dec 27, 2025
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants