Skip to content
Merged
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
4 changes: 2 additions & 2 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.12.11
hooks:
- id: ruff
- id: ruff-check
args: ["--fix"]
- id: ruff-format
# The following can be removed once PLR0917 is out of preview
- name: ruff preview rules
id: ruff
id: ruff-check
args: ["--preview", "--select=PLR0917"]
- repo: https://github.com/flying-sheep/bibfmt
rev: v4.3.0
Expand Down
2 changes: 1 addition & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"editor.defaultFormatter": "biomejs.biome",
},
"python.analysis.typeCheckingMode": "basic",
"python.testing.pytestArgs": ["-vv", "--color=yes"],
"python.testing.pytestArgs": ["-vv", "--color=yes", "--internet-tests"],
"python.testing.pytestEnabled": true,
"python.terminal.activateEnvironment": true,
}
3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,7 @@ select = [
"I", # Import sorting
"ICN", # Follow import conventions
"ISC", # Implicit string concatenation
"N", # Naming conventions
"PERF", # Performance
"PIE", # Syntax simplifications
"PL", # Pylint
Expand Down Expand Up @@ -255,6 +256,8 @@ allowed-confusables = [ "×", "’", "–", "α" ]
[tool.ruff.lint.per-file-ignores]
# Do not assign a lambda expression, use a def
"src/scanpy/tools/_rank_genes_groups.py" = [ "E731" ]
# Old and unmaintained
"src/scanpy/tools/_sim.py" = [ "N" ]
# No need for docstrings for all benchmarks
"benchmarks/**/*.py" = [ "D102", "D103" ]
# D*: No need for docstrings for all test modules and test functions
Expand Down
13 changes: 7 additions & 6 deletions src/scanpy/_settings/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,8 @@ def wrapped(self: S, var: T, *args: P.args, **kwargs: P.kwargs) -> R:
return decorator


class SettingsMeta(SingletonMeta):
# `type` is only here because of https://github.com/astral-sh/ruff/issues/20225
class SettingsMeta(SingletonMeta, type):
# logging
_root_logger: _RootLogger
_logfile: TextIO
Expand Down Expand Up @@ -121,13 +122,13 @@ def verbosity(cls, verbosity: Verbosity | _VerbosityName | int) -> None:
_set_log_level(cls, cls._verbosity.level)

@property
def N_PCS(cls) -> int:
def N_PCS(cls) -> int: # noqa: N802
"""Default number of principal components to use."""
return cls._n_pcs

@N_PCS.setter
@_type_check_arg2(int)
def N_PCS(cls, n_pcs: int) -> None:
def N_PCS(cls, n_pcs: int) -> None: # noqa: N802
cls._n_pcs = n_pcs

@property
Expand Down Expand Up @@ -167,8 +168,8 @@ def file_format_figs(cls) -> str:

@file_format_figs.setter
@_type_check_arg2(str)
def file_format_figs(self, figure_format: str) -> None:
self._file_format_figs = figure_format
def file_format_figs(cls, figure_format: str) -> None:
cls._file_format_figs = figure_format

@property
def autosave(cls) -> bool:
Expand Down Expand Up @@ -453,7 +454,7 @@ def __str__(cls) -> str:
)


class settings(metaclass=SettingsMeta):
class settings(metaclass=SettingsMeta): # noqa: N801
"""Settings for scanpy."""

def __new__(cls) -> type[Self]:
Expand Down
89 changes: 46 additions & 43 deletions src/scanpy/_utils/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -567,7 +567,8 @@ def check_op(op):

@singledispatch
def axis_mul_or_truediv(
X: ArrayLike,
x: ArrayLike,
/,
scaling_array: np.ndarray,
axis: Literal[0, 1],
op: Callable[[Any, Any], Any],
Expand All @@ -578,15 +579,16 @@ def axis_mul_or_truediv(
check_op(op)
scaling_array = broadcast_axis(scaling_array, axis)
if op is mul:
return np.multiply(X, scaling_array, out=out)
return np.multiply(x, scaling_array, out=out)
if not allow_divide_by_zero:
scaling_array = scaling_array.copy() + (scaling_array == 0)
return np.true_divide(X, scaling_array, out=out)
return np.true_divide(x, scaling_array, out=out)


@axis_mul_or_truediv.register(CSBase)
def _(
X: CSBase,
x: CSBase,
/,
scaling_array: np.ndarray,
axis: Literal[0, 1],
op: Callable[[Any, Any], Any],
Expand All @@ -595,7 +597,7 @@ def _(
out: CSBase | None = None,
) -> CSBase:
check_op(op)
if out is not None and X.data is not out.data:
if out is not None and x.data is not out.data:
msg = "`out` argument provided but not equal to X. This behavior is not supported for sparse matrix scaling."
raise ValueError(msg)
if not allow_divide_by_zero and op is truediv:
Expand All @@ -613,14 +615,14 @@ def new_data_op(x):
def new_data_op(x):
return op(x.data, scaling_array.take(x.indices, mode="clip"))

if X.format == "csr":
indices = X.indices
indptr = X.indptr
if x.format == "csr":
indices = x.indices
indptr = x.indptr
if out is not None:
X.data = new_data_op(X)
return X
return type(X)((new_data_op(X), indices.copy(), indptr.copy()), shape=X.shape)
transposed = X.T
x.data = new_data_op(x)
return x
return type(x)((new_data_op(x), indices.copy(), indptr.copy()), shape=x.shape)
transposed = x.T
return axis_mul_or_truediv(
transposed,
scaling_array,
Expand All @@ -632,16 +634,17 @@ def new_data_op(x):


def make_axis_chunks(
X: DaskArray, axis: Literal[0, 1]
x: DaskArray, axis: Literal[0, 1]
) -> tuple[tuple[int], tuple[int]]:
if axis == 0:
return (X.chunks[axis], (1,))
return ((1,), X.chunks[axis])
return (x.chunks[axis], (1,))
return ((1,), x.chunks[axis])


@axis_mul_or_truediv.register(DaskArray)
def _(
X: DaskArray,
x: DaskArray,
/,
scaling_array: Scaling_T,
axis: Literal[0, 1],
op: Callable[[Any, Any], Any],
Expand All @@ -661,64 +664,64 @@ def _(
column_scale = axis == 1

if isinstance(scaling_array, DaskArray):
if (row_scale and X.chunksize[0] != scaling_array.chunksize[0]) or (
if (row_scale and x.chunksize[0] != scaling_array.chunksize[0]) or (
column_scale
and (
(
len(scaling_array.chunksize) == 1
and X.chunksize[1] != scaling_array.chunksize[0]
and x.chunksize[1] != scaling_array.chunksize[0]
)
or (
len(scaling_array.chunksize) == 2
and X.chunksize[1] != scaling_array.chunksize[1]
and x.chunksize[1] != scaling_array.chunksize[1]
)
)
):
warnings.warn(
"Rechunking scaling_array in user operation", UserWarning, stacklevel=3
)
scaling_array = scaling_array.rechunk(make_axis_chunks(X, axis))
scaling_array = scaling_array.rechunk(make_axis_chunks(x, axis))
else:
scaling_array = da.from_array(
scaling_array,
chunks=make_axis_chunks(X, axis),
chunks=make_axis_chunks(x, axis),
)
return da.map_blocks(
axis_mul_or_truediv,
X,
x,
scaling_array,
axis,
op,
meta=X._meta,
meta=x._meta,
out=out,
allow_divide_by_zero=allow_divide_by_zero,
)


@singledispatch
def axis_nnz(X: ArrayLike, axis: Literal[0, 1]) -> np.ndarray:
return np.count_nonzero(X, axis=axis)
def axis_nnz(x: ArrayLike, /, axis: Literal[0, 1]) -> np.ndarray:
return np.count_nonzero(x, axis=axis)


if pkg_version("scipy") >= Version("1.15"):
# newer scipy versions support the `axis` argument for count_nonzero
@axis_nnz.register(CSBase)
def _(X: CSBase, axis: Literal[0, 1]) -> np.ndarray:
return X.count_nonzero(axis=axis)
def _(x: CSBase, /, axis: Literal[0, 1]) -> np.ndarray:
return x.count_nonzero(axis=axis)
else:
# older scipy versions don’t have any way to get the nnz of a sparse array
@axis_nnz.register(CSBase)
def _(X: CSBase, axis: Literal[0, 1]) -> np.ndarray:
if isinstance(X, _CSArray):
def _(x: CSBase, /, axis: Literal[0, 1]) -> np.ndarray:
if isinstance(x, _CSArray):
from scipy.sparse import csc_array, csr_array # noqa: TID251

X = (csr_array if X.format == "csr" else csc_array)(X)
return X.getnnz(axis=axis)
x = (csr_array if x.format == "csr" else csc_array)(x)
return x.getnnz(axis=axis)


@axis_nnz.register(DaskArray)
def _(X: DaskArray, axis: Literal[0, 1]) -> DaskArray:
return X.map_blocks(
def _(x: DaskArray, /, axis: Literal[0, 1]) -> DaskArray:
return x.map_blocks(
partial(axis_nnz, axis=axis),
dtype=np.int64,
meta=np.array([], dtype=np.int64),
Expand All @@ -727,17 +730,17 @@ def _(X: DaskArray, axis: Literal[0, 1]) -> DaskArray:


@singledispatch
def check_nonnegative_integers(X: _SupportedArray) -> bool | DaskArray:
def check_nonnegative_integers(x: _SupportedArray, /) -> bool | DaskArray:
"""Check values of X to ensure it is count data."""
raise NotImplementedError


@check_nonnegative_integers.register(np.ndarray)
@check_nonnegative_integers.register(CSBase)
def _check_nonnegative_integers_in_mem(X: _MemoryArray) -> bool:
def _check_nonnegative_integers_in_mem(x: _MemoryArray, /) -> bool:
from numbers import Integral

data = X if isinstance(X, np.ndarray) else X.data
data = x if isinstance(x, np.ndarray) else x.data
# Check no negatives
if np.signbit(data).any():
return False
Expand All @@ -748,8 +751,8 @@ def _check_nonnegative_integers_in_mem(X: _MemoryArray) -> bool:


@check_nonnegative_integers.register(DaskArray)
def _check_nonnegative_integers_dask(X: DaskArray) -> DaskArray:
return X.map_blocks(check_nonnegative_integers, dtype=bool, drop_axis=(0, 1))
def _check_nonnegative_integers_dask(x: DaskArray, /) -> DaskArray:
return x.map_blocks(check_nonnegative_integers, dtype=bool, drop_axis=(0, 1))


def dematrix(x: _SA | np.matrix) -> _SA:
Expand Down Expand Up @@ -1001,11 +1004,11 @@ def _resolve_axis(
raise ValueError(msg)


def is_backed_type(X: object) -> bool:
return isinstance(X, SparseDataset | h5py.File | h5py.Dataset)
def is_backed_type(x: object, /) -> bool:
return isinstance(x, SparseDataset | h5py.File | h5py.Dataset)


def raise_not_implemented_error_if_backed_type(X: object, method_name: str) -> None:
if is_backed_type(X):
msg = f"{method_name} is not implemented for matrices of type {type(X)}"
def raise_not_implemented_error_if_backed_type(x: object, method_name: str, /) -> None:
if is_backed_type(x):
msg = f"{method_name} is not implemented for matrices of type {type(x)}"
raise NotImplementedError(msg)
8 changes: 4 additions & 4 deletions src/scanpy/datasets/_datasets.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,14 +96,14 @@ def blobs(
"""
import sklearn.datasets

X, y = sklearn.datasets.make_blobs(
x, y = sklearn.datasets.make_blobs(
n_samples=n_observations,
n_features=n_variables,
centers=n_centers,
cluster_std=cluster_std,
random_state=random_state,
)
return AnnData(X, obs=dict(blobs=y.astype(str)))
return AnnData(x, obs=dict(blobs=y.astype(str)))


@doctest_internet
Expand Down Expand Up @@ -266,13 +266,13 @@ def paul15() -> AnnData:
_utils.check_presence_download(filename, backup_url)
with h5py.File(filename, "r") as f:
# Coercing to float32 for backwards compatibility
X = f["data.debatched"][()].astype(np.float32)
x = f["data.debatched"][()].astype(np.float32)
gene_names = f["data.debatched_rownames"][()].astype(str)
cell_names = f["data.debatched_colnames"][()].astype(str)
clusters = f["cluster.id"][()].flatten().astype(int)
infogenes_names = f["info.genes_strings"][()].astype(str)
# each row has to correspond to a observation, therefore transpose
adata = AnnData(X.transpose())
adata = AnnData(x.transpose())
adata.var_names = gene_names
adata.obs_names = cell_names
# names reflecting the cell type identifications from the paper
Expand Down
Loading
Loading