diff --git a/.azure-pipelines.yml b/.azure-pipelines.yml index 8ae68378d..7e875a4b8 100644 --- a/.azure-pipelines.yml +++ b/.azure-pipelines.yml @@ -16,8 +16,8 @@ jobs: Python3.11: python.version: "3.11" RUN_COVERAGE: yes - Python3.8: - python.version: "3.8" + Python3.9: + python.version: "3.9" PreRelease: python.version: "3.11" PRERELEASE_DEPENDENCIES: yes diff --git a/anndata/__init__.py b/anndata/__init__.py index 9c26702d2..6dd2e2192 100644 --- a/anndata/__init__.py +++ b/anndata/__init__.py @@ -1,4 +1,5 @@ """Annotated multivariate observation data.""" +from __future__ import annotations try: # See https://github.com/maresb/hatch-vcs-footgun-example from setuptools_scm import get_version @@ -34,12 +35,14 @@ read_zarr, ) from ._warnings import ( + ExperimentalFeatureWarning, + ImplicitModificationWarning, OldFormatWarning, WriteWarning, - ImplicitModificationWarning, - ExperimentalFeatureWarning, ) -from . import experimental + +# Experimental needs to be imported last +from . import experimental # isort: skip def read(*args, **kwargs): diff --git a/anndata/_core/access.py b/anndata/_core/access.py index 7dacaa6dc..7447103d9 100644 --- a/anndata/_core/access.py +++ b/anndata/_core/access.py @@ -1,13 +1,16 @@ +from __future__ import annotations + from functools import reduce -from typing import NamedTuple, Tuple +from typing import TYPE_CHECKING, NamedTuple -from . import anndata +if TYPE_CHECKING: + from anndata import AnnData class ElementRef(NamedTuple): - parent: "anndata.AnnData" + parent: AnnData attrname: str - keys: Tuple[str, ...] = () + keys: tuple[str, ...] = () def __str__(self) -> str: return f".{self.attrname}" + "".join(map(lambda x: f"['{x}']", self.keys)) diff --git a/anndata/_core/aligned_mapping.py b/anndata/_core/aligned_mapping.py index 156146518..a69730e26 100644 --- a/anndata/_core/aligned_mapping.py +++ b/anndata/_core/aligned_mapping.py @@ -1,26 +1,36 @@ +from __future__ import annotations + +import warnings from abc import ABC, abstractmethod from collections import abc as cabc +from collections.abc import Iterator, Mapping, Sequence from copy import copy -from typing import Union, Optional, Type, ClassVar, TypeVar # Special types -from typing import Iterator, Mapping, Sequence # ABCs -from typing import Tuple, List, Dict # Generic base types -import warnings +from typing import ( + TYPE_CHECKING, + ClassVar, + TypeVar, + Union, +) import numpy as np import pandas as pd from scipy.sparse import spmatrix -from ..utils import deprecated, ensure_df_homogeneous, dim_len -from . import raw, anndata -from .views import as_view, view_update +from anndata._warnings import ExperimentalFeatureWarning, ImplicitModificationWarning +from anndata.compat import AwkArray + +from ..utils import deprecated, dim_len, ensure_df_homogeneous from .access import ElementRef from .index import _subset -from anndata.compat import AwkArray -from anndata._warnings import ExperimentalFeatureWarning, ImplicitModificationWarning +from .views import as_view, view_update + +if TYPE_CHECKING: + from .anndata import AnnData + from .raw import Raw OneDIdx = Union[Sequence[int], Sequence[bool], slice] -TwoDIdx = Tuple[OneDIdx, OneDIdx] +TwoDIdx = tuple[OneDIdx, OneDIdx] I = TypeVar("I", OneDIdx, TwoDIdx, covariant=True) # TODO: pd.DataFrame only allowed in AxisArrays? @@ -36,16 +46,16 @@ class AlignedMapping(cabc.MutableMapping, ABC): _allow_df: ClassVar[bool] """If this mapping supports heterogeneous DataFrames""" - _view_class: ClassVar[Type["AlignedViewMixin"]] + _view_class: ClassVar[type[AlignedViewMixin]] """The view class for this aligned mapping.""" - _actual_class: ClassVar[Type["AlignedActualMixin"]] + _actual_class: ClassVar[type[AlignedActualMixin]] """The actual class (which has it’s own data) for this aligned mapping.""" def __repr__(self): return f"{type(self).__name__} with keys: {', '.join(self.keys())}" - def _ipython_key_completions_(self) -> List[str]: + def _ipython_key_completions_(self) -> list[str]: return list(self.keys()) def _validate_value(self, val: V, key: str) -> V: @@ -94,7 +104,7 @@ def attrname(self) -> str: @property @abstractmethod - def axes(self) -> Tuple[int, ...]: + def axes(self) -> tuple[int, ...]: """Which axes of the parent is this aligned to?""" pass @@ -104,7 +114,7 @@ def is_view(self) -> bool: pass @property - def parent(self) -> Union["anndata.AnnData", "raw.Raw"]: + def parent(self) -> AnnData | Raw: return self._parent def copy(self): @@ -117,7 +127,7 @@ def copy(self): d[k] = v.copy() return d - def _view(self, parent: "anndata.AnnData", subset_idx: I): + def _view(self, parent: AnnData, subset_idx: I): """Returns a subset copy-on-write view of the object.""" return self._view_class(self, parent, subset_idx) @@ -127,7 +137,7 @@ def as_dict(self) -> dict: class AlignedViewMixin: - parent: "anndata.AnnData" + parent: AnnData """Reference to parent AnnData view""" attrname: str @@ -177,7 +187,7 @@ def __len__(self) -> int: class AlignedActualMixin: - _data: Dict[str, V] + _data: dict[str, V] """Underlying mapping to the data""" is_view = False @@ -216,7 +226,7 @@ def attrname(self) -> str: return f"{self.dim}m" @property - def axes(self) -> Tuple[int]: + def axes(self) -> tuple[int]: """Axes of the parent this is aligned to""" return (self._axis,) @@ -225,7 +235,7 @@ def dim(self) -> str: """Name of the dimension this aligned to.""" return self._dimnames[self._axis] - def flipped(self) -> "AxisArraysBase": + def flipped(self) -> AxisArraysBase: """Transpose.""" new = self.copy() new.dimension = abs(self._axis - 1) @@ -265,9 +275,9 @@ def dim_names(self) -> pd.Index: class AxisArrays(AlignedActualMixin, AxisArraysBase): def __init__( self, - parent: Union["anndata.AnnData", "raw.Raw"], + parent: AnnData | Raw, axis: int, - vals: Union[Mapping, AxisArraysBase, None] = None, + vals: Mapping | AxisArraysBase | None = None, ): self._parent = parent if axis not in (0, 1): @@ -282,7 +292,7 @@ class AxisArraysView(AlignedViewMixin, AxisArraysBase): def __init__( self, parent_mapping: AxisArraysBase, - parent_view: "anndata.AnnData", + parent_view: AnnData, subset_idx: OneDIdx, ): self.parent_mapping = parent_mapping @@ -306,7 +316,7 @@ class LayersBase(AlignedMapping): axes = (0, 1) # TODO: I thought I had a more elegant solution to overriding this... - def copy(self) -> "Layers": + def copy(self) -> Layers: d = self._actual_class(self.parent) for k, v in self.items(): d[k] = v.copy() @@ -314,7 +324,7 @@ def copy(self) -> "Layers": class Layers(AlignedActualMixin, LayersBase): - def __init__(self, parent: "anndata.AnnData", vals: Optional[Mapping] = None): + def __init__(self, parent: AnnData, vals: Mapping | None = None): self._parent = parent self._data = dict() if vals is not None: @@ -325,7 +335,7 @@ class LayersView(AlignedViewMixin, LayersBase): def __init__( self, parent_mapping: LayersBase, - parent_view: "anndata.AnnData", + parent_view: AnnData, subset_idx: TwoDIdx, ): self.parent_mapping = parent_mapping @@ -351,7 +361,7 @@ def attrname(self) -> str: return f"{self.dim}p" @property - def axes(self) -> Tuple[int, int]: + def axes(self) -> tuple[int, int]: """Axes of the parent this is aligned to""" return self._axis, self._axis @@ -364,9 +374,9 @@ def dim(self) -> str: class PairwiseArrays(AlignedActualMixin, PairwiseArraysBase): def __init__( self, - parent: "anndata.AnnData", + parent: AnnData, axis: int, - vals: Optional[Mapping] = None, + vals: Mapping | None = None, ): self._parent = parent if axis not in (0, 1): @@ -381,7 +391,7 @@ class PairwiseArraysView(AlignedViewMixin, PairwiseArraysBase): def __init__( self, parent_mapping: PairwiseArraysBase, - parent_view: "anndata.AnnData", + parent_view: AnnData, subset_idx: OneDIdx, ): self.parent_mapping = parent_mapping diff --git a/anndata/_core/anndata.py b/anndata/_core/anndata.py index a4fb2a6e3..b505c8d8e 100644 --- a/anndata/_core/anndata.py +++ b/anndata/_core/anndata.py @@ -3,60 +3,66 @@ """ from __future__ import annotations -import warnings import collections.abc as cabc +import warnings from collections import OrderedDict +from collections.abc import Iterable, Mapping, MutableMapping, Sequence from copy import copy, deepcopy from enum import Enum from functools import partial, singledispatch from pathlib import Path -from os import PathLike from textwrap import dedent -from typing import Any, Union, Optional, Literal # Meta -from typing import Iterable, Sequence, Mapping, MutableMapping # Generic ABCs -from typing import Tuple, List # Generic +from typing import ( # Meta # Generic ABCs # Generic + TYPE_CHECKING, + Any, + Literal, +) import h5py -from natsort import natsorted import numpy as np -from numpy import ma import pandas as pd +from natsort import natsorted +from numpy import ma from pandas.api.types import infer_dtype, is_string_dtype from scipy import sparse -from scipy.sparse import issparse, csr_matrix +from scipy.sparse import csr_matrix, issparse from anndata._warnings import ImplicitModificationWarning -from .raw import Raw -from .index import _normalize_indices, _subset, Index, Index1D, get_vector -from .file_backing import AnnDataFileManager, to_memory + +from .. import utils +from ..compat import ( + CupyArray, + CupySparseMatrix, + DaskArray, + ZappyArray, + ZarrArray, + _move_adj_mtx, +) +from ..logging import anndata_logger as logger +from ..utils import convert_to_dict, dim_len, ensure_df_homogeneous from .access import ElementRef from .aligned_mapping import ( AxisArrays, AxisArraysView, - PairwiseArrays, - PairwiseArraysView, Layers, LayersView, + PairwiseArrays, + PairwiseArraysView, ) +from .file_backing import AnnDataFileManager, to_memory +from .index import Index, Index1D, _normalize_indices, _subset, get_vector +from .raw import Raw +from .sparse_dataset import sparse_dataset from .views import ( ArrayView, - DictView, DataFrameView, - as_view, + DictView, _resolve_idxs, + as_view, ) -from .sparse_dataset import sparse_dataset -from .. import utils -from ..utils import convert_to_dict, ensure_df_homogeneous, dim_len -from ..logging import anndata_logger as logger -from ..compat import ( - ZarrArray, - ZappyArray, - DaskArray, - CupyArray, - CupySparseMatrix, - _move_adj_mtx, -) + +if TYPE_CHECKING: + from os import PathLike class StorageType(Enum): @@ -328,22 +334,22 @@ class AnnData(metaclass=utils.DeprecationMixinMeta): def __init__( self, - X: Optional[Union[np.ndarray, sparse.spmatrix, pd.DataFrame]] = None, - obs: Optional[Union[pd.DataFrame, Mapping[str, Iterable[Any]]]] = None, - var: Optional[Union[pd.DataFrame, Mapping[str, Iterable[Any]]]] = None, - uns: Optional[Mapping[str, Any]] = None, - obsm: Optional[Union[np.ndarray, Mapping[str, Sequence[Any]]]] = None, - varm: Optional[Union[np.ndarray, Mapping[str, Sequence[Any]]]] = None, - layers: Optional[Mapping[str, Union[np.ndarray, sparse.spmatrix]]] = None, - raw: Optional[Mapping[str, Any]] = None, - dtype: Optional[Union[np.dtype, type, str]] = None, - shape: Optional[Tuple[int, int]] = None, - filename: Optional[PathLike] = None, - filemode: Optional[Literal["r", "r+"]] = None, + X: np.ndarray | sparse.spmatrix | 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, + raw: Mapping[str, Any] | None = None, + dtype: np.dtype | type | str | None = None, + shape: tuple[int, int] | None = None, + filename: PathLike | None = None, + filemode: Literal["r", "r+"] | None = None, asview: bool = False, *, - obsp: Optional[Union[np.ndarray, Mapping[str, Sequence[Any]]]] = None, - varp: Optional[Union[np.ndarray, Mapping[str, Sequence[Any]]]] = None, + obsp: np.ndarray | Mapping[str, Sequence[Any]] | None = None, + varp: np.ndarray | Mapping[str, Sequence[Any]] | None = None, oidx: Index1D = None, vidx: Index1D = None, ): @@ -369,7 +375,7 @@ def __init__( filemode=filemode, ) - def _init_as_view(self, adata_ref: "AnnData", oidx: Index, vidx: Index): + def _init_as_view(self, adata_ref: AnnData, oidx: Index, vidx: Index): if adata_ref.isbacked and adata_ref.is_view: raise ValueError( "Currently, you cannot index repeatedly into a backed AnnData, " @@ -643,12 +649,12 @@ def __eq__(self, other): ) @property - def shape(self) -> Tuple[int, int]: + def shape(self) -> tuple[int, int]: """Shape of data matrix (:attr:`n_obs`, :attr:`n_vars`).""" return self.n_obs, self.n_vars @property - def X(self) -> Optional[Union[np.ndarray, sparse.spmatrix, ArrayView]]: + def X(self) -> np.ndarray | sparse.spmatrix | ArrayView | None: """Data matrix of shape :attr:`n_obs` × :attr:`n_vars`.""" if self.isbacked: if not self.file.is_open: @@ -679,7 +685,7 @@ def X(self) -> Optional[Union[np.ndarray, sparse.spmatrix, ArrayView]]: # return X @X.setter - def X(self, value: Optional[Union[np.ndarray, sparse.spmatrix]]): + def X(self, value: np.ndarray | sparse.spmatrix | None): if value is None: if self.isbacked: raise NotImplementedError( @@ -744,7 +750,7 @@ def X(self): self.X = None @property - def layers(self) -> Union[Layers, LayersView]: + def layers(self) -> Layers | LayersView: """\ Dictionary-like object with values of the same dimensions as :attr:`X`. @@ -811,7 +817,7 @@ def raw(self) -> Raw: return self._raw @raw.setter - def raw(self, value: "AnnData"): + def raw(self, value: AnnData): if value is None: del self.raw elif not isinstance(value, AnnData): @@ -967,7 +973,7 @@ def uns(self): self.uns = OrderedDict() @property - def obsm(self) -> Union[AxisArrays, AxisArraysView]: + def obsm(self) -> AxisArrays | AxisArraysView: """\ Multi-dimensional annotation of observations (mutable structured :class:`~numpy.ndarray`). @@ -990,7 +996,7 @@ def obsm(self): self.obsm = dict() @property - def varm(self) -> Union[AxisArrays, AxisArraysView]: + def varm(self) -> AxisArrays | AxisArraysView: """\ Multi-dimensional annotation of variables/features (mutable structured :class:`~numpy.ndarray`). @@ -1013,7 +1019,7 @@ def varm(self): self.varm = dict() @property - def obsp(self) -> Union[PairwiseArrays, PairwiseArraysView]: + def obsp(self) -> PairwiseArrays | PairwiseArraysView: """\ Pairwise annotation of observations, a mutable mapping with array-like values. @@ -1036,7 +1042,7 @@ def obsp(self): self.obsp = dict() @property - def varp(self) -> Union[PairwiseArrays, PairwiseArraysView]: + def varp(self) -> PairwiseArrays | PairwiseArraysView: """\ Pairwise annotation of variables/features, a mutable mapping with array-like values. @@ -1058,23 +1064,23 @@ def varp(self, value): def varp(self): self.varp = dict() - def obs_keys(self) -> List[str]: + def obs_keys(self) -> list[str]: """List keys of observation annotation :attr:`obs`.""" return self._obs.keys().tolist() - def var_keys(self) -> List[str]: + def var_keys(self) -> list[str]: """List keys of variable annotation :attr:`var`.""" return self._var.keys().tolist() - def obsm_keys(self) -> List[str]: + def obsm_keys(self) -> list[str]: """List keys of observation annotation :attr:`obsm`.""" return list(self._obsm.keys()) - def varm_keys(self) -> List[str]: + def varm_keys(self) -> list[str]: """List keys of variable annotation :attr:`varm`.""" return list(self._varm.keys()) - def uns_keys(self) -> List[str]: + def uns_keys(self) -> list[str]: """List keys of unstructured annotation.""" return sorted(list(self._uns.keys())) @@ -1089,7 +1095,7 @@ def is_view(self) -> bool: return self._is_view @property - def filename(self) -> Optional[Path]: + def filename(self) -> Path | None: """\ Change to backing mode by setting the filename of a `.h5ad` file. @@ -1101,7 +1107,7 @@ def filename(self) -> Optional[Path]: return self.file.filename @filename.setter - def filename(self, filename: Optional[PathLike]): + def filename(self, filename: PathLike | None): # convert early for later comparison filename = None if filename is None else Path(filename) # change from backing-mode back to full loading into memory @@ -1140,7 +1146,7 @@ def _set_backed(self, attr, value): write_attribute(self.file._file, attr, value) - def _normalize_indices(self, index: Optional[Index]) -> Tuple[slice, slice]: + def _normalize_indices(self, index: Index | None) -> tuple[slice, slice]: return _normalize_indices(index, self.obs_names, self.var_names) # TODO: this is not quite complete... @@ -1158,7 +1164,7 @@ def __delitem__(self, index: Index): if obs == slice(None): del self._var.iloc[var, :] - def __getitem__(self, index: Index) -> "AnnData": + def __getitem__(self, index: Index) -> AnnData: """Returns a sliced view of the object.""" oidx, vidx = self._normalize_indices(index) return AnnData(self, oidx=oidx, vidx=vidx, asview=True) @@ -1237,7 +1243,7 @@ def rename_categories(self, key: str, categories: Sequence[Any]): f"Omitting {k1}/{k2} as old categories do not match." ) - def strings_to_categoricals(self, df: Optional[pd.DataFrame] = None): + def strings_to_categoricals(self, df: pd.DataFrame | None = None): """\ Transform string annotations to categoricals. @@ -1309,7 +1315,7 @@ def _inplace_subset_obs(self, index: Index1D): # TODO: Update, possibly remove def __setitem__( - self, index: Index, val: Union[int, float, np.ndarray, sparse.spmatrix] + self, index: Index, val: int | float | np.ndarray | sparse.spmatrix ): if self.is_view: raise ValueError("Object is view and cannot be accessed with `[]`.") @@ -1324,7 +1330,7 @@ def __setitem__( def __len__(self) -> int: return self.shape[0] - def transpose(self) -> "AnnData": + def transpose(self) -> AnnData: """\ Transpose whole object. @@ -1404,7 +1410,7 @@ def _get_X(self, use_raw=False, layer=None): else: return self.X - def obs_vector(self, k: str, *, layer: Optional[str] = None) -> np.ndarray: + def obs_vector(self, k: str, *, layer: str | None = None) -> np.ndarray: """\ Convenience function for returning a 1 dimensional ndarray of values from :attr:`X`, :attr:`layers`\\ `[k]`, or :attr:`obs`. @@ -1436,7 +1442,7 @@ def obs_vector(self, k: str, *, layer: Optional[str] = None) -> np.ndarray: layer = None return get_vector(self, k, "obs", "var", layer=layer) - def var_vector(self, k, *, layer: Optional[str] = None) -> np.ndarray: + def var_vector(self, k, *, layer: str | None = None) -> np.ndarray: """\ Convenience function for returning a 1 dimensional ndarray of values from :attr:`X`, :attr:`layers`\\ `[k]`, or :attr:`obs`. @@ -1519,7 +1525,7 @@ def _mutated_copy(self, **kwargs): new["raw"] = self.raw.copy() return AnnData(**new) - def to_memory(self, copy=False) -> "AnnData": + def to_memory(self, copy=False) -> AnnData: """Return a new AnnData object with all backed arrays loaded into memory. Params @@ -1564,7 +1570,7 @@ def to_memory(self, copy=False) -> "AnnData": return AnnData(**new) - def copy(self, filename: Optional[PathLike] = None) -> "AnnData": + def copy(self, filename: PathLike | None = None) -> AnnData: """Full copy, optionally on disk.""" if not self.isbacked: if self.is_view and self._has_X(): @@ -1592,14 +1598,14 @@ def copy(self, filename: Optional[PathLike] = None) -> "AnnData": def concatenate( self, - *adatas: "AnnData", + *adatas: AnnData, join: str = "inner", batch_key: str = "batch", batch_categories: Sequence[Any] = None, - uns_merge: Optional[str] = None, - index_unique: Optional[str] = "-", + uns_merge: str | None = None, + index_unique: str | None = "-", fill_value=None, - ) -> "AnnData": + ) -> AnnData: """\ Concatenate along the observations axis. @@ -1811,7 +1817,7 @@ def concatenate( [0., 0., 2., 1.], [0., 6., 5., 0.]], dtype=float32) """ - from .merge import concat, merge_outer, merge_dataframes, merge_same + from .merge import concat, merge_dataframes, merge_outer, merge_same warnings.warn( "The AnnData.concatenate method is deprecated in favour of the " @@ -1925,9 +1931,9 @@ def _check_dimensions(self, key=None): def write_h5ad( self, - filename: Optional[PathLike] = None, - compression: Optional[Literal["gzip", "lzf"]] = None, - compression_opts: Union[int, Any] = None, + filename: PathLike | None = None, + compression: Literal["gzip", "lzf"] | None = None, + compression_opts: int | Any = None, as_dense: Sequence[str] = (), ): """\ @@ -2047,8 +2053,8 @@ def write_loom(self, filename: PathLike, write_obsm_varm: bool = False): def write_zarr( self, - store: Union[MutableMapping, PathLike], - chunks: Union[bool, int, Tuple[int, ...], None] = None, + store: MutableMapping | PathLike, + chunks: bool | int | tuple[int, ...] | None = None, ): """\ Write a hierarchical Zarr array store. @@ -2064,7 +2070,7 @@ def write_zarr( write_zarr(store, self, chunks=chunks) - def chunked_X(self, chunk_size: Optional[int] = None): + def chunked_X(self, chunk_size: int | None = None): """\ Return an iterator over the rows of the data matrix :attr:`X`. @@ -2087,7 +2093,7 @@ def chunked_X(self, chunk_size: Optional[int] = None): def chunk_X( self, - select: Union[int, Sequence[int], np.ndarray] = 1000, + select: int | Sequence[int] | np.ndarray = 1000, replace: bool = True, ): """\ diff --git a/anndata/_core/file_backing.py b/anndata/_core/file_backing.py index eca759dd5..c50a2ea8d 100644 --- a/anndata/_core/file_backing.py +++ b/anndata/_core/file_backing.py @@ -1,14 +1,19 @@ +from __future__ import annotations + +from collections.abc import Iterator, Mapping from functools import singledispatch -from os import PathLike from pathlib import Path -from typing import Optional, Union, Iterator, Literal -from collections.abc import Mapping +from typing import TYPE_CHECKING, Literal import h5py -from . import anndata +from ..compat import AwkArray, DaskArray, ZarrArray, ZarrGroup from .sparse_dataset import BaseCompressedSparseDataset -from ..compat import ZarrArray, ZarrGroup, DaskArray, AwkArray + +if TYPE_CHECKING: + from os import PathLike + + from . import anndata class AnnDataFileManager: @@ -16,9 +21,9 @@ class AnnDataFileManager: def __init__( self, - adata: "anndata.AnnData", - filename: Optional[PathLike] = None, - filemode: Optional[Literal["r", "r+"]] = None, + adata: anndata.AnnData, + filename: PathLike | None = None, + filemode: Literal["r", "r+"] | None = None, ): self._adata = adata self.filename = filename @@ -41,13 +46,13 @@ def __iter__(self) -> Iterator[str]: def __getitem__( self, key: str - ) -> Union[h5py.Group, h5py.Dataset, BaseCompressedSparseDataset]: + ) -> h5py.Group | h5py.Dataset | BaseCompressedSparseDataset: return self._file[key] def __setitem__( self, key: str, - value: Union[h5py.Group, h5py.Dataset, BaseCompressedSparseDataset], + value: h5py.Group | h5py.Dataset | BaseCompressedSparseDataset, ): self._file[key] = value @@ -59,13 +64,13 @@ def filename(self) -> Path: return self._filename @filename.setter - def filename(self, filename: Optional[PathLike]): + def filename(self, filename: PathLike | None): self._filename = None if filename is None else Path(filename) def open( self, - filename: Optional[PathLike] = None, - filemode: Optional[Literal["r", "r+"]] = None, + filename: PathLike | None = None, + filemode: Literal["r", "r+"] | None = None, ): if filename is not None: self.filename = filename diff --git a/anndata/_core/index.py b/anndata/_core/index.py index 0a14f1841..b98efc8be 100644 --- a/anndata/_core/index.py +++ b/anndata/_core/index.py @@ -1,18 +1,21 @@ +from __future__ import annotations + import collections.abc as cabc +from collections.abc import Sequence from functools import singledispatch from itertools import repeat -from typing import Union, Sequence, Optional, Tuple import h5py import numpy as np import pandas as pd -from scipy.sparse import spmatrix, issparse, csc_matrix +from scipy.sparse import csc_matrix, issparse, spmatrix + from ..compat import AwkArray, DaskArray, Index, Index1D def _normalize_indices( - index: Optional[Index], names0: pd.Index, names1: pd.Index -) -> Tuple[slice, slice]: + index: Index | None, names0: pd.Index, names1: pd.Index +) -> tuple[slice, slice]: # deal with tuples of length 1 if isinstance(index, tuple) and len(index) == 1: index = index[0] @@ -35,17 +38,15 @@ def _normalize_indices( def _normalize_index( - indexer: Union[ - slice, - np.integer, - int, - str, - Sequence[Union[int, np.integer]], - np.ndarray, - pd.Index, - ], + indexer: slice + | np.integer + | int + | str + | Sequence[int | np.integer] + | np.ndarray + | pd.Index, index: pd.Index, -) -> Union[slice, int, np.ndarray]: # ndarray of int +) -> slice | int | np.ndarray: # ndarray of int if not isinstance(index, pd.RangeIndex): assert ( index.dtype != float and index.dtype != int @@ -104,7 +105,7 @@ def name_idx(i): raise IndexError(f"Unknown indexer {indexer!r} of type {type(indexer)}") -def unpack_index(index: Index) -> Tuple[Index1D, Index1D]: +def unpack_index(index: Index) -> tuple[Index1D, Index1D]: if not isinstance(index, tuple): return index, slice(None) elif len(index) == 2: @@ -116,7 +117,7 @@ def unpack_index(index: Index) -> Tuple[Index1D, Index1D]: @singledispatch -def _subset(a: Union[np.ndarray, pd.DataFrame], subset_idx: Index): +def _subset(a: np.ndarray | pd.DataFrame, subset_idx: Index): # Select as combination of indexes, not coordinates # Correcting for indexing behaviour of np.ndarray if all(isinstance(x, cabc.Iterable) for x in subset_idx): diff --git a/anndata/_core/merge.py b/anndata/_core/merge.py index d80a12e57..0ecc9e913 100644 --- a/anndata/_core/merge.py +++ b/anndata/_core/merge.py @@ -3,34 +3,37 @@ """ from __future__ import annotations +import typing from collections import OrderedDict from collections.abc import ( Callable, Collection, + Iterable, Mapping, MutableSet, - Iterable, Sequence, ) from functools import reduce, singledispatch from itertools import repeat from operator import and_, or_, sub -from typing import Any, Optional, TypeVar, Union, Literal -import typing -from warnings import warn, filterwarnings +from typing import Any, Literal, TypeVar +from warnings import filterwarnings, warn -from natsort import natsorted import numpy as np import pandas as pd -from pandas.api.extensions import ExtensionDtype +from natsort import natsorted from scipy import sparse from scipy.sparse import spmatrix -from .anndata import AnnData -from ..compat import AwkArray, DaskArray, CupySparseMatrix, CupyArray, CupyCSRMatrix +from anndata._warnings import ExperimentalFeatureWarning + +from ..compat import AwkArray, CupyArray, CupyCSRMatrix, CupySparseMatrix, DaskArray from ..utils import asarray, dim_len +from .anndata import AnnData from .index import _subset, make_slice -from anndata._warnings import ExperimentalFeatureWarning + +if typing.TYPE_CHECKING: + from pandas.api.extensions import ExtensionDtype T = TypeVar("T") @@ -62,14 +65,14 @@ def copy(self): def add(self, val): self.dict[val] = None - def union(self, *vals) -> "OrderedSet": + def union(self, *vals) -> OrderedSet: return reduce(or_, vals, self) def discard(self, val): if val in self: del self.dict[val] - def difference(self, *vals) -> "OrderedSet": + def difference(self, *vals) -> OrderedSet: return reduce(sub, vals, self) @@ -341,6 +344,7 @@ def _cp_block_diag(mats, format=None, dtype=None): def _dask_block_diag(mats): from itertools import permutations + import dask.array as da blocks = np.zeros((len(mats), len(mats)), dtype=object) @@ -361,7 +365,7 @@ def _dask_block_diag(mats): ################### -def unique_value(vals: Collection[T]) -> Union[T, MissingVal]: +def unique_value(vals: Collection[T]) -> T | MissingVal: """ Given a collection vals, returns the unique value (if one exists), otherwise returns MissingValue. @@ -373,7 +377,7 @@ def unique_value(vals: Collection[T]) -> Union[T, MissingVal]: return unique_val -def first(vals: Collection[T]) -> Union[T, MissingVal]: +def first(vals: Collection[T]) -> T | MissingVal: """ Given a collection of vals, return the first non-missing one.If they're all missing, return MissingVal. @@ -384,7 +388,7 @@ def first(vals: Collection[T]) -> Union[T, MissingVal]: return MissingVal -def only(vals: Collection[T]) -> Union[T, MissingVal]: +def only(vals: Collection[T]) -> T | MissingVal: """Return the only value in the collection, otherwise MissingVal.""" if len(vals) == 1: return vals[0] @@ -457,7 +461,7 @@ def merge_only(ds: Collection[Mapping]) -> Mapping: def resolve_merge_strategy( - strategy: Union[str, Callable, None] + strategy: str | Callable | None, ) -> Callable[[Collection[Mapping]], Mapping]: if not isinstance(strategy, Callable): strategy = MERGE_STRATEGIES[strategy] @@ -1017,16 +1021,16 @@ def concat_Xs(adatas, reindexers, axis, fill_value): def concat( - adatas: Union[Collection[AnnData], "typing.Mapping[str, AnnData]"], + adatas: Collection[AnnData] | typing.Mapping[str, AnnData], *, axis: Literal[0, 1] = 0, join: Literal["inner", "outer"] = "inner", - merge: Union[StrategiesLiteral, Callable, None] = None, - uns_merge: Union[StrategiesLiteral, Callable, None] = None, - label: Optional[str] = None, - keys: Optional[Collection] = None, - index_unique: Optional[str] = None, - fill_value: Optional[Any] = None, + merge: StrategiesLiteral | Callable | None = None, + uns_merge: StrategiesLiteral | Callable | None = None, + label: str | None = None, + keys: Collection | None = None, + index_unique: str | None = None, + fill_value: Any | None = None, pairwise: bool = False, ) -> AnnData: """Concatenates AnnData objects along an axis. diff --git a/anndata/_core/raw.py b/anndata/_core/raw.py index baa951c56..2b2e27277 100644 --- a/anndata/_core/raw.py +++ b/anndata/_core/raw.py @@ -1,27 +1,33 @@ -from typing import Union, Mapping, Sequence, Tuple +from __future__ import annotations + +from typing import TYPE_CHECKING import h5py import numpy as np import pandas as pd -from scipy import sparse from scipy.sparse import issparse -from . import anndata -from .index import _normalize_index, _subset, unpack_index, get_vector +from ..compat import CupyArray, CupySparseMatrix from .aligned_mapping import AxisArrays +from .index import _normalize_index, _subset, get_vector, unpack_index from .sparse_dataset import BaseCompressedSparseDataset, sparse_dataset -from ..compat import CupyArray, CupySparseMatrix +if TYPE_CHECKING: + from collections.abc import Mapping, Sequence + + from scipy import sparse + + from .anndata import AnnData # TODO: Implement views for Raw class Raw: def __init__( self, - adata: "anndata.AnnData", - X: Union[np.ndarray, sparse.spmatrix, None] = None, - var: Union[pd.DataFrame, Mapping[str, Sequence], None] = None, - varm: Union[AxisArrays, Mapping[str, np.ndarray], None] = None, + adata: AnnData, + X: np.ndarray | sparse.spmatrix | None = None, + var: pd.DataFrame | Mapping[str, Sequence] | None = None, + varm: AxisArrays | Mapping[str, np.ndarray] | None = None, ): from .anndata import _gen_dataframe @@ -56,7 +62,7 @@ def _get_X(self, layer=None): return self.X @property - def X(self) -> Union[BaseCompressedSparseDataset, np.ndarray, sparse.spmatrix]: + def X(self) -> BaseCompressedSparseDataset | np.ndarray | sparse.spmatrix: # TODO: Handle unsorted array of integer indices for h5py.Datasets if not self._adata.isbacked: return self._X @@ -149,7 +155,9 @@ def copy(self): def to_adata(self): """Create full AnnData object.""" - return anndata.AnnData( + from anndata import AnnData + + return AnnData( X=self.X.copy(), var=self.var.copy(), varm=None if self._varm is None else self._varm.copy(), @@ -191,12 +199,12 @@ def obs_vector(self, k: str) -> np.ndarray: # This exists to accommodate AlignedMappings, # until we implement a proper RawView or get rid of Raw in favor of modes. class _RawViewHack: - def __init__(self, raw: Raw, vidx: Union[slice, np.ndarray]): + def __init__(self, raw: Raw, vidx: slice | np.ndarray): self.parent_raw = raw self.vidx = vidx @property - def shape(self) -> Tuple[int, int]: + def shape(self) -> tuple[int, int]: return self.parent_raw.n_obs, len(self.var_names) @property diff --git a/anndata/_core/sparse_dataset.py b/anndata/_core/sparse_dataset.py index 83eb4befa..453b0a590 100644 --- a/anndata/_core/sparse_dataset.py +++ b/anndata/_core/sparse_dataset.py @@ -12,17 +12,18 @@ # - think about supporting the COO format from __future__ import annotations -from abc import ABC import collections.abc as cabc -from itertools import accumulate, chain -from typing import Literal, Union, NamedTuple, Tuple, Sequence, Iterable, Type import warnings +from abc import ABC +from itertools import accumulate, chain +from typing import TYPE_CHECKING, Literal, NamedTuple import h5py import numpy as np import scipy.sparse as ss from scipy.sparse import _sparsetools -from anndata.compat import ZarrGroup, H5Group + +from anndata.compat import H5Group, ZarrGroup from ..compat import _read_attr @@ -32,13 +33,16 @@ except ImportError: _cs_matrix = ss.spmatrix -from .index import unpack_index, Index, _subset +from .index import Index, _subset, unpack_index + +if TYPE_CHECKING: + from collections.abc import Iterable, Sequence class BackedFormat(NamedTuple): format: str - backed_type: Type["BackedSparseMatrix"] - memory_type: Type[ss.spmatrix] + backed_type: type[BackedSparseMatrix] + memory_type: type[ss.spmatrix] class BackedSparseMatrix(_cs_matrix): @@ -192,7 +196,7 @@ def slice_as_int(s: slice, l: int) -> int: def get_compressed_vectors( x: BackedSparseMatrix, row_idxs: Iterable[int] -) -> Tuple[Sequence, Sequence, Sequence]: +) -> tuple[Sequence, Sequence, Sequence]: slices = [slice(*(x.indptr[i : i + 2])) for i in row_idxs] data = np.concatenate([x.data[s] for s in slices]) indices = np.concatenate([x.indices[s] for s in slices]) @@ -202,7 +206,7 @@ def get_compressed_vectors( def get_compressed_vector( x: BackedSparseMatrix, idx: int -) -> Tuple[Sequence, Sequence, Sequence]: +) -> tuple[Sequence, Sequence, Sequence]: s = slice(*(x.indptr[idx : idx + 2])) data = x.data[s] indices = x.indices[s] @@ -217,14 +221,14 @@ def get_format(data: ss.spmatrix) -> str: raise ValueError(f"Data type {type(data)} is not supported.") -def get_memory_class(format: str) -> Type[ss.spmatrix]: +def get_memory_class(format: str) -> type[ss.spmatrix]: for fmt, _, memory_class in FORMATS: if format == fmt: return memory_class raise ValueError(f"Format string {format} is not supported.") -def get_backed_class(format: str) -> Type[BackedSparseMatrix]: +def get_backed_class(format: str) -> type[BackedSparseMatrix]: for fmt, backed_class, _ in FORMATS: if format == fmt: return backed_class @@ -284,7 +288,7 @@ def name(self) -> str: return self.group.name @property - def shape(self) -> Tuple[int, int]: + def shape(self) -> tuple[int, int]: shape = _read_attr(self.group.attrs, "shape", None) if shape is None: # TODO warn @@ -304,7 +308,7 @@ def value(self) -> ss.spmatrix: def __repr__(self) -> str: return f"{type(self).__name__}: backend {self.backend}, shape {self.shape}, data_dtype {self.dtype}" - def __getitem__(self, index: Union[Index, Tuple[()]]) -> Union[float, ss.spmatrix]: + def __getitem__(self, index: Index | tuple[()]) -> float | ss.spmatrix: row, col = self._normalize_index(index) mtx = self._to_backed() sub = mtx[row, col] @@ -316,8 +320,8 @@ def __getitem__(self, index: Union[Index, Tuple[()]]) -> Union[float, ss.spmatri return sub def _normalize_index( - self, index: Union[Index, Tuple[()]] - ) -> Tuple[np.ndarray, np.ndarray]: + self, index: Index | tuple[()] + ) -> tuple[np.ndarray, np.ndarray]: if index == (): index = slice(None) row, col = unpack_index(index) @@ -325,7 +329,7 @@ def _normalize_index( row, col = np.ix_(row, col) return row, col - def __setitem__(self, index: Union[Index, Tuple[()]], value): + def __setitem__(self, index: Index | tuple[()], value): warnings.warn( "__setitem__ will likely be removed in the near future. We do not recommend relying on its stability.", PendingDeprecationWarning, diff --git a/anndata/_core/views.py b/anndata/_core/views.py index aa8dbb625..36faf5fbe 100644 --- a/anndata/_core/views.py +++ b/anndata/_core/views.py @@ -1,32 +1,36 @@ from __future__ import annotations +import warnings from contextlib import contextmanager from copy import deepcopy -from collections.abc import Sequence, KeysView, Callable, Iterable from functools import reduce, singledispatch, wraps -from typing import Any, Literal -import warnings +from typing import TYPE_CHECKING, Any, Literal import numpy as np import pandas as pd from pandas.api.types import is_bool_dtype from scipy import sparse -import anndata from anndata._warnings import ImplicitModificationWarning -from .access import ElementRef + from ..compat import ( - ZappyArray, AwkArray, - DaskArray, CupyArray, CupyCSCMatrix, CupyCSRMatrix, + DaskArray, + ZappyArray, ) +from .access import ElementRef + +if TYPE_CHECKING: + from collections.abc import Callable, Iterable, KeysView, Sequence + + from anndata import AnnData @contextmanager -def view_update(adata_view: anndata.AnnData, attr_name: str, keys: tuple[str, ...]): +def view_update(adata_view: AnnData, attr_name: str, keys: tuple[str, ...]): """Context manager for updating a view of an AnnData object. Contains logic for "actualizing" a view. Yields the object to be modified in-place. @@ -79,7 +83,7 @@ class _ViewMixin(_SetItemMixin): def __init__( self, *args, - view_args: tuple["anndata.AnnData", str, tuple[str, ...]] = None, + view_args: tuple[AnnData, str, tuple[str, ...]] = None, **kwargs, ): if view_args is not None: @@ -100,7 +104,7 @@ class ArrayView(_SetItemMixin, np.ndarray): def __new__( cls, input_array: Sequence[Any], - view_args: tuple["anndata.AnnData", str, tuple[str, ...]] = None, + view_args: tuple[AnnData, str, tuple[str, ...]] = None, ): arr = np.asanyarray(input_array).view(cls) @@ -172,7 +176,7 @@ class DaskArrayView(_SetItemMixin, DaskArray): def __new__( cls, input_array: DaskArray, - view_args: tuple["anndata.AnnData", str, tuple[str, ...]] = None, + view_args: tuple[AnnData, str, tuple[str, ...]] = None, ): arr = super().__new__( cls, @@ -226,7 +230,7 @@ class CupyArrayView(_ViewMixin, CupyArray): def __new__( cls, input_array: Sequence[Any], - view_args: tuple["anndata.AnnData", str, tuple[str, ...]] = None, + view_args: tuple[AnnData, str, tuple[str, ...]] = None, ): import cupy as cp @@ -316,9 +320,10 @@ def as_view_cupy_csc(mtx, view_args): try: - from ..compat import awkward as ak import weakref + from ..compat import awkward as ak + # Registry to store weak references from AwkwardArrayViews to their parent AnnData container _registry = weakref.WeakValueDictionary() _PARAM_NAME = "_view_args" diff --git a/anndata/_io/__init__.py b/anndata/_io/__init__.py index f305d42ab..7bb6ba506 100644 --- a/anndata/_io/__init__.py +++ b/anndata/_io/__init__.py @@ -1,3 +1,6 @@ +from __future__ import annotations + +from .h5ad import read_h5ad, write_h5ad from .read import ( read_csv, read_excel, @@ -8,7 +11,6 @@ read_umi_tools, read_zarr, ) -from .h5ad import read_h5ad, write_h5ad from .write import write_csvs, write_loom diff --git a/anndata/_io/h5ad.py b/anndata/_io/h5ad.py index 5ba94e8bd..bfddc6504 100644 --- a/anndata/_io/h5ad.py +++ b/anndata/_io/h5ad.py @@ -1,41 +1,50 @@ +from __future__ import annotations + import re from functools import partial -from warnings import warn from pathlib import Path from types import MappingProxyType -from typing import Callable, Type, TypeVar, Union, Literal -from typing import Collection, Sequence, Mapping +from typing import ( + TYPE_CHECKING, + Callable, + Literal, + TypeVar, +) +from warnings import warn import h5py import numpy as np import pandas as pd from scipy import sparse -from .._core.sparse_dataset import BaseCompressedSparseDataset -from .._core.file_backing import AnnDataFileManager, filename +from anndata._warnings import OldFormatWarning + from .._core.anndata import AnnData +from .._core.file_backing import AnnDataFileManager, filename +from .._core.sparse_dataset import BaseCompressedSparseDataset from ..compat import ( - _from_fixed_length_strings, - _decode_structured_array, _clean_uns, + _decode_structured_array, + _from_fixed_length_strings, ) from ..experimental import read_dispatched +from .specs import read_elem, write_elem from .utils import ( H5PY_V3, + _read_legacy_raw, + idx_chunks_along_axis, report_read_key_on_error, report_write_key_on_error, - idx_chunks_along_axis, - _read_legacy_raw, ) -from .specs import read_elem, write_elem -from anndata._warnings import OldFormatWarning +if TYPE_CHECKING: + from collections.abc import Collection, Mapping, Sequence T = TypeVar("T") def write_h5ad( - filepath: Union[Path, str], + filepath: Path | str, adata: AnnData, *, as_dense: Sequence[str] = (), @@ -122,7 +131,7 @@ def write_sparse_as_dense(f, key, value, dataset_kwargs=MappingProxyType({})): del f[key] -def read_h5ad_backed(filename: Union[str, Path], mode: Literal["r", "r+"]) -> AnnData: +def read_h5ad_backed(filename: str | Path, mode: Literal["r", "r+"]) -> AnnData: d = dict(filename=filename, filemode=mode) f = h5py.File(filename, mode) @@ -151,11 +160,11 @@ def read_h5ad_backed(filename: Union[str, Path], mode: Literal["r", "r+"]) -> An def read_h5ad( - filename: Union[str, Path], - backed: Union[Literal["r", "r+"], bool, None] = None, + filename: str | Path, + backed: Literal["r", "r+"] | bool | None = None, *, as_sparse: Sequence[str] = (), - as_sparse_fmt: Type[sparse.spmatrix] = sparse.csr_matrix, + as_sparse_fmt: type[sparse.spmatrix] = sparse.csr_matrix, chunk_size: int = 6000, # TODO, probably make this 2d chunks ) -> AnnData: """\ @@ -256,7 +265,7 @@ def callback(func, elem_name: str, elem, iospec): def _read_raw( - f: Union[h5py.File, AnnDataFileManager], + f: h5py.File | AnnDataFileManager, as_sparse: Collection[str] = (), rdasp: Callable[[h5py.Dataset], sparse.spmatrix] = None, *, diff --git a/anndata/_io/read.py b/anndata/_io/read.py index 91115a81b..68f7fbd27 100644 --- a/anndata/_io/read.py +++ b/anndata/_io/read.py @@ -1,11 +1,12 @@ -from pathlib import Path +from __future__ import annotations + +import bz2 +import gzip +from collections import OrderedDict from os import PathLike, fspath +from pathlib import Path from types import MappingProxyType -from typing import Union, Optional, Mapping, Tuple -from typing import Iterable, Iterator, Generator -from collections import OrderedDict -import gzip -import bz2 +from typing import TYPE_CHECKING from warnings import warn import h5py @@ -17,6 +18,9 @@ from ..compat import _deprecate_positional_args from .utils import is_float +if TYPE_CHECKING: + from collections.abc import Generator, Iterable, Iterator, Mapping + try: from .zarr import read_zarr except ImportError as _e: @@ -27,9 +31,9 @@ def read_zarr(*_, **__): def read_csv( - filename: Union[PathLike, Iterator[str]], - delimiter: Optional[str] = ",", - first_column_names: Optional[bool] = None, + filename: PathLike | Iterator[str], + delimiter: str | None = ",", + first_column_names: bool | None = None, dtype: str = "float32", ) -> AnnData: """\ @@ -53,9 +57,7 @@ def read_csv( return read_text(filename, delimiter, first_column_names, dtype) -def read_excel( - filename: PathLike, sheet: Union[str, int], dtype: str = "float32" -) -> AnnData: +def read_excel(filename: PathLike, sheet: str | int, dtype: str = "float32") -> AnnData: """\ Read `.xlsx` (Excel) file. @@ -137,7 +139,7 @@ def read_hdf(filename: PathLike, key: str) -> AnnData: def _fmt_loom_axis_attrs( input: Mapping, idx_name: str, dimm_mapping: Mapping[str, Iterable[str]] -) -> Tuple[pd.DataFrame, Mapping[str, np.ndarray]]: +) -> tuple[pd.DataFrame, Mapping[str, np.ndarray]]: axis_df = pd.DataFrame() axis_mapping = {} for key, names in dimm_mapping.items(): @@ -163,9 +165,9 @@ def read_loom( cleanup: bool = False, X_name: str = "spliced", obs_names: str = "CellID", - obsm_names: Optional[Mapping[str, Iterable[str]]] = None, + obsm_names: Mapping[str, Iterable[str]] | None = None, var_names: str = "Gene", - varm_names: Optional[Mapping[str, Iterable[str]]] = None, + varm_names: Mapping[str, Iterable[str]] | None = None, dtype: str = "float32", obsm_mapping: Mapping[str, Iterable[str]] = MappingProxyType({}), varm_mapping: Mapping[str, Iterable[str]] = MappingProxyType({}), @@ -320,9 +322,9 @@ def read_mtx(filename: PathLike, dtype: str = "float32") -> AnnData: def read_text( - filename: Union[PathLike, Iterator[str]], - delimiter: Optional[str] = None, - first_column_names: Optional[bool] = None, + filename: PathLike | Iterator[str], + delimiter: str | None = None, + first_column_names: bool | None = None, dtype: str = "float32", ) -> AnnData: """\ @@ -368,8 +370,8 @@ def _iter_lines(file_like: Iterable[str]) -> Generator[str, None, None]: def _read_text( f: Iterator[str], - delimiter: Optional[str], - first_column_names: Optional[bool], + delimiter: str | None, + first_column_names: bool | None, dtype: str, ) -> AnnData: comments = [] diff --git a/anndata/_io/specs/__init__.py b/anndata/_io/specs/__init__.py index 28281a1e0..ceff8b3d6 100644 --- a/anndata/_io/specs/__init__.py +++ b/anndata/_io/specs/__init__.py @@ -1,6 +1,15 @@ +from __future__ import annotations + from . import methods -from .registry import write_elem, get_spec, read_elem, Reader, Writer, IOSpec -from .registry import _REGISTRY # noqa: F401 +from .registry import ( + _REGISTRY, # noqa: F401 + IOSpec, + Reader, + Writer, + get_spec, + read_elem, + write_elem, +) __all__ = [ "methods", diff --git a/anndata/_io/specs/methods.py b/anndata/_io/specs/methods.py index d608c5806..00cd66ea7 100644 --- a/anndata/_io/specs/methods.py +++ b/anndata/_io/specs/methods.py @@ -1,11 +1,10 @@ from __future__ import annotations -from os import PathLike from collections.abc import Mapping -from itertools import product from functools import partial -from typing import Union, Literal +from itertools import product from types import MappingProxyType +from typing import TYPE_CHECKING, Literal from warnings import warn import h5py @@ -15,24 +14,30 @@ import anndata as ad from anndata import AnnData, Raw +from anndata._core import views from anndata._core.index import _normalize_indices from anndata._core.merge import intersect_keys from anndata._core.sparse_dataset import CSCDataset, CSRDataset, sparse_dataset -from anndata._core import views +from anndata._io.utils import H5PY_V3, check_key +from anndata._warnings import OldFormatWarning from anndata.compat import ( + AwkArray, + CupyArray, + CupyCSCMatrix, + CupyCSRMatrix, + DaskArray, ZarrArray, ZarrGroup, - DaskArray, - _read_attr, - _from_fixed_length_strings, _decode_structured_array, + _from_fixed_length_strings, + _read_attr, ) -from anndata._io.utils import check_key, H5PY_V3 -from anndata._warnings import OldFormatWarning -from anndata.compat import AwkArray, CupyArray, CupyCSRMatrix, CupyCSCMatrix from .registry import _REGISTRY, IOSpec, read_elem, read_elem_partial +if TYPE_CHECKING: + from os import PathLike + H5Array = h5py.Dataset H5Group = h5py.Group H5File = h5py.File @@ -736,7 +741,7 @@ def read_dataframe_0_1_0(elem, _reader): return df -def read_series(dataset: h5py.Dataset) -> Union[np.ndarray, pd.Categorical]: +def read_series(dataset: h5py.Dataset) -> np.ndarray | pd.Categorical: # For reading older dataframes if "categories" in dataset.attrs: if isinstance(dataset, ZarrArray): diff --git a/anndata/_io/specs/registry.py b/anndata/_io/specs/registry.py index 1a20fb320..1f0b137f4 100644 --- a/anndata/_io/specs/registry.py +++ b/anndata/_io/specs/registry.py @@ -1,14 +1,16 @@ from __future__ import annotations -from collections.abc import Mapping, Callable, Iterable +from collections.abc import Callable, Iterable, Mapping from dataclasses import dataclass from functools import singledispatch, wraps from types import MappingProxyType -from typing import Any, Union +from typing import TYPE_CHECKING, Any +from anndata._io.utils import report_read_key_on_error, report_write_key_on_error from anndata.compat import _read_attr -from anndata._types import StorageType, GroupStorageType -from anndata._io.utils import report_write_key_on_error, report_read_key_on_error + +if TYPE_CHECKING: + from anndata._types import GroupStorageType, StorageType # TODO: This probably should be replaced by a hashable Mapping due to conversion b/w "_" and "-" # TODO: Should filetype be included in the IOSpec if it changes the encoding? Or does the intent that these things be "the same" overrule that? @@ -66,7 +68,7 @@ def __init__(self): self.write: dict[ tuple[type, type | tuple[type, str], frozenset[str]], Callable ] = {} - self.write_specs: dict[Union[type, tuple[type, str]], IOSpec] = {} + self.write_specs: dict[type | tuple[type, str], IOSpec] = {} def register_write( self, @@ -226,9 +228,7 @@ def _iter_patterns(elem): class Reader: - def __init__( - self, registry: IORegistry, callback: Union[Callable, None] = None - ) -> None: + def __init__(self, registry: IORegistry, callback: Callable | None = None) -> None: self.registry = registry self.callback = callback @@ -255,18 +255,16 @@ class Writer: def __init__( self, registry: IORegistry, - callback: Union[ - Callable[ - [ - GroupStorageType, - str, - StorageType, - dict, - ], - None, + callback: Callable[ + [ + GroupStorageType, + str, + StorageType, + dict, ], None, - ] = None, + ] + | None = None, ): self.registry = registry self.callback = callback @@ -290,6 +288,7 @@ def write_elem( ): from functools import partial from pathlib import PurePosixPath + import h5py if isinstance(store, h5py.File): diff --git a/anndata/_io/utils.py b/anndata/_io/utils.py index 33aca7cde..e6bccde01 100644 --- a/anndata/_io/utils.py +++ b/anndata/_io/utils.py @@ -4,12 +4,13 @@ from typing import Callable, Literal from warnings import warn -from packaging import version import h5py +from packaging import version -from .._core.sparse_dataset import BaseCompressedSparseDataset from anndata.compat import H5Group, ZarrGroup, add_note +from .._core.sparse_dataset import BaseCompressedSparseDataset + # For allowing h5py v3 # https://github.com/scverse/anndata/issues/442 H5PY_V3 = version.parse(h5py.__version__).major >= 3 diff --git a/anndata/_io/write.py b/anndata/_io/write.py index 28ce012d0..5a4d2ca62 100644 --- a/anndata/_io/write.py +++ b/anndata/_io/write.py @@ -1,15 +1,20 @@ +from __future__ import annotations + +import math import warnings -from pathlib import Path from os import PathLike, fspath +from pathlib import Path +from typing import TYPE_CHECKING -import pandas as pd -import math import numpy as np +import pandas as pd from scipy.sparse import issparse -from .. import AnnData -from ..logging import get_logger from .._warnings import WriteWarning +from ..logging import get_logger + +if TYPE_CHECKING: + from .. import AnnData logger = get_logger(__name__) diff --git a/anndata/_io/zarr.py b/anndata/_io/zarr.py index d85e1fc19..022ee8a1d 100644 --- a/anndata/_io/zarr.py +++ b/anndata/_io/zarr.py @@ -1,32 +1,36 @@ -from collections.abc import MutableMapping +from __future__ import annotations + from pathlib import Path -from typing import TypeVar, Union +from typing import TYPE_CHECKING, TypeVar from warnings import warn import numpy as np -from scipy import sparse import pandas as pd import zarr +from scipy import sparse + +from anndata._warnings import OldFormatWarning from .._core.anndata import AnnData from ..compat import ( - _from_fixed_length_strings, _clean_uns, + _from_fixed_length_strings, ) from ..experimental import read_dispatched, write_dispatched +from .specs import read_elem from .utils import ( - report_read_key_on_error, _read_legacy_raw, + report_read_key_on_error, ) -from .specs import read_elem -from anndata._warnings import OldFormatWarning +if TYPE_CHECKING: + from collections.abc import MutableMapping T = TypeVar("T") def write_zarr( - store: Union[MutableMapping, str, Path], + store: MutableMapping | str | Path, adata: AnnData, chunks=None, **ds_kwargs, @@ -50,7 +54,7 @@ def callback(func, s, k, elem, dataset_kwargs, iospec): write_dispatched(f, "/", adata, callback=callback, dataset_kwargs=ds_kwargs) -def read_zarr(store: Union[str, Path, MutableMapping, zarr.Group]) -> AnnData: +def read_zarr(store: str | Path | MutableMapping | zarr.Group) -> AnnData: """\ Read from a hierarchical Zarr array store. diff --git a/anndata/_types.py b/anndata/_types.py index d5b6b1c5c..7f57e380f 100644 --- a/anndata/_types.py +++ b/anndata/_types.py @@ -1,6 +1,8 @@ """ Defines some useful types for this library. Should probably be cleaned up before thinking about exporting. """ +from __future__ import annotations + from typing import Union from anndata.compat import H5Array, H5Group, ZarrArray, ZarrGroup diff --git a/anndata/_warnings.py b/anndata/_warnings.py index 5bc0c461c..1820a6d96 100644 --- a/anndata/_warnings.py +++ b/anndata/_warnings.py @@ -1,3 +1,6 @@ +from __future__ import annotations + + class WriteWarning(UserWarning): pass diff --git a/anndata/compat/__init__.py b/anndata/compat/__init__.py index 1507f1be4..d6ab5c2f3 100644 --- a/anndata/compat/__init__.py +++ b/anndata/compat/__init__.py @@ -1,20 +1,20 @@ from __future__ import annotations +import os +from codecs import decode +from collections.abc import Mapping from contextlib import AbstractContextManager from dataclasses import dataclass, field - from functools import singledispatch, wraps -from codecs import decode -from inspect import signature, Parameter -import os +from inspect import Parameter, signature from pathlib import Path -from typing import Any, Tuple, Union, Mapping, Optional +from typing import Any, Union from warnings import warn import h5py -from scipy.sparse import spmatrix, issparse import numpy as np import pandas as pd +from scipy.sparse import issparse, spmatrix from .exceptiongroups import add_note # noqa: F401 @@ -24,7 +24,7 @@ class Empty: Index1D = Union[slice, int, str, np.int64, np.ndarray] -Index = Union[Index1D, Tuple[Index1D, Index1D], spmatrix] +Index = Union[Index1D, tuple[Index1D, Index1D], spmatrix] H5Group = h5py.Group H5Array = h5py.Dataset @@ -105,12 +105,16 @@ def __repr__(): try: + from cupy import ndarray as CupyArray from cupyx.scipy.sparse import ( - spmatrix as CupySparseMatrix, - csr_matrix as CupyCSRMatrix, csc_matrix as CupyCSCMatrix, ) - from cupy import ndarray as CupyArray + from cupyx.scipy.sparse import ( + csr_matrix as CupyCSRMatrix, + ) + from cupyx.scipy.sparse import ( + spmatrix as CupySparseMatrix, + ) except ImportError: class CupySparseMatrix: @@ -140,7 +144,7 @@ def __repr__(): @singledispatch -def _read_attr(attrs: Mapping, name: str, default: Optional[Any] = Empty): +def _read_attr(attrs: Mapping, name: str, default: Any | None = Empty): if default is Empty: return attrs[name] else: @@ -149,7 +153,7 @@ def _read_attr(attrs: Mapping, name: str, default: Optional[Any] = Empty): @_read_attr.register(h5py.AttributeManager) def _read_attr_hdf5( - attrs: h5py.AttributeManager, name: str, default: Optional[Any] = Empty + attrs: h5py.AttributeManager, name: str, default: Any | None = Empty ): """ Read an HDF5 attribute and perform all necessary conversions. @@ -200,7 +204,7 @@ def _from_fixed_length_strings(value): def _decode_structured_array( - arr: np.ndarray, dtype: Optional[np.dtype] = None, copy: bool = False + arr: np.ndarray, dtype: np.dtype | None = None, copy: bool = False ) -> np.ndarray: """ h5py 3.0 now reads all strings as bytes. There is a helper method which can convert these to strings, @@ -250,7 +254,7 @@ def _to_fixed_length_strings(value: np.ndarray) -> np.ndarray: ############################# -def _clean_uns(adata: "AnnData"): # noqa: F821 +def _clean_uns(adata: AnnData): # noqa: F821 """ Compat function for when categorical keys were stored in uns. This used to be buggy because when storing categorical columns in obs and var with @@ -342,7 +346,7 @@ def inner_f(*args, **kwargs): # extra_args > 0 args_msg = [ - "{}={}".format(name, arg) + f"{name}={arg}" for name, arg in zip(kwonly_args[:extra_args], args[-extra_args:]) ] args_msg = ", ".join(args_msg) diff --git a/anndata/compat/exceptiongroups.py b/anndata/compat/exceptiongroups.py index f64090017..49d6c3a65 100644 --- a/anndata/compat/exceptiongroups.py +++ b/anndata/compat/exceptiongroups.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import sys diff --git a/anndata/core.py b/anndata/core.py index c4b254c0e..8e6ef0382 100644 --- a/anndata/core.py +++ b/anndata/core.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from warnings import warn warn("Please only import from anndata, not anndata.core", DeprecationWarning) diff --git a/anndata/experimental/__init__.py b/anndata/experimental/__init__.py index 13667e214..486f14e8d 100644 --- a/anndata/experimental/__init__.py +++ b/anndata/experimental/__init__.py @@ -1,12 +1,12 @@ from __future__ import annotations -from .multi_files import AnnCollection -from .pytorch import AnnLoader +from anndata._core.sparse_dataset import CSCDataset, CSRDataset, sparse_dataset +from anndata._io.specs import IOSpec, read_elem, write_elem -from anndata._io.specs import read_elem, write_elem, IOSpec -from anndata._core.sparse_dataset import sparse_dataset, CSRDataset, CSCDataset from ._dispatch_io import read_dispatched, write_dispatched from .merge import concat_on_disk +from .multi_files import AnnCollection +from .pytorch import AnnLoader __all__ = [ "AnnCollection", diff --git a/anndata/experimental/_dispatch_io.py b/anndata/experimental/_dispatch_io.py index 2df14b4f1..4df4d417a 100644 --- a/anndata/experimental/_dispatch_io.py +++ b/anndata/experimental/_dispatch_io.py @@ -1,11 +1,11 @@ from __future__ import annotations from types import MappingProxyType -from typing import Callable, Any +from typing import TYPE_CHECKING, Any, Callable - -from anndata._io.specs import IOSpec -from anndata._types import StorageType, GroupStorageType +if TYPE_CHECKING: + from anndata._io.specs import IOSpec + from anndata._types import GroupStorageType, StorageType def read_dispatched( @@ -39,7 +39,7 @@ def read_dispatched( :doc:`/tutorials/notebooks/{read,write}_dispatched` """ - from anndata._io.specs import Reader, _REGISTRY + from anndata._io.specs import _REGISTRY, Reader reader = Reader(_REGISTRY, callback=callback) @@ -90,7 +90,7 @@ def write_dispatched( :doc:`/tutorials/notebooks/{read,write}_dispatched` """ - from anndata._io.specs import Writer, _REGISTRY + from anndata._io.specs import _REGISTRY, Writer writer = Writer(_REGISTRY, callback=callback) diff --git a/anndata/experimental/merge.py b/anndata/experimental/merge.py index 2cc91c5b2..87f986c7c 100644 --- a/anndata/experimental/merge.py +++ b/anndata/experimental/merge.py @@ -1,18 +1,14 @@ +from __future__ import annotations + import os import shutil +from collections.abc import Collection, Iterable, Mapping, MutableMapping, Sequence from functools import singledispatch from pathlib import Path from typing import ( Any, Callable, - Collection, - Iterable, Literal, - Mapping, - Optional, - Sequence, - Union, - MutableMapping, ) import numpy as np @@ -104,12 +100,12 @@ def _gen_slice_to_append( @singledispatch -def as_group(store, *args, **kwargs) -> Union[ZarrGroup, H5Group]: +def as_group(store, *args, **kwargs) -> ZarrGroup | H5Group: raise NotImplementedError("This is not yet implemented.") -@as_group.register -def _(store: os.PathLike, *args, **kwargs) -> Union[ZarrGroup, H5Group]: +@as_group.register(os.PathLike) +def _(store: os.PathLike, *args, **kwargs) -> ZarrGroup | H5Group: if store.suffix == ".h5ad": import h5py @@ -119,8 +115,8 @@ def _(store: os.PathLike, *args, **kwargs) -> Union[ZarrGroup, H5Group]: return zarr.open_group(store, *args, **kwargs) -@as_group.register -def _(store: str, *args, **kwargs) -> Union[ZarrGroup, H5Group]: +@as_group.register(str) +def _(store: str, *args, **kwargs) -> ZarrGroup | H5Group: return as_group(Path(store), *args, **kwargs) @@ -135,7 +131,7 @@ def _(store, *args, **kwargs): ################### -def read_as_backed(group: Union[ZarrGroup, H5Group]): +def read_as_backed(group: ZarrGroup | H5Group): """ Read the group until BaseCompressedSparseDataset, Array or EAGER_TYPES are encountered. @@ -156,7 +152,7 @@ def callback(func, elem_name: str, elem, iospec): return read_dispatched(group, callback=callback) -def _df_index(df: Union[ZarrGroup, H5Group]) -> pd.Index: +def _df_index(df: ZarrGroup | H5Group) -> pd.Index: index_key = df.attrs["_index"] return pd.Index(read_elem(df[index_key])) @@ -167,9 +163,9 @@ def _df_index(df: Union[ZarrGroup, H5Group]) -> pd.Index: def write_concat_dense( - arrays: Sequence[Union[ZarrArray, H5Array]], - output_group: Union[ZarrGroup, H5Group], - output_path: Union[ZarrGroup, H5Group], + arrays: Sequence[ZarrArray | H5Array], + output_group: ZarrGroup | H5Group, + output_path: ZarrGroup | H5Group, axis: Literal[0, 1] = 0, reindexers: Reindexer = None, fill_value=None, @@ -196,8 +192,8 @@ def write_concat_dense( def write_concat_sparse( datasets: Sequence[BaseCompressedSparseDataset], - output_group: Union[ZarrGroup, H5Group], - output_path: Union[ZarrGroup, H5Group], + output_group: ZarrGroup | H5Group, + output_path: ZarrGroup | H5Group, max_loaded_elems: int, axis: Literal[0, 1] = 0, reindexers: Reindexer = None, @@ -235,7 +231,7 @@ def write_concat_sparse( def _write_concat_mappings( mappings, - output_group: Union[ZarrGroup, H5Group], + output_group: ZarrGroup | H5Group, keys, path, max_loaded_elems, @@ -269,7 +265,7 @@ def _write_concat_mappings( def _write_concat_arrays( - arrays: Sequence[Union[ZarrArray, H5Array, BaseCompressedSparseDataset]], + arrays: Sequence[ZarrArray | H5Array | BaseCompressedSparseDataset], output_group, output_path, max_loaded_elems, @@ -314,9 +310,7 @@ def _write_concat_arrays( def _write_concat_sequence( - arrays: Sequence[ - Union[pd.DataFrame, BaseCompressedSparseDataset, H5Array, ZarrArray] - ], + arrays: Sequence[pd.DataFrame | BaseCompressedSparseDataset | H5Array | ZarrArray], output_group, output_path, max_loaded_elems, @@ -401,26 +395,21 @@ def _write_dim_annot(groups, output_group, dim, concat_indices, label, label_col def concat_on_disk( - in_files: Union[ - Collection[Union[str, os.PathLike]], - MutableMapping[str, Union[str, os.PathLike]], - ], - out_file: Union[str, os.PathLike], + in_files: Collection[str | os.PathLike] | MutableMapping[str, str | os.PathLike], + out_file: str | os.PathLike, *, overwrite: bool = False, max_loaded_elems: int = 100_000_000, axis: Literal[0, 1] = 0, join: Literal["inner", "outer"] = "inner", - merge: Union[ - StrategiesLiteral, Callable[[Collection[Mapping]], Mapping], None - ] = None, - uns_merge: Union[ - StrategiesLiteral, Callable[[Collection[Mapping]], Mapping], None - ] = None, - label: Optional[str] = None, - keys: Optional[Collection[str]] = None, - index_unique: Optional[str] = None, - fill_value: Optional[Any] = None, + merge: StrategiesLiteral | Callable[[Collection[Mapping]], Mapping] | None = None, + uns_merge: StrategiesLiteral + | Callable[[Collection[Mapping]], Mapping] + | None = None, + label: str | None = None, + keys: Collection[str] | None = None, + index_unique: str | None = None, + fill_value: Any | None = None, pairwise: bool = False, ) -> None: """Concatenates multiple AnnData objects along a specified axis using their diff --git a/anndata/experimental/multi_files/__init__.py b/anndata/experimental/multi_files/__init__.py index 86d4e8f44..956ebb8d2 100644 --- a/anndata/experimental/multi_files/__init__.py +++ b/anndata/experimental/multi_files/__init__.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from ._anncollection import AnnCollection __all__ = ["AnnCollection"] diff --git a/anndata/experimental/multi_files/_anncollection.py b/anndata/experimental/multi_files/_anncollection.py index 8eea18b0d..390b771c7 100644 --- a/anndata/experimental/multi_files/_anncollection.py +++ b/anndata/experimental/multi_files/_anncollection.py @@ -1,18 +1,20 @@ -from collections.abc import Mapping +from __future__ import annotations + +import warnings +from collections.abc import Mapping, Sequence from functools import reduce -from h5py import Dataset +from typing import Callable, Literal, Union + import numpy as np import pandas as pd -import warnings - -from typing import Dict, Union, Optional, Sequence, Callable, Literal +from h5py import Dataset +from ..._core.aligned_mapping import AxisArrays from ..._core.anndata import AnnData -from ..._core.index import _normalize_indices, _normalize_index, Index -from ..._core.views import _resolve_idx +from ..._core.index import Index, _normalize_index, _normalize_indices from ..._core.merge import concat_arrays, inner_concat_aligned_mapping from ..._core.sparse_dataset import BaseCompressedSparseDataset -from ..._core.aligned_mapping import AxisArrays +from ..._core.views import _resolve_idx ATTRS = ["obs", "obsm", "layers"] @@ -571,8 +573,8 @@ def attrs_keys(self): return self.reference.attrs_keys -DictCallable = Dict[str, Callable] -ConvertType = Union[Callable, DictCallable, Dict[str, DictCallable]] +DictCallable = dict[str, Callable] +ConvertType = Union[Callable, DictCallable, dict[str, DictCallable]] class AnnCollection(_ConcatViewMixin, _IterateViewMixin): @@ -665,14 +667,14 @@ class AnnCollection(_ConcatViewMixin, _IterateViewMixin): def __init__( self, - adatas: Union[Sequence[AnnData], Dict[str, AnnData]], - join_obs: Optional[Literal["inner", "outer"]] = "inner", - join_obsm: Optional[Literal["inner"]] = None, - join_vars: Optional[Literal["inner"]] = None, - label: Optional[str] = None, - keys: Optional[Sequence[str]] = None, - index_unique: Optional[str] = None, - convert: Optional[ConvertType] = None, + adatas: Sequence[AnnData] | dict[str, AnnData], + join_obs: Literal["inner", "outer"] | None = "inner", + join_obsm: Literal["inner"] | None = None, + join_vars: Literal["inner"] | None = None, + label: str | None = None, + keys: Sequence[str] | None = None, + index_unique: str | None = None, + convert: ConvertType | None = None, harmonize_dtypes: bool = True, indices_strict: bool = True, ): @@ -933,7 +935,7 @@ def __repr__(self): class LazyAttrData(_IterateViewMixin): - def __init__(self, adset: AnnCollection, attr: str, key: Optional[str] = None): + def __init__(self, adset: AnnCollection, attr: str, key: str | None = None): self.adset = adset self.attr = attr self.key = key diff --git a/anndata/experimental/pytorch/__init__.py b/anndata/experimental/pytorch/__init__.py index d4fbffce7..36c9441fe 100644 --- a/anndata/experimental/pytorch/__init__.py +++ b/anndata/experimental/pytorch/__init__.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from ._annloader import AnnLoader __all__ = ["AnnLoader"] diff --git a/anndata/experimental/pytorch/_annloader.py b/anndata/experimental/pytorch/_annloader.py index 15ad53a52..8cc883921 100644 --- a/anndata/experimental/pytorch/_annloader.py +++ b/anndata/experimental/pytorch/_annloader.py @@ -1,18 +1,22 @@ -from scipy.sparse import issparse -from math import ceil +from __future__ import annotations + from copy import copy from functools import partial -from typing import Dict, Union, Sequence +from math import ceil +from typing import TYPE_CHECKING import numpy as np +from scipy.sparse import issparse from ..._core.anndata import AnnData from ..multi_files._anncollection import AnnCollection, _ConcatViewMixin +if TYPE_CHECKING: + from collections.abc import Sequence try: import torch - from torch.utils.data import Sampler, BatchSampler, DataLoader + from torch.utils.data import BatchSampler, DataLoader, Sampler except ImportError: Sampler, BatchSampler, DataLoader = object, object, object @@ -123,7 +127,7 @@ class AnnLoader(DataLoader): def __init__( self, - adatas: Union[Sequence[AnnData], Dict[str, AnnData]], + adatas: Sequence[AnnData] | dict[str, AnnData], batch_size: int = 1, shuffle: bool = False, use_default_converter: bool = True, diff --git a/anndata/logging.py b/anndata/logging.py index f5feac09c..a2a890c51 100644 --- a/anndata/logging.py +++ b/anndata/logging.py @@ -1,5 +1,7 @@ -import os +from __future__ import annotations + import logging +import os _previous_memory_usage = None diff --git a/anndata/readwrite.py b/anndata/readwrite.py index dfe5a7074..f3d07f732 100644 --- a/anndata/readwrite.py +++ b/anndata/readwrite.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from warnings import warn warn("Please only import from anndata, not anndata.readwrite", DeprecationWarning) diff --git a/anndata/tests/conftest.py b/anndata/tests/conftest.py index ef8a3b50e..e16197cc7 100644 --- a/anndata/tests/conftest.py +++ b/anndata/tests/conftest.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import warnings import pytest @@ -5,7 +7,6 @@ import anndata from anndata.tests.helpers import subset_func # noqa: F401 - # TODO: Should be done in pyproject.toml, see anndata/conftest.py warnings.filterwarnings("ignore", category=anndata.OldFormatWarning) diff --git a/anndata/tests/helpers.py b/anndata/tests/helpers.py index 450f604d9..641bdc791 100644 --- a/anndata/tests/helpers.py +++ b/anndata/tests/helpers.py @@ -1,34 +1,33 @@ from __future__ import annotations -from contextlib import contextmanager -from functools import singledispatch, wraps, partial +import random import re -from string import ascii_letters -from typing import Tuple, Optional, Type -from collections.abc import Mapping, Collection import warnings +from collections.abc import Collection, Mapping +from contextlib import contextmanager +from functools import partial, singledispatch, wraps +from string import ascii_letters import h5py import numpy as np import pandas as pd -from pandas.api.types import is_numeric_dtype import pytest +from pandas.api.types import is_numeric_dtype from scipy import sparse -import random from anndata import AnnData, Raw -from anndata._core.views import ArrayView -from anndata._core.sparse_dataset import BaseCompressedSparseDataset from anndata._core.aligned_mapping import AlignedMapping -from anndata.utils import asarray +from anndata._core.sparse_dataset import BaseCompressedSparseDataset +from anndata._core.views import ArrayView from anndata.compat import ( AwkArray, - DaskArray, - CupySparseMatrix, CupyArray, CupyCSCMatrix, CupyCSRMatrix, + CupySparseMatrix, + DaskArray, ) +from anndata.utils import asarray # Give this to gen_adata when dask array support is expected. GEN_ADATA_DASK_ARGS = dict( @@ -156,24 +155,24 @@ def gen_typed_df_t2_size(m, n, index=None, columns=None) -> pd.DataFrame: # TODO: Use hypothesis for this? def gen_adata( - shape: Tuple[int, int], + shape: tuple[int, int], X_type=sparse.csr_matrix, X_dtype=np.float32, # obs_dtypes, # var_dtypes, - obsm_types: "Collection[Type]" = ( + obsm_types: Collection[type] = ( sparse.csr_matrix, np.ndarray, pd.DataFrame, AwkArray, ), - varm_types: "Collection[Type]" = ( + varm_types: Collection[type] = ( sparse.csr_matrix, np.ndarray, pd.DataFrame, AwkArray, ), - layers_types: "Collection[Type]" = (sparse.csr_matrix, np.ndarray, pd.DataFrame), + layers_types: Collection[type] = (sparse.csr_matrix, np.ndarray, pd.DataFrame), sparse_fmt: str = "csr", ) -> AnnData: """\ @@ -529,7 +528,7 @@ def assert_is_not_none(x): # can't put an assert in a lambda @assert_equal.register(AnnData) def assert_adata_equal( - a: AnnData, b: AnnData, exact: bool = False, elem_name: Optional[str] = None + a: AnnData, b: AnnData, exact: bool = False, elem_name: str | None = None ): """\ Check whether two AnnData objects are equivalent, @@ -681,16 +680,16 @@ def as_cupy_type(val, typ=None): val = val.toarray() return cp.array(val) elif issubclass(typ, CupyCSRMatrix): - import cupyx.scipy.sparse as cpsparse import cupy as cp + import cupyx.scipy.sparse as cpsparse if isinstance(val, np.ndarray): return cpsparse.csr_matrix(cp.array(val)) else: return cpsparse.csr_matrix(val) elif issubclass(typ, CupyCSCMatrix): - import cupyx.scipy.sparse as cpsparse import cupy as cp + import cupyx.scipy.sparse as cpsparse if isinstance(val, np.ndarray): return cpsparse.csc_matrix(cp.array(val)) diff --git a/anndata/tests/test_anncollection.py b/anndata/tests/test_anncollection.py index b8def9508..aaef199e7 100644 --- a/anndata/tests/test_anncollection.py +++ b/anndata/tests/test_anncollection.py @@ -1,10 +1,11 @@ -import pytest -import anndata as ad -import numpy as np +from __future__ import annotations +import numpy as np +import pytest from scipy.sparse import csr_matrix, issparse - from sklearn.preprocessing import LabelEncoder + +import anndata as ad from anndata.experimental.multi_files import AnnCollection _dense = lambda a: a.toarray() if issparse(a) else a diff --git a/anndata/tests/test_annot.py b/anndata/tests/test_annot.py index 025c7d5a6..0ea609906 100644 --- a/anndata/tests/test_annot.py +++ b/anndata/tests/test_annot.py @@ -1,12 +1,13 @@ """Test handling of values in `obs`/ `var`""" -from natsort import natsorted +from __future__ import annotations + import numpy as np import pandas as pd +import pytest +from natsort import natsorted import anndata as ad -import pytest - @pytest.mark.parametrize("dtype", [object, "string"]) def test_str_to_categorical(dtype): diff --git a/anndata/tests/test_awkward.py b/anndata/tests/test_awkward.py index 87280d5a2..993fb91de 100644 --- a/anndata/tests/test_awkward.py +++ b/anndata/tests/test_awkward.py @@ -1,15 +1,16 @@ """Tests related to awkward arrays""" -import pytest +from __future__ import annotations + import numpy as np import numpy.testing as npt +import pandas as pd +import pytest -from anndata.tests.helpers import assert_equal, gen_adata, gen_awkward +import anndata +from anndata import AnnData, ImplicitModificationWarning, read_h5ad from anndata.compat import awkward as ak -from anndata import ImplicitModificationWarning +from anndata.tests.helpers import assert_equal, gen_adata, gen_awkward from anndata.utils import dim_len -from anndata import AnnData, read_h5ad -import anndata -import pandas as pd @pytest.mark.parametrize( diff --git a/anndata/tests/test_backed_sparse.py b/anndata/tests/test_backed_sparse.py index 2809b7c9f..d781226ad 100644 --- a/anndata/tests/test_backed_sparse.py +++ b/anndata/tests/test_backed_sparse.py @@ -1,15 +1,16 @@ +from __future__ import annotations + import h5py import numpy as np import pytest +import zarr from scipy import sparse import anndata as ad from anndata._core.anndata import AnnData from anndata._core.sparse_dataset import sparse_dataset -from anndata.tests.helpers import assert_equal, subset_func from anndata.experimental import read_dispatched - -import zarr +from anndata.tests.helpers import assert_equal, subset_func subset_func2 = subset_func diff --git a/anndata/tests/test_base.py b/anndata/tests/test_base.py index 17d6a9476..14127271f 100644 --- a/anndata/tests/test_base.py +++ b/anndata/tests/test_base.py @@ -1,20 +1,19 @@ from __future__ import annotations -from itertools import product import re import warnings +from itertools import product import numpy as np -from numpy import ma import pandas as pd import pytest +from numpy import ma from scipy import sparse as sp from scipy.sparse import csr_matrix, issparse from anndata import AnnData from anndata.tests.helpers import assert_equal, gen_adata - # some test objects that we use below adata_dense = AnnData(np.array([[1, 2], [3, 4]])) adata_dense.layers["test"] = adata_dense.X diff --git a/anndata/tests/test_concatenate.py b/anndata/tests/test_concatenate.py index 92e8d2895..78caa8a19 100644 --- a/anndata/tests/test_concatenate.py +++ b/anndata/tests/test_concatenate.py @@ -1,33 +1,34 @@ +from __future__ import annotations + +import warnings from collections.abc import Hashable from copy import deepcopy -from itertools import chain, product from functools import partial, singledispatch -from typing import Any, List, Callable -import warnings +from itertools import chain, product +from typing import Any, Callable import numpy as np -from numpy import ma import pandas as pd import pytest +from boltons.iterutils import default_exit, remap, research +from numpy import ma from scipy import sparse -from boltons.iterutils import research, remap, default_exit - from anndata import AnnData, Raw, concat -from anndata._core.index import _subset from anndata._core import merge +from anndata._core.index import _subset +from anndata.compat import AwkArray, DaskArray from anndata.tests import helpers from anndata.tests.helpers import ( - assert_equal, - as_dense_dask_array, - gen_adata, - GEN_ADATA_DASK_ARGS, BASE_MATRIX_PARAMS, - DASK_MATRIX_PARAMS, CUPY_MATRIX_PARAMS, + DASK_MATRIX_PARAMS, + GEN_ADATA_DASK_ARGS, + as_dense_dask_array, + assert_equal, + gen_adata, ) from anndata.utils import asarray -from anndata.compat import DaskArray, AwkArray @singledispatch @@ -963,7 +964,7 @@ def map_values(mapping, path, key, old_parent, new_parent, new_items): return ret -def permute_nested_values(dicts: "List[dict]", gen_val: "Callable[[int], Any]"): +def permute_nested_values(dicts: list[dict], gen_val: Callable[[int], Any]): """ This function permutes the values of a nested mapping, for testing that out merge method work regardless of the values types. @@ -1389,9 +1390,10 @@ def test_concat_X_dtype(): # Tests how dask plays with other types on concatenation. def test_concat_different_types_dask(merge_strategy, array_type): + import dask.array as da from scipy import sparse + import anndata as ad - import dask.array as da varm_array = sparse.random(5, 20, density=0.5, format="csr") diff --git a/anndata/tests/test_concatenate_disk.py b/anndata/tests/test_concatenate_disk.py index 1ffbb63ef..0192df452 100644 --- a/anndata/tests/test_concatenate_disk.py +++ b/anndata/tests/test_concatenate_disk.py @@ -1,23 +1,21 @@ -from typing import Mapping +from __future__ import annotations + +from collections.abc import Mapping import numpy as np import pandas as pd import pytest from scipy import sparse -from anndata.experimental.merge import concat_on_disk, as_group -from anndata.experimental import write_elem, read_elem - from anndata import AnnData, concat +from anndata.experimental import read_elem, write_elem +from anndata.experimental.merge import as_group, concat_on_disk from anndata.tests.helpers import ( assert_equal, gen_adata, ) - - from anndata.utils import asarray - GEN_ADATA_OOC_CONCAT_ARGS = dict( obsm_types=( sparse.csr_matrix, diff --git a/anndata/tests/test_dask.py b/anndata/tests/test_dask.py index cb745a8f5..7bd353f24 100644 --- a/anndata/tests/test_dask.py +++ b/anndata/tests/test_dask.py @@ -1,19 +1,22 @@ """ For tests using dask """ -import anndata as ad +from __future__ import annotations + import pandas as pd -from anndata._core.anndata import AnnData import pytest + +import anndata as ad +from anndata._core.anndata import AnnData +from anndata.compat import DaskArray +from anndata.experimental import read_elem, write_elem +from anndata.experimental.merge import as_group from anndata.tests.helpers import ( - as_dense_dask_array, GEN_ADATA_DASK_ARGS, - gen_adata, + as_dense_dask_array, assert_equal, + gen_adata, ) -from anndata.experimental import write_elem, read_elem -from anndata.experimental.merge import as_group -from anndata.compat import DaskArray pytest.importorskip("dask.array") @@ -243,6 +246,7 @@ def test_assign_X(adata): """Check if assignment works""" import dask.array as da import numpy as np + from anndata.compat import DaskArray adata.X = da.ones(adata.X.shape) @@ -306,8 +310,8 @@ def test_dask_to_memory_copy_unbacked(): def test_to_memory_raw(): - import numpy as np import dask.array as da + import numpy as np orig = gen_adata((20, 10), **GEN_ADATA_DASK_ARGS) orig.X = da.ones((20, 10)) @@ -327,8 +331,8 @@ def test_to_memory_raw(): def test_to_memory_copy_raw(): - import numpy as np import dask.array as da + import numpy as np orig = gen_adata((20, 10), **GEN_ADATA_DASK_ARGS) orig.X = da.ones((20, 10)) diff --git a/anndata/tests/test_dask_view_mem.py b/anndata/tests/test_dask_view_mem.py index 6d758477b..bb758a223 100644 --- a/anndata/tests/test_dask_view_mem.py +++ b/anndata/tests/test_dask_view_mem.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import pytest import anndata as ad @@ -43,8 +45,8 @@ def alloc_cache(): **{ "layers": dict(m=da.random.random(*size)), "obsm": dict(m=da.random.random(*size)), - "obs": dict(m=da.random.random((N))), - "var": dict(m=da.random.random((N))), + "obs": dict(m=da.random.random(N)), + "var": dict(m=da.random.random(N)), "varm": dict(m=da.random.random(*size)), }, ) diff --git a/anndata/tests/test_deprecations.py b/anndata/tests/test_deprecations.py index d632e3ea2..52d6501ab 100644 --- a/anndata/tests/test_deprecations.py +++ b/anndata/tests/test_deprecations.py @@ -3,6 +3,8 @@ This includes correct behaviour as well as throwing warnings. """ +from __future__ import annotations + import warnings import h5py @@ -10,9 +12,8 @@ import pytest from scipy import sparse -from anndata import AnnData import anndata as ad - +from anndata import AnnData from anndata.tests.helpers import assert_equal @@ -101,8 +102,8 @@ def test_dtype_warning(): def test_deprecated_write_attribute(tmp_path): pth = tmp_path / "file.h5" A = np.random.randn(20, 10) - from anndata._io.utils import read_attribute, write_attribute from anndata._io.specs import read_elem + from anndata._io.utils import read_attribute, write_attribute with h5py.File(pth, "w") as f: with pytest.warns(DeprecationWarning, match="write_elem"): @@ -129,6 +130,7 @@ def test_deprecated_read(tmp_path): def test_deprecated_sparse_dataset_values(): import zarr + from anndata.experimental import sparse_dataset, write_elem mtx = sparse.random(50, 50, format="csr") diff --git a/anndata/tests/test_get_vector.py b/anndata/tests/test_get_vector.py index ca2ce18a7..baf0fd7d6 100644 --- a/anndata/tests/test_get_vector.py +++ b/anndata/tests/test_get_vector.py @@ -1,7 +1,9 @@ +from __future__ import annotations + import numpy as np import pandas as pd -from scipy import sparse import pytest +from scipy import sparse import anndata as ad diff --git a/anndata/tests/test_gpu.py b/anndata/tests/test_gpu.py index 434567ca8..c6f49a696 100644 --- a/anndata/tests/test_gpu.py +++ b/anndata/tests/test_gpu.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import pytest from scipy import sparse @@ -16,8 +18,8 @@ def test_gpu(): @pytest.mark.gpu def test_adata_raw_gpu(): - from cupyx.scipy import sparse as cupy_sparse import cupy as cp + from cupyx.scipy import sparse as cupy_sparse adata = AnnData( X=cupy_sparse.random(500, 50, density=0.01, format="csr", dtype=cp.float32) @@ -28,8 +30,8 @@ def test_adata_raw_gpu(): @pytest.mark.gpu def test_raw_gpu(): - from cupyx.scipy import sparse as cupy_sparse import cupy as cp + from cupyx.scipy import sparse as cupy_sparse adata = AnnData( X=cupy_sparse.random(500, 50, density=0.01, format="csr", dtype=cp.float32) diff --git a/anndata/tests/test_hdf5_backing.py b/anndata/tests/test_hdf5_backing.py index ab308e363..61c0c905c 100644 --- a/anndata/tests/test_hdf5_backing.py +++ b/anndata/tests/test_hdf5_backing.py @@ -1,16 +1,18 @@ +from __future__ import annotations + from pathlib import Path import joblib -import pytest import numpy as np +import pytest from scipy import sparse import anndata as ad from anndata.tests.helpers import ( - as_dense_dask_array, GEN_ADATA_DASK_ARGS, - gen_adata, + as_dense_dask_array, assert_equal, + gen_adata, subset_func, ) from anndata.utils import asarray diff --git a/anndata/tests/test_helpers.py b/anndata/tests/test_helpers.py index f540c43f4..52f1d6cb1 100644 --- a/anndata/tests/test_helpers.py +++ b/anndata/tests/test_helpers.py @@ -1,21 +1,23 @@ +from __future__ import annotations + from string import ascii_letters +import numpy as np import pandas as pd import pytest -import numpy as np from scipy import sparse import anndata as ad +from anndata.compat import add_note from anndata.tests.helpers import ( + asarray, assert_equal, - gen_awkward, - report_name, gen_adata, - asarray, + gen_awkward, pytest_8_raises, + report_name, ) from anndata.utils import dim_len -from anndata.compat import add_note # Testing to see if all error types can have the key name appended. # Currently fails for 22/118 since they have required arguments. Not sure what to do about that. diff --git a/anndata/tests/test_inplace_subset.py b/anndata/tests/test_inplace_subset.py index b90421965..110d2574a 100644 --- a/anndata/tests/test_inplace_subset.py +++ b/anndata/tests/test_inplace_subset.py @@ -1,11 +1,13 @@ +from __future__ import annotations + import numpy as np import pytest from scipy import sparse from anndata.tests.helpers import ( + as_dense_dask_array, assert_equal, gen_adata, - as_dense_dask_array, ) from anndata.utils import asarray diff --git a/anndata/tests/test_io_backwards_compat.py b/anndata/tests/test_io_backwards_compat.py index a060d1779..fb12c8161 100644 --- a/anndata/tests/test_io_backwards_compat.py +++ b/anndata/tests/test_io_backwards_compat.py @@ -1,10 +1,12 @@ +from __future__ import annotations + from pathlib import Path +import pandas as pd import pytest +from scipy import sparse import anndata as ad -import pandas as pd -from scipy import sparse from anndata.tests.helpers import assert_equal ARCHIVE_PTH = Path(__file__).parent / "data/archives" diff --git a/anndata/tests/test_io_conversion.py b/anndata/tests/test_io_conversion.py index dd5e9ab61..29a5d27e9 100644 --- a/anndata/tests/test_io_conversion.py +++ b/anndata/tests/test_io_conversion.py @@ -1,13 +1,15 @@ """\ This file contains tests for conversion made during io. """ +from __future__ import annotations + import h5py import numpy as np import pytest from scipy import sparse import anndata as ad -from anndata.tests.helpers import gen_adata, assert_equal +from anndata.tests.helpers import assert_equal, gen_adata @pytest.fixture( diff --git a/anndata/tests/test_io_dispatched.py b/anndata/tests/test_io_dispatched.py index b43eb67c2..511179d4c 100644 --- a/anndata/tests/test_io_dispatched.py +++ b/anndata/tests/test_io_dispatched.py @@ -1,17 +1,19 @@ +from __future__ import annotations + import re -from scipy import sparse import h5py import zarr +from scipy import sparse import anndata as ad from anndata.experimental import ( read_dispatched, - write_dispatched, read_elem, + write_dispatched, write_elem, ) -from anndata.tests.helpers import gen_adata, assert_equal +from anndata.tests.helpers import assert_equal, gen_adata def test_read_dispatched_w_regex(): @@ -79,7 +81,7 @@ def test_read_dispatched_null_case(): def test_write_dispatched_chunks(): - from itertools import repeat, chain + from itertools import chain, repeat def determine_chunks(elem_shape, specified_chunks): chunk_iterator = chain(specified_chunks, repeat(None)) diff --git a/anndata/tests/test_io_elementwise.py b/anndata/tests/test_io_elementwise.py index 200292200..34a42e7ff 100644 --- a/anndata/tests/test_io_elementwise.py +++ b/anndata/tests/test_io_elementwise.py @@ -9,19 +9,18 @@ import numpy as np import pandas as pd import pytest -from scipy import sparse import zarr +from scipy import sparse import anndata as ad -from anndata._io.specs import _REGISTRY, get_spec, IOSpec +from anndata._io.specs import _REGISTRY, IOSpec, get_spec, read_elem, write_elem from anndata._io.specs.registry import IORegistryError -from anndata.compat import _read_attr, H5Group, ZarrGroup -from anndata._io.specs import write_elem, read_elem +from anndata.compat import H5Group, ZarrGroup, _read_attr from anndata.tests.helpers import ( - assert_equal, as_cupy_type, - pytest_8_raises, + assert_equal, gen_adata, + pytest_8_raises, ) diff --git a/anndata/tests/test_io_partial.py b/anndata/tests/test_io_partial.py index b75e5ccf1..d43aaca1c 100644 --- a/anndata/tests/test_io_partial.py +++ b/anndata/tests/test_io_partial.py @@ -1,14 +1,18 @@ +from __future__ import annotations + from importlib.util import find_spec -from anndata import AnnData -from anndata._io.specs import read_elem -from anndata._io.specs.registry import read_elem_partial -from anndata._io import write_h5ad, write_zarr -from scipy.sparse import csr_matrix from pathlib import Path + +import h5py import numpy as np import pytest import zarr -import h5py +from scipy.sparse import csr_matrix + +from anndata import AnnData +from anndata._io import write_h5ad, write_zarr +from anndata._io.specs import read_elem +from anndata._io.specs.registry import read_elem_partial X = np.array([[1.0, 0.0, 3.0], [4.0, 0.0, 6.0], [0.0, 8.0, 0.0]], dtype="float32") X_check = np.array([[4.0, 0.0], [0.0, 8.0]], dtype="float32") diff --git a/anndata/tests/test_io_utils.py b/anndata/tests/test_io_utils.py index 8b94a5feb..c70091474 100644 --- a/anndata/tests/test_io_utils.py +++ b/anndata/tests/test_io_utils.py @@ -1,16 +1,18 @@ +from __future__ import annotations + from contextlib import suppress -import pytest -import zarr import h5py import pandas as pd +import pytest +import zarr import anndata as ad from anndata._io.specs.registry import IORegistryError -from anndata.compat import _clean_uns from anndata._io.utils import ( report_read_key_on_error, ) +from anndata.compat import _clean_uns from anndata.experimental import read_elem, write_elem from anndata.tests.helpers import pytest_8_raises diff --git a/anndata/tests/test_io_warnings.py b/anndata/tests/test_io_warnings.py index 284d86ecc..dfc33ccf1 100644 --- a/anndata/tests/test_io_warnings.py +++ b/anndata/tests/test_io_warnings.py @@ -1,6 +1,8 @@ +from __future__ import annotations + +import warnings from importlib.util import find_spec from pathlib import Path -import warnings import pytest diff --git a/anndata/tests/test_layers.py b/anndata/tests/test_layers.py index 4f9f00973..4b6a7f287 100644 --- a/anndata/tests/test_layers.py +++ b/anndata/tests/test_layers.py @@ -1,11 +1,13 @@ -from importlib.util import find_spec +from __future__ import annotations + import warnings +from importlib.util import find_spec -import pytest import numpy as np import pandas as pd +import pytest -from anndata import AnnData, read_loom, read_h5ad +from anndata import AnnData, read_h5ad, read_loom from anndata.tests.helpers import gen_typed_df_t2_size X = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]]) diff --git a/anndata/tests/test_obsmvarm.py b/anndata/tests/test_obsmvarm.py index 1c08e7545..e1e802a9d 100644 --- a/anndata/tests/test_obsmvarm.py +++ b/anndata/tests/test_obsmvarm.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import joblib import numpy as np import pandas as pd diff --git a/anndata/tests/test_obspvarp.py b/anndata/tests/test_obspvarp.py index 8ff025d76..5b3e063d3 100644 --- a/anndata/tests/test_obspvarp.py +++ b/anndata/tests/test_obspvarp.py @@ -1,4 +1,6 @@ # TODO: These tests should share code with test_layers, and test_obsmvarm +from __future__ import annotations + import warnings import joblib diff --git a/anndata/tests/test_raw.py b/anndata/tests/test_raw.py index 5686a4edc..7e4689d60 100644 --- a/anndata/tests/test_raw.py +++ b/anndata/tests/test_raw.py @@ -1,10 +1,11 @@ +from __future__ import annotations + import numpy as np import pytest import anndata as ad from anndata._core.anndata import ImplicitModificationWarning -from anndata.tests.helpers import assert_equal, gen_adata, GEN_ADATA_DASK_ARGS - +from anndata.tests.helpers import GEN_ADATA_DASK_ARGS, assert_equal, gen_adata # ------------------------------------------------------------------------------- # Some test data diff --git a/anndata/tests/test_readwrite.py b/anndata/tests/test_readwrite.py index c78d3833a..98de43a61 100644 --- a/anndata/tests/test_readwrite.py +++ b/anndata/tests/test_readwrite.py @@ -1,29 +1,33 @@ +from __future__ import annotations + +import re +import warnings from contextlib import contextmanager from importlib.util import find_spec -from os import PathLike from pathlib import Path -import re from string import ascii_letters -import warnings +from typing import TYPE_CHECKING import h5py import numpy as np import pandas as pd import pytest -from scipy.sparse import csr_matrix, csc_matrix import zarr +from scipy.sparse import csc_matrix, csr_matrix import anndata as ad from anndata._io.specs.registry import IORegistryError -from anndata.compat import _read_attr, DaskArray - +from anndata.compat import DaskArray, _read_attr from anndata.tests.helpers import ( - gen_adata, - assert_equal, as_dense_dask_array, + assert_equal, + gen_adata, pytest_8_raises, ) +if TYPE_CHECKING: + from os import PathLike + HERE = Path(__file__).parent @@ -481,7 +485,7 @@ def test_write_csv_view(typ, tmp_path): def md5_path(pth: PathLike) -> bytes: checksum = hashlib.md5() - with open(pth, "rb") as f: + with pth.open("rb") as f: while True: buf = f.read(checksum.block_size * 100) if not buf: @@ -489,7 +493,7 @@ def md5_path(pth: PathLike) -> bytes: checksum.update(buf) return checksum.digest() - def hash_dir_contents(dir: Path) -> "dict[str, bytes]": + def hash_dir_contents(dir: Path) -> dict[str, bytes]: root_pth = str(dir) return { str(k)[len(root_pth) :]: md5_path(k) for k in dir.rglob("*") if k.is_file() @@ -652,7 +656,7 @@ def test_write_string_types(tmp_path, diskfmt): with pytest_8_raises(TypeError, match=r"writing key 'obs'") as exc_info: write(adata_pth) - assert str("b'c'") in str(exc_info.value) + assert "b'c'" in str(exc_info.value) @pytest.mark.parametrize( diff --git a/anndata/tests/test_repr.py b/anndata/tests/test_repr.py index 862b9cd65..18fffb74f 100644 --- a/anndata/tests/test_repr.py +++ b/anndata/tests/test_repr.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import re from string import ascii_letters diff --git a/anndata/tests/test_structured_arrays.py b/anndata/tests/test_structured_arrays.py index 580a72554..81b6be22f 100644 --- a/anndata/tests/test_structured_arrays.py +++ b/anndata/tests/test_structured_arrays.py @@ -1,12 +1,13 @@ -from itertools import product, combinations +from __future__ import annotations + +from itertools import combinations, product import numpy as np import pytest -from anndata.tests.helpers import gen_vstr_recarray - -from anndata import AnnData import anndata as ad +from anndata import AnnData +from anndata.tests.helpers import gen_vstr_recarray @pytest.fixture(params=["h5ad", "zarr"]) diff --git a/anndata/tests/test_transpose.py b/anndata/tests/test_transpose.py index e2a77dbb7..720733496 100644 --- a/anndata/tests/test_transpose.py +++ b/anndata/tests/test_transpose.py @@ -1,10 +1,11 @@ -from scipy import sparse -import numpy as np +from __future__ import annotations +import numpy as np import pytest +from scipy import sparse import anndata as ad -from anndata.tests.helpers import gen_adata, assert_equal, shares_memory +from anndata.tests.helpers import assert_equal, gen_adata, shares_memory def test_transpose_orig(): diff --git a/anndata/tests/test_uns.py b/anndata/tests/test_uns.py index 013c0dea5..ef0f4f8fe 100644 --- a/anndata/tests/test_uns.py +++ b/anndata/tests/test_uns.py @@ -1,6 +1,7 @@ +from __future__ import annotations + import numpy as np import pandas as pd - import pytest from anndata import AnnData diff --git a/anndata/tests/test_utils.py b/anndata/tests/test_utils.py index 7d766f304..f57fc5d6e 100644 --- a/anndata/tests/test_utils.py +++ b/anndata/tests/test_utils.py @@ -1,11 +1,14 @@ -import pandas as pd -from scipy import sparse +from __future__ import annotations + from itertools import repeat + +import pandas as pd import pytest +from scipy import sparse import anndata as ad -from anndata.utils import make_index_unique from anndata.tests.helpers import gen_typed_df +from anndata.utils import make_index_unique def test_make_index_unique(): diff --git a/anndata/tests/test_views.py b/anndata/tests/test_views.py index ff9a67746..b195c13b4 100644 --- a/anndata/tests/test_views.py +++ b/anndata/tests/test_views.py @@ -1,30 +1,31 @@ +from __future__ import annotations + from copy import deepcopy from operator import mul import joblib import numpy as np -from scipy import sparse import pandas as pd import pytest +from dask.base import normalize_token, tokenize +from scipy import sparse import anndata as ad from anndata._core.index import _normalize_index -from anndata._core.views import ArrayView, SparseCSRView, SparseCSCView +from anndata._core.views import ArrayView, SparseCSCView, SparseCSRView from anndata.compat import CupyCSCMatrix, DaskArray -from anndata.utils import asarray from anndata.tests.helpers import ( - gen_adata, - subset_func, - slice_subset, - single_subset, - assert_equal, - GEN_ADATA_DASK_ARGS, BASE_MATRIX_PARAMS, - DASK_MATRIX_PARAMS, CUPY_MATRIX_PARAMS, + DASK_MATRIX_PARAMS, + GEN_ADATA_DASK_ARGS, + assert_equal, + gen_adata, + single_subset, + slice_subset, + subset_func, ) -from dask.base import tokenize, normalize_token - +from anndata.utils import asarray # ------------------------------------------------------------------------------ # Some test data @@ -343,11 +344,11 @@ def test_set_scalar_subset_X(matrix_type, subset_func): if isinstance(adata.X, CupyCSCMatrix): # Comparison broken for CSC matrices # https://github.com/cupy/cupy/issues/7757 - assert asarray((orig_X_val.tocsr() != adata.X.tocsr())).sum() == mul( + assert asarray(orig_X_val.tocsr() != adata.X.tocsr()).sum() == mul( *adata_subset.shape ) else: - assert asarray((orig_X_val != adata.X)).sum() == mul(*adata_subset.shape) + assert asarray(orig_X_val != adata.X).sum() == mul(*adata_subset.shape) # TODO: Use different kind of subsetting for adata and view diff --git a/anndata/tests/test_x.py b/anndata/tests/test_x.py index 9ec8800e6..5f381c8c1 100644 --- a/anndata/tests/test_x.py +++ b/anndata/tests/test_x.py @@ -1,16 +1,16 @@ """Tests for the attribute .X""" +from __future__ import annotations + import numpy as np import pandas as pd +import pytest from scipy import sparse import anndata as ad from anndata import AnnData +from anndata.tests.helpers import assert_equal, gen_adata from anndata.utils import asarray -import pytest - -from anndata.tests.helpers import gen_adata, assert_equal - UNLABELLED_ARRAY_TYPES = [ pytest.param(sparse.csr_matrix, id="csr"), pytest.param(sparse.csc_matrix, id="csc"), diff --git a/anndata/utils.py b/anndata/utils.py index 375bdf208..b5fc5c16c 100644 --- a/anndata/utils.py +++ b/anndata/utils.py @@ -1,16 +1,20 @@ +from __future__ import annotations + import warnings -from functools import wraps, singledispatch -from typing import Mapping, Any, Sequence, Union +from functools import singledispatch, wraps +from typing import TYPE_CHECKING, Any import h5py -import pandas as pd import numpy as np +import pandas as pd from scipy import sparse -from .logging import get_logger - from ._core.sparse_dataset import BaseCompressedSparseDataset from .compat import CupyArray, CupySparseMatrix, DaskArray +from .logging import get_logger + +if TYPE_CHECKING: + from collections.abc import Mapping, Sequence logger = get_logger(__name__) @@ -259,7 +263,7 @@ def warn_names_duplicates(attr: str): def ensure_df_homogeneous( df: pd.DataFrame, name: str -) -> Union[np.ndarray, sparse.csr_matrix]: +) -> np.ndarray | sparse.csr_matrix: # TODO: rename this function, I would not expect this to return a non-dataframe if all(isinstance(dt, pd.SparseDtype) for dt in df.dtypes): arr = df.sparse.to_coo().tocsr() diff --git a/benchmarks/benchmarks/readwrite.py b/benchmarks/benchmarks/readwrite.py index 8d9b88e81..c273a436b 100644 --- a/benchmarks/benchmarks/readwrite.py +++ b/benchmarks/benchmarks/readwrite.py @@ -19,20 +19,20 @@ * io for backed objects * Reading dense as sparse, writing sparse as dense """ +from __future__ import annotations + +import sys import tempfile from pathlib import Path -import sys -from memory_profiler import memory_usage import numpy as np import pooch - -from .utils import sedate, get_peak_mem, get_actualsize +from memory_profiler import memory_usage # from . import datasets - import anndata +from .utils import get_actualsize, get_peak_mem, sedate PBMC_3K_URL = "http://falexwolf.de/data/pbmc3k_raw.h5ad" diff --git a/benchmarks/benchmarks/utils.py b/benchmarks/benchmarks/utils.py index 75fd4505e..05438811d 100644 --- a/benchmarks/benchmarks/utils.py +++ b/benchmarks/benchmarks/utils.py @@ -1,12 +1,14 @@ +from __future__ import annotations + +import gc +import sys from string import ascii_lowercase from time import sleep -from memory_profiler import memory_usage import numpy as np import pandas as pd +from memory_profiler import memory_usage from scipy import sparse -import sys -import gc from anndata import AnnData @@ -121,10 +123,10 @@ def gen_adata(n_obs, n_var, attr_set): if "obs,var" in attr_set: adata.obs = pd.DataFrame( {k: np.random.randint(0, 100, n_obs) for k in ascii_lowercase}, - index=["cell{}".format(i) for i in range(n_obs)], + index=[f"cell{i}" for i in range(n_obs)], ) adata.var = pd.DataFrame( {k: np.random.randint(0, 100, n_var) for k in ascii_lowercase}, - index=["gene{}".format(i) for i in range(n_var)], + index=[f"gene{i}" for i in range(n_var)], ) return adata diff --git a/conftest.py b/conftest.py index fe4dace31..1825ef24c 100644 --- a/conftest.py +++ b/conftest.py @@ -2,13 +2,16 @@ # 1. to allow ignoring warnings without test collection failing on CI # 2. as a pytest plugin/config that applies to doctests as well # TODO: Fix that, e.g. with the `pytest -p anndata.testing._pytest` pattern. +from __future__ import annotations -from pathlib import Path +from typing import TYPE_CHECKING import pytest from anndata.compat import chdir +if TYPE_CHECKING: + from pathlib import Path doctest_marker = pytest.mark.usefixtures("doctest_env") diff --git a/docs/conf.py b/docs/conf.py index 0e2c3f96a..a25b0f6cf 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,9 +1,13 @@ +from __future__ import annotations + import sys -from pathlib import Path from datetime import datetime from importlib import metadata +from pathlib import Path +from typing import TYPE_CHECKING -from sphinx.application import Sphinx +if TYPE_CHECKING: + from sphinx.application import Sphinx HERE = Path(__file__).parent sys.path[:0] = [str(HERE / "extensions")] diff --git a/docs/release-notes/0.10.0.md b/docs/release-notes/0.10.0.md index c60f548ab..871be24d7 100644 --- a/docs/release-notes/0.10.0.md +++ b/docs/release-notes/0.10.0.md @@ -31,6 +31,8 @@ ```{rubric} Other updates ``` +- Bump minimum python version to 3.9 {pr}`1117` {user}`flying-sheep` + ```{rubric} Deprecations ``` diff --git a/pyproject.toml b/pyproject.toml index 41682d2aa..e375d4700 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,18 +5,18 @@ requires = ["hatchling", "hatch-vcs"] [project] name = "anndata" description = "Annotated data." -requires-python = ">=3.8" +requires-python = ">=3.9" license = "BSD-3-Clause" authors = [ - {name = "Philipp Angerer"}, - {name = "Alex Wolf"}, - {name = "Isaac Virshup"}, - {name = "Sergei Rybakov"}, + { name = "Philipp Angerer" }, + { name = "Alex Wolf" }, + { name = "Isaac Virshup" }, + { name = "Sergei Rybakov" }, ] maintainers = [ - {name = "Isaac Virshup", email = "ivirshup@gmail.com"}, - {name = "Philipp Angerer", email = "philipp.angerer@helmholtz-munich.de"}, - {name = "Alex Wolf", email = "f.alex.wolf@gmx.de"}, + { name = "Isaac Virshup", email = "ivirshup@gmail.com" }, + { name = "Philipp Angerer", email = "philipp.angerer@helmholtz-munich.de" }, + { name = "Alex Wolf", email = "f.alex.wolf@gmx.de" }, ] readme = "README.md" classifiers = [ @@ -29,7 +29,6 @@ classifiers = [ "Operating System :: Microsoft :: Windows", "Operating System :: POSIX :: Linux", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", @@ -40,7 +39,7 @@ dependencies = [ # pandas <1.1.1 has pandas/issues/35446 # pandas 2.1.0rc0 has pandas/issues/54622 "pandas >=1.1.1, !=2.1.0rc0", - "numpy>=1.16.5", # required by pandas 1.x + "numpy>=1.16.5", # required by pandas 1.x "scipy>1.4", "h5py>=3", "exceptiongroup; python_version<'3.11'", @@ -74,7 +73,7 @@ doc = [ "scanpydoc>=0.9", "zarr", "awkward>=2.0.7", - "IPython", # For syntax highlighting in notebooks + "IPython", # For syntax highlighting in notebooks "myst_parser", ] test = [ @@ -92,15 +91,10 @@ test = [ "awkward>=2.3", "pytest_memray", ] -gpu = [ - "cupy", -] +gpu = ["cupy"] [tool.hatch.build] -exclude = [ - "anndata/tests/test_*.py", - "anndata/tests/data", -] +exclude = ["anndata/tests/test_*.py", "anndata/tests/data"] [tool.hatch.version] source = "vcs" [tool.hatch.build.hooks.vcs] @@ -108,20 +102,13 @@ version-file = "anndata/_version.py" [tool.coverage.run] source = ["anndata"] -omit = [ - "setup.py", - "versioneer.py", - "anndata/_version.py", - "**/test_*.py", -] +omit = ["setup.py", "versioneer.py", "anndata/_version.py", "**/test_*.py"] [tool.pytest.ini_options] addopts = "--doctest-modules" python_files = "test_*.py" testpaths = ["anndata", "docs/concatenation.rst"] -filterwarnings = [ - 'ignore:X\.dtype being converted to np.float32:FutureWarning' -] +filterwarnings = ['ignore:X\.dtype being converted to np.float32:FutureWarning'] # For some reason this effects how logging is shown when tests are run xfail_strict = true markers = ["gpu: mark test to run on GPU"] @@ -130,19 +117,27 @@ markers = ["gpu: mark test to run on GPU"] ignore = [ # line too long -> we accept long comment lines; black gets rid of long code lines "E501", - # Do not assign a lambda expression, use a def -> Scanpy allows lambda expression assignments, + # Do not assign a lambda expression, use a def -> AnnData allows lambda expression assignments, "E731", # allow I, O, l as variable names -> I is the identity matrix, i, j, k, l is reasonable indexing notation "E741", ] select = [ - "E", - "F", - "W", + "E", # Error detected by Pycodestyle + "F", # Errors detected by Pyflakes + "W", # Warning detected by Pycodestyle + "UP", # pyupgrade + "I", # isort + "TCH", # manage type checking blocks + "ICN", # Follow import conventions + "PTH", # Pathlib instead of os.path ] [tool.ruff.per-file-ignores] # E721 comparing types, but we specifically are checking that we aren't getting subtypes (views) "anndata/tests/test_readwrite.py" = ["E721"] +[tool.ruff.isort] +known-first-party = ["anndata"] +required-imports = ["from __future__ import annotations"] [tool.codespell] skip = ".git,*.pdf,*.svg"