Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion hatch.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down
11 changes: 5 additions & 6 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" },
Expand All @@ -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",
Expand Down Expand Up @@ -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]
Expand Down
6 changes: 3 additions & 3 deletions src/anndata/_core/aligned_mapping.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
4 changes: 2 additions & 2 deletions src/anndata/_core/extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
6 changes: 3 additions & 3 deletions src/anndata/_core/merge.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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.
Expand All @@ -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]
Expand Down
6 changes: 3 additions & 3 deletions src/anndata/_io/specs/lazy_methods.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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,
Expand Down
10 changes: 5 additions & 5 deletions src/anndata/_io/specs/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)

Expand Down
8 changes: 4 additions & 4 deletions src/anndata/_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand All @@ -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,
Expand Down Expand Up @@ -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}"
Expand Down
8 changes: 4 additions & 4 deletions src/anndata/_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion src/anndata/compat/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
8 changes: 4 additions & 4 deletions src/anndata/experimental/backed/_lazy_arrays.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions src/anndata/experimental/pytorch/_annloader.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
13 changes: 6 additions & 7 deletions src/anndata/typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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`."""
6 changes: 4 additions & 2 deletions tests/test_io_elementwise.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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}
Expand Down
Loading