Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
40 changes: 27 additions & 13 deletions src/anndata/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
23 changes: 22 additions & 1 deletion tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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"
)
Loading