Skip to content

adata.extensions module#4

Open
katosh wants to merge 4 commits intohtml_repfrom
extensions_register
Open

adata.extensions module#4
katosh wants to merge 4 commits intohtml_repfrom
extensions_register

Conversation

@katosh
Copy link
Copy Markdown
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
Copy Markdown

@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 thread src/anndata/extensions.py Outdated
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
Copy Markdown

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
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 30, 2026

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.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7e8712c5-a8fa-41ef-8150-772376df955b

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

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch extensions_register

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.

@katosh katosh force-pushed the extensions_register branch from 7e4ddbc to 8696776 Compare March 30, 2026 19:13
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.

1 participant