From 6c48d03226fdfe403de5d812dd1738b240c1d86b Mon Sep 17 00:00:00 2001 From: Ilia Kats Date: Mon, 30 Mar 2026 10:26:55 +0200 Subject: [PATCH 1/5] bump min Python to 3.12, min AnnData to 0.11 --- pyproject.toml | 8 +++----- src/mudata/_core/compat.py | 29 ----------------------------- src/mudata/_core/mudata.py | 3 +-- tests/test_pull_push.py | 6 +++--- tests/test_update.py | 4 ++-- 5 files changed, 9 insertions(+), 41 deletions(-) delete mode 100644 src/mudata/_core/compat.py diff --git a/pyproject.toml b/pyproject.toml index 0c6f694..38651a5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,14 +14,12 @@ maintainers = [ authors = [ { name = "Danila Bredikhin" }, ] -requires-python = ">=3.10" +requires-python = ">=3.12" classifiers = [ "Intended Audience :: Science/Research", "License :: OSI Approved :: BSD License", "Operating System :: OS Independent", "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", @@ -29,7 +27,7 @@ classifiers = [ ] dynamic = [ "version" ] dependencies = [ - "anndata>=0.10.8", + "anndata>=0.11", "h5py", "numpy", "pandas>=1.4", @@ -78,7 +76,7 @@ envs.docs.scripts.open = "python -m webbrowser -t docs/_build/html/index.html" envs.docs.scripts.clean = "git clean -fdX -- {args:docs}" envs.hatch-test.dependency-groups = [ "dev", "test" ] envs.hatch-test.matrix = [ - { deps = [ "stable" ], python = [ "3.11", "3.14" ] }, + { deps = [ "stable" ], python = [ "3.12", "3.14" ] }, { deps = [ "pre" ], python = [ "3.14" ] } ] # If the matrix variable `deps` is set to "pre", diff --git a/src/mudata/_core/compat.py b/src/mudata/_core/compat.py deleted file mode 100644 index cc7f7a0..0000000 --- a/src/mudata/_core/compat.py +++ /dev/null @@ -1,29 +0,0 @@ -from __future__ import annotations - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from collections.abc import Mapping - - from anndata import AnnData - from anndata._core.raw import Raw - -try: - from anndata._core.aligned_mapping import AlignedView, AxisArrays, PairwiseArrays -except ImportError: - # anndata < 0.10.9 - from anndata._core.aligned_mapping import AlignedViewMixin as AlignedView - from anndata._core.aligned_mapping import AxisArrays as AxisArraysLegacy - from anndata._core.aligned_mapping import AxisArraysBase - from anndata._core.aligned_mapping import PairwiseArrays as PairwiseArraysLegacy - - class AxisArrays(AxisArraysLegacy): - def __init__(self, parent: AnnData | Raw, axis: int, store: Mapping | AxisArraysBase | None = None): - super().__init__(parent, axis=axis, vals=store) - - class PairwiseArrays(PairwiseArraysLegacy): - def __init__(self, parent: AnnData, axis: int, store: Mapping | None = None): - super().__init__(parent, axis=axis, vals=store) - - -__all__ = ["AlignedView", "AxisArrays", "PairwiseArrays"] diff --git a/src/mudata/_core/mudata.py b/src/mudata/_core/mudata.py index 3f9cde3..702cefd 100644 --- a/src/mudata/_core/mudata.py +++ b/src/mudata/_core/mudata.py @@ -17,12 +17,11 @@ import numpy as np import pandas as pd from anndata import AnnData -from anndata._core.aligned_mapping import AxisArraysBase +from anndata._core.aligned_mapping import AlignedView, AxisArrays, AxisArraysBase, PairwiseArrays from anndata._core.views import DataFrameView from anndata.utils import convert_to_dict from scverse_misc import Deprecation, deprecated -from .compat import AlignedView, AxisArrays, PairwiseArrays from .config import OPTIONS from .file_backing import MuDataFileManager from .repr import MUDATA_CSS, block_matrix, details_block_table diff --git a/tests/test_pull_push.py b/tests/test_pull_push.py index e47617b..bfbd786 100644 --- a/tests/test_pull_push.py +++ b/tests/test_pull_push.py @@ -1,5 +1,5 @@ from collections.abc import Callable -from typing import Literal, TypeAlias +from typing import Literal import numpy as np import pandas as pd @@ -8,8 +8,8 @@ from mudata import MuData -Axis: TypeAlias = Literal[0, 1] -AxisAttr: TypeAlias = Literal["obs", "var"] +type Axis = Literal[0, 1] +type AxisAttr = Literal["obs", "var"] @pytest.fixture(params=(0, 1)) diff --git a/tests/test_update.py b/tests/test_update.py index a573c79..d900a81 100644 --- a/tests/test_update.py +++ b/tests/test_update.py @@ -1,6 +1,6 @@ from collections.abc import Mapping, Sequence from functools import reduce -from typing import Literal, TypeAlias +from typing import Literal import numpy as np import pandas as pd @@ -10,7 +10,7 @@ from mudata import MuData -Axis: TypeAlias = Literal[0, 1] +type Axis = Literal[0, 1] @pytest.fixture(params=(0, 1)) From bbf1477b08ecb1689a906f71c32aa593e98e9258 Mon Sep 17 00:00:00 2001 From: Ilia Kats Date: Mon, 30 Mar 2026 11:08:18 +0200 Subject: [PATCH 2/5] use public read_elem/write_elem --- src/mudata/_core/io.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mudata/_core/io.py b/src/mudata/_core/io.py index c4aa730..430ba82 100644 --- a/src/mudata/_core/io.py +++ b/src/mudata/_core/io.py @@ -21,8 +21,8 @@ from anndata import AnnData from anndata._io.h5ad import _read_raw from anndata._io.h5ad import read_dataframe as read_h5ad_dataframe -from anndata._io.specs.registry import read_elem, write_elem from anndata.compat import _read_attr +from anndata.io import read_elem, write_elem from scipy import sparse from .file_backing import AnnDataFileManager, MuDataFileManager From a69f8df8f7ecf3fa51e416a403dd3e2ba8ab049b Mon Sep 17 00:00:00 2001 From: Ilia Kats Date: Mon, 30 Mar 2026 11:24:39 +0200 Subject: [PATCH 3/5] use anndata's read_zarr instead of our custom implementation MuData/AnnData read from Zarr cannot be backed, so there is no need for custom handling --- src/mudata/_core/io.py | 39 +++++---------------------------------- 1 file changed, 5 insertions(+), 34 deletions(-) diff --git a/src/mudata/_core/io.py b/src/mudata/_core/io.py index 430ba82..a0d5e50 100644 --- a/src/mudata/_core/io.py +++ b/src/mudata/_core/io.py @@ -23,6 +23,8 @@ from anndata._io.h5ad import read_dataframe as read_h5ad_dataframe from anndata.compat import _read_attr from anndata.io import read_elem, write_elem +from anndata.io import read_zarr as anndata_read_zarr +from anndata.io import write_zarr as anndata_write_zarr from scipy import sparse from .file_backing import AnnDataFileManager, MuDataFileManager @@ -162,7 +164,7 @@ def write_zarr( if adata.raw is not None: adata.strings_to_categoricals(adata.raw.var) - if write_data or not adata.isbacked: + if write_data: if chunks is not None and not isinstance(adata.X, sparse.spmatrix): write_elem(group, "X", adata.X, dataset_kwargs=dict(chunks=chunks, **kwargs)) else: @@ -445,15 +447,11 @@ def read_zarr(store: str | PathLike | MutableMapping | zarr.Group | zarr.abc.sto import zarr from anndata._io.zarr import read_dataframe as read_zarr_dataframe - if isinstance(store, Path): - store = str(store) - f = zarr.open(store, mode="r") d = {} if "mod" not in f.keys(): return ad.read_zarr(store) - manager = MuDataFileManager() for k in f.keys(): if k in {"obs", "var"}: d[k] = read_zarr_dataframe(f[k]) @@ -461,7 +459,7 @@ def read_zarr(store: str | PathLike | MutableMapping | zarr.Group | zarr.abc.sto mods = {} gmods = f[k] for m in gmods.keys(): - mods[m] = _read_zarr_mod(gmods[m], manager) + mods[m] = anndata_read_zarr(gmods[m]) mod_order = None if "mod-order" in gmods.attrs: @@ -474,38 +472,11 @@ def read_zarr(store: str | PathLike | MutableMapping | zarr.Group | zarr.abc.sto d[k] = read_elem(f[k]) mu = MuData._init_from_dict_(**d) - mu.file = manager + mu.file = MuDataFileManager() return mu -def _read_zarr_mod(g: zarr.Group, manager: MuDataFileManager = None, backed: bool = False) -> dict: - from anndata._io.zarr import _read_legacy_raw - from anndata._io.zarr import read_dataframe as read_zarr_dataframe - - d = {} - - for k in g.keys(): - if k in ("obs", "var"): - d[k] = read_zarr_dataframe(g[k]) - elif k == "X": - X = g["X"] - if not backed: - d["X"] = read_elem(X) - elif k != "raw": - d[k] = read_elem(g[k]) - ad = AnnData(**d) - if manager is not None: - ad.file = AnnDataFileManager(ad, Path(g.name).name, manager) - - raw = _read_legacy_raw( - g, d.get("raw"), read_zarr_dataframe, read_elem, attrs=("var", "varm") if backed else ("var", "varm", "X") - ) - if raw: - ad._raw = ad.Raw(ad, **raw) - return ad - - def _read_h5mu_mod(g: h5py.Group, manager: MuDataFileManager = None, backed: bool = False) -> dict: d = {} From 082f28fb222bafce7453996bfd449e26256b50e8 Mon Sep 17 00:00:00 2001 From: Ilia Kats Date: Thu, 9 Apr 2026 10:36:23 +0200 Subject: [PATCH 4/5] use read_dispatched instead of custom logic and private functions --- src/mudata/_core/io.py | 35 +++++++++++------------------------ 1 file changed, 11 insertions(+), 24 deletions(-) diff --git a/src/mudata/_core/io.py b/src/mudata/_core/io.py index a0d5e50..b625cf6 100644 --- a/src/mudata/_core/io.py +++ b/src/mudata/_core/io.py @@ -19,9 +19,8 @@ import anndata as ad import h5py from anndata import AnnData -from anndata._io.h5ad import _read_raw -from anndata._io.h5ad import read_dataframe as read_h5ad_dataframe from anndata.compat import _read_attr +from anndata.experimental import read_dispatched from anndata.io import read_elem, write_elem from anndata.io import read_zarr as anndata_read_zarr from anndata.io import write_zarr as anndata_write_zarr @@ -404,8 +403,6 @@ def read_h5mu( ) d = {} for k in f.keys(): - if k in ["obs", "var"]: - d[k] = read_h5ad_dataframe(f[k]) if k == "mod": mods = ModDict() gmods = f[k] @@ -445,7 +442,6 @@ def read_zarr(store: str | PathLike | MutableMapping | zarr.Group | zarr.abc.sto The file name or a Zarr store. """ import zarr - from anndata._io.zarr import read_dataframe as read_zarr_dataframe f = zarr.open(store, mode="r") d = {} @@ -453,8 +449,6 @@ def read_zarr(store: str | PathLike | MutableMapping | zarr.Group | zarr.abc.sto return ad.read_zarr(store) for k in f.keys(): - if k in {"obs", "var"}: - d[k] = read_zarr_dataframe(f[k]) if k == "mod": mods = {} gmods = f[k] @@ -477,25 +471,18 @@ def read_zarr(store: str | PathLike | MutableMapping | zarr.Group | zarr.abc.sto return mu -def _read_h5mu_mod(g: h5py.Group, manager: MuDataFileManager = None, backed: bool = False) -> dict: - d = {} +def _read_h5mu_mod(g: h5py.Group, manager: MuDataFileManager = None, backed: bool = False) -> AnnData: + modname = Path(g.name).name + Xpath = g.name + "/X" + rawXpath = g.name + "/raw/X" - for k in g.keys(): - if k in ("obs", "var"): - d[k] = read_h5ad_dataframe(g[k]) - elif k == "X": - X = g["X"] - if not backed: - d["X"] = read_elem(X) - elif k != "raw": - d[k] = read_elem(g[k]) - ad = AnnData(**d) - if manager is not None: - ad.file = AnnDataFileManager(ad, Path(g.name).name, manager) + def callback(func, elem_name, elem, iospec): + if not backed or elem_name not in (Xpath, rawXpath): + return func(elem) - raw = _read_raw(g, attrs=("var", "varm") if backed else ("var", "varm", "X")) - if raw: - ad._raw = ad.Raw(ad, **raw) + ad = read_dispatched(g, callback=callback) + if manager is not None: + ad.file = AnnDataFileManager(ad, modname, manager) return ad From 0461a48ecfc36680339bb8bf60284aa5fd5c1327 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 16 Apr 2026 16:25:12 +0000 Subject: [PATCH 5/5] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/mudata/_core/io.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/mudata/_core/io.py b/src/mudata/_core/io.py index b625cf6..30f4d47 100644 --- a/src/mudata/_core/io.py +++ b/src/mudata/_core/io.py @@ -23,7 +23,6 @@ from anndata.experimental import read_dispatched from anndata.io import read_elem, write_elem from anndata.io import read_zarr as anndata_read_zarr -from anndata.io import write_zarr as anndata_write_zarr from scipy import sparse from .file_backing import AnnDataFileManager, MuDataFileManager