Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
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
30 changes: 25 additions & 5 deletions src/anndata/_repr/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,18 @@
- Support for nested AnnData objects
- Graceful handling of unknown types

.. note::

For extending AnnData with custom formatters, prefer importing from
:mod:`anndata.extensions` which provides the public API::

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

Extensibility
-------------
The system is designed to be extensible via two registry patterns:
Expand All @@ -24,7 +36,7 @@

Example - format by Python type::

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


@register_formatter
Expand All @@ -42,8 +54,12 @@ def format(self, obj, context):

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

from anndata._repr import register_formatter, TypeFormatter, FormattedOutput
from anndata._repr import extract_uns_type_hint
from anndata.extensions import (
register_formatter,
TypeFormatter,
FormattedOutput,
extract_uns_type_hint,
)


@register_formatter
Expand Down Expand Up @@ -80,8 +96,12 @@ def format(self, obj, context):

Example::

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


@register_formatter
Expand Down
110 changes: 110 additions & 0 deletions src/anndata/extensions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
"""
Public API for extending AnnData functionality.

This module provides registration mechanisms for:

1. **Accessors** - Add custom namespaces to AnnData objects (e.g., `adata.myns.method()`)
2. **HTML Formatters** - Customize how types are displayed in Jupyter notebooks

Examples
--------
Register a custom accessor namespace::

import anndata as ad
from anndata.extensions import register_anndata_namespace

@register_anndata_namespace("transform")
class TransformAccessor:
def __init__(self, adata: ad.AnnData):
self._adata = adata

def log1p(self):
import numpy as np
self._adata.X = np.log1p(self._adata.X)
return self._adata

# Usage: adata.transform.log1p()

Register a custom HTML formatter for a type::

from anndata.extensions import register_formatter, TypeFormatter, FormattedOutput

@register_formatter
class MyArrayFormatter(TypeFormatter):
priority = 100 # Higher = checked first

def can_format(self, obj):
return isinstance(obj, MyArrayType)

def format(self, obj, context):
return FormattedOutput(
type_name=f"MyArray {obj.shape}",
css_class="dtype-custom",
)

Register a custom section formatter (for packages like TreeData, SpatialData)::

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

@register_formatter
class ObstSectionFormatter(SectionFormatter):
section_name = "obst"
after_section = "obsm" # Position in display order

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()
]

See Also
--------
anndata._repr : Full documentation of the HTML representation system
"""

from __future__ import annotations

# Accessor registration (from PR #1870)
from anndata._core.extensions import register_anndata_namespace

# HTML representation formatters
from anndata._repr import (
# Core formatter classes
FormattedEntry,
FormattedOutput,
FormatterContext,
FormatterRegistry,
SectionFormatter,
TypeFormatter,
# Registration function
register_formatter,
# Global registry instance
formatter_registry,
# Type hint utilities for tagged data
UNS_TYPE_HINT_KEY,
extract_uns_type_hint,
)

__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.

30 changes: 15 additions & 15 deletions tests/test_repr_html.py
Original file line number Diff line number Diff line change
Expand Up @@ -689,14 +689,14 @@ class TestFormatterRegistry:

def test_registry_has_formatters(self):
"""Test registry contains registered formatters."""
from anndata._repr.registry import formatter_registry
from anndata.extensions import formatter_registry

# Should have some formatters registered
assert len(formatter_registry._type_formatters) > 0

def test_custom_formatter_registration(self):
"""Test registering a custom formatter."""
from anndata._repr.registry import (
from anndata.extensions import (
FormattedOutput,
FormatterContext,
TypeFormatter,
Expand Down Expand Up @@ -739,7 +739,7 @@ def format(self, obj: Any, context: FormatterContext) -> FormattedOutput:

def test_fallback_formatter_for_unknown_types(self):
"""Test fallback formatter handles unknown types gracefully."""
from anndata._repr.registry import FormatterContext, formatter_registry
from anndata.extensions import FormatterContext, formatter_registry

class UnknownType:
"""An unknown type not in the registry."""
Expand All @@ -756,15 +756,15 @@ class UnknownType:

def test_formatter_priority_order(self):
"""Test formatters are checked in priority order."""
from anndata._repr.registry import formatter_registry
from anndata.extensions import formatter_registry

# Verify formatters are sorted by priority (highest first)
priorities = [f.priority for f in formatter_registry._type_formatters]
assert priorities == sorted(priorities, reverse=True)

def test_formatter_sections_filtering(self):
"""Test formatters are only applied to specified sections."""
from anndata._repr.registry import (
from anndata.extensions import (
FormattedOutput,
FormatterContext,
TypeFormatter,
Expand Down Expand Up @@ -808,7 +808,7 @@ def format(self, obj: Any, context: FormatterContext) -> FormattedOutput:

def test_formatter_sections_none_applies_everywhere(self):
"""Test formatters with sections=None apply to all sections."""
from anndata._repr.registry import (
from anndata.extensions import (
FormattedOutput,
FormatterContext,
TypeFormatter,
Expand Down Expand Up @@ -846,7 +846,7 @@ def format(self, obj: Any, context: FormatterContext) -> FormattedOutput:

def test_extension_type_graceful_handling(self):
"""Test extension types (like TreeData, MuData) are handled gracefully."""
from anndata._repr.registry import FormatterContext, formatter_registry
from anndata.extensions import FormatterContext, formatter_registry

# Simulate an extension type that has AnnData-like attributes
# We create the class in a way that properly sets __module__
Expand Down Expand Up @@ -1704,7 +1704,7 @@ def test_extract_type_hint_malformed_string_format(self):

def test_type_formatter_for_tagged_uns_data(self):
"""Test using TypeFormatter to handle tagged data in uns."""
from anndata._repr import (
from anndata.extensions import (
FormattedOutput,
TypeFormatter,
extract_uns_type_hint,
Expand Down Expand Up @@ -1761,7 +1761,7 @@ def test_unregistered_type_hint_shows_import_message(self):

def test_formatter_error_handled_gracefully(self):
"""Test that TypeFormatter errors don't crash the repr."""
from anndata._repr import (
from anndata.extensions import (
TypeFormatter,
extract_uns_type_hint,
formatter_registry,
Expand Down Expand Up @@ -1815,7 +1815,7 @@ def test_string_format_type_hint_in_html(self):

def test_type_hint_key_constant_exported(self):
"""Test that UNS_TYPE_HINT_KEY constant is properly exported."""
from anndata._repr import UNS_TYPE_HINT_KEY
from anndata.extensions import UNS_TYPE_HINT_KEY

assert UNS_TYPE_HINT_KEY == "__anndata_repr__"

Expand Down Expand Up @@ -1919,7 +1919,7 @@ class TestSectionFormatterCoverage:

def test_section_formatter_default_methods(self):
"""Test SectionFormatter default method implementations."""
from anndata._repr.registry import SectionFormatter
from anndata.extensions import SectionFormatter

class TestSectionFormatter(SectionFormatter):
@property
Expand Down Expand Up @@ -2606,14 +2606,14 @@ class TestRegistryAbstractMethods:

def test_type_formatter_is_abstract(self):
"""Verify TypeFormatter cannot be instantiated directly."""
from anndata._repr.registry import TypeFormatter
from anndata.extensions import TypeFormatter

with pytest.raises(TypeError):
TypeFormatter()

def test_section_formatter_is_abstract(self):
"""Verify SectionFormatter cannot be instantiated directly."""
from anndata._repr.registry import SectionFormatter
from anndata.extensions import SectionFormatter

with pytest.raises(TypeError):
SectionFormatter()
Expand All @@ -2624,7 +2624,7 @@ class TestCustomHtmlContent:

def test_inline_html_content(self):
"""Test inline (non-expandable) custom HTML content."""
from anndata._repr.registry import (
from anndata.extensions import (
FormattedOutput,
TypeFormatter,
formatter_registry,
Expand Down Expand Up @@ -2669,7 +2669,7 @@ def format(self, obj, context):

def test_expandable_html_content(self):
"""Test expandable custom HTML content (e.g., for TreeData visualization)."""
from anndata._repr.registry import (
from anndata.extensions import (
FormattedOutput,
TypeFormatter,
formatter_registry,
Expand Down
6 changes: 3 additions & 3 deletions tests/visual_inspect_repr_html.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@

import anndata as ad
from anndata import AnnData
from anndata._repr import (
from anndata.extensions import (
FormattedOutput,
TypeFormatter,
extract_uns_type_hint,
Expand All @@ -40,7 +40,7 @@
import networkx as nx
from treedata import TreeData

from anndata._repr import (
from anndata.extensions import (
FormattedEntry,
FormattedOutput,
FormatterContext,
Expand Down Expand Up @@ -238,7 +238,7 @@ def get_entries(self, obj, context: FormatterContext) -> list[FormattedEntry]:
try:
from mudata import MuData

from anndata._repr import (
from anndata.extensions import (
FormattedEntry,
FormattedOutput,
FormatterContext, # noqa: TC001
Expand Down