Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
3c70e11
replace two enums with literals
selmanozleyen Aug 30, 2026
43fa309
Drop the double dashes from comments and docstrings
selmanozleyen Sep 25, 2026
2387598
Accept any casing for string choices through one helper, make it a co…
selmanozleyen Sep 30, 2026
9ee5bbb
Drop the repeated flavor check and the unreachable aggregation raise
selmanozleyen Oct 1, 2026
6a01614
Keep the aggregation raise, test a spelling the fall-through would no…
selmanozleyen Oct 1, 2026
1a6b1d2
Merge branch 'main' into feat/enum-to-literal
selmanozleyen Oct 9, 2026
4963968
Replace five enum args with Literal
selmanozleyen Oct 9, 2026
f9446ee
Trim dead enums and redundant docs
selmanozleyen Oct 9, 2026
95c47d7
Drop the Transform enum and the Enum shim
selmanozleyen Oct 9, 2026
384d3b9
Let ModeEnum members be strings
selmanozleyen Oct 9, 2026
c93bd56
Convert centrality score to a Literal
selmanozleyen Oct 9, 2026
ee3fa69
Shorten the string-choices convention
selmanozleyen Oct 9, 2026
72a7dcf
Give normalize_choice a default for misses
selmanozleyen Oct 9, 2026
808eed6
Return the option type from normalize_choice
selmanozleyen Oct 9, 2026
212db6e
Collapse the enum error machinery
selmanozleyen Oct 9, 2026
f2e3b46
Annotate _missing_ as NoReturn
selmanozleyen Oct 9, 2026
396c783
Convert dendrogram and drop NicheDefinitions
selmanozleyen Oct 9, 2026
86d356a
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Oct 9, 2026
6f338f6
Convert corr_axis and complex_policy
selmanozleyen Oct 9, 2026
9a5896c
Convert corr_axis and complex_policy
selmanozleyen Oct 9, 2026
ed27b26
Convert process and segment methods
selmanozleyen Oct 9, 2026
e849a93
Convert spatial_scatter shape
selmanozleyen Oct 9, 2026
03e0650
Convert dims and fix the InferDims_t spellings
selmanozleyen Oct 9, 2026
2ce23f2
Declare introspected literals without `type`
selmanozleyen Oct 9, 2026
57f3841
Convert image features and delete the enums
selmanozleyen Oct 9, 2026
fcc9d41
Drop the enum exception from the convention
selmanozleyen Oct 9, 2026
ba29699
Cover the converted arguments, derive option lists
selmanozleyen Oct 9, 2026
90cb7b5
Merge main into feat/enum-to-literal
selmanozleyen Oct 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 0 additions & 4 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,10 +146,6 @@
("py:class", "numpy._typing._array_like.NDArray"),
("py:class", "np.number"),
("py:class", "csr_matrix"),
# no idea why those aren’t exported
("py:class", "squidpy._constants._constants.SpatialAutocorr"),
("py:class", "squidpy._constants._constants.CoordType"),
("py:class", "squidpy._constants._constants.Transform"),
("py:class", "pandas.core.frame.DataFrame"),
]
# see the solution from: https://github.com/sphinx-doc/sphinx/issues/7369
Expand Down
1 change: 1 addition & 0 deletions docs/contributing.md
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@ Consider enabling this option for [ruff][ruff-editors] and [biome][biome-editors

- **Keyword-only arguments**: a public function takes its data object (`adata`, `sdata` or `data`) positionally, and every other argument is keyword-only, e.g. `def nhood_enrichment(adata, *, cluster_key, ...)`. Avoid catch-all `**kwargs` that forward whatever they are given unchecked.
- **Parameter bags**: a `*Params` {class}`~typing.TypedDict` in {mod}`squidpy.types`, each key declaring its default as `Annotated[type, Default(value)]`, only when the knobs depend on another argument (one bag per `method`, taken as `method_params`) or several functions share them (taken as `**kwargs: Unpack[XParams]`). Resolve it with `resolve_params` from `squidpy._params`, so unknown keys raise, and keep its keys out of the signature.
- **String choices** (a `Literal[...]` such as `method="otsu"`) are case-insensitive. Resolve the argument once at the top of the public function with `value = normalize_choice(value, options, name=...)` from `squidpy._validators`, which returns the canonical spelling and raises naming the valid options. Keys the user chose (obs/obsm keys, layer, channel or cluster names, paths) stay case-sensitive. Derive a reused option set from its own annotation with `options_of` rather than retyping it: `_Feature = Literal[...]`, `_FEATURES = options_of(_Feature)`.
- **Fits**, returned by one function for others to consume, are frozen dataclasses with methods (e.g. `.transform`). Pass them whole.
- **Results**, returned instead of writing into the input, are {class}`~typing.NamedTuple`s.
- **Randomness**: take `rng: SeedLike | RNGLike | None = None` ([SPEC 7](https://scientific-python.org/specs/spec-0007/)), never `seed`/`generator`/`random_state`; the exception is `random_state` on sklearn-style estimators (the clusterers), which follow sklearn's `get_params`/`set_params` protocol. Spawn one generator per independent task with `rng.spawn(n)`, and pass `legacy_random(rng)` to APIs that only take an integer seed.
Expand Down
135 changes: 0 additions & 135 deletions src/squidpy/_constants/_constants.py

This file was deleted.

14 changes: 4 additions & 10 deletions src/squidpy/_constants/_pkg_constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@

from anndata import AnnData

from squidpy._constants._constants import Processing, SegmentationBackend

_SEP = "_"


Expand All @@ -23,18 +21,14 @@ def __get__(self, obj: Any, owner: Any) -> str:
class Key:
class img:
@classmethod
def segment(cls, backend: str | SegmentationBackend, layer_added: str | None = None) -> str:
return f"segmented_{SegmentationBackend(backend).s}" if layer_added is None else layer_added
def segment(cls, backend: str, layer_added: str | None = None) -> str:
return f"segmented_{backend}" if layer_added is None else layer_added

@classmethod
def process(
cls, method: str | Processing | Callable[[Any], Any], img_id: str, layer_added: str | None = None
) -> str:
def process(cls, method: str | Callable[[Any], Any], img_id: str, layer_added: str | None = None) -> str:
if layer_added is not None:
return layer_added
if isinstance(method, Processing):
method = method.s
elif callable(method):
if callable(method):
method = getattr(method, "__name__", "custom")

return f"{img_id}_{method}"
Expand Down
76 changes: 0 additions & 76 deletions src/squidpy/_constants/_utils.py

This file was deleted.

39 changes: 34 additions & 5 deletions src/squidpy/_validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from __future__ import annotations

from collections.abc import Hashable, Iterable, Sequence
from typing import TYPE_CHECKING, Any
from typing import TYPE_CHECKING, Any, cast, get_args, overload

from squidpy._utils import _unique_order_preserving

Expand Down Expand Up @@ -90,10 +90,39 @@ def assert_isinstance(value: Any, expected_type: type | tuple[type, ...], *, nam
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 options_of(literal: Any) -> tuple[Any, ...]:
"""Return the members of a `Literal`, so an option list is never retyped beside its annotation.

Accepts a `type X = Literal[...]` alias too, whose lazy value `typing.get_args` alone would miss.
"""
return get_args(getattr(literal, "__value__", literal))


_RAISE = object()
"""Sentinel: `normalize_choice` raises rather than returning a miss."""


@overload
def normalize_choice[T](value: Any, options: Iterable[T], *, name: str) -> T: ...
@overload
def normalize_choice[T, D](value: Any, options: Iterable[T], *, name: str, default: D) -> T | D: ...
def normalize_choice[T, D](value: Any, options: Iterable[T], *, name: str, default: D | object = _RAISE) -> T | D:
"""Return the option in *options* matching *value*, ignoring the case of strings.

Raise ValueError naming the valid options if nothing matches, or return *default*
if one was given. `None` is a legal option, so misses are signalled by *default*
alone, never by a `None` return.
"""
options = list(options)
if isinstance(value, str):
for opt in options:
if isinstance(opt, str) and opt.casefold() == value.casefold():
return opt
elif value in options:
return value
if default is not _RAISE:
return cast("D", default)
raise ValueError(f"Expected `{name}` to be one of `{options}`, got `{value!r}`.")


def assert_key_in(obj: Any, key: str, *, attr: str, obj_name: str, extra_msg: str = "") -> None:
Expand Down
5 changes: 2 additions & 3 deletions src/squidpy/datasets/_datasets.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from pathlib import Path
from typing import TYPE_CHECKING, Any, Literal

from squidpy._validators import normalize_choice
from squidpy.datasets._downloader import download
from squidpy.datasets._registry import dataset_names, get_registry
from squidpy.read._utils import PathLike
Expand Down Expand Up @@ -123,9 +124,7 @@ def visium(
# guard against the visium_10x names specifically: a valid-but-wrong-type name
# (e.g. "imc", an AnnData dataset) would otherwise pass and fail deep in the
# anndata loader with a confusing ``unexpected keyword argument 'include_hires_tiff'``.
visium_samples = dataset_names("visium_10x")
if sample_id not in visium_samples:
raise ValueError(f"Unknown Visium sample: {sample_id}. Available samples: {visium_samples}")
sample_id = normalize_choice(sample_id, dataset_names("visium_10x"), name="sample_id")

# downloads land in <datasetdir>/visium_10x/<sample_id>/
return download(sample_id, base_dir, include_hires_tiff=include_hires_tiff)
Expand Down
2 changes: 0 additions & 2 deletions src/squidpy/experimental/im/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,13 @@
from ._detect_tissue import detect_tissue
from ._make_tiles import make_tiles, make_tiles_from_spots
from ._qc_image import qc_image
from ._qc_metrics import QCMetric
from ._stain import (
StainFit,
estimate_white_point,
fit_stain_reference,
)

__all__ = [
"QCMetric",
"StainFit",
"calculate_image_features",
"detect_tissue",
Expand Down
4 changes: 2 additions & 2 deletions src/squidpy/experimental/im/_calculate_image_features.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
from spatialdata.transformations import get_transformation
from threadpoolctl import threadpool_limits

from squidpy._validators import normalize_choice
from squidpy.experimental.im._tiling import (
CellInfo,
_run_tiled,
Expand Down Expand Up @@ -775,8 +776,7 @@ def _prepare_lazy(
"""
_validate_inputs(sdata, image_key=image_key, labels_key=labels_key, shapes_key=shapes_key, scale=scale)

if align_mode not in ("strict", "rasterize"):
raise ValueError(f"`align_mode` must be 'strict' or 'rasterize'; got {align_mode!r}.")
align_mode = normalize_choice(align_mode, ("strict", "rasterize"), name="align_mode")

image_da = None
if image_key is not None:
Expand Down
Loading
Loading