diff --git a/hatch.toml b/hatch.toml index 34d03fca1..70e7e0c9a 100644 --- a/hatch.toml +++ b/hatch.toml @@ -30,7 +30,7 @@ overrides.matrix.deps.pre-install-commands = [ ] overrides.matrix.deps.python = [ - { if = [ "min" ], value = "3.11" }, + { if = [ "min" ], value = "3.12" }, { if = [ "stable", "pre" ], value = "3.13" }, ] overrides.matrix.deps.features = [ diff --git a/pyproject.toml b/pyproject.toml index 9b05b50eb..58a8396ff 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ requires = [ "hatchling", "hatch-vcs" ] [project] name = "anndata" description = "Annotated data." -requires-python = ">=3.11" +requires-python = ">=3.12" license = "BSD-3-Clause" authors = [ { name = "Philipp Angerer" }, @@ -29,18 +29,17 @@ classifiers = [ "Operating System :: Microsoft :: Windows", "Operating System :: POSIX :: Linux", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Topic :: Scientific/Engineering :: Bio-Informatics", "Topic :: Scientific/Engineering :: Visualization", ] dependencies = [ - "pandas >=2.1.0, !=2.1.2", + "pandas >=2.2.2", "numpy>=1.26", # https://github.com/scverse/anndata/issues/1434 - "scipy >=1.12", - "h5py>=3.8", + "scipy >=1.13", + "h5py>=3.10", "natsort", "packaging>=24.2", "array_api_compat>=1.7.1", @@ -108,7 +107,7 @@ lazy = [ "xarray>=2025.06.1", "aiohttp", "requests", "anndata[dask]" ] # https://github.com/dask/dask/issues/11290 # https://github.com/dask/dask/issues/11752 dask = [ - "dask[array]>=2023.5.1,!=2024.8.*,!=2024.9.*,!=2025.2.*,!=2025.3.*,!=2025.4.*,!=2025.5.*,!=2025.6.*,!=2025.7.*,!=2025.8.*", + "dask[array]>=2023.10.1,!=2024.8.*,!=2024.9.*,!=2025.2.*,!=2025.3.*,!=2025.4.*,!=2025.5.*,!=2025.6.*,!=2025.7.*,!=2025.8.*", ] [tool.hatch.version] diff --git a/src/anndata/_core/aligned_mapping.py b/src/anndata/_core/aligned_mapping.py index 0867c9e18..66479dfc4 100644 --- a/src/anndata/_core/aligned_mapping.py +++ b/src/anndata/_core/aligned_mapping.py @@ -5,7 +5,7 @@ from collections.abc import MutableMapping, Sequence from copy import copy from dataclasses import dataclass -from typing import TYPE_CHECKING, Generic, TypeVar +from typing import TYPE_CHECKING, TypeVar import numpy as np import pandas as pd @@ -135,7 +135,7 @@ def as_dict(self) -> dict: return dict(self) -class AlignedView(AlignedMappingBase, Generic[P, I]): +class AlignedView[P: "AlignedMappingBase", I: (OneDIdx, TwoDIdx)](AlignedMappingBase): is_view: ClassVar[Literal[True]] = True # override docstring @@ -394,7 +394,7 @@ class PairwiseArraysView(AlignedView[PairwiseArraysBase, OneDIdx], PairwiseArray @dataclass -class AlignedMappingProperty(property, Generic[T]): +class AlignedMappingProperty[T: AlignedMapping](property): """A :class:`property` that creates an ephemeral AlignedMapping. The actual data is stored as `f'_{self.name}'` in the parent object. diff --git a/src/anndata/_core/extensions.py b/src/anndata/_core/extensions.py index 019866673..afb33eaf7 100644 --- a/src/anndata/_core/extensions.py +++ b/src/anndata/_core/extensions.py @@ -2,7 +2,7 @@ import inspect from pathlib import Path -from typing import TYPE_CHECKING, Generic, TypeVar, get_type_hints, overload +from typing import TYPE_CHECKING, TypeVar, get_type_hints, overload from warnings import warn from ..types import ExtensionNamespace @@ -60,7 +60,7 @@ def find_stacklevel() -> int: T = TypeVar("T") -class AccessorNameSpace(ExtensionNamespace, Generic[NameSpT]): +class AccessorNameSpace[NameSpT: ExtensionNamespace](ExtensionNamespace): """Establish property-like namespace object for user-defined functionality.""" def __init__(self, name: str, namespace: type[NameSpT]) -> None: diff --git a/src/anndata/_core/merge.py b/src/anndata/_core/merge.py index e054c75db..c8df76885 100644 --- a/src/anndata/_core/merge.py +++ b/src/anndata/_core/merge.py @@ -428,7 +428,7 @@ def _dask_block_diag(mats): ################### -def unique_value(vals: Collection[T]) -> T | MissingVal: +def unique_value[T](vals: Collection[T]) -> T | MissingVal: """ Given a collection vals, returns the unique value (if one exists), otherwise returns MissingValue. @@ -440,7 +440,7 @@ def unique_value(vals: Collection[T]) -> T | MissingVal: return unique_val -def first(vals: Collection[T]) -> T | MissingVal: +def first[T](vals: Collection[T]) -> T | MissingVal: """ Given a collection of vals, return the first non-missing one.If they're all missing, return MissingVal. @@ -451,7 +451,7 @@ def first(vals: Collection[T]) -> T | MissingVal: return MissingVal -def only(vals: Collection[T]) -> T | MissingVal: +def only[T](vals: Collection[T]) -> T | MissingVal: """Return the only value in the collection, otherwise MissingVal.""" if len(vals) == 1: return vals[0] diff --git a/src/anndata/_io/specs/lazy_methods.py b/src/anndata/_io/specs/lazy_methods.py index a42687686..e101a26ef 100644 --- a/src/anndata/_io/specs/lazy_methods.py +++ b/src/anndata/_io/specs/lazy_methods.py @@ -53,9 +53,9 @@ def maybe_open_h5( ) -> Generator[H5File, None, None]: ... @overload @contextmanager -def maybe_open_h5(path_or_other: D, elem_name: str) -> Generator[D, None, None]: ... +def maybe_open_h5[D](path_or_other: D, elem_name: str) -> Generator[D, None, None]: ... @contextmanager -def maybe_open_h5( +def maybe_open_h5[D]( path_or_other: H5File | D, elem_name: str ) -> Generator[H5File | D, None, None]: if not isinstance(path_or_other, Path): @@ -81,7 +81,7 @@ def compute_chunk_layout_for_axis_size( return chunk -def make_dask_chunk( +def make_dask_chunk[D]( path_or_sparse_dataset: Path | D, elem_name: str, block_info: BlockInfo | None = None, diff --git a/src/anndata/_io/specs/registry.py b/src/anndata/_io/specs/registry.py index 789f1847b..a687deb9c 100644 --- a/src/anndata/_io/specs/registry.py +++ b/src/anndata/_io/specs/registry.py @@ -6,7 +6,7 @@ from dataclasses import dataclass from functools import partial, singledispatch, wraps from types import MappingProxyType -from typing import TYPE_CHECKING, Generic, TypeVar +from typing import TYPE_CHECKING, TypeVar from anndata._io.utils import report_read_key_on_error, report_write_key_on_error from anndata._settings import settings @@ -84,13 +84,13 @@ def wrapper(g: GroupStorageType, k: str, *args, **kwargs): return decorator -_R = TypeVar("_R", _ReadInternal, _ReadLazyInternal) +RI = TypeVar("RI", _ReadInternal, _ReadLazyInternal) R = TypeVar("R", Read, ReadLazy) -class IORegistry(Generic[_R, R]): +class IORegistry[RI: (_ReadInternal, _ReadLazyInternal), R: (Read, ReadLazy)]: def __init__(self): - self.read: dict[tuple[type, IOSpec, frozenset[str]], _R] = {} + self.read: dict[tuple[type, IOSpec, frozenset[str]], RI] = {} self.read_partial: dict[tuple[type, IOSpec, frozenset[str]], Callable] = {} self.write: dict[ tuple[type, type | tuple[type, str], frozenset[str]], _WriteInternal @@ -155,7 +155,7 @@ def register_read( src_type: type, spec: IOSpec | Mapping[str, str], modifiers: Iterable[str] = frozenset(), - ) -> Callable[[_R], _R]: + ) -> Callable[[RI], RI]: spec = proc_spec(spec) modifiers = frozenset(modifiers) diff --git a/src/anndata/_settings.py b/src/anndata/_settings.py index dec1729dd..2d2ab1485 100644 --- a/src/anndata/_settings.py +++ b/src/anndata/_settings.py @@ -11,7 +11,7 @@ from functools import partial from inspect import Parameter, signature from types import GenericAlias -from typing import TYPE_CHECKING, Generic, NamedTuple, TypeVar, cast +from typing import TYPE_CHECKING, NamedTuple, TypeVar, cast from .compat import is_zarr_v2, old_positionals @@ -51,7 +51,7 @@ def describe(self: RegisteredOption, *, as_rst: bool = False) -> str: return textwrap.dedent(doc) -class RegisteredOption(NamedTuple, Generic[T]): +class RegisteredOption[T](NamedTuple): option: str default_value: T description: str @@ -61,7 +61,7 @@ class RegisteredOption(NamedTuple, Generic[T]): describe = describe -def check_and_get_environ_var( +def check_and_get_environ_var[T]( key: str, default_value: str, allowed_values: Sequence[str] | None = None, @@ -394,7 +394,7 @@ def __doc__(self): V = TypeVar("V") -def gen_validator(_type: type[V]) -> Callable[[V], None]: +def gen_validator[V](_type: type[V]) -> Callable[[V], None]: def validate_type(val: V) -> None: if not isinstance(val, _type): msg = f"{val} not valid {_type}" diff --git a/src/anndata/_types.py b/src/anndata/_types.py index 1b9148327..75806e699 100644 --- a/src/anndata/_types.py +++ b/src/anndata/_types.py @@ -11,7 +11,7 @@ if TYPE_CHECKING: from collections.abc import Mapping - from typing import Any, TypeAlias + from typing import Any from anndata._core.xarray import Dataset2D @@ -33,9 +33,9 @@ "_WriteInternal", ] -ArrayStorageType: TypeAlias = ZarrArray | H5Array -GroupStorageType: TypeAlias = ZarrGroup | H5Group -StorageType: TypeAlias = ArrayStorageType | GroupStorageType +type ArrayStorageType = ZarrArray | H5Array +type GroupStorageType = ZarrGroup | H5Group +type StorageType = ArrayStorageType | GroupStorageType # NOTE: If you change these, be sure to update `autodoc_type_aliases` in docs/conf.py! RWAble_contra = TypeVar("RWAble_contra", bound=typing.RWAble, contravariant=True) diff --git a/src/anndata/compat/__init__.py b/src/anndata/compat/__init__.py index 6e298f826..a0741e6e0 100644 --- a/src/anndata/compat/__init__.py +++ b/src/anndata/compat/__init__.py @@ -330,7 +330,7 @@ def _to_fixed_length_strings(value: np.ndarray) -> np.ndarray: # TODO: This is a workaround for https://github.com/scverse/anndata/issues/874 # See https://github.com/h5py/h5py/pull/2311#issuecomment-1734102238 for why this is done this way. -def _require_group_write_dataframe( +def _require_group_write_dataframe[Group_T: ZarrGroup | h5py.Group]( f: Group_T, name: str, df: pd.DataFrame, *args, **kwargs ) -> Group_T: if len(df.columns) > 5_000 and isinstance(f, H5Group): diff --git a/src/anndata/experimental/backed/_lazy_arrays.py b/src/anndata/experimental/backed/_lazy_arrays.py index ca9bde00c..a7a304308 100644 --- a/src/anndata/experimental/backed/_lazy_arrays.py +++ b/src/anndata/experimental/backed/_lazy_arrays.py @@ -1,7 +1,7 @@ from __future__ import annotations from functools import cached_property -from typing import TYPE_CHECKING, Generic, TypeVar +from typing import TYPE_CHECKING, TypeVar import numpy as np import pandas as pd @@ -32,7 +32,7 @@ K = TypeVar("K", H5Array, ZarrArray) -class ZarrOrHDF5Wrapper(XZarrArrayWrapper, Generic[K]): +class ZarrOrHDF5Wrapper[K: (H5Array, ZarrArray)](XZarrArrayWrapper): def __init__(self, array: K): self.chunks = array.chunks if isinstance(array, ZarrArray): @@ -75,7 +75,7 @@ def _getitem(self, key: tuple[int | np.integer | slice | np.ndarray]): return self._array[key] -class CategoricalArray(XBackendArray, Generic[K]): +class CategoricalArray[K: (H5Array, ZarrArray)](XBackendArray): """ A wrapper class meant to enable working with lazy categorical data. We do not guarantee the stability of this API beyond that guaranteed @@ -130,7 +130,7 @@ def dtype(self): return pd.CategoricalDtype(categories=self.categories, ordered=self._ordered) -class MaskedArray(XBackendArray, Generic[K]): +class MaskedArray[K: (H5Array, ZarrArray)](XBackendArray): """ A wrapper class meant to enable working with lazy masked data. We do not guarantee the stability of this API beyond that guaranteed diff --git a/src/anndata/experimental/pytorch/_annloader.py b/src/anndata/experimental/pytorch/_annloader.py index d5e9fbf81..fef3c695c 100644 --- a/src/anndata/experimental/pytorch/_annloader.py +++ b/src/anndata/experimental/pytorch/_annloader.py @@ -22,12 +22,12 @@ if TYPE_CHECKING: from collections.abc import Callable, Generator, Sequence - from typing import TypeAlias, Union + from typing import Union from scipy.sparse import spmatrix # need to use Union because of autodoc_mock_imports - Array: TypeAlias = Union[torch.Tensor, np.ndarray, spmatrix] # noqa: UP007 + type Array = Union[torch.Tensor, np.ndarray, spmatrix] # noqa: UP007 # Custom sampler to get proper batches instead of joined separate indices diff --git a/src/anndata/typing.py b/src/anndata/typing.py index 25e279248..74b62c2bd 100644 --- a/src/anndata/typing.py +++ b/src/anndata/typing.py @@ -32,7 +32,8 @@ Index = _Index """1D or 2D index an :class:`~anndata.AnnData` object can be sliced with.""" -XDataType: TypeAlias = ( +# Both of the following two types are used with `get_args` hence the need for `TypeAlias` +XDataType: TypeAlias = ( # noqa: UP040 np.ndarray | ma.MaskedArray | CSMatrix @@ -46,20 +47,18 @@ | CupyArray | CupySparseMatrix ) -ArrayDataStructureTypes: TypeAlias = XDataType | AwkArray | XDataArray +ArrayDataStructureTypes: TypeAlias = XDataType | AwkArray | XDataArray # noqa: UP040 -InMemoryArrayOrScalarType: TypeAlias = ( +type InMemoryArrayOrScalarType = ( pd.DataFrame | np.number | str | ArrayDataStructureTypes ) -AxisStorable: TypeAlias = ( +type AxisStorable = ( InMemoryArrayOrScalarType | dict[str, "AxisStorable"] | list["AxisStorable"] ) """A serializable object, excluding :class:`anndata.AnnData` objects i.e., something that can be stored in `uns` or `obsm`.""" -RWAble: TypeAlias = ( - AxisStorable | AnnData | pd.Categorical | pd.api.extensions.ExtensionArray -) +type RWAble = AxisStorable | AnnData | pd.Categorical | pd.api.extensions.ExtensionArray """A superset of :type:`anndata.typing.AxisStorable` (i.e., including :class:`anndata.AnnData`) which is everything can be read/written by :func:`anndata.io.read_elem` and :func:`anndata.io.write_elem`.""" diff --git a/tests/test_io_elementwise.py b/tests/test_io_elementwise.py index 7758a636d..c60151603 100644 --- a/tests/test_io_elementwise.py +++ b/tests/test_io_elementwise.py @@ -76,7 +76,7 @@ def create_dense_store( return store -def create_sparse_store( +def create_sparse_store[G: (H5Group, ZarrGroup)]( sparse_format: Literal["csc", "csr"], store: G, shape=DEFAULT_SHAPE ) -> G: """Returns a store @@ -225,7 +225,9 @@ def test_io_spec(store, value, encoding_type): pytest.param(np.asarray("test"), "string", id="scalar_string"), ], ) -def test_io_spec_compressed_scalars(store: G, value: np.ndarray, encoding_type: str): +def test_io_spec_compressed_scalars( + store: H5Group | ZarrGroup, value: np.ndarray, encoding_type: str +): key = f"key_for_{encoding_type}" write_elem( store, key, value, dataset_kwargs={"compression": "gzip", "compression_opts": 5}