diff --git a/src/squidpy/_compat.py b/src/squidpy/_compat.py index 3bf141706..486e11234 100644 --- a/src/squidpy/_compat.py +++ b/src/squidpy/_compat.py @@ -1,11 +1,30 @@ from __future__ import annotations +from importlib import import_module from importlib.metadata import version +from typing import Any from packaging.version import Version -from scanpy.plotting._tools.scatterplots import _add_categorical_legend as add_categorical_legend -from scanpy.plotting._tools.scatterplots import _panel_grid as panel_grid -from scanpy.plotting._utils import add_colors_for_categorical_sample_annotation + + +def _scanpy_plotting_layout() -> tuple[str, Any | None, Any | None]: + try: + legacy = import_module("scanpy.plotting.legacy") + except ModuleNotFoundError as error: + if error.name != "scanpy.plotting.legacy": + raise + return "scanpy.plotting", None, import_module("scanpy").settings + return "scanpy.plotting.legacy", legacy.mpl_settings, None + + +_scanpy_plotting_path, _scanpy_mpl_settings, _scanpy_settings = _scanpy_plotting_layout() +_scanpy_plotting_utils = import_module(f"{_scanpy_plotting_path}._utils") +_scanpy_scatterplots = import_module(f"{_scanpy_plotting_path}._tools.scatterplots") +_scanpy_palettes = import_module(f"{_scanpy_plotting_path}.palettes") + +add_categorical_legend = _scanpy_scatterplots._add_categorical_legend +panel_grid = _scanpy_scatterplots._panel_grid +default_palette = _scanpy_palettes.default_102 __all__ = [ # scanpy @@ -13,19 +32,37 @@ "add_categorical_legend", "panel_grid", "add_colors_for_categorical_sample_annotation", + "default_palette", + "scanpy_frameon", + "scanpy_vector_friendly", # anndata "ArrayView", "SparseCSCView", "SparseCSRView", ] -# See https://github.com/scverse/squidpy/issues/1061 for more details. -# Scanpy 0.11.x-0.12.x renamed set_default_colors_for_categorical_obs to _set_default_colors_for_categorical_obs -# and then changed it back. Try underscore version first, fall back to non-underscore. -try: - from scanpy.plotting._utils import _set_default_colors_for_categorical_obs as set_default_colors_for_categorical_obs -except ImportError: - from scanpy.plotting._utils import set_default_colors_for_categorical_obs +add_colors_for_categorical_sample_annotation = _scanpy_plotting_utils.add_colors_for_categorical_sample_annotation +set_default_colors_for_categorical_obs = getattr( + _scanpy_plotting_utils, + "_set_default_colors_for_categorical_obs", + None, +) +if set_default_colors_for_categorical_obs is None: + set_default_colors_for_categorical_obs = _scanpy_plotting_utils.set_default_colors_for_categorical_obs + + +def scanpy_frameon() -> bool: + if _scanpy_mpl_settings is not None: + return _scanpy_mpl_settings.FRAMEON + assert _scanpy_settings is not None + return _scanpy_settings._frameon + + +def scanpy_vector_friendly() -> bool: + if _scanpy_mpl_settings is not None: + return _scanpy_mpl_settings.VECTOR_FRIENDLY + assert _scanpy_settings is not None + return _scanpy_settings._vector_friendly CAN_USE_SPARSE_ARRAY = Version(version("anndata")) >= Version("0.11.0rc1") diff --git a/src/squidpy/im/_container.py b/src/squidpy/im/_container.py index 7b439a0ec..1656c550d 100644 --- a/src/squidpy/im/_container.py +++ b/src/squidpy/im/_container.py @@ -19,10 +19,10 @@ from dask import delayed from matplotlib.colors import ListedColormap from scanpy import logging as logg -from scanpy.plotting.palettes import default_102 as default_palette from skimage.transform import rescale from skimage.util import img_as_float +from squidpy._compat import default_palette from squidpy._constants._constants import InferDimensions from squidpy._constants._pkg_constants import Key from squidpy._docs import d, inject_docs diff --git a/src/squidpy/pl/_spatial_utils.py b/src/squidpy/pl/_spatial_utils.py index 9cf5a30e8..2fbd23e55 100644 --- a/src/squidpy/pl/_spatial_utils.py +++ b/src/squidpy/pl/_spatial_utils.py @@ -29,13 +29,12 @@ from matplotlib_scalebar.scalebar import ScaleBar from pandas import CategoricalDtype from scanpy import logging as logg -from scanpy import settings as sc_settings from skimage.color import label2rgb from skimage.morphology import erosion, square from skimage.segmentation import find_boundaries from skimage.util import map_array -from squidpy._compat import add_categorical_legend +from squidpy._compat import add_categorical_legend, scanpy_frameon, scanpy_vector_friendly from squidpy._constants._constants import ScatterShape from squidpy._constants._pkg_constants import Key from squidpy._utils import NDArrayA @@ -569,7 +568,7 @@ def _plot_edges( ax=ax, **kwargs, ) - edge_collection.set_rasterized(sc_settings._vector_friendly) + edge_collection.set_rasterized(scanpy_vector_friendly()) ax.add_collection(edge_collection) @@ -904,7 +903,7 @@ def _panel_grid( def _set_ax_title(fig_params: FigParams, count: int, value_to_plot: str | None = None) -> Axes: ax = fig_params.axs[count] if fig_params.axs is not None else fig_params.ax - if not (sc_settings._frameon if fig_params.frameon is None else fig_params.frameon): + if not (scanpy_frameon() if fig_params.frameon is None else fig_params.frameon): ax.axis("off") if fig_params.title is None: @@ -959,7 +958,7 @@ def _plot_scatter( coords[:, 1], s=outline_params.bg_size, c=outline_params.bg_color, - rasterized=sc_settings._vector_friendly, + rasterized=scanpy_vector_friendly(), cmap=cmap_params.cmap, norm=norm, **kwargs, @@ -970,7 +969,7 @@ def _plot_scatter( coords[:, 1], s=outline_params.gap_size, c=outline_params.gap_color, - rasterized=sc_settings._vector_friendly, + rasterized=scanpy_vector_friendly(), cmap=cmap_params.cmap, norm=norm, **kwargs, @@ -981,7 +980,7 @@ def _plot_scatter( coords[:, 1], c=np.array(color_vector), s=size, - rasterized=sc_settings._vector_friendly, + rasterized=scanpy_vector_friendly(), cmap=cmap_params.cmap, norm=norm, **kwargs, diff --git a/tests/plotting/test_spatial_static.py b/tests/plotting/test_spatial_static.py index 260515b68..dc3f9920c 100644 --- a/tests/plotting/test_spatial_static.py +++ b/tests/plotting/test_spatial_static.py @@ -3,6 +3,7 @@ import platform from collections.abc import Sequence from functools import partial +from types import SimpleNamespace import matplotlib.pyplot as plt import numpy as np @@ -12,6 +13,7 @@ from anndata import AnnData from matplotlib.colors import ListedColormap +import squidpy._compat as compat from squidpy import pl from squidpy._constants._pkg_constants import Key from squidpy.gr import spatial_neighbors_grid, spatial_neighbors_radius @@ -28,6 +30,66 @@ # ".png" is appended to , no need to set it +def test_scanpy_plotting_legacy_layout(monkeypatch: pytest.MonkeyPatch) -> None: + settings = object() + legacy = SimpleNamespace(mpl_settings=settings) + monkeypatch.setattr(compat, "import_module", lambda name: legacy) + + assert compat._scanpy_plotting_layout() == ("scanpy.plotting.legacy", settings, None) + + +def test_scanpy_plotting_released_layout(monkeypatch: pytest.MonkeyPatch) -> None: + settings = object() + + def import_released(name: str): + if name == "scanpy.plotting.legacy": + raise ModuleNotFoundError(name=name) + return SimpleNamespace(settings=settings) + + monkeypatch.setattr(compat, "import_module", import_released) + + assert compat._scanpy_plotting_layout() == ("scanpy.plotting", None, settings) + + +def test_scanpy_plotting_layout_preserves_import_errors(monkeypatch: pytest.MonkeyPatch) -> None: + def import_broken(name: str): + raise ModuleNotFoundError(name="scanpy_dependency") + + monkeypatch.setattr(compat, "import_module", import_broken) + + with pytest.raises(ModuleNotFoundError) as error: + compat._scanpy_plotting_layout() + assert error.value.name == "scanpy_dependency" + + +@pytest.mark.parametrize( + ("accessor", "legacy_name", "released_name"), + [ + (compat.scanpy_frameon, "FRAMEON", "_frameon"), + (compat.scanpy_vector_friendly, "VECTOR_FRIENDLY", "_vector_friendly"), + ], +) +def test_scanpy_plotting_setting_compatibility( + monkeypatch: pytest.MonkeyPatch, + accessor, + legacy_name: str, + released_name: str, +) -> None: + legacy_settings = SimpleNamespace(**{legacy_name: False}) + released_settings = SimpleNamespace(**{released_name: True}) + monkeypatch.setattr(compat, "_scanpy_mpl_settings", legacy_settings) + monkeypatch.setattr(compat, "_scanpy_settings", released_settings) + + assert accessor() is False + setattr(legacy_settings, legacy_name, True) + assert accessor() is True + + monkeypatch.setattr(compat, "_scanpy_mpl_settings", None) + assert accessor() is True + setattr(released_settings, released_name, False) + assert accessor() is False + + class TestSpatialStatic(PlotTester, metaclass=PlotTesterMeta): def test_tol_plot_spatial_scatter_image(self, adata_hne: AnnData): pl.spatial_scatter(adata_hne, na_color="lightgrey")