From 7e764d9bc4d7ff53bc8e8b4159da950ae5d407b2 Mon Sep 17 00:00:00 2001 From: Tim Treis Date: Tue, 10 Mar 2026 17:46:51 +0100 Subject: [PATCH 1/9] Consolidate validation utilities into squidpy/_validators.py Move generic validators (_assert_positive, _assert_non_negative, _assert_in_range, _assert_non_empty_sequence, _check_tuple_needles, _get_valid_values) from gr/_utils.py to a new squidpy/_validators.py module, fixing layering violations where pl/, im/, and tl/ modules imported from gr/_utils.py. Add new validators: _assert_isinstance, _assert_one_of, _assert_key_in_adata, _assert_key_in_sdata. Replace ~20 ad-hoc isinstance checks in gr/_niche.py, ~12 sdata key checks in experimental/im/_make_tiles.py, and scattered adata key checks across pl/ and gr/ modules. Backwards compatibility preserved via re-exports in gr/_utils.py. Co-Authored-By: Claude Opus 4.6 --- src/squidpy/_validators.py | 117 ++++++++++ src/squidpy/experimental/im/_make_tiles.py | 49 ++-- src/squidpy/gr/_niche.py | 105 ++++----- src/squidpy/gr/_ppatterns.py | 4 +- src/squidpy/gr/_utils.py | 80 +------ src/squidpy/im/_container.py | 4 +- src/squidpy/im/_coords.py | 2 +- src/squidpy/im/_feature_mixin.py | 2 +- src/squidpy/pl/_graph.py | 7 +- src/squidpy/pl/_spatial_utils.py | 9 +- src/squidpy/pl/_utils.py | 7 +- src/squidpy/tl/_var_by_distance.py | 4 +- tests/test_validators.py | 254 +++++++++++++++++++++ 13 files changed, 452 insertions(+), 192 deletions(-) create mode 100644 src/squidpy/_validators.py create mode 100644 tests/test_validators.py diff --git a/src/squidpy/_validators.py b/src/squidpy/_validators.py new file mode 100644 index 000000000..4866dade9 --- /dev/null +++ b/src/squidpy/_validators.py @@ -0,0 +1,117 @@ +"""Generic validation utilities for squidpy.""" + +from __future__ import annotations + +from collections.abc import Hashable, Iterable, Sequence +from typing import TYPE_CHECKING, Any + +from squidpy._utils import _unique_order_preserving + +if TYPE_CHECKING: + from anndata import AnnData + from spatialdata import SpatialData + + +def _check_tuple_needles( + needles: Sequence[tuple[Any, Any]], + haystack: Sequence[Any], + msg: str, + reraise: bool = True, +) -> Sequence[tuple[Any, Any]]: + filtered = [] + + for needle in needles: + if not isinstance(needle, Sequence): + raise TypeError(f"Expected a `Sequence`, found `{type(needle).__name__}`.") + if len(needle) != 2: + raise ValueError(f"Expected a `tuple` of length `2`, found `{len(needle)}`.") + a, b = needle + + if a not in haystack: + if reraise: + raise ValueError(msg.format(a)) + else: + continue + if b not in haystack: + if reraise: + raise ValueError(msg.format(b)) + else: + continue + + filtered.append((a, b)) + + return filtered + + +def _assert_non_empty_sequence( + seq: Hashable | Iterable[Hashable], *, name: str, convert_scalar: bool = True +) -> list[Any]: + if isinstance(seq, str) or not isinstance(seq, Iterable): + if not convert_scalar: + raise TypeError(f"Expected a sequence, found `{type(seq)}`.") + seq = (seq,) + + res, _ = _unique_order_preserving(seq) + if not len(res): + raise ValueError(f"No {name} have been selected.") + + return res + + +def _get_valid_values(needle: Sequence[Any], haystack: Sequence[Any]) -> Sequence[Any]: + res = [n for n in needle if n in haystack] + if not len(res): + raise ValueError(f"No valid values were found. Valid values are `{sorted(set(haystack))}`.") + return res + + +def _assert_positive(value: float, *, name: str) -> None: + if value <= 0: + raise ValueError(f"Expected `{name}` to be positive, found `{value}`.") + + +def _assert_non_negative(value: float, *, name: str) -> None: + if value < 0: + raise ValueError(f"Expected `{name}` to be non-negative, found `{value}`.") + + +def _assert_in_range(value: float, minn: float, maxx: float, *, name: str) -> None: + if not (minn <= value <= maxx): + raise ValueError(f"Expected `{name}` to be in interval `[{minn}, {maxx}]`, found `{value}`.") + + +def _assert_isinstance(value: Any, expected_type: type | tuple[type, ...], *, name: str) -> None: + """Raise TypeError if *value* is not an instance of *expected_type*.""" + if not isinstance(value, expected_type): + if isinstance(expected_type, tuple): + type_names = " or ".join(t.__name__ for t in expected_type) + else: + type_names = expected_type.__name__ + raise TypeError(f"Expected `{name}` to be of type `{type_names}`, got `{type(value).__name__}`.") + + +def _assert_one_of(value: Any, options: Sequence[Any], *, name: str) -> None: + """Raise ValueError if *value* is not in *options*.""" + if value not in options: + raise ValueError(f"Expected `{name}` to be one of `{list(options)}`, got `{value!r}`.") + + +def _assert_key_in(obj: Any, key: str, *, attr: str, obj_name: str, extra_msg: str = "") -> None: + """Raise KeyError if *key* not in ``getattr(obj, attr)``.""" + container = getattr(obj, attr) + if key not in container: + available = list(container.keys()) if hasattr(container, "keys") else list(container) + msg = f"Key `{key!r}` not found in `{obj_name}.{attr}`. Available keys: {available}." + if extra_msg: + msg = f"{msg} {extra_msg}" + raise KeyError(msg) + + +def _assert_key_in_adata(adata: AnnData, key: str, *, attr: str, extra_msg: str = "") -> None: + """Raise KeyError if *key* not in ``getattr(adata, attr)``.""" + _assert_key_in(adata, key, attr=attr, obj_name="adata", extra_msg=extra_msg) + + +def _assert_key_in_sdata(sdata: SpatialData, key: str, *, attr: str, extra_msg: str = "") -> None: + """Raise KeyError if *key* not in ``getattr(sdata, attr)``.""" + _assert_key_in(sdata, key, attr=attr, obj_name="sdata", extra_msg=extra_msg) diff --git a/src/squidpy/experimental/im/_make_tiles.py b/src/squidpy/experimental/im/_make_tiles.py index bd50d398e..58a7e65e8 100644 --- a/src/squidpy/experimental/im/_make_tiles.py +++ b/src/squidpy/experimental/im/_make_tiles.py @@ -17,6 +17,7 @@ from spatialdata.transformations import get_transformation, set_transformation from squidpy._utils import _yx_from_shape +from squidpy._validators import _assert_in_range, _assert_key_in_sdata, _assert_positive from ._utils import _get_element_data @@ -170,8 +171,6 @@ def _get_largest_scale_dimensions( image_key: str, ) -> tuple[int, int]: """Get the dimensions (H, W) of the largest/finest scale of an image.""" - if image_key not in sdata.images: - raise KeyError(f"Image key '{image_key}' not found in sdata.images. Available: {list(sdata.images.keys())}") img_node = sdata.images[image_key] # Use _get_element_data with "scale0" which is always the largest scale @@ -359,12 +358,10 @@ def make_tiles( make_tiles_from_spots Create tiles centered on Visium spots instead of a regular grid. """ - if image_key not in sdata.images: - raise KeyError(f"Image key '{image_key}' not found in sdata.images. Available: {list(sdata.images.keys())}") - if tile_size[0] <= 0 or tile_size[1] <= 0: - raise ValueError(f"tile_size must have positive values, got {tile_size}.") - if not 0 <= min_tissue_fraction <= 1: - raise ValueError(f"min_tissue_fraction must be in [0, 1], got {min_tissue_fraction}.") + _assert_key_in_sdata(sdata, image_key, attr="images") + _assert_positive(tile_size[0], name="tile_size[0]") + _assert_positive(tile_size[1], name="tile_size[1]") + _assert_in_range(min_tissue_fraction, 0, 1, name="min_tissue_fraction") # Derive mask key for centering if needed mask_key_for_grid = image_mask_key @@ -431,8 +428,7 @@ def make_tiles( ) except (ImportError, KeyError, ValueError, RuntimeError) as e: # pragma: no cover - defensive logger.warning("detect_tissue failed (%s); tiles will not be classified.", e) - if classification_mask_key not in sdata.labels: - raise KeyError(f"Tissue mask '{classification_mask_key}' not found in sdata.labels.") + _assert_key_in_sdata(sdata, classification_mask_key, attr="labels") # Use a mask scale that aligns with the full-resolution image; avoid coarsest "auto" selection. if scale == "auto": label_node = sdata.labels.get(classification_mask_key) @@ -532,12 +528,10 @@ def make_tiles_from_spots( Helper used to derive tissue masks automatically when needed. """ - if spots_key not in sdata.shapes: - raise KeyError(f"Spots key '{spots_key}' not found in sdata.shapes") - if image_key is not None and image_key not in sdata.images: - raise KeyError(f"Image key '{image_key}' not found in sdata.images. Available: {list(sdata.images.keys())}") - if not 0 <= min_tissue_fraction <= 1: - raise ValueError(f"min_tissue_fraction must be in [0, 1], got {min_tissue_fraction}.") + _assert_key_in_sdata(sdata, spots_key, attr="shapes") + if image_key is not None: + _assert_key_in_sdata(sdata, image_key, attr="images") + _assert_in_range(min_tissue_fraction, 0, 1, name="min_tissue_fraction") target_cs: str | None = None if image_key is not None: @@ -585,9 +579,6 @@ def make_tiles_from_spots( if classification_mask_key is not None: if image_key is not None: - if image_key not in sdata.images: - raise KeyError(f"Image key '{image_key}' not found in sdata.images") - mask_key = classification_mask_key if mask_key in sdata.labels: target_hw = _get_largest_scale_dimensions(sdata, image_key) @@ -605,8 +596,7 @@ def make_tiles_from_spots( shapes_key=shapes_key, ) else: - if classification_mask_key not in sdata.labels: - raise KeyError(f"Tissue mask '{classification_mask_key}' not found in sdata.labels.") + _assert_key_in_sdata(sdata, classification_mask_key, attr="labels") # Without an image we cannot infer the best scale; default to finest scale unless user specified. scale_used = "scale0" if scale == "auto" else scale _filter_tiles( @@ -697,8 +687,7 @@ def _filter_tiles( mask_key = f"{image_key}_tissue" else: raise ValueError("tissue_mask_key must be provided when image_key is None.") - if mask_key not in sdata.labels: - raise KeyError(f"Tissue mask '{mask_key}' not found in sdata.labels.") + _assert_key_in_sdata(sdata, mask_key, attr="labels") mask = _get_mask_from_labels(sdata, mask_key, scale) H_mask, W_mask = mask.shape @@ -766,10 +755,6 @@ def _make_tiles( scale: str = "auto", ) -> _TileGrid: """Construct a tile grid for an image, optionally centered on a tissue mask.""" - # Validate image key - if image_key not in sdata.images: - raise KeyError(f"Image key '{image_key}' not found in sdata.images") - # Get image dimensions from the largest/finest scale H, W = _get_largest_scale_dimensions(sdata, image_key) @@ -780,10 +765,7 @@ def _make_tiles( return _TileGrid(H, W, tile_size=tile_size) # Path 2: Center grid on tissue mask centroid - if image_mask_key not in sdata.labels: - raise KeyError( - f"Mask key '{image_mask_key}' not found in sdata.labels. Available keys: {list(sdata.labels.keys())}" - ) + _assert_key_in_sdata(sdata, image_mask_key, attr="labels") # Get mask and compute centroid label_node = sdata.labels[image_mask_key] @@ -842,8 +824,6 @@ def _get_spot_coordinates( spots_key: str, ) -> tuple[np.ndarray, np.ndarray]: """Extract spot centers (x, y) and IDs from a shapes table.""" - if spots_key not in sdata.shapes: - raise KeyError(f"Spots key '{spots_key}' not found in sdata.shapes. Available: {list(sdata.shapes.keys())}") gdf = sdata.shapes[spots_key] if "geometry" not in gdf: raise ValueError(f"Shapes '{spots_key}' lack geometry column required for spot coordinates.") @@ -893,9 +873,6 @@ def _derive_tile_size_from_spots(coords: np.ndarray) -> tuple[int, int]: def _get_mask_from_labels(sdata: sd.SpatialData, mask_key: str, scale: str) -> np.ndarray: """Extract a 2D mask array from ``sdata.labels`` at the requested scale.""" - if mask_key not in sdata.labels: - raise KeyError(f"Mask key '{mask_key}' not found in sdata.labels") - label_node = sdata.labels[mask_key] mask_da = _get_element_data(label_node, scale, "label", mask_key) diff --git a/src/squidpy/gr/_niche.py b/src/squidpy/gr/_niche.py index 7de1f7063..e447c965f 100644 --- a/src/squidpy/gr/_niche.py +++ b/src/squidpy/gr/_niche.py @@ -20,6 +20,7 @@ from squidpy._constants._constants import NicheDefinitions from squidpy._docs import d, inject_docs +from squidpy._validators import _assert_isinstance, _assert_key_in_adata, _assert_one_of __all__ = ["calculate_niche"] @@ -175,16 +176,19 @@ def calculate_niche( orig_adata = data adata = data.copy() - if spatial_connectivities_key not in adata.obsp.keys(): - raise KeyError( - f"Key '{spatial_connectivities_key}' not found in `adata.obsp`. " - "If you haven't computed a spatial neighborhood graph yet, use `sq.gr.spatial_neighbors`." - ) + _assert_key_in_adata( + adata, + spatial_connectivities_key, + attr="obsp", + extra_msg="If you haven't computed a spatial neighborhood graph yet, use `sq.gr.spatial_neighbors`.", + ) - if flavor == "spatialleiden" and (latent_connectivities_key not in adata.obsp.keys()): - raise KeyError( - f"Key '{latent_connectivities_key}' not found in `adata.obsp`. " - "If you haven't computed a latent neighborhood graph yet, use `sc.pp.neighbors`." + if flavor == "spatialleiden": + _assert_key_in_adata( + adata, + latent_connectivities_key, + attr="obsp", + extra_msg="If you haven't computed a latent neighborhood graph yet, use `sc.pp.neighbors`.", ) result_columns = _get_result_columns( @@ -195,8 +199,7 @@ def calculate_niche( ) if library_key is not None: - if library_key not in adata.obs.columns: - raise KeyError(f"'{library_key}' not found in `adata.obs`.") + _assert_key_in_adata(adata, library_key, attr="obs") logg.info(f"Stratifying by library_key '{library_key}'") @@ -572,10 +575,7 @@ def _get_cellcharter_niches( if use_rep is not None: # Use provided embedding from adata.obsm - if use_rep not in adata.obsm: - raise KeyError( - f"Embedding key '{use_rep}' not found in adata.obsm. Available keys: {list(adata.obsm.keys())}" - ) + _assert_key_in_adata(adata, use_rep, attr="obsm") embedding = adata.obsm[use_rep] # Ensure embedding has the right number of components if embedding.shape[1] < n_components: @@ -839,17 +839,12 @@ def _validate_niche_args( TypeError If arguments are of incorrect type. """ - if not isinstance(data, AnnData | SpatialData): - raise TypeError(f"'data' must be an AnnData or SpatialData object, got {type(data).__name__}") + _assert_isinstance(data, (AnnData, SpatialData), name="data") - if flavor not in ["neighborhood", "utag", "cellcharter", "spatialleiden"]: - raise ValueError( - f"Invalid flavor '{flavor}'. Please choose one of 'neighborhood', 'utag', 'cellcharter', 'spatialleiden'." - ) + _assert_one_of(flavor, ["neighborhood", "utag", "cellcharter", "spatialleiden"], name="flavor") if library_key is not None: - if not isinstance(library_key, str): - raise TypeError(f"'library_key' must be a string, got {type(library_key).__name__}") + _assert_isinstance(library_key, str, name="library_key") if isinstance(data, AnnData): if library_key not in data.obs.columns: raise ValueError(f"'library_key' must be a column in 'adata.obs', got {library_key}") @@ -861,8 +856,8 @@ def _validate_niche_args( if library_key not in data.tables[table_key].obs.columns: raise ValueError(f"'library_key' must be a column in 'adata.obs', got {library_key}") - if n_neighbors is not None and not isinstance(n_neighbors, int): - raise TypeError(f"'n_neighbors' must be an integer, got {type(n_neighbors).__name__}") + if n_neighbors is not None: + _assert_isinstance(n_neighbors, int, name="n_neighbors") if resolutions is not None: if not isinstance(resolutions, float | tuple | list): @@ -880,14 +875,12 @@ def _validate_niche_args( ): raise TypeError("Each item in the list 'resolutions' must be a float or a tuple of floats.") - if n_hop_weights is not None and not isinstance(n_hop_weights, list): - raise TypeError(f"'n_hop_weights' must be a list of floats, got {type(n_hop_weights).__name__}") + if n_hop_weights is not None: + _assert_isinstance(n_hop_weights, list, name="n_hop_weights") - if not isinstance(scale, bool): - raise TypeError(f"'scale' must be a boolean, got {type(scale).__name__}") + _assert_isinstance(scale, bool, name="scale") - if not isinstance(abs_nhood, bool): - raise TypeError(f"'abs_nhood' must be a boolean, got {type(abs_nhood).__name__}") + _assert_isinstance(abs_nhood, bool, name="abs_nhood") # Define parameters used by each flavor flavor_param_specs = { @@ -976,55 +969,43 @@ def _validate_niche_args( # Flavor-specific validations if flavor == "neighborhood": - if not isinstance(groups, str): - raise TypeError(f"'groups' must be a string, got {type(groups).__name__}") + _assert_isinstance(groups, str, name="groups") - if min_niche_size is not None and not isinstance(min_niche_size, int): - raise TypeError(f"'min_niche_size' must be an integer, got {type(min_niche_size).__name__}") + if min_niche_size is not None: + _assert_isinstance(min_niche_size, int, name="min_niche_size") if distance is not None and isinstance(distance, int) and distance < 1: raise ValueError(f"'distance' must be at least 1, got {distance}") elif flavor == "cellcharter": - if distance is not None and not isinstance(distance, int): - raise TypeError(f"'distance' must be an integer, got {type(distance).__name__}") + if distance is not None: + _assert_isinstance(distance, int, name="distance") if distance is not None and distance < 1: raise ValueError(f"'distance' must be at least 1, got {distance}") - if aggregation is not None and not isinstance(aggregation, str): - raise TypeError(f"'aggregation' must be a string, got {type(aggregation).__name__}") - if aggregation not in ["mean", "variance"]: - raise ValueError(f"'aggregation' must be one of 'mean' or 'variance', got {aggregation}") + if aggregation is not None: + _assert_isinstance(aggregation, str, name="aggregation") + _assert_one_of(aggregation, ["mean", "variance"], name="aggregation") - if not isinstance(n_components, int): - raise TypeError(f"'n_components' must be an integer, got {type(n_components).__name__}") + _assert_isinstance(n_components, int, name="n_components") if n_components < 1: raise ValueError(f"'n_components' must be at least 1, got {n_components}") - if not isinstance(random_state, int): - raise TypeError(f"'random_state' must be an integer, got {type(random_state).__name__}") + _assert_isinstance(random_state, int, name="random_state") - if use_rep is not None and not isinstance(use_rep, str): - raise TypeError(f"'use_rep' must be a string, got {type(use_rep).__name__}") + if use_rep is not None: + _assert_isinstance(use_rep, str, name="use_rep") # for mypy if resolutions is None: resolutions = [0.0] elif flavor == "spatialleiden": - if not isinstance(latent_connectivities_key, str): - raise TypeError( - f"'latent_connectivities_key' must be a string, got {type(latent_connectivities_key).__name__}" - ) - if not isinstance(spatial_connectivities_key, str): - raise TypeError( - f"'spatial_connectivities_key' must be a string, got {type(spatial_connectivities_key).__name__}" - ) + _assert_isinstance(latent_connectivities_key, str, name="latent_connectivities_key") + _assert_isinstance(spatial_connectivities_key, str, name="spatial_connectivities_key") - if not isinstance(layer_ratio, float | int): - raise TypeError(f"'layer_ratio' must be a float, got {type(layer_ratio).__name__}") - if not isinstance(n_iterations, int): - raise TypeError(f"'n_iterations' must be an integer, got {type(n_iterations).__name__}") + _assert_isinstance(layer_ratio, (float, int), name="layer_ratio") + _assert_isinstance(n_iterations, int, name="n_iterations") if not ( isinstance(use_weights, bool) or ( @@ -1034,14 +1015,12 @@ def _validate_niche_args( ) ): raise TypeError(f"'use_weights' must be a bool or a tuple of two bools, got {use_weights!r}") - if not isinstance(random_state, int): - raise TypeError(f"'random_state' must be an integer, got {type(random_state).__name__}") + _assert_isinstance(random_state, int, name="random_state") if resolutions is None: resolutions = [1.0] - if not isinstance(inplace, bool): - raise TypeError(f"'inplace' must be a boolean, got {type(inplace).__name__}") + _assert_isinstance(inplace, bool, name="inplace") def _check_unnecessary_args(flavor: str, param_dict: dict[str, Any], param_specs: dict[str, Any]) -> None: diff --git a/src/squidpy/gr/_ppatterns.py b/src/squidpy/gr/_ppatterns.py index 2080d73fa..7aa6d054c 100644 --- a/src/squidpy/gr/_ppatterns.py +++ b/src/squidpy/gr/_ppatterns.py @@ -24,6 +24,7 @@ from squidpy._constants._pkg_constants import Key from squidpy._docs import d, inject_docs from squidpy._utils import NDArrayA, Signal, SigQueue, _get_n_cores, deprecated_params, parallelize +from squidpy._validators import _assert_key_in_adata from squidpy.gr._utils import ( _assert_categorical_obs, _assert_connectivity_key, @@ -158,8 +159,7 @@ def extract_obs(adata: AnnData, cols: str | Sequence[str] | None) -> tuple[NDArr return adata.obs[cols].T.to_numpy(), cols def extract_obsm(adata: AnnData, ixs: int | Sequence[int] | None) -> tuple[NDArrayA | spmatrix, Sequence[Any]]: - if layer not in adata.obsm: - raise KeyError(f"Key `{layer!r}` not found in `adata.obsm`.") + _assert_key_in_adata(adata, layer, attr="obsm") if ixs is None: ixs = list(np.arange(adata.obsm[layer].shape[1])) ixs = list(np.ravel([ixs])) diff --git a/src/squidpy/gr/_utils.py b/src/squidpy/gr/_utils.py index 40efc10d1..68cb3471d 100644 --- a/src/squidpy/gr/_utils.py +++ b/src/squidpy/gr/_utils.py @@ -2,7 +2,7 @@ from __future__ import annotations -from collections.abc import Hashable, Iterable, Sequence +from collections.abc import Sequence from contextlib import contextmanager from typing import Any @@ -17,38 +17,15 @@ from squidpy._compat import ArrayView, SparseCSCView, SparseCSRView from squidpy._docs import d -from squidpy._utils import NDArrayA, _unique_order_preserving - - -def _check_tuple_needles( - needles: Sequence[tuple[Any, Any]], - haystack: Sequence[Any], - msg: str, - reraise: bool = True, -) -> Sequence[tuple[Any, Any]]: - filtered = [] - - for needle in needles: - if not isinstance(needle, Sequence): - raise TypeError(f"Expected a `Sequence`, found `{type(needle).__name__}`.") - if len(needle) != 2: - raise ValueError(f"Expected a `tuple` of length `2`, found `{len(needle)}`.") - a, b = needle - - if a not in haystack: - if reraise: - raise ValueError(msg.format(a)) - else: - continue - if b not in haystack: - if reraise: - raise ValueError(msg.format(b)) - else: - continue - - filtered.append((a, b)) - - return filtered +from squidpy._utils import NDArrayA +from squidpy._validators import ( # noqa: F401 — re-exported for backwards compatibility + _assert_in_range, + _assert_non_empty_sequence, + _assert_non_negative, + _assert_positive, + _check_tuple_needles, + _get_valid_values, +) def _assert_categorical_obs(adata: AnnData, key: str) -> None: @@ -73,43 +50,6 @@ def _assert_spatial_basis(adata: AnnData, key: str) -> None: raise KeyError(f"Spatial basis `{key}` not found in `adata.obsm`.") -def _assert_non_empty_sequence( - seq: Hashable | Iterable[Hashable], *, name: str, convert_scalar: bool = True -) -> list[Any]: - if isinstance(seq, str) or not isinstance(seq, Iterable): - if not convert_scalar: - raise TypeError(f"Expected a sequence, found `{type(seq)}`.") - seq = (seq,) - - res, _ = _unique_order_preserving(seq) - if not len(res): - raise ValueError(f"No {name} have been selected.") - - return res - - -def _get_valid_values(needle: Sequence[Any], haystack: Sequence[Any]) -> Sequence[Any]: - res = [n for n in needle if n in haystack] - if not len(res): - raise ValueError(f"No valid values were found. Valid values are `{sorted(set(haystack))}`.") - return res - - -def _assert_positive(value: float, *, name: str) -> None: - if value <= 0: - raise ValueError(f"Expected `{name}` to be positive, found `{value}`.") - - -def _assert_non_negative(value: float, *, name: str) -> None: - if value < 0: - raise ValueError(f"Expected `{name}` to be non-negative, found `{value}`.") - - -def _assert_in_range(value: float, minn: float, maxx: float, *, name: str) -> None: - if not (minn <= value <= maxx): - raise ValueError(f"Expected `{name}` to be in interval `[{minn}, {maxx}]`, found `{value}`.") - - def _save_data(adata: AnnData, *, attr: str, key: str, data: Any, prefix: bool = True, time: Any | None = None) -> None: obj = getattr(adata, attr) obj[key] = data diff --git a/src/squidpy/im/_container.py b/src/squidpy/im/_container.py index 9615effc8..b11b6d68f 100644 --- a/src/squidpy/im/_container.py +++ b/src/squidpy/im/_container.py @@ -27,13 +27,13 @@ from squidpy._constants._pkg_constants import Key from squidpy._docs import d, inject_docs from squidpy._utils import NDArrayA, singledispatchmethod -from squidpy.gr._utils import ( +from squidpy._validators import ( _assert_in_range, _assert_non_empty_sequence, _assert_non_negative, _assert_positive, - _assert_spatial_basis, ) +from squidpy.gr._utils import _assert_spatial_basis from squidpy.im._coords import ( _NULL_COORDS, _NULL_PADDING, diff --git a/src/squidpy/im/_coords.py b/src/squidpy/im/_coords.py index 11f3b3e0d..e26f20a43 100644 --- a/src/squidpy/im/_coords.py +++ b/src/squidpy/im/_coords.py @@ -9,7 +9,7 @@ from squidpy._constants._pkg_constants import Key from squidpy._utils import NDArrayA -from squidpy.gr._utils import _assert_non_negative +from squidpy._validators import _assert_non_negative def _circular_mask(arr: NDArrayA, y: int, x: int, radius: float) -> NDArrayA: diff --git a/src/squidpy/im/_feature_mixin.py b/src/squidpy/im/_feature_mixin.py index a1a503c19..6bd1afa81 100644 --- a/src/squidpy/im/_feature_mixin.py +++ b/src/squidpy/im/_feature_mixin.py @@ -12,7 +12,7 @@ from squidpy._constants._pkg_constants import Key from squidpy._docs import d from squidpy._utils import NDArrayA -from squidpy.gr._utils import _assert_non_empty_sequence +from squidpy._validators import _assert_non_empty_sequence from squidpy.im._coords import _NULL_PADDING, CropCoords Feature_t: TypeAlias = dict[str, Any] diff --git a/src/squidpy/pl/_graph.py b/src/squidpy/pl/_graph.py index 852906225..0c75801cd 100644 --- a/src/squidpy/pl/_graph.py +++ b/src/squidpy/pl/_graph.py @@ -17,11 +17,8 @@ from squidpy._constants._constants import RipleyStat from squidpy._constants._pkg_constants import Key from squidpy._docs import d -from squidpy.gr._utils import ( - _assert_categorical_obs, - _assert_non_empty_sequence, - _get_valid_values, -) +from squidpy._validators import _assert_non_empty_sequence, _get_valid_values +from squidpy.gr._utils import _assert_categorical_obs from squidpy.pl._color_utils import Palette_t, _get_palette, _maybe_set_colors from squidpy.pl._utils import _heatmap, save_fig diff --git a/src/squidpy/pl/_spatial_utils.py b/src/squidpy/pl/_spatial_utils.py index c0ba5198c..c7a03bddf 100644 --- a/src/squidpy/pl/_spatial_utils.py +++ b/src/squidpy/pl/_spatial_utils.py @@ -39,6 +39,7 @@ from squidpy._constants._constants import ScatterShape from squidpy._constants._pkg_constants import Key from squidpy._utils import NDArrayA +from squidpy._validators import _assert_key_in_adata from squidpy.im._coords import CropCoords from squidpy.pl._color_utils import _get_palette, _maybe_set_colors from squidpy.pl._utils import _assert_value_in_obs @@ -130,8 +131,7 @@ def _get_library_id( raise ValueError(f"Could not fetch `library_id`, check that `spatial_key: {spatial_key}` is correct.") return library_id if library_key is not None: - if library_key not in adata.obs: - raise KeyError(f"`library_key: {library_key}` not in `adata.obs`.") + _assert_key_in_adata(adata, library_key, attr="obs") if library_id is None: library_id = adata.obs[library_key].cat.categories.tolist() _assert_value_in_obs(adata, key=library_key, val=library_id) @@ -555,10 +555,7 @@ def _plot_edges( from networkx import Graph from networkx.drawing import draw_networkx_edges - if connectivity_key not in adata.obsp: - raise KeyError( - f"Unable to find `connectivity_key: {connectivity_key}` in `adata.obsp`. Please set `connectivity_key`." - ) + _assert_key_in_adata(adata, connectivity_key, attr="obsp", extra_msg="Please set `connectivity_key`.") g = Graph(adata.obsp[connectivity_key]) if not len(g.edges): diff --git a/src/squidpy/pl/_utils.py b/src/squidpy/pl/_utils.py index 48fce8629..ac84b12cb 100644 --- a/src/squidpy/pl/_utils.py +++ b/src/squidpy/pl/_utils.py @@ -39,6 +39,7 @@ from squidpy._constants._pkg_constants import Key from squidpy._docs import d from squidpy._utils import NDArrayA +from squidpy._validators import _assert_in_range, _assert_key_in_adata from squidpy.gr._utils import _assert_categorical_obs Vector_name_t = tuple[pd.Series | NDArrayA | None, str | None] @@ -469,8 +470,7 @@ def _contrasting_color(r: int, g: int, b: int) -> str: def _get_black_or_white(value: float, cmap: mcolors.Colormap) -> str: - if not (0.0 <= value <= 1.0): - raise ValueError(f"Value must be in range `[0, 1]`, found `{value}`.") + _assert_in_range(value, 0.0, 1.0, name="value") r, g, b, *_ = (int(c * 255) for c in cmap(value)) return _contrasting_color(r, g, b) @@ -654,8 +654,7 @@ def sanitize_anndata(adata: AnnData) -> None: def _assert_value_in_obs(adata: AnnData, key: str, val: Sequence[Any] | Any) -> None: - if key not in adata.obs: - raise KeyError(f"Key `{key}` not found in `adata.obs`.") + _assert_key_in_adata(adata, key, attr="obs") if not isinstance(val, list): val = [val] val = set(val) - set(adata.obs[key].unique()) diff --git a/src/squidpy/tl/_var_by_distance.py b/src/squidpy/tl/_var_by_distance.py index 6578f3d63..027087d0f 100644 --- a/src/squidpy/tl/_var_by_distance.py +++ b/src/squidpy/tl/_var_by_distance.py @@ -72,7 +72,7 @@ def var_by_distance( raise ValueError(f"Expected a 1D array for 'groups', but got shape {groups.shape}.") anchor = cast(list[str], groups.astype(str).tolist()) else: - raise TypeError(f"Invalid type for groups: {type(groups)}.") + raise TypeError(f"Expected `groups` to be of type `str or list or ndarray`, got `{type(groups).__name__}`.") # prepare batch key for iteration (None alone in product will result in neutral element) batch: list[str] | list[None] # mypy @@ -89,7 +89,7 @@ def var_by_distance( else: batch = adata.obs[library_key].unique().tolist() else: - raise TypeError(f"Invalid type for library_key: {type(library_key)}.") + raise TypeError(f"Expected `library_key` to be of type `str`, got `{type(library_key).__name__}`.") batch_design_matrices = {} max_distances = {} diff --git a/tests/test_validators.py b/tests/test_validators.py new file mode 100644 index 000000000..79f0ac1a0 --- /dev/null +++ b/tests/test_validators.py @@ -0,0 +1,254 @@ +"""Tests for squidpy._validators.""" + +from __future__ import annotations + +from unittest.mock import MagicMock + +import pytest + +from squidpy._validators import ( + _assert_in_range, + _assert_isinstance, + _assert_key_in_adata, + _assert_key_in_sdata, + _assert_non_empty_sequence, + _assert_non_negative, + _assert_one_of, + _assert_positive, + _check_tuple_needles, + _get_valid_values, +) + + +# --------------------------------------------------------------------------- +# _assert_positive +# --------------------------------------------------------------------------- +class TestAssertPositive: + def test_positive_value(self): + _assert_positive(1.0, name="x") + _assert_positive(0.001, name="x") + + def test_zero_raises(self): + with pytest.raises(ValueError, match="positive"): + _assert_positive(0, name="x") + + def test_negative_raises(self): + with pytest.raises(ValueError, match="positive"): + _assert_positive(-1, name="x") + + +# --------------------------------------------------------------------------- +# _assert_non_negative +# --------------------------------------------------------------------------- +class TestAssertNonNegative: + def test_non_negative_value(self): + _assert_non_negative(0, name="x") + _assert_non_negative(1, name="x") + + def test_negative_raises(self): + with pytest.raises(ValueError, match="non-negative"): + _assert_non_negative(-0.1, name="x") + + +# --------------------------------------------------------------------------- +# _assert_in_range +# --------------------------------------------------------------------------- +class TestAssertInRange: + def test_in_range(self): + _assert_in_range(0.5, 0, 1, name="x") + _assert_in_range(0, 0, 1, name="x") + _assert_in_range(1, 0, 1, name="x") + + def test_out_of_range(self): + with pytest.raises(ValueError, match="interval"): + _assert_in_range(1.1, 0, 1, name="x") + with pytest.raises(ValueError, match="interval"): + _assert_in_range(-0.1, 0, 1, name="x") + + +# --------------------------------------------------------------------------- +# _assert_non_empty_sequence +# --------------------------------------------------------------------------- +class TestAssertNonEmptySequence: + def test_list(self): + assert _assert_non_empty_sequence(["a", "b"], name="items") == ["a", "b"] + + def test_scalar_conversion(self): + assert _assert_non_empty_sequence("a", name="items") == ["a"] + + def test_no_scalar_conversion(self): + with pytest.raises(TypeError, match="sequence"): + _assert_non_empty_sequence(42, name="items", convert_scalar=False) + + def test_empty_raises(self): + with pytest.raises(ValueError, match="No items"): + _assert_non_empty_sequence([], name="items") + + +# --------------------------------------------------------------------------- +# _get_valid_values +# --------------------------------------------------------------------------- +class TestGetValidValues: + def test_valid(self): + assert _get_valid_values(["a", "b"], ["a", "b", "c"]) == ["a", "b"] + + def test_partial(self): + assert _get_valid_values(["a", "z"], ["a", "b"]) == ["a"] + + def test_none_valid(self): + with pytest.raises(ValueError, match="No valid values"): + _get_valid_values(["z"], ["a", "b"]) + + +# --------------------------------------------------------------------------- +# _check_tuple_needles +# --------------------------------------------------------------------------- +class TestCheckTupleNeedles: + def test_valid_needles(self): + result = _check_tuple_needles([("a", "b")], ["a", "b", "c"], "Value `{}` not found.") + assert result == [("a", "b")] + + def test_invalid_needle_reraise(self): + with pytest.raises(ValueError, match="z"): + _check_tuple_needles([("z", "b")], ["a", "b"], "Value `{}` not found.") + + def test_invalid_needle_no_reraise(self): + result = _check_tuple_needles([("z", "b")], ["a", "b"], "Value `{}` not found.", reraise=False) + assert result == [] + + def test_wrong_length(self): + with pytest.raises(ValueError, match="length"): + _check_tuple_needles([("a",)], ["a"], "msg {}") + + def test_not_sequence(self): + with pytest.raises(TypeError, match="Sequence"): + _check_tuple_needles([42], ["a"], "msg {}") + + +# --------------------------------------------------------------------------- +# _assert_isinstance +# --------------------------------------------------------------------------- +class TestAssertIsinstance: + def test_correct_type(self): + _assert_isinstance("hello", str, name="x") + _assert_isinstance(42, int, name="x") + + def test_tuple_of_types(self): + _assert_isinstance("hello", (str, int), name="x") + _assert_isinstance(42, (str, int), name="x") + + def test_wrong_type(self): + with pytest.raises(TypeError, match="str"): + _assert_isinstance(42, str, name="x") + + def test_wrong_type_tuple(self): + with pytest.raises(TypeError, match="str or int"): + _assert_isinstance(3.14, (str, int), name="x") + + +# --------------------------------------------------------------------------- +# _assert_one_of +# --------------------------------------------------------------------------- +class TestAssertOneOf: + def test_valid(self): + _assert_one_of("a", ["a", "b", "c"], name="x") + + def test_invalid(self): + with pytest.raises(ValueError, match="one of"): + _assert_one_of("z", ["a", "b"], name="x") + + +# --------------------------------------------------------------------------- +# _assert_key_in_adata +# --------------------------------------------------------------------------- +class TestAssertKeyInAdata: + def test_key_present(self): + adata = MagicMock() + adata.obs = {"cell_type": [1, 2, 3]} + _assert_key_in_adata(adata, "cell_type", attr="obs") + + def test_key_missing(self): + adata = MagicMock() + adata.obs = {"cell_type": [1, 2, 3]} + with pytest.raises(KeyError, match="missing_key"): + _assert_key_in_adata(adata, "missing_key", attr="obs") + + def test_extra_msg(self): + adata = MagicMock() + adata.obs = {} + with pytest.raises(KeyError, match="Run this first"): + _assert_key_in_adata(adata, "key", attr="obs", extra_msg="Run this first.") + + def test_lists_available_keys(self): + adata = MagicMock() + adata.obs = {"a": 1, "b": 2} + with pytest.raises(KeyError, match="Available keys"): + _assert_key_in_adata(adata, "missing", attr="obs") + + def test_container_without_keys_method(self): + """Fallback to list(container) when .keys() is not available.""" + adata = MagicMock() + adata.obsm = ["X_pca", "X_umap"] # list has no .keys() + with pytest.raises(KeyError, match="X_spatial"): + _assert_key_in_adata(adata, "X_spatial", attr="obsm") + + +# --------------------------------------------------------------------------- +# _assert_key_in_sdata +# --------------------------------------------------------------------------- +class TestAssertKeyInSdata: + def test_key_present(self): + sdata = MagicMock() + sdata.images = {"image1": "data"} + _assert_key_in_sdata(sdata, "image1", attr="images") + + def test_key_missing(self): + sdata = MagicMock() + sdata.images = {"image1": "data"} + with pytest.raises(KeyError, match="missing"): + _assert_key_in_sdata(sdata, "missing", attr="images") + + def test_extra_msg(self): + sdata = MagicMock() + sdata.labels = {} + with pytest.raises(KeyError, match="Please provide"): + _assert_key_in_sdata(sdata, "mask", attr="labels", extra_msg="Please provide a mask.") + + def test_lists_available_keys(self): + sdata = MagicMock() + sdata.images = {"img1": "data", "img2": "data"} + with pytest.raises(KeyError, match="Available keys"): + _assert_key_in_sdata(sdata, "missing", attr="images") + + +# --------------------------------------------------------------------------- +# _assert_isinstance edge cases +# --------------------------------------------------------------------------- +class TestAssertIsinstanceEdgeCases: + def test_bool_is_subclass_of_int(self): + """bool is a subclass of int — _assert_isinstance(True, int) passes.""" + _assert_isinstance(True, int, name="x") + + def test_none_type(self): + with pytest.raises(TypeError, match="str"): + _assert_isinstance(None, str, name="x") + + +# --------------------------------------------------------------------------- +# Re-export smoke test +# --------------------------------------------------------------------------- +class TestReExports: + def test_gr_utils_reexports(self): + from squidpy.gr._utils import ( + _assert_in_range, + _assert_non_empty_sequence, + _assert_non_negative, + _assert_positive, + _check_tuple_needles, + _get_valid_values, + ) + + # Just verify they are the same objects + from squidpy._validators import _assert_positive as _ap + + assert _assert_positive is _ap From 1ee0ef7853ad42e22c8cedd29538bf2b1bd84881 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 10 Mar 2026 16:52:00 +0000 Subject: [PATCH 2/9] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- tests/test_validators.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/test_validators.py b/tests/test_validators.py index 79f0ac1a0..4256b7512 100644 --- a/tests/test_validators.py +++ b/tests/test_validators.py @@ -239,6 +239,8 @@ def test_none_type(self): # --------------------------------------------------------------------------- class TestReExports: def test_gr_utils_reexports(self): + # Just verify they are the same objects + from squidpy._validators import _assert_positive as _ap from squidpy.gr._utils import ( _assert_in_range, _assert_non_empty_sequence, @@ -248,7 +250,4 @@ def test_gr_utils_reexports(self): _get_valid_values, ) - # Just verify they are the same objects - from squidpy._validators import _assert_positive as _ap - assert _assert_positive is _ap From 5fb81089bbe49b417406f65c5856b72d7eae0610 Mon Sep 17 00:00:00 2001 From: Tim Treis Date: Tue, 10 Mar 2026 17:56:12 +0100 Subject: [PATCH 3/9] Fix ruff F401: use all re-exported imports in smoke test Co-Authored-By: Claude Opus 4.6 --- tests/test_validators.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/tests/test_validators.py b/tests/test_validators.py index 4256b7512..77cd19d30 100644 --- a/tests/test_validators.py +++ b/tests/test_validators.py @@ -239,8 +239,15 @@ def test_none_type(self): # --------------------------------------------------------------------------- class TestReExports: def test_gr_utils_reexports(self): - # Just verify they are the same objects - from squidpy._validators import _assert_positive as _ap + """Verify all re-exported validators are the same objects as the originals.""" + from squidpy._validators import ( + _assert_in_range as _orig_air, + _assert_non_empty_sequence as _orig_anes, + _assert_non_negative as _orig_ann, + _assert_positive as _orig_ap, + _check_tuple_needles as _orig_ctn, + _get_valid_values as _orig_gvv, + ) from squidpy.gr._utils import ( _assert_in_range, _assert_non_empty_sequence, @@ -250,4 +257,9 @@ def test_gr_utils_reexports(self): _get_valid_values, ) - assert _assert_positive is _ap + assert _assert_positive is _orig_ap + assert _assert_non_negative is _orig_ann + assert _assert_in_range is _orig_air + assert _assert_non_empty_sequence is _orig_anes + assert _check_tuple_needles is _orig_ctn + assert _get_valid_values is _orig_gvv From 57cb3db4b0572ebe39e8317b606a15a536c25796 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 10 Mar 2026 16:56:23 +0000 Subject: [PATCH 4/9] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- tests/test_validators.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/test_validators.py b/tests/test_validators.py index 77cd19d30..21ba7c7d8 100644 --- a/tests/test_validators.py +++ b/tests/test_validators.py @@ -242,10 +242,20 @@ def test_gr_utils_reexports(self): """Verify all re-exported validators are the same objects as the originals.""" from squidpy._validators import ( _assert_in_range as _orig_air, + ) + from squidpy._validators import ( _assert_non_empty_sequence as _orig_anes, + ) + from squidpy._validators import ( _assert_non_negative as _orig_ann, + ) + from squidpy._validators import ( _assert_positive as _orig_ap, + ) + from squidpy._validators import ( _check_tuple_needles as _orig_ctn, + ) + from squidpy._validators import ( _get_valid_values as _orig_gvv, ) from squidpy.gr._utils import ( From ee429146cfdb6320ba41efeafbc0451e80593679 Mon Sep 17 00:00:00 2001 From: Tim Treis Date: Wed, 11 Mar 2026 13:55:50 +0100 Subject: [PATCH 5/9] Drop leading underscore from validator function names Since _validators.py is already a private module, make the functions public within it (e.g. assert_positive instead of _assert_positive). Re-exports in gr/_utils.py still provide the old _-prefixed names for backwards compatibility. Co-Authored-By: Claude Opus 4.6 --- src/squidpy/_validators.py | 26 ++-- src/squidpy/experimental/im/_make_tiles.py | 24 +-- src/squidpy/gr/_niche.py | 52 +++---- src/squidpy/gr/_ppatterns.py | 4 +- src/squidpy/gr/_utils.py | 12 +- src/squidpy/im/_container.py | 34 ++--- src/squidpy/im/_coords.py | 10 +- src/squidpy/im/_feature_mixin.py | 20 +-- src/squidpy/pl/_graph.py | 10 +- src/squidpy/pl/_spatial_utils.py | 6 +- src/squidpy/pl/_utils.py | 6 +- tests/test_validators.py | 164 ++++++++++----------- 12 files changed, 179 insertions(+), 189 deletions(-) diff --git a/src/squidpy/_validators.py b/src/squidpy/_validators.py index 4866dade9..debba1db0 100644 --- a/src/squidpy/_validators.py +++ b/src/squidpy/_validators.py @@ -12,7 +12,7 @@ from spatialdata import SpatialData -def _check_tuple_needles( +def check_tuple_needles( needles: Sequence[tuple[Any, Any]], haystack: Sequence[Any], msg: str, @@ -43,7 +43,7 @@ def _check_tuple_needles( return filtered -def _assert_non_empty_sequence( +def assert_non_empty_sequence( seq: Hashable | Iterable[Hashable], *, name: str, convert_scalar: bool = True ) -> list[Any]: if isinstance(seq, str) or not isinstance(seq, Iterable): @@ -58,29 +58,29 @@ def _assert_non_empty_sequence( return res -def _get_valid_values(needle: Sequence[Any], haystack: Sequence[Any]) -> Sequence[Any]: +def get_valid_values(needle: Sequence[Any], haystack: Sequence[Any]) -> Sequence[Any]: res = [n for n in needle if n in haystack] if not len(res): raise ValueError(f"No valid values were found. Valid values are `{sorted(set(haystack))}`.") return res -def _assert_positive(value: float, *, name: str) -> None: +def assert_positive(value: float, *, name: str) -> None: if value <= 0: raise ValueError(f"Expected `{name}` to be positive, found `{value}`.") -def _assert_non_negative(value: float, *, name: str) -> None: +def assert_non_negative(value: float, *, name: str) -> None: if value < 0: raise ValueError(f"Expected `{name}` to be non-negative, found `{value}`.") -def _assert_in_range(value: float, minn: float, maxx: float, *, name: str) -> None: +def assert_in_range(value: float, minn: float, maxx: float, *, name: str) -> None: if not (minn <= value <= maxx): raise ValueError(f"Expected `{name}` to be in interval `[{minn}, {maxx}]`, found `{value}`.") -def _assert_isinstance(value: Any, expected_type: type | tuple[type, ...], *, name: str) -> None: +def assert_isinstance(value: Any, expected_type: type | tuple[type, ...], *, name: str) -> None: """Raise TypeError if *value* is not an instance of *expected_type*.""" if not isinstance(value, expected_type): if isinstance(expected_type, tuple): @@ -90,13 +90,13 @@ def _assert_isinstance(value: Any, expected_type: type | tuple[type, ...], *, na raise TypeError(f"Expected `{name}` to be of type `{type_names}`, got `{type(value).__name__}`.") -def _assert_one_of(value: Any, options: Sequence[Any], *, name: str) -> None: +def assert_one_of(value: Any, options: Sequence[Any], *, name: str) -> None: """Raise ValueError if *value* is not in *options*.""" if value not in options: raise ValueError(f"Expected `{name}` to be one of `{list(options)}`, got `{value!r}`.") -def _assert_key_in(obj: Any, key: str, *, attr: str, obj_name: str, extra_msg: str = "") -> None: +def assert_key_in(obj: Any, key: str, *, attr: str, obj_name: str, extra_msg: str = "") -> None: """Raise KeyError if *key* not in ``getattr(obj, attr)``.""" container = getattr(obj, attr) if key not in container: @@ -107,11 +107,11 @@ def _assert_key_in(obj: Any, key: str, *, attr: str, obj_name: str, extra_msg: s raise KeyError(msg) -def _assert_key_in_adata(adata: AnnData, key: str, *, attr: str, extra_msg: str = "") -> None: +def assert_key_in_adata(adata: AnnData, key: str, *, attr: str, extra_msg: str = "") -> None: """Raise KeyError if *key* not in ``getattr(adata, attr)``.""" - _assert_key_in(adata, key, attr=attr, obj_name="adata", extra_msg=extra_msg) + assert_key_in(adata, key, attr=attr, obj_name="adata", extra_msg=extra_msg) -def _assert_key_in_sdata(sdata: SpatialData, key: str, *, attr: str, extra_msg: str = "") -> None: +def assert_key_in_sdata(sdata: SpatialData, key: str, *, attr: str, extra_msg: str = "") -> None: """Raise KeyError if *key* not in ``getattr(sdata, attr)``.""" - _assert_key_in(sdata, key, attr=attr, obj_name="sdata", extra_msg=extra_msg) + assert_key_in(sdata, key, attr=attr, obj_name="sdata", extra_msg=extra_msg) diff --git a/src/squidpy/experimental/im/_make_tiles.py b/src/squidpy/experimental/im/_make_tiles.py index 58a7e65e8..1c840fa3e 100644 --- a/src/squidpy/experimental/im/_make_tiles.py +++ b/src/squidpy/experimental/im/_make_tiles.py @@ -17,7 +17,7 @@ from spatialdata.transformations import get_transformation, set_transformation from squidpy._utils import _yx_from_shape -from squidpy._validators import _assert_in_range, _assert_key_in_sdata, _assert_positive +from squidpy._validators import assert_in_range, assert_key_in_sdata, assert_positive from ._utils import _get_element_data @@ -358,10 +358,10 @@ def make_tiles( make_tiles_from_spots Create tiles centered on Visium spots instead of a regular grid. """ - _assert_key_in_sdata(sdata, image_key, attr="images") - _assert_positive(tile_size[0], name="tile_size[0]") - _assert_positive(tile_size[1], name="tile_size[1]") - _assert_in_range(min_tissue_fraction, 0, 1, name="min_tissue_fraction") + assert_key_in_sdata(sdata, image_key, attr="images") + assert_positive(tile_size[0], name="tile_size[0]") + assert_positive(tile_size[1], name="tile_size[1]") + assert_in_range(min_tissue_fraction, 0, 1, name="min_tissue_fraction") # Derive mask key for centering if needed mask_key_for_grid = image_mask_key @@ -428,7 +428,7 @@ def make_tiles( ) except (ImportError, KeyError, ValueError, RuntimeError) as e: # pragma: no cover - defensive logger.warning("detect_tissue failed (%s); tiles will not be classified.", e) - _assert_key_in_sdata(sdata, classification_mask_key, attr="labels") + assert_key_in_sdata(sdata, classification_mask_key, attr="labels") # Use a mask scale that aligns with the full-resolution image; avoid coarsest "auto" selection. if scale == "auto": label_node = sdata.labels.get(classification_mask_key) @@ -528,10 +528,10 @@ def make_tiles_from_spots( Helper used to derive tissue masks automatically when needed. """ - _assert_key_in_sdata(sdata, spots_key, attr="shapes") + assert_key_in_sdata(sdata, spots_key, attr="shapes") if image_key is not None: - _assert_key_in_sdata(sdata, image_key, attr="images") - _assert_in_range(min_tissue_fraction, 0, 1, name="min_tissue_fraction") + assert_key_in_sdata(sdata, image_key, attr="images") + assert_in_range(min_tissue_fraction, 0, 1, name="min_tissue_fraction") target_cs: str | None = None if image_key is not None: @@ -596,7 +596,7 @@ def make_tiles_from_spots( shapes_key=shapes_key, ) else: - _assert_key_in_sdata(sdata, classification_mask_key, attr="labels") + assert_key_in_sdata(sdata, classification_mask_key, attr="labels") # Without an image we cannot infer the best scale; default to finest scale unless user specified. scale_used = "scale0" if scale == "auto" else scale _filter_tiles( @@ -687,7 +687,7 @@ def _filter_tiles( mask_key = f"{image_key}_tissue" else: raise ValueError("tissue_mask_key must be provided when image_key is None.") - _assert_key_in_sdata(sdata, mask_key, attr="labels") + assert_key_in_sdata(sdata, mask_key, attr="labels") mask = _get_mask_from_labels(sdata, mask_key, scale) H_mask, W_mask = mask.shape @@ -765,7 +765,7 @@ def _make_tiles( return _TileGrid(H, W, tile_size=tile_size) # Path 2: Center grid on tissue mask centroid - _assert_key_in_sdata(sdata, image_mask_key, attr="labels") + assert_key_in_sdata(sdata, image_mask_key, attr="labels") # Get mask and compute centroid label_node = sdata.labels[image_mask_key] diff --git a/src/squidpy/gr/_niche.py b/src/squidpy/gr/_niche.py index e447c965f..e09e858ad 100644 --- a/src/squidpy/gr/_niche.py +++ b/src/squidpy/gr/_niche.py @@ -20,7 +20,7 @@ from squidpy._constants._constants import NicheDefinitions from squidpy._docs import d, inject_docs -from squidpy._validators import _assert_isinstance, _assert_key_in_adata, _assert_one_of +from squidpy._validators import assert_isinstance, assert_key_in_adata, assert_one_of __all__ = ["calculate_niche"] @@ -176,7 +176,7 @@ def calculate_niche( orig_adata = data adata = data.copy() - _assert_key_in_adata( + assert_key_in_adata( adata, spatial_connectivities_key, attr="obsp", @@ -184,7 +184,7 @@ def calculate_niche( ) if flavor == "spatialleiden": - _assert_key_in_adata( + assert_key_in_adata( adata, latent_connectivities_key, attr="obsp", @@ -199,7 +199,7 @@ def calculate_niche( ) if library_key is not None: - _assert_key_in_adata(adata, library_key, attr="obs") + assert_key_in_adata(adata, library_key, attr="obs") logg.info(f"Stratifying by library_key '{library_key}'") @@ -575,7 +575,7 @@ def _get_cellcharter_niches( if use_rep is not None: # Use provided embedding from adata.obsm - _assert_key_in_adata(adata, use_rep, attr="obsm") + assert_key_in_adata(adata, use_rep, attr="obsm") embedding = adata.obsm[use_rep] # Ensure embedding has the right number of components if embedding.shape[1] < n_components: @@ -839,12 +839,12 @@ def _validate_niche_args( TypeError If arguments are of incorrect type. """ - _assert_isinstance(data, (AnnData, SpatialData), name="data") + assert_isinstance(data, (AnnData, SpatialData), name="data") - _assert_one_of(flavor, ["neighborhood", "utag", "cellcharter", "spatialleiden"], name="flavor") + assert_one_of(flavor, ["neighborhood", "utag", "cellcharter", "spatialleiden"], name="flavor") if library_key is not None: - _assert_isinstance(library_key, str, name="library_key") + assert_isinstance(library_key, str, name="library_key") if isinstance(data, AnnData): if library_key not in data.obs.columns: raise ValueError(f"'library_key' must be a column in 'adata.obs', got {library_key}") @@ -857,7 +857,7 @@ def _validate_niche_args( raise ValueError(f"'library_key' must be a column in 'adata.obs', got {library_key}") if n_neighbors is not None: - _assert_isinstance(n_neighbors, int, name="n_neighbors") + assert_isinstance(n_neighbors, int, name="n_neighbors") if resolutions is not None: if not isinstance(resolutions, float | tuple | list): @@ -876,11 +876,11 @@ def _validate_niche_args( raise TypeError("Each item in the list 'resolutions' must be a float or a tuple of floats.") if n_hop_weights is not None: - _assert_isinstance(n_hop_weights, list, name="n_hop_weights") + assert_isinstance(n_hop_weights, list, name="n_hop_weights") - _assert_isinstance(scale, bool, name="scale") + assert_isinstance(scale, bool, name="scale") - _assert_isinstance(abs_nhood, bool, name="abs_nhood") + assert_isinstance(abs_nhood, bool, name="abs_nhood") # Define parameters used by each flavor flavor_param_specs = { @@ -969,43 +969,43 @@ def _validate_niche_args( # Flavor-specific validations if flavor == "neighborhood": - _assert_isinstance(groups, str, name="groups") + assert_isinstance(groups, str, name="groups") if min_niche_size is not None: - _assert_isinstance(min_niche_size, int, name="min_niche_size") + assert_isinstance(min_niche_size, int, name="min_niche_size") if distance is not None and isinstance(distance, int) and distance < 1: raise ValueError(f"'distance' must be at least 1, got {distance}") elif flavor == "cellcharter": if distance is not None: - _assert_isinstance(distance, int, name="distance") + assert_isinstance(distance, int, name="distance") if distance is not None and distance < 1: raise ValueError(f"'distance' must be at least 1, got {distance}") if aggregation is not None: - _assert_isinstance(aggregation, str, name="aggregation") - _assert_one_of(aggregation, ["mean", "variance"], name="aggregation") + assert_isinstance(aggregation, str, name="aggregation") + assert_one_of(aggregation, ["mean", "variance"], name="aggregation") - _assert_isinstance(n_components, int, name="n_components") + assert_isinstance(n_components, int, name="n_components") if n_components < 1: raise ValueError(f"'n_components' must be at least 1, got {n_components}") - _assert_isinstance(random_state, int, name="random_state") + assert_isinstance(random_state, int, name="random_state") if use_rep is not None: - _assert_isinstance(use_rep, str, name="use_rep") + assert_isinstance(use_rep, str, name="use_rep") # for mypy if resolutions is None: resolutions = [0.0] elif flavor == "spatialleiden": - _assert_isinstance(latent_connectivities_key, str, name="latent_connectivities_key") - _assert_isinstance(spatial_connectivities_key, str, name="spatial_connectivities_key") + assert_isinstance(latent_connectivities_key, str, name="latent_connectivities_key") + assert_isinstance(spatial_connectivities_key, str, name="spatial_connectivities_key") - _assert_isinstance(layer_ratio, (float, int), name="layer_ratio") - _assert_isinstance(n_iterations, int, name="n_iterations") + assert_isinstance(layer_ratio, (float, int), name="layer_ratio") + assert_isinstance(n_iterations, int, name="n_iterations") if not ( isinstance(use_weights, bool) or ( @@ -1015,12 +1015,12 @@ def _validate_niche_args( ) ): raise TypeError(f"'use_weights' must be a bool or a tuple of two bools, got {use_weights!r}") - _assert_isinstance(random_state, int, name="random_state") + assert_isinstance(random_state, int, name="random_state") if resolutions is None: resolutions = [1.0] - _assert_isinstance(inplace, bool, name="inplace") + assert_isinstance(inplace, bool, name="inplace") def _check_unnecessary_args(flavor: str, param_dict: dict[str, Any], param_specs: dict[str, Any]) -> None: diff --git a/src/squidpy/gr/_ppatterns.py b/src/squidpy/gr/_ppatterns.py index 7aa6d054c..a960b6862 100644 --- a/src/squidpy/gr/_ppatterns.py +++ b/src/squidpy/gr/_ppatterns.py @@ -24,7 +24,7 @@ from squidpy._constants._pkg_constants import Key from squidpy._docs import d, inject_docs from squidpy._utils import NDArrayA, Signal, SigQueue, _get_n_cores, deprecated_params, parallelize -from squidpy._validators import _assert_key_in_adata +from squidpy._validators import assert_key_in_adata from squidpy.gr._utils import ( _assert_categorical_obs, _assert_connectivity_key, @@ -159,7 +159,7 @@ def extract_obs(adata: AnnData, cols: str | Sequence[str] | None) -> tuple[NDArr return adata.obs[cols].T.to_numpy(), cols def extract_obsm(adata: AnnData, ixs: int | Sequence[int] | None) -> tuple[NDArrayA | spmatrix, Sequence[Any]]: - _assert_key_in_adata(adata, layer, attr="obsm") + assert_key_in_adata(adata, layer, attr="obsm") if ixs is None: ixs = list(np.arange(adata.obsm[layer].shape[1])) ixs = list(np.ravel([ixs])) diff --git a/src/squidpy/gr/_utils.py b/src/squidpy/gr/_utils.py index 68cb3471d..a9b4147a1 100644 --- a/src/squidpy/gr/_utils.py +++ b/src/squidpy/gr/_utils.py @@ -19,12 +19,12 @@ from squidpy._docs import d from squidpy._utils import NDArrayA from squidpy._validators import ( # noqa: F401 — re-exported for backwards compatibility - _assert_in_range, - _assert_non_empty_sequence, - _assert_non_negative, - _assert_positive, - _check_tuple_needles, - _get_valid_values, + assert_in_range as _assert_in_range, + assert_non_empty_sequence as _assert_non_empty_sequence, + assert_non_negative as _assert_non_negative, + assert_positive as _assert_positive, + check_tuple_needles as _check_tuple_needles, + get_valid_values as _get_valid_values, ) diff --git a/src/squidpy/im/_container.py b/src/squidpy/im/_container.py index b11b6d68f..663c58313 100644 --- a/src/squidpy/im/_container.py +++ b/src/squidpy/im/_container.py @@ -28,10 +28,10 @@ from squidpy._docs import d, inject_docs from squidpy._utils import NDArrayA, singledispatchmethod from squidpy._validators import ( - _assert_in_range, - _assert_non_empty_sequence, - _assert_non_negative, - _assert_positive, + assert_in_range, + assert_non_empty_sequence, + assert_non_negative, + assert_positive, ) from squidpy.gr._utils import _assert_spatial_basis from squidpy.im._coords import ( @@ -518,9 +518,9 @@ def crop_corner( size = self._convert_to_pixel_space(size) ys, xs = size - _assert_positive(ys, name="height") - _assert_positive(xs, name="width") - _assert_positive(scale, name="scale") + assert_positive(ys, name="height") + assert_positive(xs, name="width") + assert_positive(scale, name="scale") orig = CropCoords(x0=x, y0=y, x1=x + xs, y1=y + ys) @@ -659,15 +659,15 @@ def crop_center( %(crop_corner.returns)s """ y, x = self._convert_to_pixel_space((y, x)) - _assert_in_range(y, 0, self.shape[0], name="height") - _assert_in_range(x, 0, self.shape[1], name="width") + assert_in_range(y, 0, self.shape[0], name="height") + assert_in_range(x, 0, self.shape[1], name="width") if not isinstance(radius, Iterable): radius = (radius, radius) (yr, xr) = self._convert_to_pixel_space(radius) - _assert_non_negative(yr, name="radius height") - _assert_non_negative(xr, name="radius width") + assert_non_negative(yr, name="radius height") + assert_non_negative(xr, name="radius width") return self.crop_corner( # type: ignore[no-any-return] y=y - yr, x=x - xr, size=(yr * 2 + 1, xr * 2 + 1), **kwargs @@ -708,8 +708,8 @@ def generate_equal_crops( y, x = self.shape ys, xs = size - _assert_in_range(ys, 0, y, name="height") - _assert_in_range(xs, 0, x, name="width") + assert_in_range(ys, 0, y, name="height") + assert_in_range(xs, 0, x, name="width") unique_ycoord = np.arange(start=0, stop=(y // ys + (y % ys != 0)) * ys, step=ys) unique_xcoord = np.arange(start=0, stop=(x // xs + (x % xs != 0)) * xs, step=xs) @@ -770,13 +770,13 @@ def generate_spot_crops( The type of the crops depends on ``as_array`` and the number of dimensions on ``squeeze``. """ self._assert_not_empty() - _assert_positive(spot_scale, name="scale") + assert_positive(spot_scale, name="scale") _assert_spatial_basis(adata, spatial_key) # limit to obs_names if obs_names is None: obs_names = adata.obs_names - obs_names = _assert_non_empty_sequence(obs_names, name="observations") + obs_names = assert_non_empty_sequence(obs_names, name="observations") adata = adata[obs_names, :] scale = self.data.attrs.get(Key.img.scale, 1) @@ -1497,10 +1497,10 @@ def _get_size(self, size: FoI_t | tuple[FoI_t | None, FoI_t | None] | None) -> t def _convert_to_pixel_space(self, size: tuple[FoI_t, FoI_t]) -> tuple[int, int]: y, x = size if isinstance(y, float): - _assert_in_range(y, 0, 1, name="y") + assert_in_range(y, 0, 1, name="y") y = int(self.shape[0] * y) if isinstance(x, float): - _assert_in_range(x, 0, 1, name="x") + assert_in_range(x, 0, 1, name="x") x = int(self.shape[1] * x) return y, x diff --git a/src/squidpy/im/_coords.py b/src/squidpy/im/_coords.py index e26f20a43..ef93ee40f 100644 --- a/src/squidpy/im/_coords.py +++ b/src/squidpy/im/_coords.py @@ -9,7 +9,7 @@ from squidpy._constants._pkg_constants import Key from squidpy._utils import NDArrayA -from squidpy._validators import _assert_non_negative +from squidpy._validators import assert_non_negative def _circular_mask(arr: NDArrayA, y: int, x: int, radius: float) -> NDArrayA: @@ -139,10 +139,10 @@ class CropPadding(TupleSerializer): y_post: float def __post_init__(self) -> None: - _assert_non_negative(self.x_pre, name="x_pre") - _assert_non_negative(self.y_pre, name="y_pre") - _assert_non_negative(self.x_post, name="x_post") - _assert_non_negative(self.y_post, name="y_post") + assert_non_negative(self.x_pre, name="x_pre") + assert_non_negative(self.y_pre, name="y_pre") + assert_non_negative(self.x_post, name="x_post") + assert_non_negative(self.y_post, name="y_post") @property def T(self) -> CropPadding: diff --git a/src/squidpy/im/_feature_mixin.py b/src/squidpy/im/_feature_mixin.py index 6bd1afa81..1b59f25a8 100644 --- a/src/squidpy/im/_feature_mixin.py +++ b/src/squidpy/im/_feature_mixin.py @@ -12,7 +12,7 @@ from squidpy._constants._pkg_constants import Key from squidpy._docs import d from squidpy._utils import NDArrayA -from squidpy._validators import _assert_non_empty_sequence +from squidpy._validators import assert_non_empty_sequence from squidpy.im._coords import _NULL_PADDING, CropCoords Feature_t: TypeAlias = dict[str, Any] @@ -113,9 +113,9 @@ def features_summary( library_id = self._get_library_id(library_id) arr = self[layer].sel(z=library_id) - quantiles = _assert_non_empty_sequence(quantiles, name="quantiles") + quantiles = assert_non_empty_sequence(quantiles, name="quantiles") channels = _get_channels(arr, channels) - channels = _assert_non_empty_sequence(channels, name="channels") + channels = assert_non_empty_sequence(channels, name="channels") features = {} for c in channels: @@ -164,7 +164,7 @@ def features_histogram( arr = self[layer].sel(z=library_id) channels = _get_channels(arr, channels) - channels = _assert_non_empty_sequence(channels, name="channels") + channels = assert_non_empty_sequence(channels, name="channels") # if v_range is None, use whole-image range if v_range is None: @@ -238,12 +238,12 @@ def features_texture( layer = self._get_layer(layer) library_id = self._get_library_id(library_id) - props = _assert_non_empty_sequence(props, name="properties") - angles = _assert_non_empty_sequence(angles, name="angles") - distances = _assert_non_empty_sequence(distances, name="distances") + props = assert_non_empty_sequence(props, name="properties") + angles = assert_non_empty_sequence(angles, name="angles") + distances = assert_non_empty_sequence(distances, name="distances") channels = _get_channels(self[layer], channels) - channels = _assert_non_empty_sequence(channels, name="channels") + channels = assert_non_empty_sequence(channels, name="channels") arr = self[layer].sel(z=library_id)[..., channels].values @@ -365,7 +365,7 @@ def convert_to_full_image_coordinates(x: NDArrayA, y: NDArrayA) -> NDArrayA: label_layer = self._get_layer(label_layer) library_id = self._get_library_id(library_id) - props = _assert_non_empty_sequence(props, name="properties") + props = assert_non_empty_sequence(props, name="properties") for prop in props: if prop not in _valid_seg_prop: raise ValueError(f"Invalid property `{prop}`. Valid properties are `{_valid_seg_prop}`.") @@ -377,7 +377,7 @@ def convert_to_full_image_coordinates(x: NDArrayA, y: NDArrayA) -> NDArrayA: if intensity_layer is None: raise ValueError("Please specify `intensity_layer` if using intensity properties.") channels = _get_channels(self[intensity_layer], channels) - channels = _assert_non_empty_sequence(channels, name="channels") + channels = assert_non_empty_sequence(channels, name="channels") else: channels = () diff --git a/src/squidpy/pl/_graph.py b/src/squidpy/pl/_graph.py index 0c75801cd..f2077603b 100644 --- a/src/squidpy/pl/_graph.py +++ b/src/squidpy/pl/_graph.py @@ -17,7 +17,7 @@ from squidpy._constants._constants import RipleyStat from squidpy._constants._pkg_constants import Key from squidpy._docs import d -from squidpy._validators import _assert_non_empty_sequence, _get_valid_values +from squidpy._validators import assert_non_empty_sequence, get_valid_values from squidpy.gr._utils import _assert_categorical_obs from squidpy.pl._color_utils import Palette_t, _get_palette, _maybe_set_colors from squidpy.pl._utils import _heatmap, save_fig @@ -88,8 +88,8 @@ def centrality_scores( palette = _get_palette(adata, cluster_key=cluster_key, categories=clusters, palette=palette) score = scores if score is None else score - score = _assert_non_empty_sequence(score, name="centrality scores") - score = sorted(_get_valid_values(score, scores)) + score = assert_non_empty_sequence(score, name="centrality scores") + score = sorted(get_valid_values(score, scores)) fig, axs = plt.subplots(1, len(score), figsize=figsize, dpi=dpi, constrained_layout=True) axs = np.ravel(axs) # make into iterable @@ -361,8 +361,8 @@ def co_occurrence( categories = adata.obs[cluster_key].cat.categories clusters = categories if clusters is None else clusters - clusters = _assert_non_empty_sequence(clusters, name="clusters") - clusters = sorted(_get_valid_values(clusters, categories)) + clusters = assert_non_empty_sequence(clusters, name="clusters") + clusters = sorted(get_valid_values(clusters, categories)) palette = _get_palette(adata, cluster_key=cluster_key, categories=categories, palette=palette) diff --git a/src/squidpy/pl/_spatial_utils.py b/src/squidpy/pl/_spatial_utils.py index c7a03bddf..081449488 100644 --- a/src/squidpy/pl/_spatial_utils.py +++ b/src/squidpy/pl/_spatial_utils.py @@ -39,7 +39,7 @@ from squidpy._constants._constants import ScatterShape from squidpy._constants._pkg_constants import Key from squidpy._utils import NDArrayA -from squidpy._validators import _assert_key_in_adata +from squidpy._validators import assert_key_in_adata from squidpy.im._coords import CropCoords from squidpy.pl._color_utils import _get_palette, _maybe_set_colors from squidpy.pl._utils import _assert_value_in_obs @@ -131,7 +131,7 @@ def _get_library_id( raise ValueError(f"Could not fetch `library_id`, check that `spatial_key: {spatial_key}` is correct.") return library_id if library_key is not None: - _assert_key_in_adata(adata, library_key, attr="obs") + assert_key_in_adata(adata, library_key, attr="obs") if library_id is None: library_id = adata.obs[library_key].cat.categories.tolist() _assert_value_in_obs(adata, key=library_key, val=library_id) @@ -555,7 +555,7 @@ def _plot_edges( from networkx import Graph from networkx.drawing import draw_networkx_edges - _assert_key_in_adata(adata, connectivity_key, attr="obsp", extra_msg="Please set `connectivity_key`.") + assert_key_in_adata(adata, connectivity_key, attr="obsp", extra_msg="Please set `connectivity_key`.") g = Graph(adata.obsp[connectivity_key]) if not len(g.edges): diff --git a/src/squidpy/pl/_utils.py b/src/squidpy/pl/_utils.py index ac84b12cb..38ba2fbeb 100644 --- a/src/squidpy/pl/_utils.py +++ b/src/squidpy/pl/_utils.py @@ -39,7 +39,7 @@ from squidpy._constants._pkg_constants import Key from squidpy._docs import d from squidpy._utils import NDArrayA -from squidpy._validators import _assert_in_range, _assert_key_in_adata +from squidpy._validators import assert_in_range, assert_key_in_adata from squidpy.gr._utils import _assert_categorical_obs Vector_name_t = tuple[pd.Series | NDArrayA | None, str | None] @@ -470,7 +470,7 @@ def _contrasting_color(r: int, g: int, b: int) -> str: def _get_black_or_white(value: float, cmap: mcolors.Colormap) -> str: - _assert_in_range(value, 0.0, 1.0, name="value") + assert_in_range(value, 0.0, 1.0, name="value") r, g, b, *_ = (int(c * 255) for c in cmap(value)) return _contrasting_color(r, g, b) @@ -654,7 +654,7 @@ def sanitize_anndata(adata: AnnData) -> None: def _assert_value_in_obs(adata: AnnData, key: str, val: Sequence[Any] | Any) -> None: - _assert_key_in_adata(adata, key, attr="obs") + assert_key_in_adata(adata, key, attr="obs") if not isinstance(val, list): val = [val] val = set(val) - set(adata.obs[key].unique()) diff --git a/tests/test_validators.py b/tests/test_validators.py index 21ba7c7d8..e44d2b529 100644 --- a/tests/test_validators.py +++ b/tests/test_validators.py @@ -7,231 +7,231 @@ import pytest from squidpy._validators import ( - _assert_in_range, - _assert_isinstance, - _assert_key_in_adata, - _assert_key_in_sdata, - _assert_non_empty_sequence, - _assert_non_negative, - _assert_one_of, - _assert_positive, - _check_tuple_needles, - _get_valid_values, + assert_in_range, + assert_isinstance, + assert_key_in_adata, + assert_key_in_sdata, + assert_non_empty_sequence, + assert_non_negative, + assert_one_of, + assert_positive, + check_tuple_needles, + get_valid_values, ) # --------------------------------------------------------------------------- -# _assert_positive +# assert_positive # --------------------------------------------------------------------------- class TestAssertPositive: def test_positive_value(self): - _assert_positive(1.0, name="x") - _assert_positive(0.001, name="x") + assert_positive(1.0, name="x") + assert_positive(0.001, name="x") def test_zero_raises(self): with pytest.raises(ValueError, match="positive"): - _assert_positive(0, name="x") + assert_positive(0, name="x") def test_negative_raises(self): with pytest.raises(ValueError, match="positive"): - _assert_positive(-1, name="x") + assert_positive(-1, name="x") # --------------------------------------------------------------------------- -# _assert_non_negative +# assert_non_negative # --------------------------------------------------------------------------- class TestAssertNonNegative: def test_non_negative_value(self): - _assert_non_negative(0, name="x") - _assert_non_negative(1, name="x") + assert_non_negative(0, name="x") + assert_non_negative(1, name="x") def test_negative_raises(self): with pytest.raises(ValueError, match="non-negative"): - _assert_non_negative(-0.1, name="x") + assert_non_negative(-0.1, name="x") # --------------------------------------------------------------------------- -# _assert_in_range +# assert_in_range # --------------------------------------------------------------------------- class TestAssertInRange: def test_in_range(self): - _assert_in_range(0.5, 0, 1, name="x") - _assert_in_range(0, 0, 1, name="x") - _assert_in_range(1, 0, 1, name="x") + assert_in_range(0.5, 0, 1, name="x") + assert_in_range(0, 0, 1, name="x") + assert_in_range(1, 0, 1, name="x") def test_out_of_range(self): with pytest.raises(ValueError, match="interval"): - _assert_in_range(1.1, 0, 1, name="x") + assert_in_range(1.1, 0, 1, name="x") with pytest.raises(ValueError, match="interval"): - _assert_in_range(-0.1, 0, 1, name="x") + assert_in_range(-0.1, 0, 1, name="x") # --------------------------------------------------------------------------- -# _assert_non_empty_sequence +# assert_non_empty_sequence # --------------------------------------------------------------------------- class TestAssertNonEmptySequence: def test_list(self): - assert _assert_non_empty_sequence(["a", "b"], name="items") == ["a", "b"] + assert assert_non_empty_sequence(["a", "b"], name="items") == ["a", "b"] def test_scalar_conversion(self): - assert _assert_non_empty_sequence("a", name="items") == ["a"] + assert assert_non_empty_sequence("a", name="items") == ["a"] def test_no_scalar_conversion(self): with pytest.raises(TypeError, match="sequence"): - _assert_non_empty_sequence(42, name="items", convert_scalar=False) + assert_non_empty_sequence(42, name="items", convert_scalar=False) def test_empty_raises(self): with pytest.raises(ValueError, match="No items"): - _assert_non_empty_sequence([], name="items") + assert_non_empty_sequence([], name="items") # --------------------------------------------------------------------------- -# _get_valid_values +# get_valid_values # --------------------------------------------------------------------------- class TestGetValidValues: def test_valid(self): - assert _get_valid_values(["a", "b"], ["a", "b", "c"]) == ["a", "b"] + assert get_valid_values(["a", "b"], ["a", "b", "c"]) == ["a", "b"] def test_partial(self): - assert _get_valid_values(["a", "z"], ["a", "b"]) == ["a"] + assert get_valid_values(["a", "z"], ["a", "b"]) == ["a"] def test_none_valid(self): with pytest.raises(ValueError, match="No valid values"): - _get_valid_values(["z"], ["a", "b"]) + get_valid_values(["z"], ["a", "b"]) # --------------------------------------------------------------------------- -# _check_tuple_needles +# check_tuple_needles # --------------------------------------------------------------------------- class TestCheckTupleNeedles: def test_valid_needles(self): - result = _check_tuple_needles([("a", "b")], ["a", "b", "c"], "Value `{}` not found.") + result = check_tuple_needles([("a", "b")], ["a", "b", "c"], "Value `{}` not found.") assert result == [("a", "b")] def test_invalid_needle_reraise(self): with pytest.raises(ValueError, match="z"): - _check_tuple_needles([("z", "b")], ["a", "b"], "Value `{}` not found.") + check_tuple_needles([("z", "b")], ["a", "b"], "Value `{}` not found.") def test_invalid_needle_no_reraise(self): - result = _check_tuple_needles([("z", "b")], ["a", "b"], "Value `{}` not found.", reraise=False) + result = check_tuple_needles([("z", "b")], ["a", "b"], "Value `{}` not found.", reraise=False) assert result == [] def test_wrong_length(self): with pytest.raises(ValueError, match="length"): - _check_tuple_needles([("a",)], ["a"], "msg {}") + check_tuple_needles([("a",)], ["a"], "msg {}") def test_not_sequence(self): with pytest.raises(TypeError, match="Sequence"): - _check_tuple_needles([42], ["a"], "msg {}") + check_tuple_needles([42], ["a"], "msg {}") # --------------------------------------------------------------------------- -# _assert_isinstance +# assert_isinstance # --------------------------------------------------------------------------- class TestAssertIsinstance: def test_correct_type(self): - _assert_isinstance("hello", str, name="x") - _assert_isinstance(42, int, name="x") + assert_isinstance("hello", str, name="x") + assert_isinstance(42, int, name="x") def test_tuple_of_types(self): - _assert_isinstance("hello", (str, int), name="x") - _assert_isinstance(42, (str, int), name="x") + assert_isinstance("hello", (str, int), name="x") + assert_isinstance(42, (str, int), name="x") def test_wrong_type(self): with pytest.raises(TypeError, match="str"): - _assert_isinstance(42, str, name="x") + assert_isinstance(42, str, name="x") def test_wrong_type_tuple(self): with pytest.raises(TypeError, match="str or int"): - _assert_isinstance(3.14, (str, int), name="x") + assert_isinstance(3.14, (str, int), name="x") # --------------------------------------------------------------------------- -# _assert_one_of +# assert_one_of # --------------------------------------------------------------------------- class TestAssertOneOf: def test_valid(self): - _assert_one_of("a", ["a", "b", "c"], name="x") + assert_one_of("a", ["a", "b", "c"], name="x") def test_invalid(self): with pytest.raises(ValueError, match="one of"): - _assert_one_of("z", ["a", "b"], name="x") + assert_one_of("z", ["a", "b"], name="x") # --------------------------------------------------------------------------- -# _assert_key_in_adata +# assert_key_in_adata # --------------------------------------------------------------------------- class TestAssertKeyInAdata: def test_key_present(self): adata = MagicMock() adata.obs = {"cell_type": [1, 2, 3]} - _assert_key_in_adata(adata, "cell_type", attr="obs") + assert_key_in_adata(adata, "cell_type", attr="obs") def test_key_missing(self): adata = MagicMock() adata.obs = {"cell_type": [1, 2, 3]} with pytest.raises(KeyError, match="missing_key"): - _assert_key_in_adata(adata, "missing_key", attr="obs") + assert_key_in_adata(adata, "missing_key", attr="obs") def test_extra_msg(self): adata = MagicMock() adata.obs = {} with pytest.raises(KeyError, match="Run this first"): - _assert_key_in_adata(adata, "key", attr="obs", extra_msg="Run this first.") + assert_key_in_adata(adata, "key", attr="obs", extra_msg="Run this first.") def test_lists_available_keys(self): adata = MagicMock() adata.obs = {"a": 1, "b": 2} with pytest.raises(KeyError, match="Available keys"): - _assert_key_in_adata(adata, "missing", attr="obs") + assert_key_in_adata(adata, "missing", attr="obs") def test_container_without_keys_method(self): """Fallback to list(container) when .keys() is not available.""" adata = MagicMock() adata.obsm = ["X_pca", "X_umap"] # list has no .keys() with pytest.raises(KeyError, match="X_spatial"): - _assert_key_in_adata(adata, "X_spatial", attr="obsm") + assert_key_in_adata(adata, "X_spatial", attr="obsm") # --------------------------------------------------------------------------- -# _assert_key_in_sdata +# assert_key_in_sdata # --------------------------------------------------------------------------- class TestAssertKeyInSdata: def test_key_present(self): sdata = MagicMock() sdata.images = {"image1": "data"} - _assert_key_in_sdata(sdata, "image1", attr="images") + assert_key_in_sdata(sdata, "image1", attr="images") def test_key_missing(self): sdata = MagicMock() sdata.images = {"image1": "data"} with pytest.raises(KeyError, match="missing"): - _assert_key_in_sdata(sdata, "missing", attr="images") + assert_key_in_sdata(sdata, "missing", attr="images") def test_extra_msg(self): sdata = MagicMock() sdata.labels = {} with pytest.raises(KeyError, match="Please provide"): - _assert_key_in_sdata(sdata, "mask", attr="labels", extra_msg="Please provide a mask.") + assert_key_in_sdata(sdata, "mask", attr="labels", extra_msg="Please provide a mask.") def test_lists_available_keys(self): sdata = MagicMock() sdata.images = {"img1": "data", "img2": "data"} with pytest.raises(KeyError, match="Available keys"): - _assert_key_in_sdata(sdata, "missing", attr="images") + assert_key_in_sdata(sdata, "missing", attr="images") # --------------------------------------------------------------------------- -# _assert_isinstance edge cases +# assert_isinstance edge cases # --------------------------------------------------------------------------- class TestAssertIsinstanceEdgeCases: def test_bool_is_subclass_of_int(self): - """bool is a subclass of int — _assert_isinstance(True, int) passes.""" - _assert_isinstance(True, int, name="x") + """bool is a subclass of int — assert_isinstance(True, int) passes.""" + assert_isinstance(True, int, name="x") def test_none_type(self): with pytest.raises(TypeError, match="str"): - _assert_isinstance(None, str, name="x") + assert_isinstance(None, str, name="x") # --------------------------------------------------------------------------- @@ -241,22 +241,12 @@ class TestReExports: def test_gr_utils_reexports(self): """Verify all re-exported validators are the same objects as the originals.""" from squidpy._validators import ( - _assert_in_range as _orig_air, - ) - from squidpy._validators import ( - _assert_non_empty_sequence as _orig_anes, - ) - from squidpy._validators import ( - _assert_non_negative as _orig_ann, - ) - from squidpy._validators import ( - _assert_positive as _orig_ap, - ) - from squidpy._validators import ( - _check_tuple_needles as _orig_ctn, - ) - from squidpy._validators import ( - _get_valid_values as _orig_gvv, + assert_in_range as orig_air, + assert_non_empty_sequence as orig_anes, + assert_non_negative as orig_ann, + assert_positive as orig_ap, + check_tuple_needles as orig_ctn, + get_valid_values as orig_gvv, ) from squidpy.gr._utils import ( _assert_in_range, @@ -267,9 +257,9 @@ def test_gr_utils_reexports(self): _get_valid_values, ) - assert _assert_positive is _orig_ap - assert _assert_non_negative is _orig_ann - assert _assert_in_range is _orig_air - assert _assert_non_empty_sequence is _orig_anes - assert _check_tuple_needles is _orig_ctn - assert _get_valid_values is _orig_gvv + assert _assert_positive is orig_ap + assert _assert_non_negative is orig_ann + assert _assert_in_range is orig_air + assert _assert_non_empty_sequence is orig_anes + assert _check_tuple_needles is orig_ctn + assert _get_valid_values is orig_gvv From 3c57f2fe5ff2e37ca246b8f11ee3f871172d5dec Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 11 Mar 2026 12:56:15 +0000 Subject: [PATCH 6/9] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/squidpy/gr/_utils.py | 10 ++++++++++ tests/test_validators.py | 10 ++++++++++ 2 files changed, 20 insertions(+) diff --git a/src/squidpy/gr/_utils.py b/src/squidpy/gr/_utils.py index a9b4147a1..a8513f034 100644 --- a/src/squidpy/gr/_utils.py +++ b/src/squidpy/gr/_utils.py @@ -20,10 +20,20 @@ from squidpy._utils import NDArrayA from squidpy._validators import ( # noqa: F401 — re-exported for backwards compatibility assert_in_range as _assert_in_range, +) +from squidpy._validators import ( assert_non_empty_sequence as _assert_non_empty_sequence, +) +from squidpy._validators import ( assert_non_negative as _assert_non_negative, +) +from squidpy._validators import ( assert_positive as _assert_positive, +) +from squidpy._validators import ( check_tuple_needles as _check_tuple_needles, +) +from squidpy._validators import ( get_valid_values as _get_valid_values, ) diff --git a/tests/test_validators.py b/tests/test_validators.py index e44d2b529..caafdb253 100644 --- a/tests/test_validators.py +++ b/tests/test_validators.py @@ -242,10 +242,20 @@ def test_gr_utils_reexports(self): """Verify all re-exported validators are the same objects as the originals.""" from squidpy._validators import ( assert_in_range as orig_air, + ) + from squidpy._validators import ( assert_non_empty_sequence as orig_anes, + ) + from squidpy._validators import ( assert_non_negative as orig_ann, + ) + from squidpy._validators import ( assert_positive as orig_ap, + ) + from squidpy._validators import ( check_tuple_needles as orig_ctn, + ) + from squidpy._validators import ( get_valid_values as orig_gvv, ) from squidpy.gr._utils import ( From 1bcbedbcc6a61a53956f369ded584a6b1f529ddb Mon Sep 17 00:00:00 2001 From: Tim Treis Date: Wed, 11 Mar 2026 14:10:17 +0100 Subject: [PATCH 7/9] Drop leading underscore from validator names and remove re-exports Since _validators.py is already a private module, make functions public within it (e.g. assert_positive instead of _assert_positive). Migrate all remaining callers in gr/ to import directly from _validators instead of going through gr/_utils.py re-exports, then remove the re-exports. Co-Authored-By: Claude Opus 4.6 --- src/squidpy/gr/_build.py | 6 +++--- src/squidpy/gr/_ligrec.py | 7 +++--- src/squidpy/gr/_nhood.py | 4 ++-- src/squidpy/gr/_ppatterns.py | 5 ++--- src/squidpy/gr/_sepal.py | 4 ++-- src/squidpy/gr/_utils.py | 23 +++----------------- tests/test_validators.py | 41 ------------------------------------ 7 files changed, 15 insertions(+), 75 deletions(-) diff --git a/src/squidpy/gr/_build.py b/src/squidpy/gr/_build.py index 5ed20fc3a..32630955d 100644 --- a/src/squidpy/gr/_build.py +++ b/src/squidpy/gr/_build.py @@ -42,9 +42,9 @@ from squidpy._constants._pkg_constants import Key from squidpy._docs import d, inject_docs from squidpy._utils import NDArrayA +from squidpy._validators import assert_positive from squidpy.gr._utils import ( _assert_categorical_obs, - _assert_positive, _assert_spatial_basis, _save_data, ) @@ -191,8 +191,8 @@ def spatial_neighbors( adata = adata.tables[table_key] library_key = region_key - _assert_positive(n_rings, name="n_rings") - _assert_positive(n_neighs, name="n_neighs") + assert_positive(n_rings, name="n_rings") + assert_positive(n_neighs, name="n_neighs") _assert_spatial_basis(adata, spatial_key) transform = Transform.NONE if transform is None else Transform(transform) diff --git a/src/squidpy/gr/_ligrec.py b/src/squidpy/gr/_ligrec.py index a4beecd8f..242ec16f2 100644 --- a/src/squidpy/gr/_ligrec.py +++ b/src/squidpy/gr/_ligrec.py @@ -21,10 +21,9 @@ from squidpy._constants._pkg_constants import Key from squidpy._docs import d, inject_docs from squidpy._utils import NDArrayA, Signal, SigQueue, _get_n_cores, parallelize +from squidpy._validators import assert_positive, check_tuple_needles from squidpy.gr._utils import ( _assert_categorical_obs, - _assert_positive, - _check_tuple_needles, _genesymbols, _save_data, ) @@ -362,7 +361,7 @@ def test( ------- %(ligrec_test_returns)s """ - _assert_positive(n_perms, name="n_perms") + assert_positive(n_perms, name="n_perms") _assert_categorical_obs(self._adata, key=cluster_key) if corr_method is not None: @@ -386,7 +385,7 @@ def test( if all(isinstance(c, str) for c in clusters): clusters = list(product(clusters, repeat=2)) # type: ignore[assignment] clusters = sorted( - _check_tuple_needles( + check_tuple_needles( clusters, # type: ignore[arg-type] self._filtered_data["clusters"].cat.categories, msg="Invalid cluster `{0!r}`.", diff --git a/src/squidpy/gr/_nhood.py b/src/squidpy/gr/_nhood.py index 23b13accb..d032cdab1 100644 --- a/src/squidpy/gr/_nhood.py +++ b/src/squidpy/gr/_nhood.py @@ -21,10 +21,10 @@ from squidpy._constants._pkg_constants import Key from squidpy._docs import d, inject_docs from squidpy._utils import NDArrayA, Signal, SigQueue, _get_n_cores, parallelize +from squidpy._validators import assert_positive from squidpy.gr._utils import ( _assert_categorical_obs, _assert_connectivity_key, - _assert_positive, _save_data, _shuffle_group, ) @@ -176,7 +176,7 @@ def nhood_enrichment( connectivity_key = Key.obsp.spatial_conn(connectivity_key) _assert_categorical_obs(adata, cluster_key) _assert_connectivity_key(adata, connectivity_key) - _assert_positive(n_perms, name="n_perms") + assert_positive(n_perms, name="n_perms") adj = adata.obsp[connectivity_key] original_clust = adata.obs[cluster_key] diff --git a/src/squidpy/gr/_ppatterns.py b/src/squidpy/gr/_ppatterns.py index a960b6862..3163035db 100644 --- a/src/squidpy/gr/_ppatterns.py +++ b/src/squidpy/gr/_ppatterns.py @@ -24,11 +24,10 @@ from squidpy._constants._pkg_constants import Key from squidpy._docs import d, inject_docs from squidpy._utils import NDArrayA, Signal, SigQueue, _get_n_cores, deprecated_params, parallelize -from squidpy._validators import assert_key_in_adata +from squidpy._validators import assert_key_in_adata, assert_positive from squidpy.gr._utils import ( _assert_categorical_obs, _assert_connectivity_key, - _assert_positive, _assert_spatial_basis, _save_data, ) @@ -200,7 +199,7 @@ def extract_obsm(adata: AnnData, ixs: int | Sequence[int] | None) -> tuple[NDArr n_jobs = _get_n_cores(n_jobs) start = logg.info(f"Calculating {mode}'s statistic for `{n_perms}` permutations using `{n_jobs}` core(s)") if n_perms is not None: - _assert_positive(n_perms, name="n_perms") + assert_positive(n_perms, name="n_perms") perms = list(np.arange(n_perms)) score_perms = parallelize( diff --git a/src/squidpy/gr/_sepal.py b/src/squidpy/gr/_sepal.py index 7e60085d1..cce8ebca6 100644 --- a/src/squidpy/gr/_sepal.py +++ b/src/squidpy/gr/_sepal.py @@ -15,9 +15,9 @@ from squidpy._constants._pkg_constants import Key from squidpy._docs import d, inject_docs from squidpy._utils import NDArrayA, Signal, SigQueue, _get_n_cores, parallelize +from squidpy._validators import assert_non_empty_sequence from squidpy.gr._utils import ( _assert_connectivity_key, - _assert_non_empty_sequence, _assert_spatial_basis, _extract_expression, _save_data, @@ -106,7 +106,7 @@ def sepal( genes = adata.var_names.values if "highly_variable" in adata.var.columns: genes = genes[adata.var["highly_variable"].values] - genes = _assert_non_empty_sequence(genes, name="genes") + genes = assert_non_empty_sequence(genes, name="genes") n_jobs = _get_n_cores(n_jobs) diff --git a/src/squidpy/gr/_utils.py b/src/squidpy/gr/_utils.py index a8513f034..be0437701 100644 --- a/src/squidpy/gr/_utils.py +++ b/src/squidpy/gr/_utils.py @@ -18,24 +18,7 @@ from squidpy._compat import ArrayView, SparseCSCView, SparseCSRView from squidpy._docs import d from squidpy._utils import NDArrayA -from squidpy._validators import ( # noqa: F401 — re-exported for backwards compatibility - assert_in_range as _assert_in_range, -) -from squidpy._validators import ( - assert_non_empty_sequence as _assert_non_empty_sequence, -) -from squidpy._validators import ( - assert_non_negative as _assert_non_negative, -) -from squidpy._validators import ( - assert_positive as _assert_positive, -) -from squidpy._validators import ( - check_tuple_needles as _check_tuple_needles, -) -from squidpy._validators import ( - get_valid_values as _get_valid_values, -) +from squidpy._validators import assert_non_empty_sequence def _assert_categorical_obs(adata: AnnData, key: str) -> None: @@ -85,10 +68,10 @@ def _extract_expression( if use_raw: genes = list(set(adata.raw.var_names) & set(genes)) # type: ignore[arg-type] - genes = _assert_non_empty_sequence(genes, name="genes") + genes = assert_non_empty_sequence(genes, name="genes") res = adata.raw[:, genes].X else: - genes = _assert_non_empty_sequence(genes, name="genes") + genes = assert_non_empty_sequence(genes, name="genes") if layer is None: res = adata[:, genes].X diff --git a/tests/test_validators.py b/tests/test_validators.py index caafdb253..c0296072e 100644 --- a/tests/test_validators.py +++ b/tests/test_validators.py @@ -232,44 +232,3 @@ def test_bool_is_subclass_of_int(self): def test_none_type(self): with pytest.raises(TypeError, match="str"): assert_isinstance(None, str, name="x") - - -# --------------------------------------------------------------------------- -# Re-export smoke test -# --------------------------------------------------------------------------- -class TestReExports: - def test_gr_utils_reexports(self): - """Verify all re-exported validators are the same objects as the originals.""" - from squidpy._validators import ( - assert_in_range as orig_air, - ) - from squidpy._validators import ( - assert_non_empty_sequence as orig_anes, - ) - from squidpy._validators import ( - assert_non_negative as orig_ann, - ) - from squidpy._validators import ( - assert_positive as orig_ap, - ) - from squidpy._validators import ( - check_tuple_needles as orig_ctn, - ) - from squidpy._validators import ( - get_valid_values as orig_gvv, - ) - from squidpy.gr._utils import ( - _assert_in_range, - _assert_non_empty_sequence, - _assert_non_negative, - _assert_positive, - _check_tuple_needles, - _get_valid_values, - ) - - assert _assert_positive is orig_ap - assert _assert_non_negative is orig_ann - assert _assert_in_range is orig_air - assert _assert_non_empty_sequence is orig_anes - assert _check_tuple_needles is orig_ctn - assert _get_valid_values is orig_gvv From 346413e4cca3af57284e1001436e9024d4f35fb9 Mon Sep 17 00:00:00 2001 From: Tim Treis Date: Wed, 11 Mar 2026 14:27:50 +0100 Subject: [PATCH 8/9] Style: use idiomatic empty-list checks and add precondition docstrings - Replace `if not len(res):` with `if not res:` in _validators.py - Add "callers must validate" docstrings to internal _make_tiles.py helpers Co-Authored-By: Claude Opus 4.6 --- src/squidpy/_validators.py | 4 ++-- src/squidpy/experimental/im/_make_tiles.py | 20 ++++++++++++++++---- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/src/squidpy/_validators.py b/src/squidpy/_validators.py index debba1db0..bb064a10c 100644 --- a/src/squidpy/_validators.py +++ b/src/squidpy/_validators.py @@ -52,7 +52,7 @@ def assert_non_empty_sequence( seq = (seq,) res, _ = _unique_order_preserving(seq) - if not len(res): + if not res: raise ValueError(f"No {name} have been selected.") return res @@ -60,7 +60,7 @@ def assert_non_empty_sequence( def get_valid_values(needle: Sequence[Any], haystack: Sequence[Any]) -> Sequence[Any]: res = [n for n in needle if n in haystack] - if not len(res): + if not res: raise ValueError(f"No valid values were found. Valid values are `{sorted(set(haystack))}`.") return res diff --git a/src/squidpy/experimental/im/_make_tiles.py b/src/squidpy/experimental/im/_make_tiles.py index 1c840fa3e..ef04d6302 100644 --- a/src/squidpy/experimental/im/_make_tiles.py +++ b/src/squidpy/experimental/im/_make_tiles.py @@ -170,7 +170,10 @@ def _get_largest_scale_dimensions( sdata: sd.SpatialData, image_key: str, ) -> tuple[int, int]: - """Get the dimensions (H, W) of the largest/finest scale of an image.""" + """Get the dimensions (H, W) of the largest/finest scale of an image. + + Callers must validate *image_key* before calling this helper. + """ img_node = sdata.images[image_key] # Use _get_element_data with "scale0" which is always the largest scale @@ -754,7 +757,10 @@ def _make_tiles( center_grid_on_tissue: bool = False, scale: str = "auto", ) -> _TileGrid: - """Construct a tile grid for an image, optionally centered on a tissue mask.""" + """Construct a tile grid for an image, optionally centered on a tissue mask. + + Callers must validate *image_key* before calling this helper. + """ # Get image dimensions from the largest/finest scale H, W = _get_largest_scale_dimensions(sdata, image_key) @@ -823,7 +829,10 @@ def _get_spot_coordinates( sdata: sd.SpatialData, spots_key: str, ) -> tuple[np.ndarray, np.ndarray]: - """Extract spot centers (x, y) and IDs from a shapes table.""" + """Extract spot centers (x, y) and IDs from a shapes table. + + Callers must validate *spots_key* before calling this helper. + """ gdf = sdata.shapes[spots_key] if "geometry" not in gdf: raise ValueError(f"Shapes '{spots_key}' lack geometry column required for spot coordinates.") @@ -872,7 +881,10 @@ def _derive_tile_size_from_spots(coords: np.ndarray) -> tuple[int, int]: def _get_mask_from_labels(sdata: sd.SpatialData, mask_key: str, scale: str) -> np.ndarray: - """Extract a 2D mask array from ``sdata.labels`` at the requested scale.""" + """Extract a 2D mask array from ``sdata.labels`` at the requested scale. + + Callers must validate *mask_key* before calling this helper. + """ label_node = sdata.labels[mask_key] mask_da = _get_element_data(label_node, scale, "label", mask_key) From afac26cb98a9be42ec92902be1d74fff2ede6a80 Mon Sep 17 00:00:00 2001 From: Tim Treis Date: Wed, 11 Mar 2026 16:18:24 +0100 Subject: [PATCH 9/9] Use explicit length checks instead of truthiness for empty-list detection Co-Authored-By: Claude Opus 4.6 --- src/squidpy/_validators.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/squidpy/_validators.py b/src/squidpy/_validators.py index bb064a10c..e83a6a748 100644 --- a/src/squidpy/_validators.py +++ b/src/squidpy/_validators.py @@ -52,7 +52,7 @@ def assert_non_empty_sequence( seq = (seq,) res, _ = _unique_order_preserving(seq) - if not res: + if len(res) == 0: raise ValueError(f"No {name} have been selected.") return res @@ -60,7 +60,7 @@ def assert_non_empty_sequence( def get_valid_values(needle: Sequence[Any], haystack: Sequence[Any]) -> Sequence[Any]: res = [n for n in needle if n in haystack] - if not res: + if len(res) == 0: raise ValueError(f"No valid values were found. Valid values are `{sorted(set(haystack))}`.") return res