From cbee0a3e837bf19f8c67487355239e3e5fa6c8ad Mon Sep 17 00:00:00 2001 From: "Philipp A." Date: Tue, 25 Feb 2025 16:48:14 +0100 Subject: [PATCH 1/8] Adopt the Scientific Python deprecation schedule: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Change supported Python versions to 3.11–3.13 --- .azure-pipelines.yml | 14 ++++---- .github/workflows/benchmark.yml | 2 +- .readthedocs.yml | 2 +- benchmarks/asv.conf.json | 2 +- ci/scripts/min-deps.py | 15 +++----- docs/conf.py | 1 + hatch.toml | 4 +-- pyproject.toml | 4 +-- src/scanpy/_compat.py | 34 ------------------- src/scanpy/logging.py | 4 +-- src/scanpy/readwrite.py | 4 +-- .../scanpy/_pytest/fixtures/__init__.py | 3 +- 12 files changed, 25 insertions(+), 64 deletions(-) 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 4efc304cb6..9c57c4e746 100755 --- a/ci/scripts/min-deps.py +++ b/ci/scripts/min-deps.py @@ -1,26 +1,20 @@ #!/usr/bin/env python3 # /// script -# dependencies = [ -# "tomli; python_version < '3.11'", -# "packaging", -# ] +# requires-python = ">=3.11" +# dependencies = [ "packaging" ] # /// from __future__ import annotations 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 7306185eb2..60da6f797d 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -201,6 +201,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/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 c7cd71fd9d..57f246d8cf 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,9 +39,9 @@ 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", ] diff --git a/src/scanpy/_compat.py b/src/scanpy/_compat.py index 68bacb7bcc..7b7ed2465f 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 7bd678f568..d12333beae 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 c568519cd7..1910d9393e 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 @@ -1031,7 +1031,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 7578473786..7d9012aacc 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 os 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 From 091add4ee10122cad4381c60bdf21e226ba3ef87 Mon Sep 17 00:00:00 2001 From: "Philipp A." Date: Tue, 25 Feb 2025 16:50:07 +0100 Subject: [PATCH 2/8] relnote --- docs/release-notes/3485.breaking.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 docs/release-notes/3485.breaking.md 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` From 905058ffd62ee5a417ed2dc27f237d879f4fb0ce Mon Sep 17 00:00:00 2001 From: "Philipp A." Date: Thu, 27 Feb 2025 14:11:35 +0100 Subject: [PATCH 3/8] fix chdir --- src/testing/scanpy/_pytest/fixtures/__init__.py | 2 +- tests/test_package_structure.py | 11 ----------- 2 files changed, 1 insertion(+), 12 deletions(-) diff --git a/src/testing/scanpy/_pytest/fixtures/__init__.py b/src/testing/scanpy/_pytest/fixtures/__init__.py index 7d9012aacc..27c4da4e0a 100644 --- a/src/testing/scanpy/_pytest/fixtures/__init__.py +++ b/src/testing/scanpy/_pytest/fixtures/__init__.py @@ -6,7 +6,7 @@ from __future__ import annotations import warnings -from os import chdir +from contextlib import chdir from typing import TYPE_CHECKING import numpy as np diff --git a/tests/test_package_structure.py b/tests/test_package_structure.py index 3541c561a5..f6a7fd5773 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")) From f837c73398228341ca9b28a3bc5a0e6b284462f1 Mon Sep 17 00:00:00 2001 From: "Philipp A." Date: Thu, 27 Feb 2025 14:31:10 +0100 Subject: [PATCH 4/8] remove test_function_headers test --- src/scanpy/_utils/__init__.py | 1 - tests/test_package_structure.py | 30 ------------------------------ 2 files changed, 31 deletions(-) diff --git a/src/scanpy/_utils/__init__.py b/src/scanpy/_utils/__init__.py index 4965163f1e..1374133cd7 100644 --- a/src/scanpy/_utils/__init__.py +++ b/src/scanpy/_utils/__init__.py @@ -249,7 +249,6 @@ def _doc_params(**kwds): """ def dec(obj): - obj.__orig_doc__ = obj.__doc__ obj.__doc__ = dedent(obj.__doc__).format_map(kwds) return obj diff --git a/tests/test_package_structure.py b/tests/test_package_structure.py index f6a7fd5773..3402c3872e 100644 --- a/tests/test_package_structure.py +++ b/tests/test_package_structure.py @@ -66,36 +66,6 @@ def test_import_future_anndata_import_warning(): importlib.reload(scanpy) -@pytest.mark.parametrize(("f", "qualname"), api_functions) -def test_function_headers(f, qualname): - filename = getsourcefile(f) - lines, lineno = getsourcelines(f) - if f.__doc__ is None: - msg = f"Function `{qualname}` has no docstring" - text = lines[0] - else: - lines = getattr(f, "__orig_doc__", f.__doc__).split("\n") - broken = [ - i for i, l in enumerate(lines) if l.strip() and not l.startswith(" ") - ] - if not any(broken): - return - msg = f'''\ -Header of function `{qualname}`’s docstring should start with one-line description -and be consistently indented like this: - -␣␣␣␣"""\\ -␣␣␣␣My one-line␣description. - -␣␣␣␣… -␣␣␣␣""" - -The displayed line is under-indented. -''' - text = f">{lines[broken[0]]}<" - raise SyntaxError(msg, (filename, lineno, 2, text)) - - def param_is_pos(p: Parameter) -> bool: return p.kind in { Parameter.POSITIONAL_ONLY, From 6a4a5370bf732dd510a2e8777c53f64267655210 Mon Sep 17 00:00:00 2001 From: "Philipp A." Date: Thu, 27 Feb 2025 20:13:36 +0100 Subject: [PATCH 5/8] fix deps --- pyproject.toml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index c5f625b120..f68584bfa1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,12 +47,12 @@ classifiers = [ ] dependencies = [ "anndata>=0.8", - "numpy>=1.24", + "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", "statsmodels>=0.13", @@ -60,7 +60,7 @@ dependencies = [ "networkx>=2.7", "natsort", "joblib", - "numba>=0.57", + "numba>=0.58", "umap-learn>=0.5,!=0.5.0", "pynndescent>=0.5", "packaging>=21.3", @@ -149,7 +149,7 @@ scanorama = [ "scanorama" ] # Scanorama dataset integration scrublet = [ "scikit-image" ] # 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] From 2d7b7e4c24051afd6be1878a56266439b9eaa39f Mon Sep 17 00:00:00 2001 From: "Philipp A." Date: Thu, 27 Feb 2025 20:28:40 +0100 Subject: [PATCH 6/8] allow higher skimage --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index f68584bfa1..986e8843df 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -54,14 +54,14 @@ dependencies = [ "seaborn>=0.13", "h5py>=3.8", "tqdm", - "scikit-learn>=1.1,<1.6.0", + "scikit-learn>=1.2,<1.6", "statsmodels>=0.13", "patsy!=1.0.0", # https://github.com/pydata/patsy/issues/215 "networkx>=2.7", "natsort", "joblib", "numba>=0.58", - "umap-learn>=0.5,!=0.5.0", + "umap-learn>=0.5,!=0.5", "pynndescent>=0.5", "packaging>=21.3", "session-info2", From 660692989d856a8bbdf9b711bb93d35138e35ebe Mon Sep 17 00:00:00 2001 From: "Philipp A." Date: Thu, 27 Feb 2025 20:36:50 +0100 Subject: [PATCH 7/8] bump nx instead --- pyproject.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 986e8843df..6620ee3f2a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -54,10 +54,10 @@ dependencies = [ "seaborn>=0.13", "h5py>=3.8", "tqdm", - "scikit-learn>=1.2,<1.6", + "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.58", @@ -146,7 +146,7 @@ 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]>=2023.5.1,<2024.8.0" ] # Use the Dask parallelization engine From 63990bea48304e1101f4aaf591d0dbe4652eb255 Mon Sep 17 00:00:00 2001 From: "Philipp A." Date: Thu, 27 Feb 2025 21:01:13 +0100 Subject: [PATCH 8/8] bump anndata --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 6620ee3f2a..a1eec00020 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -46,7 +46,7 @@ classifiers = [ "Topic :: Scientific/Engineering :: Visualization", ] dependencies = [ - "anndata>=0.8", + "anndata>=0.9", "numpy>=1.25", "matplotlib>=3.6", "pandas >=2.0",