diff --git a/src/anndata/utils.py b/src/anndata/utils.py index bee9882f7..6913d09d2 100644 --- a/src/anndata/utils.py +++ b/src/anndata/utils.py @@ -438,24 +438,38 @@ def module_get_attr_redirect( raise AttributeError(msg) +#: Canonical order of the standard ``AnnData`` section attributes as used +#: by :func:`iter_outer`, the text repr and the HTML repr. Exposed as a +#: public constant so consumers that only need the names (e.g. membership +#: checks, ordering decisions, introspecting layout without accessing the +#: data) can reference it directly without driving :func:`iter_outer`, +#: which reconstructs aligned mappings and reopens the backing file per +#: yield. +STANDARD_SECTIONS: tuple[AnnDataElem, ...] = ( + "X", + "obs", + "var", + "uns", + "obsm", + "varm", + "obsp", + "varp", + "layers", + "raw", +) + + def iter_outer( adata, ) -> Generator[ tuple[AnnDataElem, AxisStorable | _XDataType | Dataset2D | pd.DataFrame] ]: - """Iterate over key-value pairs of the parent "elems" like aw, obs, varp etc""" - for attr_name in [ - "X", - "obs", - "var", - "uns", - "obsm", - "varm", - "obsp", - "varp", - "layers", - "raw", - ]: + """Iterate over key-value pairs of the parent "elems" like ``X``, ``obs``, + ``varp`` etc. + + The section order is :data:`STANDARD_SECTIONS`. + """ + for attr_name in STANDARD_SECTIONS: was_closed = adata.isbacked and not adata.file.is_open yield (attr_name, getattr(adata, attr_name)) if was_closed: diff --git a/tests/test_utils.py b/tests/test_utils.py index a5fce76cf..e1e202af5 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -2,13 +2,14 @@ from itertools import repeat +import numpy as np import pandas as pd import pytest from scipy import sparse import anndata as ad from anndata.tests.helpers import gen_typed_df -from anndata.utils import make_index_unique +from anndata.utils import STANDARD_SECTIONS, iter_outer, make_index_unique def test_make_index_unique() -> None: @@ -59,3 +60,23 @@ def test_adata_unique_indices() -> None: pd.testing.assert_index_equal(v.obsm["df"].index, v.obs_names) pd.testing.assert_index_equal(v.varm["df"].index, v.var_names) + + +def test_standard_sections_is_iter_outer_order() -> None: + """``STANDARD_SECTIONS`` must match the section order ``iter_outer`` yields. + + Consumers that need only names (membership tests, layout introspection) + rely on this equivalence to avoid the extra cost of actually driving the + generator. + """ + adata = ad.AnnData(np.zeros((3, 4))) + assert tuple(name for name, _ in iter_outer(adata)) == STANDARD_SECTIONS + + +def test_standard_sections_contents() -> None: + """Every name in ``STANDARD_SECTIONS`` is accessible on a plain AnnData.""" + adata = ad.AnnData(np.zeros((3, 4))) + for name in STANDARD_SECTIONS: + assert hasattr(adata, name), ( + f"STANDARD_SECTIONS contains {name!r} but AnnData has no such attribute" + )