diff --git a/.azure-pipelines.yml b/.azure-pipelines.yml index f0e181f1ba..6485b3c2b2 100644 --- a/.azure-pipelines.yml +++ b/.azure-pipelines.yml @@ -3,7 +3,7 @@ trigger: - "*.*.x" variables: - python.version: '3.12' + python.version: '3.13' PYTEST_ADDOPTS: '-v --color=yes --internet-tests --nunit-xml=test-data/test-results.xml' TEST_EXTRA: 'test-full' DEPENDENCIES_VERSION: "latest" # |"pre-release" | "minimum-version" @@ -15,16 +15,16 @@ jobs: vmImage: 'ubuntu-22.04' strategy: matrix: - Python3.10: - python.version: '3.10' - Python3.12: {} + Python3.11: + python.version: '3.11' + Python3.13: {} minimal_dependencies: TEST_EXTRA: 'test-min' anndata_dev: DEPENDENCIES_VERSION: "pre-release" TEST_TYPE: "coverage" minimum_versions: - python.version: '3.10' + python.version: '3.11' DEPENDENCIES_VERSION: "minimum-version" TEST_TYPE: "coverage" @@ -120,8 +120,8 @@ jobs: - task: UsePythonVersion@0 inputs: - versionSpec: '3.12' - displayName: 'Use Python 3.12' + versionSpec: '3.13' + displayName: 'Use Python 3.13' - script: | python -m pip install --upgrade pip diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index 68e274ad54..348d92c782 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -16,7 +16,7 @@ jobs: strategy: fail-fast: false matrix: - python: ["3.12"] + python: ["3.13"] os: [ubuntu-latest] env: diff --git a/.readthedocs.yml b/.readthedocs.yml index adcdfb80d7..0ede485a47 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -4,7 +4,7 @@ submodules: build: os: ubuntu-24.04 tools: - python: '3.12' + python: '3.13' jobs: post_checkout: # unshallow so version can be derived from tag diff --git a/benchmarks/asv.conf.json b/benchmarks/asv.conf.json index 404e0b83dd..b4b1295379 100644 --- a/benchmarks/asv.conf.json +++ b/benchmarks/asv.conf.json @@ -54,7 +54,7 @@ // The Pythons you'd like to test against. If not provided, defaults // to the current version of Python used to run `asv`. - // "pythons": ["3.10", "3.12"], + // "pythons": ["3.11", "3.13"], // The list of conda channel names to be searched for benchmark // dependency packages in the specified order diff --git a/ci/scripts/min-deps.py b/ci/scripts/min-deps.py index 1ab93c49cc..595b480b83 100755 --- a/ci/scripts/min-deps.py +++ b/ci/scripts/min-deps.py @@ -1,9 +1,7 @@ #!/usr/bin/env python3 # /// script -# dependencies = [ -# "tomli; python_version < '3.11'", -# "packaging", -# ] +# requires-python = ">=3.11" +# dependencies = [ "packaging" ] # /// """Parse a pyproject.toml file and output a list of minimum dependencies.""" @@ -11,17 +9,13 @@ import argparse import sys +import tomllib from collections import deque from contextlib import ExitStack from functools import cached_property from pathlib import Path from typing import TYPE_CHECKING -if sys.version_info >= (3, 11): - import tomllib -else: - import tomli as tomllib - from packaging.requirements import Requirement from packaging.version import Version @@ -48,7 +42,8 @@ def min_dep(req: Requirement) -> Requirement: spec for spec in req.specifier if spec.operator in {"==", "~=", ">=", ">"} ] if not filter_specs: - return Requirement(req_name) + # TODO: handle markers + return Requirement(f"{req_name}{req.specifier}") min_version = Version("0.0.0.a1") for spec in filter_specs: diff --git a/docs/conf.py b/docs/conf.py index f10717ad99..7a9f1acebd 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -203,6 +203,7 @@ def setup(app: Sphinx): # -- Suppress link warnings ---------------------------------------------------- qualname_overrides = { + "pathlib._local.Path": "pathlib.Path", "sklearn.neighbors._dist_metrics.DistanceMetric": "sklearn.metrics.DistanceMetric", "scanpy.plotting._matrixplot.MatrixPlot": "scanpy.pl.MatrixPlot", "scanpy.plotting._dotplot.DotPlot": "scanpy.pl.DotPlot", diff --git a/docs/release-notes/3485.breaking.md b/docs/release-notes/3485.breaking.md new file mode 100644 index 0000000000..11737a356a --- /dev/null +++ b/docs/release-notes/3485.breaking.md @@ -0,0 +1 @@ +Adopt the Scientific Python [deprecation schedule](https://scientific-python.org/specs/spec-0000/): remove Python 3.10 support and add Python 3.13 support {smaller}`P Angerer` diff --git a/hatch.toml b/hatch.toml index d61f91f577..07bc637503 100644 --- a/hatch.toml +++ b/hatch.toml @@ -25,8 +25,8 @@ overrides.matrix.deps.pre-install-commands = [ { if = [ "min" ], value = "uv run ci/scripts/min-deps.py pyproject.toml --all-extras -o ci/scanpy-min-deps.txt" }, ] overrides.matrix.deps.python = [ - { if = [ "min" ], value = "3.10" }, - { if = [ "stable", "full", "pre" ], value = "3.12" }, + { if = [ "min" ], value = "3.11" }, + { if = [ "stable", "full", "pre" ], value = "3.13" }, ] overrides.matrix.deps.features = [ { if = [ "full" ], value = "test-full" }, diff --git a/pyproject.toml b/pyproject.toml index 08b0dc5e34..a1eec00020 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ requires = [ "hatchling", "hatch-vcs" ] [project] name = "scanpy" description = "Single-Cell Analysis in Python." -requires-python = ">=3.10" +requires-python = ">=3.11" license = "BSD-3-clause" authors = [ { name = "Alex Wolf" }, @@ -39,29 +39,29 @@ classifiers = [ "Operating System :: Microsoft :: Windows", "Operating System :: POSIX :: Linux", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "Topic :: Scientific/Engineering :: Bio-Informatics", "Topic :: Scientific/Engineering :: Visualization", ] dependencies = [ - "anndata>=0.8", - "numpy>=1.24", + "anndata>=0.9", + "numpy>=1.25", "matplotlib>=3.6", - "pandas >=1.5", - "scipy>=1.8", + "pandas >=2.0", + "scipy>=1.11", "seaborn>=0.13", - "h5py>=3.7", + "h5py>=3.8", "tqdm", - "scikit-learn>=1.1,<1.6.0", + "scikit-learn>=1.1,<1.6", "statsmodels>=0.13", "patsy!=1.0.0", # https://github.com/pydata/patsy/issues/215 - "networkx>=2.7", + "networkx>=2.8", "natsort", "joblib", - "numba>=0.57", - "umap-learn>=0.5,!=0.5.0", + "numba>=0.58", + "umap-learn>=0.5,!=0.5", "pynndescent>=0.5", "packaging>=21.3", "session-info2", @@ -146,10 +146,10 @@ magic = [ "magic-impute>=2.0" ] # MAGIC imputation method skmisc = [ "scikit-misc>=0.1.3" ] # highly_variable_genes method 'seurat_v3' harmony = [ "harmonypy" ] # Harmony dataset integration scanorama = [ "scanorama" ] # Scanorama dataset integration -scrublet = [ "scikit-image" ] # Doublet detection with automatic thresholds +scrublet = [ "scikit-image>=0.20" ] # Doublet detection with automatic thresholds # Acceleration rapids = [ "cudf>=0.9", "cuml>=0.9", "cugraph>=0.9" ] # GPU accelerated calculation of neighbors -dask = [ "dask[array]>=2022.09.2,<2024.8.0" ] # Use the Dask parallelization engine +dask = [ "dask[array]>=2023.5.1,<2024.8.0" ] # Use the Dask parallelization engine dask-ml = [ "dask-ml", "scanpy[dask]" ] # Dask-ML for sklearn-like API [tool.hatch.build.targets.wheel] diff --git a/src/scanpy/_compat.py b/src/scanpy/_compat.py index 4f3aa9dd3c..0cdda60d80 100644 --- a/src/scanpy/_compat.py +++ b/src/scanpy/_compat.py @@ -1,12 +1,9 @@ from __future__ import annotations -import os import sys import warnings -from dataclasses import dataclass, field from functools import WRAPPER_ASSIGNMENTS, cache, partial, wraps from importlib.util import find_spec -from pathlib import Path from typing import TYPE_CHECKING, Literal, ParamSpec, TypeVar, cast, overload import numpy as np @@ -63,25 +60,6 @@ def fullname(typ: type) -> str: return f"{module}.{name}" -if sys.version_info >= (3, 11): - from contextlib import chdir -else: - import os - from contextlib import AbstractContextManager - - @dataclass - class chdir(AbstractContextManager): - path: Path - _old_cwd: list[Path] = field(default_factory=list) - - def __enter__(self) -> None: - self._old_cwd.append(Path.cwd()) - os.chdir(self.path) - - def __exit__(self, *_excinfo) -> None: - os.chdir(self._old_cwd.pop()) - - def pkg_metadata(package: str) -> PackageMetadata: from importlib.metadata import metadata @@ -106,18 +84,6 @@ def old_positionals(*old_positionals: str): return lambda func: func -if sys.version_info >= (3, 11): - - def add_note(exc: BaseException, note: str) -> None: - exc.add_note(note) -else: - - def add_note(exc: BaseException, note: str) -> None: - if not hasattr(exc, "__notes__"): - exc.__notes__ = [] - exc.__notes__.append(note) - - if sys.version_info >= (3, 13): from warnings import deprecated as _deprecated else: diff --git a/src/scanpy/logging.py b/src/scanpy/logging.py index 4d9310820f..ea063af75d 100644 --- a/src/scanpy/logging.py +++ b/src/scanpy/logging.py @@ -4,7 +4,7 @@ import logging import sys -from datetime import datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta from functools import partial, update_wrapper from logging import CRITICAL, DEBUG, ERROR, INFO, WARNING from typing import TYPE_CHECKING, overload @@ -45,7 +45,7 @@ def log( ) -> datetime: from ._settings import settings - now = datetime.now(timezone.utc) + now = datetime.now(UTC) time_passed: timedelta = None if time is None else now - time extra = { **(extra or {}), diff --git a/src/scanpy/readwrite.py b/src/scanpy/readwrite.py index 1d18c8fa55..aaceb490fb 100644 --- a/src/scanpy/readwrite.py +++ b/src/scanpy/readwrite.py @@ -36,7 +36,7 @@ from matplotlib.image import imread from . import logging as logg -from ._compat import add_note, deprecated, old_positionals +from ._compat import deprecated, old_positionals from ._settings import settings from ._utils import _empty @@ -1025,7 +1025,7 @@ def _download(url: str, path: Path): try: from certifi import where except ImportError as e: - add_note(e, f"{msg} Please install `certifi` and try again.") + e.add_note(f"{msg} Please install `certifi` and try again.") raise else: logg.warning(f"{msg} Trying to use certifi.") diff --git a/src/testing/scanpy/_pytest/fixtures/__init__.py b/src/testing/scanpy/_pytest/fixtures/__init__.py index 2b24935aeb..a80d338767 100644 --- a/src/testing/scanpy/_pytest/fixtures/__init__.py +++ b/src/testing/scanpy/_pytest/fixtures/__init__.py @@ -6,6 +6,7 @@ from __future__ import annotations import warnings +from contextlib import chdir from typing import TYPE_CHECKING import numpy as np @@ -39,8 +40,6 @@ def float_dtype(request): @pytest.fixture def _doctest_env(cache: pytest.Cache, tmp_path: Path) -> Generator[None, None, None]: - from scanpy._compat import chdir - showwarning_orig = warnings.showwarning def showwarning(message, category, filename, lineno, file=None, line=None): # noqa: PLR0917 diff --git a/tests/test_package_structure.py b/tests/test_package_structure.py index 3c12c0cdc0..73e731dc8d 100644 --- a/tests/test_package_structure.py +++ b/tests/test_package_structure.py @@ -1,7 +1,6 @@ from __future__ import annotations import importlib -import os from collections import defaultdict from inspect import Parameter, signature from pathlib import Path @@ -54,16 +53,6 @@ ] -@pytest.fixture -def in_project_dir(): - wd_orig = Path.cwd() - os.chdir(proj_dir) - try: - yield proj_dir - finally: - os.chdir(wd_orig) - - @pytest.mark.xfail(reason="TODO: unclear if we want this to totally match, let’s see") def test_descend_classes_and_funcs(): funcs = set(descend_classes_and_funcs(scanpy, "scanpy"))