Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Wrap legacy APIs #2702

Merged
merged 47 commits into from
Dec 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
ac39c9d
check for legacy APIs
flying-sheep Oct 20, 2023
1082454
Fix one of them
flying-sheep Oct 20, 2023
0e6dfa3
Merge branch 'master' into short-apis
flying-sheep Nov 30, 2023
4ae8d1c
dca
flying-sheep Nov 30, 2023
bfe9435
Use explicit public modules
flying-sheep Nov 30, 2023
bf1cfa7
WIP validate signatures
flying-sheep Nov 30, 2023
659ca8a
Remaining sig conventions
flying-sheep Nov 30, 2023
cc21ffe
pp legacy API
flying-sheep Nov 30, 2023
de8b8a2
external legacy api
flying-sheep Nov 30, 2023
dc3a8a3
tl legacy api
flying-sheep Nov 30, 2023
e88d9e2
pp and tl remaining legacy apis
flying-sheep Nov 30, 2023
484dc1f
Plot classes legacy API
flying-sheep Nov 30, 2023
4c40ee3
more plots
flying-sheep Nov 30, 2023
d74724c
anndata plots legacy APIs
flying-sheep Dec 3, 2023
cfb2b6e
external legacy API
flying-sheep Dec 3, 2023
e43ec85
paga legacy APIs
flying-sheep Dec 3, 2023
756b98d
rest of legacy API
flying-sheep Dec 3, 2023
0309a1f
Remove undocumented from __all__
flying-sheep Dec 3, 2023
96e3082
fix accidental sig change
flying-sheep Dec 3, 2023
1a47c94
Loosen too-restricted use of APIs
flying-sheep Dec 3, 2023
286ac3c
Fix for 3.9
flying-sheep Dec 3, 2023
c18aa36
Fix return types a bit
flying-sheep Dec 3, 2023
b5ab99e
Fix _wraps_plot_scatter usage
flying-sheep Dec 3, 2023
61a4fd6
maybe fix docs
flying-sheep Dec 4, 2023
04b7d6d
Merge branch 'master' into short-apis
flying-sheep Dec 4, 2023
1267c6b
Merge branch 'master' into short-apis
flying-sheep Dec 4, 2023
e6b8de9
no ArrayLike
flying-sheep Dec 5, 2023
a3f4ed2
layer typing
flying-sheep Dec 5, 2023
18712b1
relnotes
flying-sheep Dec 5, 2023
7732d28
WIP
flying-sheep Dec 5, 2023
1c70fe7
rest
flying-sheep Dec 5, 2023
01e093f
Better way to configure new rule
flying-sheep Dec 5, 2023
c94c5d2
fix phenograph
flying-sheep Dec 7, 2023
be92425
neighbors init
flying-sheep Dec 7, 2023
344717f
Backwards compat
flying-sheep Dec 7, 2023
d2dbf36
Unify plot return sections
flying-sheep Dec 14, 2023
2cfbf12
Unify show behavior
flying-sheep Dec 14, 2023
d9b360d
add missing subsample function
flying-sheep Dec 14, 2023
9900a77
relax dca API
flying-sheep Dec 18, 2023
87cc30b
Make all <=3
flying-sheep Dec 18, 2023
06b353b
relax phate
flying-sheep Dec 18, 2023
8f498ae
relax trimap
flying-sheep Dec 18, 2023
a3aee2d
relax groupby
flying-sheep Dec 18, 2023
258e1b2
relax embedding_density
flying-sheep Dec 18, 2023
fb8bf74
Use FutureWarning
flying-sheep Dec 18, 2023
bdc3f0a
Merge branch 'master' into short-apis
flying-sheep Dec 18, 2023
8aec872
merge ignore
flying-sheep Dec 18, 2023
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
4 changes: 4 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ repos:
- id: ruff
args: ["--fix"]
- id: ruff-format
# The following can be removed once PLR0917 is out of preview
- name: ruff preview rules
id: ruff
args: ["--preview", "--select=PLR0917"]
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.5.0
hooks:
Expand Down
6 changes: 3 additions & 3 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -183,21 +183,21 @@ def setup(app: Sphinx):

qualname_overrides = {
"sklearn.neighbors._dist_metrics.DistanceMetric": "sklearn.metrics.DistanceMetric",
# If the docs are built with an old version of numpy, this will make it work:
"numpy.random.RandomState": "numpy.random.mtrand.RandomState",
"scanpy.plotting._matrixplot.MatrixPlot": "scanpy.pl.MatrixPlot",
"scanpy.plotting._dotplot.DotPlot": "scanpy.pl.DotPlot",
"scanpy.plotting._stacked_violin.StackedViolin": "scanpy.pl.StackedViolin",
"pandas.core.series.Series": "pandas.Series",
}

nitpick_ignore = [
# Technical issues
("py:class", "numpy.int64"), # documented as “attribute”
# Will probably be documented
("py:class", "scanpy._settings.Verbosity"),
("py:class", "scanpy.neighbors.OnFlySymMatrix"),
# Currently undocumented
# https://github.com/mwaskom/seaborn/issues/1810
("py:class", "seaborn.ClusterGrid"),
("py:class", "seaborn.matrix.ClusterGrid"),
("py:class", "samalg.SAM"),
# Won’t be documented
("py:class", "scanpy.plotting._utils._AxesSubplot"),
Expand Down
2 changes: 1 addition & 1 deletion docs/extensions/cite.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from sphinx.application import Sphinx


def cite_role(
def cite_role( # noqa: PLR0917
name: str,
rawsource: str,
text: str,
Expand Down
2 changes: 1 addition & 1 deletion docs/extensions/debug_docstrings.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
_pd_orig = sphinx.ext.napoleon._process_docstring


def pd_new(app, what, name, obj, options, lines):
def pd_new(app, what, name, obj, options, lines): # noqa: PLR0917
_pd_orig(app, what, name, obj, options, lines)
print(*lines, sep="\n")

Expand Down
2 changes: 1 addition & 1 deletion docs/extensions/function_images.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from sphinx.ext.autodoc import Options


def insert_function_images(
def insert_function_images( # noqa: PLR0917
app: Sphinx, what: str, name: str, obj: Any, options: Options, lines: list[str]
):
path = app.config.api_dir / f"{name}.png"
Expand Down
2 changes: 2 additions & 0 deletions docs/release-notes/1.10.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,5 @@
```

* Dropped support for Python 3.8. [More details here](https://numpy.org/neps/nep-0029-deprecation_policy.html). {pr}`2695` {smaller}`P Angerer`
* Deprecated specifying large numbers of function parameters by position as opposed to by name/keyword in all public APIs.
e.g. prefer `sc.tl.umap(adata, min_dist=0.1, spread=0.8)` over `sc.tl.umap(adata, 0.1, 0.8)` {pr}`2702` {smaller}`P Angerer`
4 changes: 4 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ dependencies = [
"umap-learn>=0.3.10",
"packaging",
"session-info",
"legacy-api-wrap>=1.4", # for positional API deprecations
"get-annotations; python_version < '3.10'",
]
dynamic = ["version"]
Expand Down Expand Up @@ -168,6 +169,8 @@ markers = [
"gpu: tests that use a GPU (currently unused, but needs to be specified here as we import anndata.tests.helpers, which uses it)",
]
filterwarnings = [
# legacy-api-wrap: internal use of positional API
"error:The specified parameters:FutureWarning",
# When calling `.show()` in tests, this is raised
"ignore:FigureCanvasAgg is non-interactive:UserWarning",
# We explicitly handle these errors in tests
Expand Down Expand Up @@ -202,6 +205,7 @@ select = [
"TID251", # Banned imports
"ICN", # Follow import conventions
"PTH", # Pathlib instead of os.path
"PLR0917", # Ban APIs with too many positional parameters
]
ignore = [
# line too long -> we accept long comment lines; black gets rid of long code lines
Expand Down
5 changes: 5 additions & 0 deletions scanpy/_compat.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
from __future__ import annotations

from dataclasses import dataclass, field
from functools import partial
from pathlib import Path

from legacy_api_wrap import legacy_api
from packaging import version

try:
Expand Down Expand Up @@ -61,3 +63,6 @@ def pkg_version(package):
from importlib.metadata import version as v

return version.parse(v(package))


old_positionals = partial(legacy_api, category=FutureWarning)
42 changes: 30 additions & 12 deletions scanpy/_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from typing import TYPE_CHECKING, Any, Literal, TextIO, Union

from . import logging
from ._compat import old_positionals
from .logging import _RootLogger, _set_log_file, _set_log_level

if TYPE_CHECKING:
Expand All @@ -36,6 +37,8 @@


class Verbosity(IntEnum):
"""Logging verbosity levels."""

error = 0
warning = 1
info = 2
Expand Down Expand Up @@ -102,14 +105,14 @@ def __init__(
file_format_figs: str = "pdf",
autosave: bool = False,
autoshow: bool = True,
writedir: str | Path = "./write/",
cachedir: str | Path = "./cache/",
datasetdir: str | Path = "./data/",
figdir: str | Path = "./figures/",
writedir: Path | str = "./write/",
cachedir: Path | str = "./cache/",
datasetdir: Path | str = "./data/",
figdir: Path | str = "./figures/",
cache_compression: str | None = "lzf",
max_memory=15,
n_jobs=1,
logfile: str | Path | None = None,
logfile: Path | str | None = None,
categories_to_ignore: Iterable[str] = ("N/A", "dontknow", "no_gate", "?"),
_frameon: bool = True,
_vector_friendly: bool = False,
Expand Down Expand Up @@ -269,7 +272,7 @@ def writedir(self) -> Path:
return self._writedir

@writedir.setter
def writedir(self, writedir: str | Path):
def writedir(self, writedir: Path | str):
_type_check(writedir, "writedir", (str, Path))
self._writedir = Path(writedir)

Expand All @@ -281,7 +284,7 @@ def cachedir(self) -> Path:
return self._cachedir

@cachedir.setter
def cachedir(self, cachedir: str | Path):
def cachedir(self, cachedir: Path | str):
_type_check(cachedir, "cachedir", (str, Path))
self._cachedir = Path(cachedir)

Expand All @@ -293,7 +296,7 @@ def datasetdir(self) -> Path:
return self._datasetdir

@datasetdir.setter
def datasetdir(self, datasetdir: str | Path):
def datasetdir(self, datasetdir: Path | str):
_type_check(datasetdir, "datasetdir", (str, Path))
self._datasetdir = Path(datasetdir).resolve()

Expand All @@ -305,7 +308,7 @@ def figdir(self) -> Path:
return self._figdir

@figdir.setter
def figdir(self, figdir: str | Path):
def figdir(self, figdir: Path | str):
_type_check(figdir, "figdir", (str, Path))
self._figdir = Path(figdir)

Expand Down Expand Up @@ -365,7 +368,7 @@ def logpath(self) -> Path | None:
return self._logpath

@logpath.setter
def logpath(self, logpath: str | Path | None):
def logpath(self, logpath: Path | str | None):
_type_check(logpath, "logfile", (str, Path))
# set via “file object” branch of logfile.setter
self.logfile = Path(logpath).open("a")
Expand All @@ -385,7 +388,7 @@ def logfile(self) -> TextIO:
return self._logfile

@logfile.setter
def logfile(self, logfile: str | Path | TextIO | None):
def logfile(self, logfile: Path | str | TextIO | None):
if not hasattr(logfile, "write") and logfile:
self.logpath = logfile
else: # file object
Expand Down Expand Up @@ -413,8 +416,23 @@ def categories_to_ignore(self, categories_to_ignore: Iterable[str]):
# Functions
# --------------------------------------------------------------------------------

@old_positionals(
"scanpy",
"dpi",
"dpi_save",
"frameon",
"vector_friendly",
"fontsize",
"figsize",
"color_map",
"format",
"facecolor",
"transparent",
"ipython_format",
)
def set_figure_params(
self,
*,
scanpy: bool = True,
dpi: int = 80,
dpi_save: int = 150,
Expand All @@ -427,7 +445,7 @@ def set_figure_params(
facecolor: str | None = None,
transparent: bool = False,
ipython_format: str = "png2x",
):
) -> None:
"""\
Set resolution/size, styling and format of figures.

Expand Down
79 changes: 47 additions & 32 deletions scanpy/_utils/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ def __repr__(self) -> str:
_empty = Empty.token

# e.g. https://scikit-learn.org/stable/modules/generated/sklearn.decomposition.PCA.html
AnyRandom = Union[None, int, random.RandomState] # maybe in the future random.Generator
AnyRandom = Union[int, random.RandomState, None] # maybe in the future random.Generator

EPS = 1e-15

Expand Down Expand Up @@ -96,42 +96,56 @@ def type_doc(name: str):
)


def deprecated_arg_names(arg_mapping: Mapping[str, str]):
"""
Decorator which marks a functions keyword arguments as deprecated. It will
result in a warning being emitted when the deprecated keyword argument is
used, and the function being called with the new argument.

Parameters
----------
arg_mapping
Mapping from deprecated argument name to current argument name.
"""

def renamed_arg(old_name, new_name, *, pos_0: bool = False):
def decorator(func):
@wraps(func)
def func_wrapper(*args, **kwargs):
warnings.simplefilter("always", DeprecationWarning) # turn off filter
for old, new in arg_mapping.items():
if old in kwargs:
warnings.warn(
f"Keyword argument '{old}' has been "
f"deprecated in favour of '{new}'. "
f"'{old}' will be removed in a future version.",
category=DeprecationWarning,
stacklevel=2,
def wrapper(*args, **kwargs):
if old_name in kwargs:
f_name = func.__name__
pos_str = (
(
f" at first position. Call it as `{f_name}(val, ...)` "
f"instead of `{f_name}({old_name}=val, ...)`"
)
val = kwargs.pop(old)
kwargs[new] = val
# reset filter
warnings.simplefilter("default", DeprecationWarning)
if pos_0
else ""
)
msg = (
f"In function `{f_name}`, argument `{old_name}` "
f"was renamed to `{new_name}`{pos_str}."
)
warnings.warn(msg, FutureWarning, stacklevel=3)
if pos_0:
args = (kwargs.pop(old_name), *args)
else:
kwargs[new_name] = kwargs.pop(old_name)
return func(*args, **kwargs)

return func_wrapper
return wrapper

return decorator


def _import_name(name: str) -> Any:
from importlib import import_module

parts = name.split(".")
obj = import_module(parts[0])
for i, name in enumerate(parts[1:]):
try:
obj = import_module(f"{obj.__name__}.{name}")
except ModuleNotFoundError:
break
else:
i = len(parts)
for name in parts[i + 1 :]:
try:
obj = getattr(obj, name)
except AttributeError:
raise RuntimeError(f"{parts[:i]}, {parts[i + 1:]}, {obj} {name}")
return obj


def _one_of_ours(obj, root: str):
return (
hasattr(obj, "__name__")
Expand All @@ -146,19 +160,19 @@ def descend_classes_and_funcs(mod: ModuleType, root: str, encountered=None):
if encountered is None:
encountered = WeakSet()
for obj in vars(mod).values():
if not _one_of_ours(obj, root):
if not _one_of_ours(obj, root) or obj in encountered:
continue
encountered.add(obj)
if callable(obj) and not isinstance(obj, MethodType):
yield obj
if isinstance(obj, type):
for m in vars(obj).values():
if callable(m) and _one_of_ours(m, root):
yield m
elif isinstance(obj, ModuleType) and obj not in encountered:
elif isinstance(obj, ModuleType):
if obj.__name__.startswith("scanpy.tests"):
# Python’s import mechanism seems to add this to `scanpy`’s attributes
continue
encountered.add(obj)
yield from descend_classes_and_funcs(obj, root, encountered)


Expand Down Expand Up @@ -264,6 +278,7 @@ def compute_association_matrix_of_groups(
adata: AnnData,
prediction: str,
reference: str,
*,
normalization: Literal["prediction", "reference"] = "prediction",
threshold: float = 0.01,
max_n_names: int | None = 2,
Expand Down Expand Up @@ -595,7 +610,7 @@ def select_groups(
return groups_order_subset, groups_masks


def warn_with_traceback(message, category, filename, lineno, file=None, line=None):
def warn_with_traceback(message, category, filename, lineno, file=None, line=None): # noqa: PLR0917
"""Get full tracebacks when warning is raised by setting

warnings.showwarning = warn_with_traceback
Expand Down
7 changes: 6 additions & 1 deletion scanpy/datasets/_datasets.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

from .. import _utils
from .. import logging as logg
from .._compat import old_positionals
from .._settings import settings
from ..readwrite import read, read_visium
from ._utils import check_datasetdir_exists, filter_oldformatwarning
Expand All @@ -20,7 +21,11 @@
HERE = Path(__file__).parent


@old_positionals(
"n_variables", "n_centers", "cluster_std", "n_observations", "random_state"
)
def blobs(
*,
n_variables: int = 11,
n_centers: int = 5,
cluster_std: float = 1.0,
Expand Down Expand Up @@ -192,7 +197,7 @@ def paul15() -> ad.AnnData:
# names reflecting the cell type identifications from the paper
cell_type = 6 * ["Ery"]
cell_type += "MEP Mk GMP GMP DC Baso Baso Mo Mo Neu Neu Eos Lymph".split()
adata.obs["paul15_clusters"] = [f"{i}{cell_type[i-1]}" for i in clusters]
adata.obs["paul15_clusters"] = [f"{i}{cell_type[i - 1]}" for i in clusters]
# make string annotations categorical (optional)
_utils.sanitize_anndata(adata)
# just keep the first of the two equivalent names per gene
Expand Down
Loading
Loading