Skip to content

Commit dd15a5c

Browse files
committed
feat(utils): expose STANDARD_SECTIONS as the canonical section order
``iter_outer`` is currently the only way to enumerate AnnData's standard section names, which forces consumers that only need the names (membership checks, layout introspection, ecosystem packages mirroring the layout) to drive the generator and pay for a full ``getattr`` per section — reconstructing aligned mappings and, for backed AnnData, reopening and closing the backing file. Expose the section order as a module-level ``tuple`` and have ``iter_outer`` iterate it: - ``STANDARD_SECTIONS: tuple[AnnDataElem, ...]`` becomes the single source of truth. - ``iter_outer`` now loops over ``STANDARD_SECTIONS``; the yield order and exception behaviour for existing callers is unchanged. - Name-only consumers read the constant directly: ``from anndata.utils import STANDARD_SECTIONS``. This is a pure refactor behaviourally — no caller semantics change — and a small addition to the public surface. Downstream consumers (rich HTML repr in PR #2236, ecosystem packages) can iterate the constant with per-section ``try/except`` to stay usable when a single section's attribute access raises.
1 parent 7861448 commit dd15a5c

2 files changed

Lines changed: 49 additions & 14 deletions

File tree

src/anndata/utils.py

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -438,24 +438,38 @@ def module_get_attr_redirect(
438438
raise AttributeError(msg)
439439

440440

441+
#: Canonical order of the standard ``AnnData`` section attributes as used
442+
#: by :func:`iter_outer`, the text repr and the HTML repr. Exposed as a
443+
#: public constant so consumers that only need the names (e.g. membership
444+
#: checks, ordering decisions, introspecting layout without accessing the
445+
#: data) can reference it directly without driving :func:`iter_outer`,
446+
#: which reconstructs aligned mappings and reopens the backing file per
447+
#: yield.
448+
STANDARD_SECTIONS: tuple[AnnDataElem, ...] = (
449+
"X",
450+
"obs",
451+
"var",
452+
"uns",
453+
"obsm",
454+
"varm",
455+
"obsp",
456+
"varp",
457+
"layers",
458+
"raw",
459+
)
460+
461+
441462
def iter_outer(
442463
adata,
443464
) -> Generator[
444465
tuple[AnnDataElem, AxisStorable | _XDataType | Dataset2D | pd.DataFrame]
445466
]:
446-
"""Iterate over key-value pairs of the parent "elems" like aw, obs, varp etc"""
447-
for attr_name in [
448-
"X",
449-
"obs",
450-
"var",
451-
"uns",
452-
"obsm",
453-
"varm",
454-
"obsp",
455-
"varp",
456-
"layers",
457-
"raw",
458-
]:
467+
"""Iterate over key-value pairs of the parent "elems" like ``X``, ``obs``,
468+
``varp`` etc.
469+
470+
The section order is :data:`STANDARD_SECTIONS`.
471+
"""
472+
for attr_name in STANDARD_SECTIONS:
459473
was_closed = adata.isbacked and not adata.file.is_open
460474
yield (attr_name, getattr(adata, attr_name))
461475
if was_closed:

tests/test_utils.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,14 @@
22

33
from itertools import repeat
44

5+
import numpy as np
56
import pandas as pd
67
import pytest
78
from scipy import sparse
89

910
import anndata as ad
1011
from anndata.tests.helpers import gen_typed_df
11-
from anndata.utils import make_index_unique
12+
from anndata.utils import STANDARD_SECTIONS, iter_outer, make_index_unique
1213

1314

1415
def test_make_index_unique() -> None:
@@ -59,3 +60,23 @@ def test_adata_unique_indices() -> None:
5960

6061
pd.testing.assert_index_equal(v.obsm["df"].index, v.obs_names)
6162
pd.testing.assert_index_equal(v.varm["df"].index, v.var_names)
63+
64+
65+
def test_standard_sections_is_iter_outer_order() -> None:
66+
"""``STANDARD_SECTIONS`` must match the section order ``iter_outer`` yields.
67+
68+
Consumers that need only names (membership tests, layout introspection)
69+
rely on this equivalence to avoid the extra cost of actually driving the
70+
generator.
71+
"""
72+
adata = ad.AnnData(np.zeros((3, 4)))
73+
assert tuple(name for name, _ in iter_outer(adata)) == STANDARD_SECTIONS
74+
75+
76+
def test_standard_sections_contents() -> None:
77+
"""Every name in ``STANDARD_SECTIONS`` is accessible on a plain AnnData."""
78+
adata = ad.AnnData(np.zeros((3, 4)))
79+
for name in STANDARD_SECTIONS:
80+
assert hasattr(adata, name), (
81+
f"STANDARD_SECTIONS contains {name!r} but AnnData has no such attribute"
82+
)

0 commit comments

Comments
 (0)