diff --git a/.azure-pipelines.yml b/.azure-pipelines.yml index ad32a3ca5..0a25c8e57 100644 --- a/.azure-pipelines.yml +++ b/.azure-pipelines.yml @@ -14,18 +14,18 @@ jobs: vmImage: "ubuntu-22.04" strategy: matrix: - Python3.12: - python.version: "3.12" + Python3.13: + python.version: "3.13" RUN_COVERAGE: yes TEST_TYPE: "coverage" - Python3.10: - python.version: "3.10" + Python3.11: + python.version: "3.11" PreRelease: - python.version: "3.12" + python.version: "3.13" DEPENDENCIES_VERSION: "pre-release" TEST_TYPE: "strict-warning" minimum_versions: - python.version: "3.10" + python.version: "3.11" DEPENDENCIES_VERSION: "minimum" TEST_TYPE: "coverage" steps: @@ -57,7 +57,7 @@ jobs: set -e uv pip install --system --compile tomli packaging deps=`python3 ci/scripts/min-deps.py pyproject.toml --extra dev test` - uv pip install --system --compile $deps pytest-cov "anndata @ ." + uv pip install --system --compile $deps pytest-cov "anndata[test,dev] @ ." displayName: "Install minimum dependencies" condition: eq(variables['DEPENDENCIES_VERSION'], 'minimum') @@ -104,8 +104,8 @@ jobs: steps: - task: UsePythonVersion@0 inputs: - versionSpec: "3.12" - displayName: "Use Python 3.12" + versionSpec: "3.13" + displayName: "Use Python 3.13" - script: | set -e diff --git a/.github/workflows/test-gpu.yml b/.github/workflows/test-gpu.yml index 1788fcf83..54260b8b6 100644 --- a/.github/workflows/test-gpu.yml +++ b/.github/workflows/test-gpu.yml @@ -64,7 +64,8 @@ jobs: - name: Install Python uses: actions/setup-python@v5 with: - python-version: ${{ env.max_python_version }} + # https://github.com/cupy/cupy/issues/8651 cupy does not support python3.13 yet + python-version: "3.12" - name: Install UV uses: hynek/setup-cached-uv@v2 diff --git a/.readthedocs.yml b/.readthedocs.yml index 8fa840e28..71adba451 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -2,7 +2,7 @@ version: 2 build: os: ubuntu-20.04 tools: - python: "3.12" + python: "3.13" jobs: post_checkout: # unshallow so version can be derived from tag diff --git a/ci/scripts/min-deps.py b/ci/scripts/min-deps.py index 4efc304cb..1e5699f93 100755 --- a/ci/scripts/min-deps.py +++ b/ci/scripts/min-deps.py @@ -10,17 +10,13 @@ import argparse import sys +import tomllib from collections import deque from contextlib import ExitStack from functools import cached_property from pathlib import Path from typing import TYPE_CHECKING -if sys.version_info >= (3, 11): - import tomllib -else: - import tomli as tomllib - from packaging.requirements import Requirement from packaging.version import Version diff --git a/docs/conf.py b/docs/conf.py index 337f6729e..7e03e9a8b 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -137,6 +137,7 @@ def setup(app: Sphinx): "pandas.DataFrame.loc": ("py:attr", "pandas.DataFrame.loc"), # should be fixed soon: https://github.com/tox-dev/sphinx-autodoc-typehints/pull/516 "types.EllipsisType": ("py:data", "types.EllipsisType"), + "pathlib._local.Path": "pathlib.Path", } autodoc_type_aliases = dict( NDArray=":data:`~numpy.typing.NDArray`", diff --git a/docs/release-notes/1768.breaking.md b/docs/release-notes/1768.breaking.md new file mode 100644 index 000000000..43449ebc7 --- /dev/null +++ b/docs/release-notes/1768.breaking.md @@ -0,0 +1 @@ +Tighten usage of {class}`scipy.sparse.spmatrix` for describing sparse matrices in types and instance checks to only {class}`scipy.sparse.csr_matrix` and {class}`scipy.sparse.csc_matrix` {user}`ilan-gold` diff --git a/docs/release-notes/1768.feature.md b/docs/release-notes/1768.feature.md new file mode 100644 index 000000000..768c79bd5 --- /dev/null +++ b/docs/release-notes/1768.feature.md @@ -0,0 +1 @@ +Adopt the Scientific Python [deprecation schedule](https://scientific-python.org/specs/spec-0000/) {user}`ilan-gold` diff --git a/hatch.toml b/hatch.toml index 12ada54da..a95abeb54 100644 --- a/hatch.toml +++ b/hatch.toml @@ -26,8 +26,8 @@ overrides.matrix.deps.pre-install-commands = [ { if = ["min"], value = "uv run ci/scripts/min-deps.py pyproject.toml --all-extras -o ci/min-deps.txt" }, ] overrides.matrix.deps.python = [ - { if = ["min"], value = "3.10" }, - { if = ["stable", "pre"], value = "3.12" }, + { if = ["min"], value = "3.11" }, + { if = ["stable", "pre"], value = "3.13" }, ] [[envs.hatch-test.matrix]] diff --git a/pyproject.toml b/pyproject.toml index b4b5ab14c..f3a9623b9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ requires = ["hatchling", "hatch-vcs"] [project] name = "anndata" description = "Annotated data." -requires-python = ">=3.10" +requires-python = ">=3.11" license = "BSD-3-Clause" authors = [ { name = "Philipp Angerer" }, @@ -29,20 +29,20 @@ classifiers = [ "Operating System :: Microsoft :: Windows", "Operating System :: POSIX :: Linux", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "Topic :: Scientific/Engineering :: Bio-Informatics", "Topic :: Scientific/Engineering :: Visualization", ] dependencies = [ # pandas <1.4 has pandas/issues/35446 # pandas 2.1.0rc0 has pandas/issues/54622 - "pandas >=1.4, !=2.1.0rc0, !=2.1.2", - "numpy>=1.23", + "pandas >=2.0.0, !=2.1.0rc0, !=2.1.2", + "numpy>=1.25", # https://github.com/scverse/anndata/issues/1434 - "scipy >1.8", - "h5py>=3.7", + "scipy >1.11", + "h5py>=3.8", "exceptiongroup; python_version<'3.11'", "natsort", "packaging>=24.2", @@ -108,13 +108,15 @@ gpu = ["cupy"] cu12 = ["cupy-cuda12x"] cu11 = ["cupy-cuda11x"] # https://github.com/dask/dask/issues/11290 -dask = ["dask[array]>=2022.09.2,!=2024.8.*,!=2024.9.*"] +dask = ["dask[array]>=2023.1.0,!=2024.8.*,!=2024.9.*"] [tool.hatch.version] source = "vcs" raw-options.version_scheme = "release-branch-semver" [tool.hatch.build.targets.wheel] packages = ["src/anndata", "src/testing"] +[tool.hatch.metadata] +allow-direct-references = true [tool.coverage.run] data_file = "test-data/coverage" diff --git a/src/anndata/__init__.py b/src/anndata/__init__.py index 94741882f..d783a2dd3 100644 --- a/src/anndata/__init__.py +++ b/src/anndata/__init__.py @@ -2,24 +2,17 @@ from __future__ import annotations -import sys from typing import TYPE_CHECKING if TYPE_CHECKING: from typing import Any -from ._version import __version__ - -# Allowing notes to be added to exceptions. See: https://github.com/scverse/anndata/issues/868 -if sys.version_info < (3, 11): - # Backport package for exception groups - import exceptiongroup # noqa: F401 - from ._core.anndata import AnnData from ._core.merge import concat from ._core.raw import Raw from ._settings import settings +from ._version import __version__ from ._warnings import ( ExperimentalFeatureWarning, ImplicitModificationWarning, diff --git a/src/anndata/_core/aligned_mapping.py b/src/anndata/_core/aligned_mapping.py index 88d5dde0d..1248d5f5d 100644 --- a/src/anndata/_core/aligned_mapping.py +++ b/src/anndata/_core/aligned_mapping.py @@ -9,10 +9,9 @@ import numpy as np import pandas as pd -from scipy.sparse import spmatrix from .._warnings import ExperimentalFeatureWarning, ImplicitModificationWarning -from ..compat import AwkArray +from ..compat import AwkArray, SpMatrix from ..utils import ( axis_len, convert_to_dict, @@ -36,7 +35,7 @@ OneDIdx = Sequence[int] | Sequence[bool] | slice TwoDIdx = tuple[OneDIdx, OneDIdx] # TODO: pd.DataFrame only allowed in AxisArrays? -Value = pd.DataFrame | spmatrix | np.ndarray +Value = pd.DataFrame | SpMatrix | np.ndarray P = TypeVar("P", bound="AlignedMappingBase") """Parent mapping an AlignedView is based on.""" diff --git a/src/anndata/_core/anndata.py b/src/anndata/_core/anndata.py index db22e4a60..73df9928d 100644 --- a/src/anndata/_core/anndata.py +++ b/src/anndata/_core/anndata.py @@ -231,14 +231,14 @@ class AnnData(metaclass=utils.DeprecationMixinMeta): ) def __init__( self, - X: np.ndarray | sparse.spmatrix | pd.DataFrame | None = None, + X: ArrayDataStructureType | pd.DataFrame | None = None, obs: pd.DataFrame | Mapping[str, Iterable[Any]] | None = None, var: pd.DataFrame | Mapping[str, Iterable[Any]] | None = None, uns: Mapping[str, Any] | None = None, *, obsm: np.ndarray | Mapping[str, Sequence[Any]] | None = None, varm: np.ndarray | Mapping[str, Sequence[Any]] | None = None, - layers: Mapping[str, np.ndarray | sparse.spmatrix] | None = None, + layers: Mapping[str, ArrayDataStructureType] | None = None, raw: Mapping[str, Any] | None = None, dtype: np.dtype | type | str | None = None, shape: tuple[int, int] | None = None, @@ -592,7 +592,7 @@ def X(self) -> ArrayDataStructureType | None: # return X @X.setter - def X(self, value: np.ndarray | sparse.spmatrix | SpArray | None): + def X(self, value: ArrayDataStructureType | None): if value is None: if self.isbacked: msg = "Cannot currently remove data matrix from backed object." @@ -1189,7 +1189,7 @@ def _inplace_subset_obs(self, index: Index1D): self._init_as_actual(adata_subset) # TODO: Update, possibly remove - def __setitem__(self, index: Index, val: float | np.ndarray | sparse.spmatrix): + def __setitem__(self, index: Index, val: ArrayDataStructureType): if self.is_view: msg = "Object is view and cannot be accessed with `[]`." raise ValueError(msg) diff --git a/src/anndata/_core/index.py b/src/anndata/_core/index.py index 32f69f182..baa4fd211 100644 --- a/src/anndata/_core/index.py +++ b/src/anndata/_core/index.py @@ -8,9 +8,9 @@ import h5py import numpy as np import pandas as pd -from scipy.sparse import issparse, spmatrix +from scipy.sparse import issparse -from ..compat import AwkArray, DaskArray, SpArray +from ..compat import AwkArray, DaskArray, SpArray, SpMatrix if TYPE_CHECKING: from ..compat import Index, Index1D @@ -69,13 +69,13 @@ def name_idx(i): elif isinstance(indexer, str): return index.get_loc(indexer) # int elif isinstance( - indexer, Sequence | np.ndarray | pd.Index | spmatrix | np.matrix | SpArray + indexer, Sequence | np.ndarray | pd.Index | SpMatrix | np.matrix | SpArray ): if hasattr(indexer, "shape") and ( (indexer.shape == (index.shape[0], 1)) or (indexer.shape == (1, index.shape[0])) ): - if isinstance(indexer, spmatrix | SpArray): + if isinstance(indexer, SpMatrix | SpArray): indexer = indexer.toarray() indexer = np.ravel(indexer) if not isinstance(indexer, np.ndarray | pd.Index): @@ -180,9 +180,9 @@ def _subset_dask(a: DaskArray, subset_idx: Index): return a[subset_idx] -@_subset.register(spmatrix) +@_subset.register(SpMatrix) @_subset.register(SpArray) -def _subset_sparse(a: spmatrix | SpArray, subset_idx: Index): +def _subset_sparse(a: SpMatrix | SpArray, subset_idx: Index): # Correcting for indexing behaviour of sparse.spmatrix if len(subset_idx) > 1 and all(isinstance(x, Iterable) for x in subset_idx): first_idx = subset_idx[0] diff --git a/src/anndata/_core/merge.py b/src/anndata/_core/merge.py index 21bdcc414..31f01bba4 100644 --- a/src/anndata/_core/merge.py +++ b/src/anndata/_core/merge.py @@ -15,20 +15,21 @@ import numpy as np import pandas as pd +import scipy from natsort import natsorted +from packaging.version import Version from scipy import sparse -from scipy.sparse import spmatrix from anndata._warnings import ExperimentalFeatureWarning from ..compat import ( - CAN_USE_SPARSE_ARRAY, AwkArray, CupyArray, CupyCSRMatrix, CupySparseMatrix, DaskArray, SpArray, + SpMatrix, _map_cat_to_str, ) from ..utils import asarray, axis_len, warn_once @@ -135,7 +136,7 @@ def equal_dask_array(a, b) -> bool: if isinstance(b, DaskArray): if tokenize(a) == tokenize(b): return True - if isinstance(a._meta, spmatrix): + if isinstance(a._meta, SpMatrix): # TODO: Maybe also do this in the other case? return da.map_blocks(equal, a, b, drop_axis=(0, 1)).all() else: @@ -165,7 +166,7 @@ def equal_series(a, b) -> bool: return a.equals(b) -@equal.register(sparse.spmatrix) +@equal.register(SpMatrix) @equal.register(SpArray) @equal.register(CupySparseMatrix) def equal_sparse(a, b) -> bool: @@ -174,7 +175,7 @@ def equal_sparse(a, b) -> bool: xp = array_api_compat.array_namespace(a.data) - if isinstance(b, CupySparseMatrix | sparse.spmatrix | SpArray): + if isinstance(b, CupySparseMatrix | SpMatrix | SpArray): if isinstance(a, CupySparseMatrix): # Comparison broken for CSC matrices # https://github.com/cupy/cupy/issues/7757 @@ -205,13 +206,12 @@ def equal_awkward(a, b) -> bool: return ak.almost_equal(a, b) -def as_sparse(x, *, use_sparse_array: bool = False): - if not isinstance(x, sparse.spmatrix | SpArray): - if CAN_USE_SPARSE_ARRAY and use_sparse_array: +def as_sparse(x, *, use_sparse_array=False): + if not isinstance(x, SpMatrix | SpArray): + if use_sparse_array: return sparse.csr_array(x) return sparse.csr_matrix(x) - else: - return x + return x def as_cp_sparse(x) -> CupySparseMatrix: @@ -537,7 +537,7 @@ def apply(self, el, *, axis, fill_value=None): return el if isinstance(el, pd.DataFrame): return self._apply_to_df(el, axis=axis, fill_value=fill_value) - elif isinstance(el, sparse.spmatrix | SpArray | CupySparseMatrix): + elif isinstance(el, SpMatrix | SpArray | CupySparseMatrix): return self._apply_to_sparse(el, axis=axis, fill_value=fill_value) elif isinstance(el, AwkArray): return self._apply_to_awkward(el, axis=axis, fill_value=fill_value) @@ -615,8 +615,8 @@ def _apply_to_array(self, el, *, axis, fill_value=None): ) def _apply_to_sparse( - self, el: sparse.spmatrix | SpArray, *, axis, fill_value=None - ) -> spmatrix: + self, el: SpMatrix | SpArray, *, axis, fill_value=None + ) -> SpMatrix: if isinstance(el, CupySparseMatrix): from cupyx.scipy import sparse else: @@ -726,11 +726,8 @@ def default_fill_value(els): This is largely due to backwards compat, and might not be the ideal solution. """ if any( - isinstance(el, sparse.spmatrix | SpArray) - or ( - isinstance(el, DaskArray) - and isinstance(el._meta, sparse.spmatrix | SpArray) - ) + isinstance(el, SpMatrix | SpArray) + or (isinstance(el, DaskArray) and isinstance(el._meta, SpMatrix | SpArray)) for el in els ): return 0 @@ -826,10 +823,10 @@ def concat_arrays(arrays, reindexers, axis=0, index=None, fill_value=None): ], axis=axis, ) - elif any(isinstance(a, sparse.spmatrix | SpArray) for a in arrays): + elif any(isinstance(a, SpMatrix | SpArray) for a in arrays): sparse_stack = (sparse.vstack, sparse.hstack)[axis] use_sparse_array = any(issubclass(type(a), SpArray) for a in arrays) - return sparse_stack( + mat = sparse_stack( [ f( as_sparse(a, use_sparse_array=use_sparse_array), @@ -840,6 +837,13 @@ def concat_arrays(arrays, reindexers, axis=0, index=None, fill_value=None): ], format="csr", ) + scipy_version = Version(scipy.__version__) + # Bug where xstack produces a matrix not an array in 1.11.* + if use_sparse_array and (scipy_version.major, scipy_version.minor) == (1, 11): + if mat.format == "csc": + return sparse.csc_array(mat) + return sparse.csr_array(mat) + return mat else: return np.concatenate( [ diff --git a/src/anndata/_core/raw.py b/src/anndata/_core/raw.py index f71c8d74d..c8b82bdf5 100644 --- a/src/anndata/_core/raw.py +++ b/src/anndata/_core/raw.py @@ -17,8 +17,7 @@ from collections.abc import Mapping, Sequence from typing import ClassVar - from scipy import sparse - + from ..compat import SpMatrix from .aligned_mapping import AxisArraysView from .anndata import AnnData from .sparse_dataset import BaseCompressedSparseDataset @@ -31,7 +30,7 @@ class Raw: def __init__( self, adata: AnnData, - X: np.ndarray | sparse.spmatrix | None = None, + X: np.ndarray | SpMatrix | None = None, var: pd.DataFrame | Mapping[str, Sequence] | None = None, varm: AxisArrays | Mapping[str, np.ndarray] | None = None, ): @@ -67,7 +66,7 @@ def _get_X(self, layer=None): return self.X @property - def X(self) -> BaseCompressedSparseDataset | np.ndarray | sparse.spmatrix: + def X(self) -> BaseCompressedSparseDataset | np.ndarray | SpMatrix: # TODO: Handle unsorted array of integer indices for h5py.Datasets if not self._adata.isbacked: return self._X diff --git a/src/anndata/_core/sparse_dataset.py b/src/anndata/_core/sparse_dataset.py index 1b9eabb5d..5436d418e 100644 --- a/src/anndata/_core/sparse_dataset.py +++ b/src/anndata/_core/sparse_dataset.py @@ -30,7 +30,7 @@ from .. import abc from .._settings import settings -from ..compat import H5Group, SpArray, ZarrArray, ZarrGroup, _read_attr +from ..compat import H5Group, SpArray, SpMatrix, ZarrArray, ZarrGroup, _read_attr from .index import _fix_slice_bounds, _subset, unpack_index if TYPE_CHECKING: @@ -329,7 +329,7 @@ def get_memory_class( if format == fmt: if use_sparray_in_io and issubclass(memory_class, SpArray): return memory_class - elif not use_sparray_in_io and issubclass(memory_class, ss.spmatrix): + elif not use_sparray_in_io and issubclass(memory_class, SpMatrix): return memory_class msg = f"Format string {format} is not supported." raise ValueError(msg) @@ -342,7 +342,7 @@ def get_backed_class( if format == fmt: if use_sparray_in_io and issubclass(backed_class, SpArray): return backed_class - elif not use_sparray_in_io and issubclass(backed_class, ss.spmatrix): + elif not use_sparray_in_io and issubclass(backed_class, SpMatrix): return backed_class msg = f"Format string {format} is not supported." raise ValueError(msg) diff --git a/src/anndata/_io/h5ad.py b/src/anndata/_io/h5ad.py index ff33dc2f3..534368e2f 100644 --- a/src/anndata/_io/h5ad.py +++ b/src/anndata/_io/h5ad.py @@ -18,6 +18,7 @@ from .._core.file_backing import filename from .._core.sparse_dataset import BaseCompressedSparseDataset from ..compat import ( + SpMatrix, _clean_uns, _decode_structured_array, _from_fixed_length_strings, @@ -82,14 +83,14 @@ def write_h5ad( f.attrs.setdefault("encoding-version", "0.1.0") if "X" in as_dense and isinstance( - adata.X, sparse.spmatrix | BaseCompressedSparseDataset + adata.X, SpMatrix | BaseCompressedSparseDataset ): write_sparse_as_dense(f, "X", adata.X, dataset_kwargs=dataset_kwargs) elif not (adata.isbacked and Path(adata.filename) == Path(filepath)): # If adata.isbacked, X should already be up to date write_elem(f, "X", adata.X, dataset_kwargs=dataset_kwargs) if "raw/X" in as_dense and isinstance( - adata.raw.X, sparse.spmatrix | BaseCompressedSparseDataset + adata.raw.X, SpMatrix | BaseCompressedSparseDataset ): write_sparse_as_dense( f, "raw/X", adata.raw.X, dataset_kwargs=dataset_kwargs @@ -115,7 +116,7 @@ def write_h5ad( def write_sparse_as_dense( f: h5py.Group, key: str, - value: sparse.spmatrix | BaseCompressedSparseDataset, + value: SpMatrix | BaseCompressedSparseDataset, *, dataset_kwargs: Mapping[str, Any] = MappingProxyType({}), ): @@ -172,7 +173,7 @@ def read_h5ad( backed: Literal["r", "r+"] | bool | None = None, *, as_sparse: Sequence[str] = (), - as_sparse_fmt: type[sparse.spmatrix] = sparse.csr_matrix, + as_sparse_fmt: type[SpMatrix] = sparse.csr_matrix, chunk_size: int = 6000, # TODO, probably make this 2d chunks ) -> AnnData: """\ @@ -273,7 +274,7 @@ def callback(func, elem_name: str, elem, iospec): def _read_raw( f: h5py.File | AnnDataFileManager, as_sparse: Collection[str] = (), - rdasp: Callable[[h5py.Dataset], sparse.spmatrix] | None = None, + rdasp: Callable[[h5py.Dataset], SpMatrix] | None = None, *, attrs: Collection[str] = ("X", "var", "varm"), ) -> dict: @@ -346,7 +347,7 @@ def read_dataset(dataset: h5py.Dataset): @report_read_key_on_error def read_dense_as_sparse( - dataset: h5py.Dataset, sparse_format: sparse.spmatrix, axis_chunk: int + dataset: h5py.Dataset, sparse_format: SpMatrix, axis_chunk: int ): if sparse_format == sparse.csr_matrix: return read_dense_as_csr(dataset, axis_chunk) diff --git a/src/anndata/_io/specs/methods.py b/src/anndata/_io/specs/methods.py index 065ded7d5..d9bd72b1e 100644 --- a/src/anndata/_io/specs/methods.py +++ b/src/anndata/_io/specs/methods.py @@ -52,7 +52,7 @@ from numpy.typing import NDArray from anndata._types import ArrayStorageType, GroupStorageType - from anndata.compat import SpArray + from anndata.compat import SpArray, SpMatrix from anndata.typing import AxisStorable, InMemoryArrayOrScalarType from .registry import Reader, Writer @@ -127,7 +127,7 @@ def wrapper( @_REGISTRY.register_read(H5Array, IOSpec("", "")) def read_basic( elem: H5File | H5Group | H5Array, *, _reader: Reader -) -> dict[str, InMemoryArrayOrScalarType] | npt.NDArray | sparse.spmatrix | SpArray: +) -> dict[str, InMemoryArrayOrScalarType] | npt.NDArray | SpMatrix | SpArray: from anndata._io import h5ad warn( @@ -149,7 +149,7 @@ def read_basic( @_REGISTRY.register_read(ZarrArray, IOSpec("", "")) def read_basic_zarr( elem: ZarrGroup | ZarrArray, *, _reader: Reader -) -> dict[str, InMemoryArrayOrScalarType] | npt.NDArray | sparse.spmatrix | SpArray: +) -> dict[str, InMemoryArrayOrScalarType] | npt.NDArray | SpMatrix | SpArray: from anndata._io import zarr warn( @@ -614,7 +614,7 @@ def write_recarray_zarr( def write_sparse_compressed( f: GroupStorageType, key: str, - value: sparse.spmatrix | SpArray, + value: SpMatrix | SpArray, *, _writer: Writer, fmt: Literal["csr", "csc"], @@ -626,7 +626,7 @@ def write_sparse_compressed( indptr_dtype = dataset_kwargs.pop("indptr_dtype", value.indptr.dtype) # Allow resizing for hdf5 - if isinstance(f, H5Group) and "maxshape" not in dataset_kwargs: + if isinstance(f, H5Group): dataset_kwargs = dict(maxshape=(None,), **dataset_kwargs) g.create_dataset("data", data=value.data, **dataset_kwargs) @@ -780,9 +780,7 @@ def chunk_slice(start: int, stop: int) -> tuple[slice | None, slice | None]: @_REGISTRY.register_read(H5Group, IOSpec("csr_matrix", "0.1.0")) @_REGISTRY.register_read(ZarrGroup, IOSpec("csc_matrix", "0.1.0")) @_REGISTRY.register_read(ZarrGroup, IOSpec("csr_matrix", "0.1.0")) -def read_sparse( - elem: GroupStorageType, *, _reader: Reader -) -> sparse.spmatrix | SpArray: +def read_sparse(elem: GroupStorageType, *, _reader: Reader) -> SpMatrix | SpArray: return sparse_dataset(elem).to_memory() diff --git a/src/anndata/_io/utils.py b/src/anndata/_io/utils.py index 2e393613c..fe2145281 100644 --- a/src/anndata/_io/utils.py +++ b/src/anndata/_io/utils.py @@ -9,7 +9,6 @@ from packaging.version import Version from .._core.sparse_dataset import BaseCompressedSparseDataset -from ..compat import add_note if TYPE_CHECKING: from collections.abc import Callable, Mapping @@ -181,7 +180,7 @@ def add_key_note( dir = "to" if op == "writ" else "from" msg = f"Error raised while {op}ing key {key!r} of {type(store)} {dir} {path}" - add_note(e, msg) + e.add_note(msg) def report_read_key_on_error(func): diff --git a/src/anndata/_settings.py b/src/anndata/_settings.py index ae066f9e5..00db4b2be 100644 --- a/src/anndata/_settings.py +++ b/src/anndata/_settings.py @@ -2,7 +2,6 @@ import inspect import os -import sys import textwrap import warnings from collections.abc import Iterable @@ -14,9 +13,6 @@ from types import GenericAlias from typing import TYPE_CHECKING, Generic, NamedTuple, TypeVar, cast -from anndata.compat import CAN_USE_SPARSE_ARRAY -from anndata.compat.exceptiongroups import add_note - if TYPE_CHECKING: from collections.abc import Callable, Sequence from typing import Any, TypeGuard @@ -53,27 +49,14 @@ def describe(self: RegisteredOption, *, as_rst: bool = False) -> str: return textwrap.dedent(doc) -if sys.version_info >= (3, 11): - - class RegisteredOption(NamedTuple, Generic[T]): - option: str - default_value: T - description: str - validate: Callable[[T], None] - type: object - - describe = describe - -else: - - class RegisteredOption(NamedTuple): - option: str - default_value: T - description: str - validate: Callable[[T], None] - type: object +class RegisteredOption(NamedTuple, Generic[T]): + option: str + default_value: T + description: str + validate: Callable[[T], None] + type: object - describe = describe + describe = describe def check_and_get_environ_var( @@ -235,7 +218,7 @@ def register( try: validate(default_value) except (ValueError, TypeError) as e: - add_note(e, f"for option {option!r}") + e.add_note(f"for option {option!r}") raise e option_type = type(default_value) if option_type is None else option_type self._registered_options[option] = RegisteredOption( @@ -432,12 +415,6 @@ def validate_bool(val: Any) -> None: def validate_sparse_settings(val: Any) -> None: validate_bool(val) - if not CAN_USE_SPARSE_ARRAY and cast(bool, val): - msg = ( - "scipy.sparse.cs{r,c}array is not available in current scipy version. " - "Falling back to scipy.sparse.cs{r,c}_matrix for reading." - ) - raise ValueError(msg) settings.register( diff --git a/src/anndata/compat/__init__.py b/src/anndata/compat/__init__.py index e30e5d4bf..75601b760 100644 --- a/src/anndata/compat/__init__.py +++ b/src/anndata/compat/__init__.py @@ -1,15 +1,10 @@ from __future__ import annotations -import os -import sys from codecs import decode from collections.abc import Mapping -from contextlib import AbstractContextManager -from dataclasses import dataclass, field from functools import partial, singledispatch, wraps from importlib.util import find_spec from inspect import Parameter, signature -from pathlib import Path from types import EllipsisType from typing import TYPE_CHECKING, TypeVar from warnings import warn @@ -18,11 +13,8 @@ import numpy as np import pandas as pd import scipy -import scipy.sparse from packaging.version import Version -from .exceptiongroups import add_note # noqa: F401 - if TYPE_CHECKING: from typing import Any @@ -31,16 +23,8 @@ ############################# -CAN_USE_SPARSE_ARRAY = Version(scipy.__version__) >= Version("1.11") - -if not CAN_USE_SPARSE_ARRAY: - - class SpArray: - @staticmethod - def __repr__(): - return "mock scipy.sparse.sparray" -else: - SpArray = scipy.sparse.sparray +SpMatrix = scipy.sparse.csr_matrix | scipy.sparse.csc_matrix +SpArray = scipy.sparse.csr_array | scipy.sparse.csc_array class Empty: @@ -56,7 +40,7 @@ class Empty: | tuple[Index1D, Index1D, EllipsisType] | tuple[EllipsisType, Index1D, Index1D] | tuple[Index1D, EllipsisType, Index1D] - | scipy.sparse.spmatrix + | SpMatrix | SpArray ) H5Group = h5py.Group @@ -69,23 +53,6 @@ class Empty: ############################# -if sys.version_info >= (3, 11): - from contextlib import chdir -else: - - @dataclass - class chdir(AbstractContextManager): - path: Path - _old_cwd: list[Path] = field(default_factory=list) - - def __enter__(self) -> None: - self._old_cwd.append(Path()) - os.chdir(self.path) - - def __exit__(self, *_exc_info) -> None: - os.chdir(self._old_cwd.pop()) - - ############################# # Optional deps ############################# diff --git a/src/anndata/compat/exceptiongroups.py b/src/anndata/compat/exceptiongroups.py deleted file mode 100644 index 49d6c3a65..000000000 --- a/src/anndata/compat/exceptiongroups.py +++ /dev/null @@ -1,14 +0,0 @@ -from __future__ import annotations - -import sys - - -def add_note(err: BaseException, msg: str) -> BaseException: - """ - Adds a note to an exception inplace and returns it. - """ - if sys.version_info < (3, 11): - err.__notes__ = getattr(err, "__notes__", []) + [msg] - else: - err.add_note(msg) - return err diff --git a/src/anndata/tests/helpers.py b/src/anndata/tests/helpers.py index 95f3a036b..61f42b699 100644 --- a/src/anndata/tests/helpers.py +++ b/src/anndata/tests/helpers.py @@ -24,7 +24,6 @@ from anndata._core.sparse_dataset import BaseCompressedSparseDataset from anndata._core.views import ArrayView from anndata.compat import ( - CAN_USE_SPARSE_ARRAY, AwkArray, CupyArray, CupyCSCMatrix, @@ -32,6 +31,7 @@ CupySparseMatrix, DaskArray, SpArray, + SpMatrix, ZarrArray, ) from anndata.utils import asarray @@ -61,21 +61,21 @@ np.ndarray, pd.DataFrame, DaskArray, - *((sparse.csr_array,) if CAN_USE_SPARSE_ARRAY else ()), + sparse.csr_array, ), varm_types=( sparse.csr_matrix, np.ndarray, pd.DataFrame, DaskArray, - *((sparse.csr_array,) if CAN_USE_SPARSE_ARRAY else ()), + sparse.csr_array, ), layers_types=( sparse.csr_matrix, np.ndarray, pd.DataFrame, DaskArray, - *((sparse.csr_array,) if CAN_USE_SPARSE_ARRAY else ()), + sparse.csr_array, ), ) @@ -84,7 +84,7 @@ sparse.csr_matrix, np.ndarray, pd.DataFrame, - *((sparse.csr_array,) if CAN_USE_SPARSE_ARRAY else ()), + sparse.csr_array, ) @@ -271,11 +271,10 @@ def maybe_add_sparse_array( random_state: np.random.Generator, shape: tuple[int, int], ): - if CAN_USE_SPARSE_ARRAY: - if sparse.csr_array in types or sparse.csr_matrix in types: - mapping["sparse_array"] = sparse.csr_array( - sparse.random(*shape, format=format, random_state=random_state) - ) + if sparse.csr_array in types or sparse.csr_matrix in types: + mapping["sparse_array"] = sparse.csr_array( + sparse.random(*shape, format=format, random_state=random_state) + ) return mapping @@ -389,18 +388,16 @@ def gen_adata( array=np.random.random((M, M)), sparse=sparse.random(M, M, format=sparse_fmt, random_state=random_state), ) - if CAN_USE_SPARSE_ARRAY: - obsp["sparse_array"] = sparse.csr_array( - sparse.random(M, M, format=sparse_fmt, random_state=random_state) - ) + obsp["sparse_array"] = sparse.csr_array( + sparse.random(M, M, format=sparse_fmt, random_state=random_state) + ) varp = dict( array=np.random.random((N, N)), sparse=sparse.random(N, N, format=sparse_fmt, random_state=random_state), ) - if CAN_USE_SPARSE_ARRAY: - varp["sparse_array"] = sparse.csr_array( - sparse.random(N, N, format=sparse_fmt, random_state=random_state) - ) + varp["sparse_array"] = sparse.csr_array( + sparse.random(N, N, format=sparse_fmt, random_state=random_state) + ) uns = dict( O_recarray=gen_vstr_recarray(N, 5), nested=dict( @@ -836,7 +833,7 @@ def as_dense_dask_array(a): return da.asarray(a, chunks=_half_chunk_size(a.shape)) -@as_dense_dask_array.register(sparse.spmatrix) +@as_dense_dask_array.register(SpMatrix) def _(a): return as_dense_dask_array(a.toarray()) @@ -853,7 +850,7 @@ def as_sparse_dask_array(a) -> DaskArray: return da.from_array(sparse.csr_matrix(a), chunks=_half_chunk_size(a.shape)) -@as_sparse_dask_array.register(sparse.spmatrix) +@as_sparse_dask_array.register(SpMatrix) def _(a): import dask.array as da @@ -1004,7 +1001,7 @@ def as_cupy(val, typ=None): if issubclass(typ, CupyArray): import cupy as cp - if isinstance(val, sparse.spmatrix): + if isinstance(val, SpMatrix): val = val.toarray() return cp.array(val) elif issubclass(typ, CupyCSRMatrix): @@ -1041,7 +1038,7 @@ def shares_memory(x, y) -> bool: return np.shares_memory(x, y) -@shares_memory.register(sparse.spmatrix) +@shares_memory.register(SpMatrix) def shares_memory_sparse(x, y): return ( np.shares_memory(x.data, y.data) @@ -1052,8 +1049,8 @@ def shares_memory_sparse(x, y): BASE_MATRIX_PARAMS = [ pytest.param(asarray, id="np_array"), - pytest.param(sparse.csr_matrix, id="scipy_csr"), - pytest.param(sparse.csc_matrix, id="scipy_csc"), + pytest.param(sparse.csr_matrix, id="scipy_csr_matrix"), + pytest.param(sparse.csc_matrix, id="scipy_csc_matrix"), pytest.param(sparse.csr_array, id="scipy_csr_array"), pytest.param(sparse.csc_array, id="scipy_csc_array"), ] diff --git a/src/anndata/typing.py b/src/anndata/typing.py index d13927bad..8012a162d 100644 --- a/src/anndata/typing.py +++ b/src/anndata/typing.py @@ -5,7 +5,6 @@ import numpy as np import pandas as pd from numpy import ma -from scipy import sparse from . import abc from ._core.anndata import AnnData @@ -16,6 +15,7 @@ DaskArray, H5Array, SpArray, + SpMatrix, ZappyArray, ZarrArray, ) @@ -31,12 +31,10 @@ Index = _Index """1D or 2D index an :class:`~anndata.AnnData` object can be sliced with.""" - ArrayDataStructureType: TypeAlias = ( np.ndarray | ma.MaskedArray - | sparse.csr_matrix - | sparse.csc_matrix + | SpMatrix | SpArray | AwkArray | H5Array diff --git a/src/testing/anndata/_pytest.py b/src/testing/anndata/_pytest.py index 5b0fd60e0..df4441c04 100644 --- a/src/testing/anndata/_pytest.py +++ b/src/testing/anndata/_pytest.py @@ -51,7 +51,8 @@ def _doctest_env( ) from scanpy import settings - from anndata.compat import chdir + from contextlib import chdir + from anndata.utils import import_name assert isinstance(request.node.parent, pytest.Module) diff --git a/tests/test_backed_hdf5.py b/tests/test_backed_hdf5.py index 2b584ad67..de2ea6d50 100644 --- a/tests/test_backed_hdf5.py +++ b/tests/test_backed_hdf5.py @@ -10,7 +10,7 @@ from scipy import sparse import anndata as ad -from anndata.compat import SpArray +from anndata.compat import SpArray, SpMatrix from anndata.tests.helpers import ( GEN_ADATA_DASK_ARGS, as_dense_dask_array, @@ -85,6 +85,8 @@ def as_dense(request): # ------------------------------------------------------------------------------- +# h5py internally calls `product` on min-versions +@pytest.mark.filterwarnings("ignore:`product` is deprecated as of NumPy 1.25.0") # TODO: Check to make sure obs, obsm, layers, ... are written and read correctly as well @pytest.mark.filterwarnings("error") def test_read_write_X(tmp_path, mtx_format, backed_mode, as_dense): @@ -200,8 +202,8 @@ def test_backed_raw_subset(tmp_path, array_type, subset_func, subset_func2): var_idx = subset_func2(mem_adata.var_names) if ( array_type is asarray - and isinstance(obs_idx, list | np.ndarray | sparse.spmatrix | SpArray) - and isinstance(var_idx, list | np.ndarray | sparse.spmatrix | SpArray) + and isinstance(obs_idx, list | np.ndarray | SpMatrix | SpArray) + and isinstance(var_idx, list | np.ndarray | SpMatrix | SpArray) ): pytest.xfail( "Fancy indexing does not work with multiple arrays on a h5py.Dataset" diff --git a/tests/test_backed_sparse.py b/tests/test_backed_sparse.py index 043103820..33ef803c6 100644 --- a/tests/test_backed_sparse.py +++ b/tests/test_backed_sparse.py @@ -14,7 +14,7 @@ from anndata._core.anndata import AnnData from anndata._core.sparse_dataset import sparse_dataset from anndata._io.specs.registry import read_elem_as_dask -from anndata.compat import CAN_USE_SPARSE_ARRAY, DaskArray, SpArray +from anndata.compat import DaskArray, SpArray, SpMatrix from anndata.experimental import read_dispatched from anndata.tests.helpers import AccessTrackingStore, assert_equal, subset_func @@ -263,8 +263,8 @@ def test_consecutive_bool( ) def test_dataset_append_memory( tmp_path: Path, - sparse_format: Callable[[ArrayLike], sparse.spmatrix], - append_method: Callable[[list[sparse.spmatrix]], sparse.spmatrix], + sparse_format: Callable[[ArrayLike], SpMatrix], + append_method: Callable[[list[SpMatrix]], SpMatrix], diskfmt: Literal["h5ad", "zarr"], ): path = tmp_path / f"test.{diskfmt.replace('ad', '')}" @@ -319,7 +319,7 @@ def test_append_array_cache_bust(tmp_path: Path, diskfmt: Literal["h5ad", "zarr" ) def test_read_array( tmp_path: Path, - sparse_format: Callable[[ArrayLike], sparse.spmatrix], + sparse_format: Callable[[ArrayLike], SpMatrix], diskfmt: Literal["h5ad", "zarr"], subset_func, subset_func2, @@ -334,12 +334,10 @@ def test_read_array( f = h5py.File(path, "a") ad.io.write_elem(f, "mtx", a) diskmtx = sparse_dataset(f["mtx"]) - if not CAN_USE_SPARSE_ARRAY: - pytest.skip("scipy.sparse.cs{r,c}array not available") ad.settings.use_sparse_array_on_read = True assert issubclass(type(diskmtx[obs_idx, var_idx]), SpArray) ad.settings.use_sparse_array_on_read = False - assert issubclass(type(diskmtx[obs_idx, var_idx]), sparse.spmatrix) + assert issubclass(type(diskmtx[obs_idx, var_idx]), SpMatrix) @pytest.mark.parametrize( @@ -351,8 +349,8 @@ def test_read_array( ) def test_dataset_append_disk( tmp_path: Path, - sparse_format: Callable[[ArrayLike], sparse.spmatrix], - append_method: Callable[[list[sparse.spmatrix]], sparse.spmatrix], + sparse_format: Callable[[ArrayLike], SpMatrix], + append_method: Callable[[list[SpMatrix]], SpMatrix], diskfmt: Literal["h5ad", "zarr"], ): path = tmp_path / f"test.{diskfmt.replace('ad', '')}" @@ -379,7 +377,7 @@ def test_dataset_append_disk( @pytest.mark.parametrize("sparse_format", [sparse.csr_matrix, sparse.csc_matrix]) def test_lazy_array_cache( tmp_path: Path, - sparse_format: Callable[[ArrayLike], sparse.spmatrix], + sparse_format: Callable[[ArrayLike], SpMatrix], ): elems = {"indptr", "indices", "data"} path = tmp_path / "test.zarr" @@ -481,7 +479,7 @@ def width_idx_kinds( ) def test_data_access( tmp_path: Path, - sparse_format: Callable[[ArrayLike], sparse.spmatrix], + sparse_format: Callable[[ArrayLike], SpMatrix], idx_maj: Idx, idx_min: Idx, exp: Sequence[str], @@ -615,13 +613,6 @@ def test_backed_sizeof( assert csr_mem.__sizeof__() > csc_disk.__sizeof__() -sparray_scipy_bug_marks = ( - [pytest.mark.skip(reason="scipy bug causes view to be allocated")] - if CAN_USE_SPARSE_ARRAY - else [] -) - - @pytest.mark.parametrize( "group_fn", [ @@ -633,7 +624,10 @@ def test_backed_sizeof( "sparse_class", [ sparse.csr_matrix, - pytest.param(sparse.csr_array, marks=[*sparray_scipy_bug_marks]), + pytest.param( + sparse.csr_array, + marks=[pytest.mark.skip(reason="scipy bug causes view to be allocated")], + ), ], ) def test_append_overflow_check(group_fn, sparse_class, tmp_path): diff --git a/tests/test_base.py b/tests/test_base.py index e1401ed74..0155df837 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -13,7 +13,6 @@ from anndata import AnnData, ImplicitModificationWarning from anndata._settings import settings -from anndata.compat import CAN_USE_SPARSE_ARRAY from anndata.tests.helpers import assert_equal, gen_adata, get_multiindex_columns_df # some test objects that we use below @@ -31,8 +30,7 @@ def test_creation(): AnnData(np.array([[1, 2], [3, 4]]), {}, {}) AnnData(ma.array([[1, 2], [3, 4]]), uns=dict(mask=[0, 1, 1, 0])) AnnData(sp.eye(2, format="csr")) - if CAN_USE_SPARSE_ARRAY: - AnnData(sp.eye_array(2)) + AnnData(sp.csr_array([[1, 0], [0, 1]])) X = np.array([[1, 2, 3], [4, 5, 6]]) adata = AnnData( X=X, diff --git a/tests/test_concatenate.py b/tests/test_concatenate.py index 378807059..8eafecac3 100644 --- a/tests/test_concatenate.py +++ b/tests/test_concatenate.py @@ -12,6 +12,7 @@ import numpy as np import pandas as pd import pytest +import scipy from boltons.iterutils import default_exit, remap, research from numpy import ma from packaging.version import Version @@ -20,7 +21,7 @@ from anndata import AnnData, Raw, concat from anndata._core import merge from anndata._core.index import _subset -from anndata.compat import AwkArray, CupySparseMatrix, DaskArray, SpArray +from anndata.compat import AwkArray, CupySparseMatrix, DaskArray, SpArray, SpMatrix from anndata.tests import helpers from anndata.tests.helpers import ( BASE_MATRIX_PARAMS, @@ -61,7 +62,7 @@ def _filled_array(a, fill_value=None): return as_dense_dask_array(_filled_array_np(a, fill_value)) -@filled_like.register(sparse.spmatrix) +@filled_like.register(SpMatrix) def _filled_sparse(a, fill_value=None): if fill_value is None: return sparse.csr_matrix(a.shape) @@ -202,7 +203,7 @@ def test_concatenate_roundtrip(join_type, array_type, concat_func, backwards_com if isinstance(orig.X, SpArray): base_type = SpArray else: - base_type = sparse.spmatrix + base_type = SpMatrix if isinstance(orig.X, CupySparseMatrix): base_type = CupySparseMatrix assert isinstance(result.X, base_type) @@ -406,7 +407,7 @@ def test_concatenate_obsm_outer(obsm_adatas, fill_val): ), ) - assert isinstance(outer.obsm["sparse"], sparse.spmatrix) + assert isinstance(outer.obsm["sparse"], SpMatrix) np.testing.assert_equal( outer.obsm["sparse"].toarray(), np.array( @@ -1496,14 +1497,18 @@ def test_concat_X_dtype(cpu_array_type, sparse_indexer_type): assert result.X.dtype == np.int8 assert result.raw.X.dtype == np.float64 if sparse.issparse(result.X): - # See https://github.com/scipy/scipy/issues/20389 for why this doesn't work with csc + # https://github.com/scipy/scipy/issues/20389 was merged in 1.15 but is still an issue with matrix if sparse_indexer_type == np.int64 and ( - issubclass(cpu_array_type, sparse.spmatrix) or adata.X.format == "csc" + ( + (issubclass(cpu_array_type, SpArray) or adata.X.format == "csc") + and Version(scipy.__version__) < Version("1.15.0") + ) + or issubclass(cpu_array_type, SpMatrix) ): pytest.xfail( "Data type int64 is not maintained for sparse matrices or csc array" ) - assert result.X.indptr.dtype == sparse_indexer_type + assert result.X.indptr.dtype == sparse_indexer_type, result.X assert result.X.indices.dtype == sparse_indexer_type diff --git a/tests/test_concatenate_disk.py b/tests/test_concatenate_disk.py index 6d0af6142..96ff3e57b 100644 --- a/tests/test_concatenate_disk.py +++ b/tests/test_concatenate_disk.py @@ -30,7 +30,7 @@ pd.DataFrame, ), varm_types=(sparse.csr_matrix, np.ndarray, pd.DataFrame), - layers_types=(sparse.spmatrix, np.ndarray, pd.DataFrame), + layers_types=(sparse.csr_matrix, np.ndarray, pd.DataFrame), ) diff --git a/tests/test_helpers.py b/tests/test_helpers.py index 9623a2f68..7f598fd91 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -12,7 +12,6 @@ CupyArray, CupyCSRMatrix, DaskArray, - add_note, ) from anndata.tests.helpers import ( BASE_MATRIX_PARAMS, @@ -279,33 +278,6 @@ def test_assert_equal_dask_sparse_arrays(): assert_equal(y, x) -@pytest.mark.parametrize( - ("error", "match"), - [ - (Exception("test"), "test"), - (add_note(AssertionError("foo"), "bar"), "bar"), - (add_note(add_note(AssertionError("foo"), "bar"), "baz"), "bar"), - (add_note(add_note(AssertionError("foo"), "bar"), "baz"), "baz"), - ], -) -def test_check_error_notes_success(error, match): - with pytest.raises(Exception, match=match): - raise error - - -@pytest.mark.parametrize( - ("error", "match"), - [ - (Exception("test"), "foo"), - (add_note(AssertionError("foo"), "bar"), "baz"), - ], -) -def test_check_error_notes_failure(error, match): - with pytest.raises(AssertionError): - with pytest.raises(Exception, match=match): - raise error - - @pytest.mark.parametrize( "input_type", BASE_MATRIX_PARAMS + DASK_MATRIX_PARAMS + CUPY_MATRIX_PARAMS ) diff --git a/tests/test_io_conversion.py b/tests/test_io_conversion.py index 217a9cc16..0d0f0196c 100644 --- a/tests/test_io_conversion.py +++ b/tests/test_io_conversion.py @@ -10,6 +10,7 @@ from scipy import sparse import anndata as ad +from anndata.compat import SpMatrix from anndata.tests.helpers import assert_equal, gen_adata @@ -99,8 +100,8 @@ def test_dense_to_sparse_memory(tmp_path, spmtx_format, to_convert): orig = gen_adata((50, 50), np.array) orig.raw = orig.copy() orig.write_h5ad(dense_path) - assert not isinstance(orig.X, sparse.spmatrix) - assert not isinstance(orig.raw.X, sparse.spmatrix) + assert not isinstance(orig.X, SpMatrix) + assert not isinstance(orig.raw.X, SpMatrix) curr = ad.read_h5ad(dense_path, as_sparse=to_convert, as_sparse_fmt=spmtx_format) diff --git a/tests/test_io_dispatched.py b/tests/test_io_dispatched.py index 0bbbf285a..8b142ce79 100644 --- a/tests/test_io_dispatched.py +++ b/tests/test_io_dispatched.py @@ -4,10 +4,9 @@ import h5py import zarr -from scipy import sparse import anndata as ad -from anndata.compat import SpArray +from anndata.compat import SpArray, SpMatrix from anndata.experimental import read_dispatched, write_dispatched from anndata.tests.helpers import assert_equal, gen_adata @@ -96,7 +95,7 @@ def set_copy(d, **kwargs): # TODO: Should the passed path be absolute? path = "/" + store.path + "/" + k if hasattr(elem, "shape") and not isinstance( - elem, sparse.spmatrix | SpArray | ad.AnnData + elem, SpMatrix | SpArray | ad.AnnData ): if re.match(r"^/((X)|(layers)).*", path): chunks = (M, N) diff --git a/tests/test_io_elementwise.py b/tests/test_io_elementwise.py index 21dfade13..439f2d515 100644 --- a/tests/test_io_elementwise.py +++ b/tests/test_io_elementwise.py @@ -22,7 +22,12 @@ get_spec, ) from anndata._io.specs.registry import IORegistryError -from anndata.compat import CAN_USE_SPARSE_ARRAY, SpArray, ZarrGroup, _read_attr +from anndata.compat import ( + SpArray, + SpMatrix, + ZarrGroup, + _read_attr, +) from anndata.experimental import read_elem_as_dask from anndata.io import read_elem, write_elem from anndata.tests.helpers import ( @@ -245,7 +250,7 @@ def test_io_spec_compressed_scalars(store: G, value: np.ndarray, encoding_type: @pytest.mark.parametrize("as_dask", [False, True]) def test_io_spec_cupy(store, value, encoding_type, as_dask): if as_dask: - if isinstance(value, sparse.spmatrix): + if isinstance(value, SpMatrix): value = as_cupy_sparse_dask_array(value, format=encoding_type[:3]) else: value = as_dense_cupy_dask_array(value) @@ -627,8 +632,6 @@ def test_read_sparse_array( else: f = h5py.File(path, "a") ad.io.write_elem(f, "mtx", a) - if not CAN_USE_SPARSE_ARRAY: - pytest.skip("scipy.sparse.cs{r,c}array not available") ad.settings.use_sparse_array_on_read = True mtx = ad.io.read_elem(f["mtx"]) assert issubclass(type(mtx), SpArray) diff --git a/tests/test_readwrite.py b/tests/test_readwrite.py index 20733daf4..8b8d4f457 100644 --- a/tests/test_readwrite.py +++ b/tests/test_readwrite.py @@ -15,12 +15,11 @@ import pytest import zarr from numba.core.errors import NumbaDeprecationWarning -from scipy import sparse from scipy.sparse import csc_array, csc_matrix, csr_array, csr_matrix import anndata as ad from anndata._io.specs.registry import IORegistryError -from anndata.compat import DaskArray, SpArray, _read_attr +from anndata.compat import DaskArray, SpArray, SpMatrix, _read_attr from anndata.tests.helpers import as_dense_dask_array, assert_equal, gen_adata if TYPE_CHECKING: @@ -159,7 +158,7 @@ def test_readwrite_kitchensink(tmp_path, storage, typ, backing_h5ad, dataset_kwa # since we tested if assigned types and loaded types are DaskArray # this would also work if they work if isinstance(adata_src.raw.X, SpArray): - assert isinstance(adata.raw.X, sparse.spmatrix) + assert isinstance(adata.raw.X, SpMatrix) else: assert isinstance(adata_src.raw.X, type(adata.raw.X) | DaskArray) assert isinstance( diff --git a/tests/test_settings.py b/tests/test_settings.py index 7929c5068..bcfcaf45c 100644 --- a/tests/test_settings.py +++ b/tests/test_settings.py @@ -244,16 +244,3 @@ class TestEnum(Enum): ) def test_describe(*, as_rst: bool, expected: str, settings: SettingsManager): assert settings.describe("test_var_3", as_rst=as_rst) == expected - - -def test_use_sparse_array_on_read(): - import anndata as ad - - if not ad.compat.CAN_USE_SPARSE_ARRAY: - with pytest.raises( - ValueError, - match=r"scipy.sparse.cs{r,c}array is not available in current scipy version", - ): - ad.settings.use_sparse_array_on_read = True - else: - ad.settings.use_sparse_array_on_read = True diff --git a/tests/test_views.py b/tests/test_views.py index 6c376f0bf..f67f315e7 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -22,7 +22,7 @@ SparseCSRArrayView, SparseCSRMatrixView, ) -from anndata.compat import CAN_USE_SPARSE_ARRAY, CupyCSCMatrix, DaskArray +from anndata.compat import CupyCSCMatrix, DaskArray from anndata.tests.helpers import ( BASE_MATRIX_PARAMS, CUPY_MATRIX_PARAMS, @@ -179,7 +179,7 @@ def test_modify_view_component(matrix_type, mapping_name, request): assert init_hash == hash_func(adata) - if "sparse_array_dask_array" in request.node.callspec.id and CAN_USE_SPARSE_ARRAY: + if "sparse_array_dask_array" in request.node.callspec.id: msg = "sparse arrays in dask are generally expected to fail but in this case they do not" pytest.fail(msg) @@ -673,9 +673,7 @@ def test_viewness_propagation_allclose(adata): assert np.allclose(a.varm["o"], b.varm["o"].copy(), equal_nan=True) -spmat = [sparse.csr_matrix, sparse.csc_matrix] -if CAN_USE_SPARSE_ARRAY: - spmat += [sparse.csr_array, sparse.csc_array] +spmat = [sparse.csr_matrix, sparse.csc_matrix, sparse.csr_array, sparse.csc_array] @pytest.mark.parametrize("spmat", spmat) @@ -693,10 +691,7 @@ def test_deepcopy_subset(adata, spmat: type): view_type = ( SparseCSRMatrixView if spmat is sparse.csr_matrix else SparseCSCMatrixView ) - if CAN_USE_SPARSE_ARRAY: - view_type = ( - SparseCSRArrayView if spmat is sparse.csr_array else SparseCSCArrayView - ) + view_type = SparseCSRArrayView if spmat is sparse.csr_array else SparseCSCArrayView assert not isinstance( adata.obsp["spmat"], view_type, @@ -704,9 +699,13 @@ def test_deepcopy_subset(adata, spmat: type): np.testing.assert_array_equal(adata.obsp["spmat"].shape, (10, 10)) -array_type = [asarray, sparse.csr_matrix, sparse.csc_matrix] -if CAN_USE_SPARSE_ARRAY: - array_type += [sparse.csr_array, sparse.csc_array] +array_type = [ + asarray, + sparse.csr_matrix, + sparse.csc_matrix, + sparse.csr_array, + sparse.csc_array, +] # https://github.com/scverse/anndata/issues/680