Skip to content

Conversation

@katosh
Copy link
Collaborator

@katosh katosh commented Dec 12, 2025

Add anndata.extensions module for unified extension API

This PR extends scverse#2236 by adding a public anndata.extensions module that consolidates extension points for external packages.

Live demo: Visual test page (Test 20 shows the unified accessor + section pattern)

Motivation

With the HTML repr system (scverse#2236) introducing register_formatter, TypeFormatter, and SectionFormatter, and the recent addition of register_anndata_namespace (scverse#1870), anndata now has multiple extension mechanisms. However, they're scattered across different locations:

Extension Type Current Location Public?
Accessors anndata.register_anndata_namespace
HTML formatters anndata._repr.*
I/O handlers anndata._io.specs._REGISTRY

This follows patterns established by pandas (pd.api.extensions) and xarray for providing a stable extension API.

Changes

  1. New anndata/extensions.py module that re-exports:

    • register_anndata_namespace (accessors)
    • register_formatter, TypeFormatter, SectionFormatter (HTML formatters)
    • FormattedOutput, FormattedEntry, FormatterContext
    • formatter_registry
    • extract_uns_type_hint, UNS_TYPE_HINT_KEY
  2. Unified accessor + section visualization: Accessors can define a _repr_section_ method to automatically get a section in the HTML repr - no separate SectionFormatter registration needed

  3. Updated tests and examples to import from anndata.extensions

  4. Updated docstrings to point users to the public API

Usage

Unified accessor with visualization (new!):

from anndata.extensions import (
    register_anndata_namespace,
    FormattedEntry,
    FormattedOutput,
)

@register_anndata_namespace("spatial")
class SpatialAccessor:
    # Optional: configure section in HTML output
    section_after = "obsm"  # Position after obsm section
    section_display_name = "spatial"  # Display name (defaults to accessor name)
    section_tooltip = "Spatial data"  # Hover tooltip
    section_doc_url = "https://spatialdata.readthedocs.io/"  # Doc link icon

    def __init__(self, adata: ad.AnnData):
        self._adata = adata

    @property
    def images(self):
        return self._adata.uns.get("spatial_images", {})

    def add_image(self, key, image):
        if "spatial_images" not in self._adata.uns:
            self._adata.uns["spatial_images"] = {}
        self._adata.uns["spatial_images"][key] = image

    def _repr_section_(self, context) -> list[FormattedEntry] | None:
        """Return entries for HTML repr, or None to hide section."""
        if not self.images:
            return None
        return [
            FormattedEntry(
                key=k,
                output=FormattedOutput(type_name=f"Image {v.shape}"),
            )
            for k, v in self.images.items()
        ]

# Usage:
adata.spatial.add_image("hires", image_array)
adata._repr_html_()  # Shows "spatial" section automatically!

Separate formatter registration (still supported):

from anndata.extensions import (
    register_formatter,
    SectionFormatter,
    FormattedEntry,
    FormattedOutput,
)

@register_formatter
class ObstSectionFormatter(SectionFormatter):
    section_name = "obst"
    after_section = "obsm"

    def should_show(self, obj):
        return hasattr(obj, "obst") and len(obj.obst) > 0

    def get_entries(self, obj, context):
        return [
            FormattedEntry(
                key=k,
                output=FormattedOutput(type_name=f"Tree ({v.n_nodes} nodes)"),
            )
            for k, v in obj.obst.items()
        ]

Future direction: Unified extension ecosystem

This aligns with the anndata roadmap (scverse#448) and ongoing discussions about extensibility. anndata already has an internal IORegistry (anndata._io.specs.registry) that handles serialization registration with register_read and register_write methods. Making this public would complete the extension story:

# Future possibility
from anndata.extensions import (
    # Accessors (already available)
    register_anndata_namespace,
    # HTML visualization (this PR)
    register_formatter,
    TypeFormatter,
    SectionFormatter,
    # I/O serialization (future - exposing existing IORegistry)
    register_io_handler,
    IOSpec,
)

This would address:

The existing IORegistry infrastructure is already well-designed with support for:

  • Type-based dispatch (register_write(dest_type, src_type, spec))
  • Version-aware specs (IOSpec("dataframe", "0.2.0"))
  • Read/write/read_partial methods

Exposing it through anndata.extensions would provide a stable public API without changing the internal implementation.

Visual Test

The visual test page includes 20 test cases demonstrating various features. Test 20 specifically shows the unified accessor + section pattern with a spatial_demo accessor.

Source: repr_html_visual_test.html gist

Checklist

  • Tests pass
  • Examples updated to use new import location
  • Docstrings updated with note about public API
  • Visual test includes unified accessor + section example (Test 20)

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: 1

🧹 Nitpick comments (1)
src/anndata/extensions.py (1)

1-70: Public re-export module matches the PR objective; examples are clear.
One consideration: importing anndata.extensions will eagerly import anndata._repr; if import-time becomes a concern, consider lazy re-exports (module __getattr__) later.

📜 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 6383dd1.

📒 Files selected for processing (4)
  • src/anndata/_repr/__init__.py (4 hunks)
  • src/anndata/extensions.py (1 hunks)
  • tests/test_repr_html.py (12 hunks)
  • tests/visual_inspect_repr_html.py (3 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
tests/test_repr_html.py (1)
src/anndata/_repr/registry.py (3)
  • FormatterContext (102-141)
  • SectionFormatter (202-277)
  • TypeFormatter (144-199)
src/anndata/extensions.py (2)
src/anndata/_core/extensions.py (1)
  • register_anndata_namespace (146-237)
src/anndata/_repr/registry.py (8)
  • FormattedEntry (88-98)
  • FormattedOutput (56-84)
  • FormatterContext (102-141)
  • FormatterRegistry (339-417)
  • SectionFormatter (202-277)
  • TypeFormatter (144-199)
  • register_formatter (523-551)
  • extract_uns_type_hint (432-520)
🪛 Ruff (0.14.8)
src/anndata/extensions.py

95-110: __all__ is not sorted

Apply an isort-style sorting to __all__

(RUF022)

🔇 Additional comments (10)
src/anndata/_repr/__init__.py (3)

13-24: Doc note correctly steers users to the new public extension API.
This aligns the docs with the PR goal without affecting runtime behavior.


39-62: Example import paths updated to anndata.extensions (good).
Keeps the extensibility guidance on the supported public surface.


99-105: SectionFormatter example updated to public imports (good).

tests/visual_inspect_repr_html.py (3)

24-29: Import migration to anndata.extensions is consistent with the new public API.


43-49: Optional-dependency block correctly imports extension symbols from the public module.


241-247: MuData block uses the public extension import surface (good).

tests/test_repr_html.py (4)

692-693: LGTM: tests now consume the public formatter_registry export.


699-705: LGTM: registry/type context symbols imported from anndata.extensions.


742-743: LGTM: public imports used for fallback behavior test.


1707-1713: LGTM: type-hint formatter test now uses the public extension API.

Comment on lines 95 to 110
__all__ = [
# Accessor registration
"register_anndata_namespace",
# HTML formatter registration
"register_formatter",
"TypeFormatter",
"SectionFormatter",
"FormattedOutput",
"FormattedEntry",
"FormatterContext",
"FormatterRegistry",
"formatter_registry",
# Type hint utilities
"extract_uns_type_hint",
"UNS_TYPE_HINT_KEY",
]
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Fix Ruff RUF022 on __all__ (either sort, or explicitly ignore).
Given other modules intentionally group exports by category, adding the same # noqa: RUF022 pattern here seems simplest.

-__all__ = [
+__all__ = [  # noqa: RUF022  # organized by category, not alphabetically
     # Accessor registration
     "register_anndata_namespace",
     # HTML formatter registration
     "register_formatter",
     "TypeFormatter",
     "SectionFormatter",
     "FormattedOutput",
     "FormattedEntry",
     "FormatterContext",
     "FormatterRegistry",
     "formatter_registry",
     # Type hint utilities
     "extract_uns_type_hint",
     "UNS_TYPE_HINT_KEY",
 ]
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
__all__ = [
# Accessor registration
"register_anndata_namespace",
# HTML formatter registration
"register_formatter",
"TypeFormatter",
"SectionFormatter",
"FormattedOutput",
"FormattedEntry",
"FormatterContext",
"FormatterRegistry",
"formatter_registry",
# Type hint utilities
"extract_uns_type_hint",
"UNS_TYPE_HINT_KEY",
]
__all__ = [ # noqa: RUF022 # organized by category, not alphabetically
# Accessor registration
"register_anndata_namespace",
# HTML formatter registration
"register_formatter",
"TypeFormatter",
"SectionFormatter",
"FormattedOutput",
"FormattedEntry",
"FormatterContext",
"FormatterRegistry",
"formatter_registry",
# Type hint utilities
"extract_uns_type_hint",
"UNS_TYPE_HINT_KEY",
]
🧰 Tools
🪛 Ruff (0.14.8)

95-110: __all__ is not sorted

Apply an isort-style sorting to __all__

(RUF022)

🤖 Prompt for AI Agents
In src/anndata/extensions.py around lines 95 to 110, the grouped __all__ export
list triggers Ruff RUF022 (unsorted __all__); to silence it without reordering,
append an explicit noqa for RUF022 to the __all__ assignment (e.g. add "# noqa:
RUF022" on the __all__ line or the closing bracket line) so the linter ignores
the unsorted export list while preserving the intentional grouping.

@settylab settylab deleted a comment from coderabbitai bot Dec 12, 2025
@settylab settylab deleted a comment from coderabbitai bot Dec 12, 2025
@settylab settylab deleted a comment from coderabbitai bot Dec 12, 2025
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