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

Backport PRs #2478 and #2235 on branch 1.9.x (Separate test utils from tests) #2528

Merged
merged 3 commits into from
Jun 23, 2023
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
20 changes: 16 additions & 4 deletions .azure-pipelines.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ trigger:
variables:
python.version: '3.9'
PIP_CACHE_DIR: $(Pipeline.Workspace)/.pip
PYTEST_ADDOPTS: '-v --color=yes --nunit-xml=nunit/test-results.xml'
ANNDATA_DEV: no
RUN_COVERAGE: no
TEST_EXTRA: 'test-full'
PRERELEASE_DEPENDENCIES: no

jobs:
- job: PyTest
Expand All @@ -23,6 +25,8 @@ jobs:
python.version: '3.9'
ANNDATA_DEV: yes
RUN_COVERAGE: yes
PRERELEASE_DEPENDENCIES: yes

steps:
- task: UsePythonVersion@0
inputs:
Expand All @@ -45,9 +49,17 @@ jobs:

- script: |
python -m pip install --upgrade pip
pip install pytest-cov wheel
pip install wheel coverage
pip install .[dev,$(TEST_EXTRA)]
displayName: 'Install dependencies'
condition: eq(variables['PRERELEASE_DEPENDENCIES'], 'no')

- script: |
python -m pip install --pre --upgrade pip
pip install --pre wheel coverage
pip install --pre .[dev,$(TEST_EXTRA)]
displayName: 'Install dependencies release candidates'
condition: eq(variables['PRERELEASE_DEPENDENCIES'], 'yes')

- script: |
pip install -v "anndata[dev,test] @ git+https://github.com/scverse/anndata"
Expand All @@ -58,13 +70,13 @@ jobs:
pip list
displayName: 'Display installed versions'

- script: |
pytest -v --color=yes --ignore=scanpy/tests/_images --nunit-xml="nunit/test-results.xml"
- script: pytest
displayName: 'PyTest'
condition: eq(variables['RUN_COVERAGE'], 'no')

- script: |
pytest -v --color=yes --ignore=scanpy/tests/_images --nunit-xml="nunit/test-results.xml" --cov=scanpy --cov-report=xml
coverage run -m pytest
coverage xml
displayName: 'PyTest (coverage)'
condition: eq(variables['RUN_COVERAGE'], 'yes')

Expand Down
5 changes: 2 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
/docs/data/

# tests
/*coverage*
/nunit/
/.cache/
/.pytest_cache/
/scanpy/tests/test*.h5ad
Expand Down Expand Up @@ -39,6 +41,3 @@ Thumbs.db
# IDEs and editors
/.idea/
/.vscode/

# Test artifacts
**/test-results.xml
55 changes: 0 additions & 55 deletions conftest.py

This file was deleted.

41 changes: 35 additions & 6 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -141,21 +141,50 @@ exclude = [
]

[tool.pytest.ini_options]
python_files = "test_*.py"
testpaths = "scanpy/tests/"
addopts = [
"--import-mode=importlib",
"-pscanpy.testing._pytest",
]
testpaths = ["scanpy"]
ignore = ["scanpy/tests/_images"]
xfail_strict = true
nunit_attach_on = "fail"
markers = [
"internet: tests which rely on internet resources (enable with `--internet-tests`)",
]

[tool.coverage.run]
source = ["scanpy"]
source_pkgs = ["scanpy"]
omit = ["*/tests/*"]
[tool.coverage.paths]
source = [".", "**/site-packages"]

[tool.black]
line-length = 88
target-version = ["py38"]
skip-string-normalization = true
exclude = """
/build/.*
"""

[tool.ruff]
select = [
"F", # Pyflakes
"E", # Pycodestyle errors
"W", # Pycodestyle warnings
"TID251", # Banned imports
]
ignore = [
# module imported but unused -> required for Scanpys API
"F401",
# line too long -> we accept long comment lines; black gets rid of long code lines
"E501",
# module level import not at top of file -> required to circumvent circular imports for Scanpys API
"E402",
# E266 too many leading '#' for block comment -> Scanpy allows them for comments into sections
"E262",
# allow I, O, l as variable names -> I is the identity matrix, i, j, k, l is reasonable indexing notation
"E741",
]
[tool.ruff.per-file-ignores]
# Do not assign a lambda expression, use a def
"scanpy/tools/_rank_genes_groups.py" = ["E731"]
[tool.ruff.flake8-tidy-imports.banned-api]
"pytest.importorskip".msg = "Use the “@needs” decorator/mark instead"
4 changes: 2 additions & 2 deletions scanpy/plotting/_tools/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from matplotlib.figure import Figure
from matplotlib.colors import Normalize
from matplotlib import pyplot as pl
from matplotlib import rcParams, cm
from matplotlib import rcParams, colormaps
from anndata import AnnData
from typing import Union, Optional, List, Sequence, Iterable, Mapping, Literal

Expand Down Expand Up @@ -1446,7 +1446,7 @@ def embedding_density(

# Make the color map
if isinstance(color_map, str):
color_map = copy(cm.get_cmap(color_map))
color_map = copy(colormaps.get_cmap(color_map))

color_map.set_over('black')
color_map.set_under('lightgray')
Expand Down
5 changes: 2 additions & 3 deletions scanpy/plotting/_tools/scatterplots.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,7 @@
from matplotlib.axes import Axes
from matplotlib.figure import Figure
from pandas.api.types import is_categorical_dtype
from matplotlib import pyplot as pl, colors
from matplotlib.cm import get_cmap
from matplotlib import pyplot as pl, colors, colormaps
from matplotlib import rcParams
from matplotlib import patheffects
from matplotlib.colors import Colormap, Normalize
Expand Down Expand Up @@ -160,7 +159,7 @@ def embedding(
raise ValueError("Cannot specify both `color_map` and `cmap`.")
else:
cmap = color_map
cmap = copy(get_cmap(cmap))
cmap = copy(colormaps.get_cmap(cmap))
cmap.set_bad(na_color)
kwargs["cmap"] = cmap
# Prevents warnings during legend creation
Expand Down
Empty file added scanpy/testing/__init__.py
Empty file.
35 changes: 7 additions & 28 deletions scanpy/tests/helpers.py → scanpy/testing/_helpers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,8 @@
import scanpy as sc
import numpy as np
import warnings
import pytest
from anndata.tests.helpers import asarray, assert_equal
from scanpy.tests._data._cached_datasets import pbmc3k


# TODO: Report more context on the fields being compared on error
# TODO: Allow specifying paths to ignore on comparison
Expand All @@ -20,7 +19,7 @@
# These functions can be used to check that functions are correctly using arugments like `layers`, `obsm`, etc.


def check_rep_mutation(func, X, *, fields=["layer", "obsm"], **kwargs):
def check_rep_mutation(func, X, *, fields=("layer", "obsm"), **kwargs):
"""Check that only the array meant to be modified is modified."""
adata = sc.AnnData(X=X.copy(), dtype=X.dtype)
for field in fields:
Expand Down Expand Up @@ -87,32 +86,12 @@ def check_rep_results(func, X, *, fields=["layer", "obsm"], **kwargs):
assert_equal(adata_X, adatas_proc[field])


def _prepare_pbmc_testdata(sparsity_func, dtype, small=False):
"""Prepares 3k PBMC dataset with batch key `batch` and defined datatype/sparsity.

Params
------
sparsity_func
sparsity function applied to adata.X (e.g. csr_matrix.toarray for dense or csr_matrix for sparse)
dtype
numpy dtype applied to adata.X (e.g. 'float32' or 'int64')
small
False (default) returns full data, True returns small subset of the data."""

adata = pbmc3k().copy()

if small:
adata = adata[:1000, :500]
sc.pp.filter_cells(adata, min_genes=1)
np.random.seed(42)
adata.obs['batch'] = np.random.randint(0, 3, size=adata.shape[0])
sc.pp.filter_genes(adata, min_cells=1)
adata.X = sparsity_func(adata.X.astype(dtype))
return adata


def _check_check_values_warnings(function, adata, expected_warning, kwargs={}):
'''Runs `function` on `adata` with provided arguments `kwargs` twice: once with `check_values=True` and once with `check_values=False`. Checks that the `expected_warning` is only raised whtn `check_values=True`.'''
"""
Runs `function` on `adata` with provided arguments `kwargs` twice:
once with `check_values=True` and once with `check_values=False`.
Checks that the `expected_warning` is only raised whtn `check_values=True`.
"""

# expecting 0 no-int warnings
with warnings.catch_warnings(record=True) as record:
Expand Down
69 changes: 69 additions & 0 deletions scanpy/testing/_helpers/data.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
"""
Functions returning copies of datasets as cheaply as possible,
i.e. without having to hit the disk or (in case of ``_pbmc3k_normalized``) recomputing normalization.
"""

from __future__ import annotations

try:
from functools import cache
except ImportError: # Python < 3.9
from functools import lru_cache

def cache(func):
return lru_cache(maxsize=None)(func)


from anndata import AnnData
import scanpy as sc


# Functions returning the same objects (easy to misuse)


_pbmc3k = cache(sc.datasets.pbmc3k)
_pbmc3k_processed = cache(sc.datasets.pbmc3k_processed)
_pbmc68k_reduced = cache(sc.datasets.pbmc68k_reduced)
_krumsiek11 = cache(sc.datasets.krumsiek11)
_paul15 = cache(sc.datasets.paul15)


# Functions returning copies


def pbmc3k() -> AnnData:
return _pbmc3k().copy()


def pbmc3k_processed() -> AnnData:
return _pbmc3k_processed().copy()


def pbmc68k_reduced() -> AnnData:
return _pbmc68k_reduced().copy()


def krumsiek11() -> AnnData:
return _krumsiek11().copy()


def paul15() -> AnnData:
return _paul15().copy()


# Derived datasets


@cache
def _pbmc3k_normalized() -> AnnData:
pbmc = pbmc3k()
pbmc.X = pbmc.X.astype("float64") # For better accuracy
sc.pp.filter_genes(pbmc, min_counts=1)
sc.pp.log1p(pbmc)
sc.pp.normalize_total(pbmc)
sc.pp.highly_variable_genes(pbmc)
return pbmc


def pbmc3k_normalized() -> AnnData:
return _pbmc3k_normalized().copy()
48 changes: 48 additions & 0 deletions scanpy/testing/_pytest/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
"""A private pytest plugin"""
import pytest

from .fixtures import * # noqa: F403


# In case pytest-nunit is not installed, defines a dummy fixture
try:
import pytest_nunit
except ModuleNotFoundError:

@pytest.fixture
def add_nunit_attachment(request):
def noop(file, description):
pass

return noop

def pytest_addoption(parser):
add_internet_tests_option(parser)
parser.addini("nunit_attach_on", "Dummy nunit replacement", default="any")

else:

def pytest_addoption(parser):
add_internet_tests_option(parser)


def add_internet_tests_option(parser):
parser.addoption(
"--internet-tests",
action="store_true",
default=False,
help=(
"Run tests that retrieve stuff from the internet. "
"This increases test time."
),
)


def pytest_collection_modifyitems(config, items):
run_internet = config.getoption("--internet-tests")
skip_internet = pytest.mark.skip(reason="need --internet-tests option to run")
for item in items:
# All tests marked with `pytest.mark.internet` get skipped unless
# `--run-internet` passed
if not run_internet and ("internet" in item.keywords):
item.add_marker(skip_internet)
Loading