Skip to content

Commit

Permalink
Merge branch 'main' into pa/spec0
Browse files Browse the repository at this point in the history
  • Loading branch information
flying-sheep authored Feb 27, 2025
2 parents f837c73 + 6e68d06 commit d7b38ac
Show file tree
Hide file tree
Showing 148 changed files with 1,186 additions and 1,327 deletions.
1 change: 1 addition & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
BSD 3-Clause License

Copyright (c) 2025 scverse
Copyright (c) 2017 F. Alexander Wolf, P. Angerer, Theis Lab
All rights reserved.

Expand Down
1 change: 1 addition & 0 deletions benchmarks/benchmarks/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""ASV benchmark suite for scanpy."""
6 changes: 3 additions & 3 deletions benchmarks/benchmarks/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ def __call__(self, **skipped: AbstractSet) -> Callable[[C], C]: ...

@cache
def _pbmc68k_reduced() -> AnnData:
"""A small datasets with a dense `.X`"""
"""A small datasets with a dense `.X`.""" # noqa: D401
adata = sc.datasets.pbmc68k_reduced()
assert isinstance(adata.X, np.ndarray)
assert not np.isfortran(adata.X)
Expand Down Expand Up @@ -179,11 +179,10 @@ def get_count_dataset(
def param_skipper(
param_names: Sequence[str], params: tuple[Sequence[object], ...]
) -> ParamSkipper:
"""Creates a decorator that will skip all combinations that contain any of the given parameters.
"""Create a decorator that will skip all combinations that contain any of the given parameters.
Examples
--------
>>> param_names = ["letters", "numbers"]
>>> params = [["a", "b"], [3, 4, 5]]
>>> skip_when = param_skipper(param_names, params)
Expand All @@ -194,6 +193,7 @@ def param_skipper(
>>> run_as_asv_benchmark(func)
b 4
b 5
"""

def skip(**skipped: AbstractSet) -> Callable[[C], C]:
Expand Down
8 changes: 4 additions & 4 deletions benchmarks/benchmarks/preprocessing_counts.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""
This module will benchmark preprocessing operations in Scanpy that run on counts
API documentation: https://scanpy.readthedocs.io/en/stable/api/preprocessing.html
"""Benchmark preprocessing operations in Scanpy that run on counts.
API documentation: <https://scanpy.readthedocs.io/en/stable/api/preprocessing.html>.
"""

from __future__ import annotations
Expand All @@ -23,7 +23,7 @@


def setup(dataset: Dataset, layer: KeyCount, *_):
"""Setup global variables before each benchmark."""
"""Set up global variables before each benchmark."""
global adata, batch_key
adata, batch_key = get_count_dataset(dataset, layer=layer)
assert "log1p" not in adata.uns
Expand Down
8 changes: 4 additions & 4 deletions benchmarks/benchmarks/preprocessing_log.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""
This module will benchmark preprocessing operations in Scanpy that run on log-transformed data
API documentation: https://scanpy.readthedocs.io/en/stable/api/preprocessing.html
"""Benchmark preprocessing operations in Scanpy that run on log-transformed data.
API documentation: <https://scanpy.readthedocs.io/en/stable/api/preprocessing.html>.
"""

from __future__ import annotations
Expand All @@ -25,7 +25,7 @@


def setup(dataset: Dataset, layer: KeyX, *_):
"""Setup global variables before each benchmark."""
"""Set up global variables before each benchmark."""
global adata, batch_key
adata, batch_key = get_dataset(dataset, layer=layer)

Expand Down
6 changes: 3 additions & 3 deletions benchmarks/benchmarks/tools.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""
This module will benchmark tool operations in Scanpy
API documentation: https://scanpy.readthedocs.io/en/stable/api/tools.html
"""Benchmark tool operations in Scanpy.
API documentation: <https://scanpy.readthedocs.io/en/stable/api/tools.html>.
"""

from __future__ import annotations
Expand Down
16 changes: 11 additions & 5 deletions ci/scripts/min-deps.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
# requires-python = ">=3.11"
# dependencies = [ "packaging" ]
# ///
"""Parse a pyproject.toml file and output a list of minimum dependencies."""

from __future__ import annotations

Expand All @@ -25,14 +26,13 @@


def min_dep(req: Requirement) -> Requirement:
"""
Given a requirement, return the minimum version specifier.
"""Given a requirement, return the minimum version specifier.
Example
-------
>>> min_dep(Requirement("numpy>=1.0"))
<Requirement('numpy==1.0.*')>
"""
req_name = req.name
if req.extras:
Expand All @@ -58,6 +58,7 @@ def min_dep(req: Requirement) -> Requirement:
def extract_min_deps(
dependencies: Iterable[Requirement], *, pyproject
) -> Generator[Requirement, None, None]:
"""Extract minimum dependencies from a list of requirements."""
dependencies = deque(dependencies) # We'll be mutating this
project_name = pyproject["project"]["name"]

Expand All @@ -77,8 +78,8 @@ def extract_min_deps(


class Args(argparse.Namespace):
"""\
Parse a pyproject.toml file and output a list of minimum dependencies.
"""Parse a pyproject.toml file and output a list of minimum dependencies.
Output is optimized for `[uv] pip install` (see `-o`/`--output` for details).
"""

Expand All @@ -89,10 +90,12 @@ class Args(argparse.Namespace):

@classmethod
def parse(cls, argv: Sequence[str] | None = None) -> Self:
"""Parse CLI arguments."""
return cls.parser().parse_args(argv, cls())

@classmethod
def parser(cls) -> argparse.ArgumentParser:
"""Construct a CLI argument parser."""
parser = argparse.ArgumentParser(
prog="min-deps",
description=cls.__doc__,
Expand Down Expand Up @@ -134,10 +137,12 @@ def parser(cls) -> argparse.ArgumentParser:

@cached_property
def pyproject(self) -> dict[str, Any]:
"""Return the parsed `pyproject.toml`."""
return tomllib.loads(self._path.read_text())

@cached_property
def extras(self) -> AbstractSet[str]:
"""Return the extras to install."""
if self._extras:
if self._all_extras:
sys.exit("Cannot specify both --extras and --all-extras")
Expand All @@ -148,6 +153,7 @@ def extras(self) -> AbstractSet[str]:


def main(argv: Sequence[str] | None = None) -> None:
"""Run main entry point."""
args = Args.parse(argv)

project_name = args.pyproject["project"]["name"]
Expand Down
5 changes: 5 additions & 0 deletions ci/scripts/towncrier_automation.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
# /// script
# dependencies = [ "towncrier", "packaging" ]
# ///
"""Script to automate towncrier release note PRs."""

from __future__ import annotations

Expand All @@ -16,11 +17,14 @@


class Args(argparse.Namespace):
"""Command line arguments."""

version: str
dry_run: bool


def parse_args(argv: Sequence[str] | None = None) -> Args:
"""Construct a CLI argument parser."""
parser = argparse.ArgumentParser(
prog="towncrier-automation",
description=(
Expand Down Expand Up @@ -52,6 +56,7 @@ def parse_args(argv: Sequence[str] | None = None) -> Args:


def main(argv: Sequence[str] | None = None) -> None:
"""Run main entry point."""
args = parse_args(argv)

# Run towncrier
Expand Down
4 changes: 3 additions & 1 deletion docs/conf.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
"""Configuration for Scanpy’s Sphinx documentation."""

from __future__ import annotations

import sys
Expand Down Expand Up @@ -33,7 +35,7 @@
project = "Scanpy"
author = "Scanpy development team"
repository_url = "https://github.com/scverse/scanpy"
copyright = f"{datetime.now():%Y}, the Scanpy development team"
copyright = f"{datetime.now():%Y}, scverse"
version = scanpy.__version__.replace(".dirty", "")

# Bumping the version updates all docs, so don't do that
Expand Down
5 changes: 4 additions & 1 deletion docs/extensions/canonical_tutorial.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
"""Extension for a stub ``canonical-tutorial`` directive."""

from __future__ import annotations

from typing import TYPE_CHECKING
Expand All @@ -16,9 +18,10 @@ class CanonicalTutorial(SphinxDirective):

required_arguments: ClassVar = 1

def run(self) -> list[nodes.Node]:
def run(self) -> list[nodes.Node]: # noqa: D102
return []


def setup(app: Sphinx) -> None:
"""App setup hook."""
app.add_directive("canonical-tutorial", CanonicalTutorial)
4 changes: 4 additions & 0 deletions docs/extensions/debug_docstrings.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
"""Extension for debugging docstrings."""

# Just do the following to see the rst of a function:
# rm ./_build/doctrees/api/generated/scanpy.<what you want>.doctree; DEBUG=1 make html
from __future__ import annotations
Expand All @@ -14,10 +16,12 @@


def pd_new(app, what, name, obj, options, lines): # noqa: PLR0917
"""Wrap ``sphinx.ext.napoleon._process_docstring``."""
_pd_orig(app, what, name, obj, options, lines)
print(*lines, sep="\n")


def setup(app: Sphinx):
"""App setup hook."""
if os.environ.get("DEBUG") is not None:
sphinx.ext.napoleon._process_docstring = pd_new
4 changes: 3 additions & 1 deletion docs/extensions/function_images.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""Images for plot functions"""
"""Images for plot functions."""

from __future__ import annotations

Expand All @@ -15,6 +15,7 @@
def insert_function_images( # noqa: PLR0917
app: Sphinx, what: str, name: str, obj: Any, options: Options, lines: list[str]
):
"""Insert images for plot functions."""
path = app.config.api_dir / f"{name}.png"
if what != "function" or not path.is_file():
return
Expand All @@ -27,5 +28,6 @@ def insert_function_images( # noqa: PLR0917


def setup(app: Sphinx):
"""App setup hook."""
app.add_config_value("api_dir", Path(), "env")
app.connect("autodoc-process-docstring", insert_function_images)
8 changes: 7 additions & 1 deletion docs/extensions/git_ref.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,17 @@


def git(*args: str) -> str:
"""Run a git command and return the output as a string."""
return subprocess.check_output(["git", *args]).strip().decode()


# https://github.com/DisnakeDev/disnake/blob/7853da70b13fcd2978c39c0b7efa59b34d298186/docs/conf.py#L192
@lru_cache
def get() -> str | None:
"""Current git reference. Uses branch/tag name if found, otherwise uses commit hash"""
"""Get current git reference.
Uses branch/tag name if found, otherwise uses commit hash.
"""
git_ref = None
try:
git_ref = git("name-rev", "--name-only", "--no-undefined", "HEAD")
Expand All @@ -37,8 +41,10 @@ def get() -> str | None:


def set_ref(app: Sphinx, config: Config):
"""`config-inited` hook to set `html_theme_options["repository_branch"]`."""
app.config["html_theme_options"]["repository_branch"] = get() or "main"


def setup(app: Sphinx) -> None:
"""App setup hook."""
app.connect("config-inited", set_ref)
4 changes: 4 additions & 0 deletions docs/extensions/has_attr_test.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
"""Extension adding a jinja2 filter that tests if an object has an attribute."""

from __future__ import annotations

from inspect import get_annotations
Expand All @@ -11,10 +13,12 @@


def has_member(obj_path: str, attr: str) -> bool:
"""Test if an object has an attribute."""
# https://jinja.palletsprojects.com/en/3.0.x/api/#custom-tests
obj = import_string(obj_path)
return hasattr(obj, attr) or attr in get_annotations(obj)


def setup(app: Sphinx):
"""App setup hook."""
DEFAULT_NAMESPACE["has_member"] = has_member
5 changes: 5 additions & 0 deletions docs/extensions/param_police.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
"""Extension to warn about numpydoc-style parameter types in docstrings."""

from __future__ import annotations

import warnings
Expand All @@ -13,6 +15,7 @@


def scanpy_log_param_types(self, fields, field_role="param", type_role="type"):
"""Wrap ``NumpyDocstring._format_docutils_params``."""
for _name, _type, _desc in fields:
if not _type or not self._obj.__module__.startswith("scanpy"):
continue
Expand All @@ -23,6 +26,7 @@ def scanpy_log_param_types(self, fields, field_role="param", type_role="type"):


def show_param_warnings(app, exception):
"""Warn about numpydoc-style parameter types in docstring."""
import inspect

for (fname, fun), params in param_warnings.items():
Expand All @@ -42,5 +46,6 @@ def show_param_warnings(app, exception):


def setup(app: Sphinx):
"""App setup hook."""
NumpyDocstring._format_docutils_params = scanpy_log_param_types
app.connect("build-finished", show_param_warnings)
2 changes: 2 additions & 0 deletions docs/extensions/patch_myst_nb.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,11 @@ def get_cell_level_config(
cell_metadata: dict[str, object],
line: int | None = None,
):
"""Correct version of ``MditRenderMixin.get_cell_level_config``."""
rv = get_orig(self, field, cell_metadata, line)
return copy(rv)


def setup(app: Sphinx):
"""App setup hook."""
MditRenderMixin.get_cell_level_config = get_cell_level_config
13 changes: 13 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,7 @@ select = [
"W", # Warning detected by Pycodestyle
"UP", # pyupgrade
"I", # isort
"D", # pydocstyle
"TC", # manage type checking blocks
"TID251", # Banned imports
"ICN", # Follow import conventions
Expand All @@ -249,13 +250,25 @@ ignore = [
"E741",
# `Literal["..."] | str` is useful for autocompletion
"PYI051",
# We ban blank lines before docstrings instead of the opposite
"D203",
# We want multiline summaries to start on the first line, not the second
"D213",
# TODO: replace our current param docs reuse with this and remove it here:
"D417",
]
[tool.ruff.lint.per-file-ignores]
# Do not assign a lambda expression, use a def
"src/scanpy/tools/_rank_genes_groups.py" = [ "E731" ]
# No need for docstrings for all benchmarks
"benchmarks/**/*.py" = [ "D102", "D103" ]
# No need for docstrings for all test modules and test functions
"tests/**/*.py" = [ "D100", "D101", "D103" ]
[tool.ruff.lint.isort]
known-first-party = [ "scanpy", "testing.scanpy" ]
required-imports = [ "from __future__ import annotations" ]
[tool.ruff.lint.pydocstyle]
convention = "numpy"
[tool.ruff.lint.flake8-tidy-imports.banned-api]
"pytest.importorskip".msg = "Use the “@needs” decorator/mark instead"
"pandas.api.types.is_categorical_dtype".msg = "Use isinstance(s.dtype, CategoricalDtype) instead"
Expand Down
2 changes: 2 additions & 0 deletions src/scanpy/__main__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
"""Scanpy CLI entry point."""

from __future__ import annotations

from .cli import console_main
Expand Down
Loading

0 comments on commit d7b38ac

Please sign in to comment.