From 63600ed368df5ff1edb104ab68800746f94ba6f4 Mon Sep 17 00:00:00 2001 From: Luca Marconato Date: Tue, 17 Feb 2026 10:45:46 +0100 Subject: [PATCH 1/6] add profimp dev dep --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 89e9e023..b1b0e679 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -55,7 +55,7 @@ dependencies = [ [project.optional-dependencies] dev = [ "bump2version", - "sentry-prevent-cli", + "profimp", ] test = [ "pytest", From ed9f72f45010567b4623811864f07601036458cf Mon Sep 17 00:00:00 2001 From: Luca Marconato Date: Wed, 18 Feb 2026 18:02:07 +0100 Subject: [PATCH 2/6] made imports lazy --- benchmarks/README.md | 60 ++++- benchmarks/benchmark_imports.py | 42 +++ benchmarks/spatialdata_benchmark.py | 7 - src/spatialdata/__init__.py | 253 ++++++++++++++---- src/spatialdata/_core/operations/aggregate.py | 7 +- src/spatialdata/_core/operations/rasterize.py | 15 +- .../_core/operations/rasterize_bins.py | 6 +- src/spatialdata/_core/operations/vectorize.py | 13 +- src/spatialdata/_core/validation.py | 11 +- src/spatialdata/datasets.py | 5 +- src/spatialdata/models/_utils.py | 3 +- src/spatialdata/transformations/operations.py | 10 +- tests/test_init.py | 23 ++ 13 files changed, 368 insertions(+), 87 deletions(-) create mode 100644 benchmarks/benchmark_imports.py create mode 100644 tests/test_init.py diff --git a/benchmarks/README.md b/benchmarks/README.md index 6c69a786..9f890362 100644 --- a/benchmarks/README.md +++ b/benchmarks/README.md @@ -14,25 +14,63 @@ pip install -e '.[docs,test,benchmark]' ## Usage -Running all the benchmarks is usually not needed. You run the benchmark using `asv run`. See the [asv documentation](https://asv.readthedocs.io/en/stable/commands.html#asv-run) for interesting arguments, like selecting the benchmarks you're interested in by providing a regex pattern `-b` or `--bench` that links to a function or class method e.g. the option `-b timeraw_import_inspect` selects the function `timeraw_import_inspect` in `benchmarks/spatialdata_benchmark.py`. You can run the benchmark in your current environment with `--python=same`. Some example benchmarks: +Running all the benchmarks is usually not needed. You run the benchmark using `asv run`. See the [asv documentation](https://asv.readthedocs.io/en/stable/commands.html#asv-run) for interesting arguments, like selecting the benchmarks you're interested in by providing a regex pattern `-b` or `--bench` that links to a function or class method. You can run the benchmark in your current environment with `--python=same`. Some example benchmarks: -Importing the SpatialData library can take around 4 seconds: +### Import time benchmarks + +Import benchmarks live in `benchmarks/benchmark_imports.py`. Each `timeraw_*` function returns a Python code snippet that asv runs in a fresh interpreter (cold import, empty module cache): + +Run all import benchmarks in your current environment: ``` -PYTHONWARNINGS="ignore" asv run --python=same --show-stderr -b timeraw_import_inspect -Couldn't load asv.plugins._mamba_helpers because -No module named 'conda' -· Discovering benchmarks -· Running 1 total benchmarks (1 commits * 1 environments * 1 benchmarks) -[ 0.00%] ·· Benchmarking existing-py_opt_homebrew_Caskroom_mambaforge_base_envs_spatialdata2_bin_python3.12 -[50.00%] ··· Running (spatialdata_benchmark.timeraw_import_inspect--). -[100.00%] ··· spatialdata_benchmark.timeraw_import_inspect 3.65±0.2s +asv run --python=same --show-stderr -b timeraw +``` + +Or a single one: + +``` +asv run --python=same --show-stderr -b timeraw_import_spatialdata +``` + +### Comparing the current branch against `main` + +The simplest way is `asv continuous`, which builds both commits, runs the benchmarks, and prints the comparison in one shot: + +```bash +asv continuous --show-stderr -v -b timeraw main faster-import ``` +Replace `faster-import` with any branch name or commit hash. The `-v` flag prints per-sample timings; drop it for a shorter summary. + +Alternatively, collect results separately and compare afterwards: + +```bash +# 1. Collect results for the tip of main and the tip of your branch +asv run --show-stderr -b timeraw main +asv run --show-stderr -b timeraw HEAD + +# 2. Print a side-by-side comparison +asv compare main HEAD +``` + +Both approaches build isolated environments from scratch. If you prefer to skip the rebuild and reuse your current environment (faster, less accurate): + +```bash +asv run --python=same --show-stderr -b timeraw HEAD + +git stash && git checkout main +asv run --python=same --show-stderr -b timeraw HEAD +git checkout - && git stash pop + +asv compare main HEAD +``` + +### Querying benchmarks + Querying using a bounding box without a spatial index is highly impacted by large amounts of points (transcripts), more than table rows (cells). ``` -$ PYTHONWARNINGS="ignore" asv run --python=same --show-stderr -b time_query_bounding_box +$ asv run --python=same --show-stderr -b time_query_bounding_box [100.00%] ··· ======== ============ ============= ============= ============== -- filter_table / n_transcripts_per_cell diff --git a/benchmarks/benchmark_imports.py b/benchmarks/benchmark_imports.py new file mode 100644 index 00000000..1cbfff65 --- /dev/null +++ b/benchmarks/benchmark_imports.py @@ -0,0 +1,42 @@ +# type: ignore +"""Benchmarks for import times of the spatialdata package and its submodules. + +Each ``timeraw_*`` function returns a snippet of Python code that asv runs in +a fresh interpreter, so the measured time reflects a cold import with an empty +module cache. +""" + + +def timeraw_import_spatialdata(): + """Time a bare ``import spatialdata``.""" + return """ + import spatialdata + """ + + +def timeraw_import_SpatialData(): + """Time importing the top-level ``SpatialData`` class.""" + return """ + from spatialdata import SpatialData + """ + + +def timeraw_import_read_zarr(): + """Time importing ``read_zarr`` from the top-level namespace.""" + return """ + from spatialdata import read_zarr + """ + + +def timeraw_import_models_elements(): + """Time importing the main element model classes.""" + return """ + from spatialdata.models import Image2DModel, Labels2DModel, PointsModel, ShapesModel, TableModel + """ + + +def timeraw_import_transformations(): + """Time importing the ``spatialdata.transformations`` submodule.""" + return """ + from spatialdata.transformations import Affine, Scale, Translation, Sequence + """ diff --git a/benchmarks/spatialdata_benchmark.py b/benchmarks/spatialdata_benchmark.py index 408ad14e..4d1020fe 100644 --- a/benchmarks/spatialdata_benchmark.py +++ b/benchmarks/spatialdata_benchmark.py @@ -20,13 +20,6 @@ def peakmem_list2(self): return sdata -def timeraw_import_inspect(): - """Time the import of the spatialdata module.""" - return """ - import spatialdata - """ - - class TimeMapRaster: """Time the.""" diff --git a/src/spatialdata/__init__.py b/src/spatialdata/__init__.py index bb24f04e..34978028 100644 --- a/src/spatialdata/__init__.py +++ b/src/spatialdata/__init__.py @@ -1,76 +1,227 @@ -from importlib.metadata import version +from __future__ import annotations -import spatialdata.models._accessor # noqa: F401 +import importlib +from importlib.metadata import version +from typing import TYPE_CHECKING, Any __version__ = version("spatialdata") -__all__ = [ +_submodules = { + "dataloader", + "datasets", "models", "transformations", - "datasets", - "dataloader", +} + +_LAZY_IMPORTS: dict[str, str] = { + # _core._deepcopy + "deepcopy": "spatialdata._core._deepcopy", + # _core._utils + "sanitize_name": "spatialdata._core._utils", + "sanitize_table": "spatialdata._core._utils", + # _core.centroids + "get_centroids": "spatialdata._core.centroids", + # _core.concatenate + "concatenate": "spatialdata._core.concatenate", + # _core.data_extent + "are_extents_equal": "spatialdata._core.data_extent", + "get_extent": "spatialdata._core.data_extent", + # _core.operations.aggregate + "aggregate": "spatialdata._core.operations.aggregate", + # _core.operations.map + "map_raster": "spatialdata._core.operations.map", + "relabel_sequential": "spatialdata._core.operations.map", + # _core.operations.rasterize + "rasterize": "spatialdata._core.operations.rasterize", + # _core.operations.rasterize_bins + "rasterize_bins": "spatialdata._core.operations.rasterize_bins", + "rasterize_bins_link_table_to_labels": "spatialdata._core.operations.rasterize_bins", + # _core.operations.transform + "transform": "spatialdata._core.operations.transform", + # _core.operations.vectorize + "to_circles": "spatialdata._core.operations.vectorize", + "to_polygons": "spatialdata._core.operations.vectorize", + # _core.query._utils + "get_bounding_box_corners": "spatialdata._core.query._utils", + # _core.query.relational_query + "filter_by_table_query": "spatialdata._core.query.relational_query", + "get_element_annotators": "spatialdata._core.query.relational_query", + "get_element_instances": "spatialdata._core.query.relational_query", + "get_values": "spatialdata._core.query.relational_query", + "join_spatialelement_table": "spatialdata._core.query.relational_query", + "match_element_to_table": "spatialdata._core.query.relational_query", + "match_sdata_to_table": "spatialdata._core.query.relational_query", + "match_table_to_element": "spatialdata._core.query.relational_query", + # _core.query.spatial_query + "bounding_box_query": "spatialdata._core.query.spatial_query", + "polygon_query": "spatialdata._core.query.spatial_query", + # _core.spatialdata + "SpatialData": "spatialdata._core.spatialdata", + # _io._utils + "get_dask_backing_files": "spatialdata._io._utils", + # _io.format + "SpatialDataFormatType": "spatialdata._io.format", + # _io.io_zarr + "read_zarr": "spatialdata._io.io_zarr", + # _utils + "get_pyramid_levels": "spatialdata._utils", + "unpad_raster": "spatialdata._utils", + # config + "settings": "spatialdata.config", +} + +__all__ = [ + # _core._deepcopy + "deepcopy", + # _core._utils + "sanitize_name", + "sanitize_table", + # _core.centroids + "get_centroids", + # _core.concatenate "concatenate", + # _core.data_extent + "are_extents_equal", + "get_extent", + # _core.operations.aggregate + "aggregate", + # _core.operations.map + "map_raster", + "relabel_sequential", + # _core.operations.rasterize "rasterize", + # _core.operations.rasterize_bins "rasterize_bins", "rasterize_bins_link_table_to_labels", + # _core.operations.transform + "transform", + # _core.operations.vectorize "to_circles", "to_polygons", - "transform", - "aggregate", - "bounding_box_query", - "polygon_query", + # _core.query._utils + "get_bounding_box_corners", + # _core.query.relational_query + "filter_by_table_query", "get_element_annotators", "get_element_instances", "get_values", "join_spatialelement_table", "match_element_to_table", - "match_table_to_element", "match_sdata_to_table", - "filter_by_table_query", + "match_table_to_element", + # _core.query.spatial_query + "bounding_box_query", + "polygon_query", + # _core.spatialdata "SpatialData", - "get_extent", - "get_centroids", + # _io._utils + "get_dask_backing_files", + # _io.format "SpatialDataFormatType", + # _io.io_zarr "read_zarr", - "unpad_raster", + # _utils "get_pyramid_levels", - "get_dask_backing_files", - "are_extents_equal", - "relabel_sequential", - "map_raster", - "deepcopy", - "sanitize_table", - "sanitize_name", + "unpad_raster", + # config "settings", ] -from spatialdata import dataloader, datasets, models, transformations -from spatialdata._core._deepcopy import deepcopy -from spatialdata._core._utils import sanitize_name, sanitize_table -from spatialdata._core.centroids import get_centroids -from spatialdata._core.concatenate import concatenate -from spatialdata._core.data_extent import are_extents_equal, get_extent -from spatialdata._core.operations.aggregate import aggregate -from spatialdata._core.operations.map import map_raster, relabel_sequential -from spatialdata._core.operations.rasterize import rasterize -from spatialdata._core.operations.rasterize_bins import rasterize_bins, rasterize_bins_link_table_to_labels -from spatialdata._core.operations.transform import transform -from spatialdata._core.operations.vectorize import to_circles, to_polygons -from spatialdata._core.query._utils import get_bounding_box_corners -from spatialdata._core.query.relational_query import ( - filter_by_table_query, - get_element_annotators, - get_element_instances, - get_values, - join_spatialelement_table, - match_element_to_table, - match_sdata_to_table, - match_table_to_element, -) -from spatialdata._core.query.spatial_query import bounding_box_query, polygon_query -from spatialdata._core.spatialdata import SpatialData -from spatialdata._io._utils import get_dask_backing_files -from spatialdata._io.format import SpatialDataFormatType -from spatialdata._io.io_zarr import read_zarr -from spatialdata._utils import get_pyramid_levels, unpad_raster -from spatialdata.config import settings +_accessor_loaded = False + + +def __getattr__(name: str) -> Any: + global _accessor_loaded + if not _accessor_loaded: + _accessor_loaded = True + import spatialdata.models._accessor # noqa: F401 + + if name in _submodules: + return importlib.import_module(f"spatialdata.{name}") + if name in _LAZY_IMPORTS: + mod = importlib.import_module(_LAZY_IMPORTS[name]) + attr = getattr(mod, name) + globals()[name] = attr + return attr + try: + return globals()[name] + except KeyError as e: + raise AttributeError(f"module 'spatialdata' has no attribute {name!r}") from e + + +def __dir__() -> list[str]: + return __all__ + ["__version__"] + + +if TYPE_CHECKING: + # submodules + from spatialdata import dataloader, datasets, models, transformations + + # _core._deepcopy + from spatialdata._core._deepcopy import deepcopy + + # _core._utils + from spatialdata._core._utils import sanitize_name, sanitize_table + + # _core.centroids + from spatialdata._core.centroids import get_centroids + + # _core.concatenate + from spatialdata._core.concatenate import concatenate + + # _core.data_extent + from spatialdata._core.data_extent import are_extents_equal, get_extent + + # _core.operations.aggregate + from spatialdata._core.operations.aggregate import aggregate + + # _core.operations.map + from spatialdata._core.operations.map import map_raster, relabel_sequential + + # _core.operations.rasterize + from spatialdata._core.operations.rasterize import rasterize + + # _core.operations.rasterize_bins + from spatialdata._core.operations.rasterize_bins import rasterize_bins, rasterize_bins_link_table_to_labels + + # _core.operations.transform + from spatialdata._core.operations.transform import transform + + # _core.operations.vectorize + from spatialdata._core.operations.vectorize import to_circles, to_polygons + + # _core.query._utils + from spatialdata._core.query._utils import get_bounding_box_corners + + # _core.query.relational_query + from spatialdata._core.query.relational_query import ( + filter_by_table_query, + get_element_annotators, + get_element_instances, + get_values, + join_spatialelement_table, + match_element_to_table, + match_sdata_to_table, + match_table_to_element, + ) + + # _core.query.spatial_query + from spatialdata._core.query.spatial_query import bounding_box_query, polygon_query + + # _core.spatialdata + from spatialdata._core.spatialdata import SpatialData + + # _io._utils + from spatialdata._io._utils import get_dask_backing_files + + # _io.format + from spatialdata._io.format import SpatialDataFormatType + + # _io.io_zarr + from spatialdata._io.io_zarr import read_zarr + + # _utils + from spatialdata._utils import get_pyramid_levels, unpad_raster + + # config + from spatialdata.config import settings diff --git a/src/spatialdata/_core/operations/aggregate.py b/src/spatialdata/_core/operations/aggregate.py index 131b8d7f..5fdbfd61 100644 --- a/src/spatialdata/_core/operations/aggregate.py +++ b/src/spatialdata/_core/operations/aggregate.py @@ -9,10 +9,8 @@ import pandas as pd from dask.dataframe import DataFrame as DaskDataFrame from geopandas import GeoDataFrame -from scipy import sparse from shapely import Point from xarray import DataArray, DataTree -from xrspatial import zonal_stats from spatialdata._core.operations._utils import _parse_element from spatialdata._core.operations.transform import transform @@ -270,6 +268,9 @@ def _aggregate_image_by_labels( ------- AnnData of shape `(by.shape[0], len(agg_func)]`. """ + from scipy import sparse + from xrspatial import zonal_stats + if isinstance(by, DataTree): assert len(by["scale0"]) == 1 by = next(iter(by["scale0"].values())) @@ -468,6 +469,8 @@ def _aggregate_shapes( columns_categories * (numel // len(columns_categories)), categories=columns_categories ) + from scipy import sparse + X = sparse.coo_matrix( ( aggregated_values.ravel(), diff --git a/src/spatialdata/_core/operations/rasterize.py b/src/spatialdata/_core/operations/rasterize.py index 12bbb68f..d5b28c28 100644 --- a/src/spatialdata/_core/operations/rasterize.py +++ b/src/spatialdata/_core/operations/rasterize.py @@ -1,5 +1,7 @@ -import dask_image.ndinterp -import datashader as ds +from __future__ import annotations + +from typing import TYPE_CHECKING + import numpy as np from dask.array import Array as DaskArray from dask.dataframe import DataFrame as DaskDataFrame @@ -7,6 +9,9 @@ from shapely import Point from xarray import DataArray, DataTree +if TYPE_CHECKING: + import datashader as ds + from spatialdata._core.operations._utils import _parse_element from spatialdata._core.operations.transform import transform from spatialdata._core.operations.vectorize import to_polygons @@ -505,6 +510,8 @@ def rasterize_images_labels( target_height: float | None = None, target_depth: float | None = None, ) -> DataArray: + import dask_image.ndinterp + min_coordinate = _parse_list_into_array(min_coordinate) max_coordinate = _parse_list_into_array(max_coordinate) # get dimensions of the target image @@ -624,6 +631,8 @@ def rasterize_shapes_points( agg_func: str | ds.reductions.Reduction | None = None, return_single_channel: bool | None = None, ) -> DataArray: + import datashader as ds + min_coordinate = _parse_list_into_array(min_coordinate) max_coordinate = _parse_list_into_array(max_coordinate) target_width, target_height, target_depth = _compute_target_dimensions( @@ -737,6 +746,8 @@ def rasterize_shapes_points( def _default_agg_func( data: DaskDataFrame | GeoDataFrame, value_key: str | None, return_single_channel: bool ) -> ds.reductions.Reduction: + import datashader as ds + if value_key is None: return ds.count() diff --git a/src/spatialdata/_core/operations/rasterize_bins.py b/src/spatialdata/_core/operations/rasterize_bins.py index f61d1758..87af14de 100644 --- a/src/spatialdata/_core/operations/rasterize_bins.py +++ b/src/spatialdata/_core/operations/rasterize_bins.py @@ -9,9 +9,7 @@ from dask.dataframe import DataFrame as DaskDataFrame from geopandas import GeoDataFrame from numpy.random import default_rng -from scipy.sparse import csc_matrix from shapely import MultiPolygon, Point, Polygon -from skimage.transform import estimate_transform from xarray import DataArray from spatialdata._core.query.relational_query import get_values @@ -126,6 +124,8 @@ def rasterize_bins( transformations = get_transformation(element, get_all=True) assert isinstance(transformations, dict) else: + from skimage.transform import estimate_transform + # get the transformation if table.n_obs < 6: raise ValueError("At least 6 bins are needed to estimate the transformation.") @@ -177,6 +177,8 @@ def rasterize_bins( keys = ([value_key] if isinstance(value_key, str) else value_key) if value_key is not None else table.var_names + from scipy.sparse import csc_matrix + if (value_key is None or any(key in table.var_names for key in keys)) and not isinstance( table.X, csc_matrix | np.ndarray ): diff --git a/src/spatialdata/_core/operations/vectorize.py b/src/spatialdata/_core/operations/vectorize.py index 40d3e31f..41458458 100644 --- a/src/spatialdata/_core/operations/vectorize.py +++ b/src/spatialdata/_core/operations/vectorize.py @@ -1,17 +1,20 @@ +from __future__ import annotations + from functools import singledispatch -from typing import Any +from typing import TYPE_CHECKING, Any import dask import numpy as np import pandas as pd import shapely -import skimage.measure from dask.dataframe import DataFrame as DaskDataFrame from geopandas import GeoDataFrame from shapely import MultiPolygon, Point, Polygon -from skimage.measure._regionprops import RegionProperties from xarray import DataArray, DataTree +if TYPE_CHECKING: + from skimage.measure._regionprops import RegionProperties + from spatialdata._core.centroids import get_centroids from spatialdata._core.operations.aggregate import aggregate from spatialdata._logging import logger @@ -226,6 +229,8 @@ def _vectorize_chunk(chunk: np.ndarray, yoff: int, xoff: int) -> GeoDataFrame: def _region_props_to_polygon(region_props: RegionProperties) -> MultiPolygon | Polygon: + import skimage.measure + mask = np.pad(region_props.image, 1) contours = skimage.measure.find_contours(mask, 0.5) @@ -244,6 +249,8 @@ def _region_props_to_polygon(region_props: RegionProperties) -> MultiPolygon | P def _vectorize_mask( mask: np.ndarray, # type: ignore[type-arg] ) -> GeoDataFrame: + import skimage.measure + if mask.max() == 0: return GeoDataFrame({"label": []}, geometry=[]) diff --git a/src/spatialdata/_core/validation.py b/src/spatialdata/_core/validation.py index 50e1b65d..df354fc3 100644 --- a/src/spatialdata/_core/validation.py +++ b/src/spatialdata/_core/validation.py @@ -1,9 +1,12 @@ +from __future__ import annotations + from collections.abc import Collection from types import TracebackType -from typing import NamedTuple, cast +from typing import TYPE_CHECKING, NamedTuple, cast -import pandas as pd -from anndata import AnnData +if TYPE_CHECKING: + import pandas as pd + from anndata import AnnData class ErrorDetails(NamedTuple): @@ -278,7 +281,7 @@ def __call__( self, location: str | tuple[str, ...] = (), expected_exception: type[BaseException] | tuple[type[BaseException], ...] | None = None, - ) -> "_ErrorDetailsCollector": + ) -> _ErrorDetailsCollector: """ Set or override error details in advance before an exception is raised. diff --git a/src/spatialdata/datasets.py b/src/spatialdata/datasets.py index de144be7..85c1c142 100644 --- a/src/spatialdata/datasets.py +++ b/src/spatialdata/datasets.py @@ -6,14 +6,12 @@ import dask.dataframe.core import numpy as np import pandas as pd -import scipy from anndata import AnnData from dask.dataframe import DataFrame as DaskDataFrame from geopandas import GeoDataFrame from numpy.random import default_rng from shapely.affinity import translate from shapely.geometry import MultiPolygon, Point, Polygon -from skimage.segmentation import slic from xarray import DataArray, DataTree from spatialdata._core.operations.aggregate import aggregate @@ -89,6 +87,9 @@ def raccoon( self, ) -> SpatialData: """Raccoon dataset.""" + import scipy.datasets + from skimage.segmentation import slic + im_data = scipy.datasets.face() im = Image2DModel.parse(im_data, dims=["y", "x", "c"]) labels_data = slic(im_data, n_segments=100, compactness=10, sigma=1) diff --git a/src/spatialdata/models/_utils.py b/src/spatialdata/models/_utils.py index 6631f6c9..26e9aafe 100644 --- a/src/spatialdata/models/_utils.py +++ b/src/spatialdata/models/_utils.py @@ -8,7 +8,6 @@ import geopandas import numpy as np import pandas as pd -from anndata import AnnData from dask.dataframe import DataFrame as DaskDataFrame from geopandas import GeoDataFrame from shapely.geometry import MultiPolygon, Point, Polygon @@ -29,6 +28,8 @@ X = "x" if TYPE_CHECKING: + from anndata import AnnData + from spatialdata.models.models import RasterSchema diff --git a/src/spatialdata/transformations/operations.py b/src/spatialdata/transformations/operations.py index 15fe3d57..31bd5ccc 100644 --- a/src/spatialdata/transformations/operations.py +++ b/src/spatialdata/transformations/operations.py @@ -3,16 +3,16 @@ import contextlib from typing import TYPE_CHECKING -import networkx as nx import numpy as np from dask.dataframe import DataFrame as DaskDataFrame from geopandas import GeoDataFrame -from skimage.transform import estimate_transform from spatialdata._logging import logger from spatialdata.transformations._utils import _get_transformations, _set_transformations if TYPE_CHECKING: + import networkx as nx + from spatialdata._core.spatialdata import SpatialData from spatialdata.models._utils import SpatialElement from spatialdata.transformations.transformations import Affine, BaseTransformation @@ -194,6 +194,8 @@ def remove_transformation( def _build_transformations_graph(sdata: SpatialData) -> nx.Graph: + import networkx as nx + g = nx.DiGraph() gen = sdata._gen_spatial_element_values() for cs in sdata.coordinate_systems: @@ -236,6 +238,8 @@ def get_transformation_between_coordinate_systems( ------- The transformation to map the source coordinate system to the target coordinate system. """ + import networkx as nx + from spatialdata.models._utils import has_type_spatial_element from spatialdata.transformations import Identity, Sequence @@ -373,6 +377,8 @@ def get_transformation_between_landmarks( >>> references_coords = PointsModel(points_reference) >>> transformation = get_transformation_between_landmarks(references_coords, moving_coords) """ + from skimage.transform import estimate_transform + from spatialdata import transform from spatialdata.models import get_axes_names from spatialdata.transformations.transformations import Affine, Sequence diff --git a/tests/test_init.py b/tests/test_init.py new file mode 100644 index 00000000..a0063f14 --- /dev/null +++ b/tests/test_init.py @@ -0,0 +1,23 @@ +import spatialdata + + +def test_package_has_version() -> None: + assert spatialdata.__version__ + + +def test_all_matches_lazy_imports() -> None: + """Ensure __all__ and _LAZY_IMPORTS stay in sync.""" + assert set(spatialdata.__all__) == set(spatialdata._LAZY_IMPORTS.keys()) + + +def test_all_are_importable() -> None: + """Every name in __all__ should be accessible on the module.""" + for name in spatialdata.__all__: + assert hasattr(spatialdata, name), f"{name!r} listed in __all__ but not resolvable" + + +def test_all_are_in_dir() -> None: + """dir(spatialdata) should expose everything in __all__.""" + module_dir = dir(spatialdata) + for name in spatialdata.__all__: + assert name in module_dir, f"{name!r} missing from dir()" From bcbef43a8a70a83d79e25aa41605b47701dc4fd3 Mon Sep 17 00:00:00 2001 From: Luca Marconato Date: Wed, 18 Feb 2026 18:12:35 +0100 Subject: [PATCH 3/6] more repeats for import benchmarks --- benchmarks/benchmark_imports.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/benchmarks/benchmark_imports.py b/benchmarks/benchmark_imports.py index 1cbfff65..61325f71 100644 --- a/benchmarks/benchmark_imports.py +++ b/benchmarks/benchmark_imports.py @@ -6,7 +6,17 @@ module cache. """ +from collections.abc import Callable + +def _timeraw(func: Callable) -> Callable: + """Set asv benchmark attributes for a cold-import timeraw function.""" + func.repeat = 5 # number of independent subprocess measurements + func.number = 1 # must be 1: second import in same process hits module cache + return func + + +@_timeraw def timeraw_import_spatialdata(): """Time a bare ``import spatialdata``.""" return """ @@ -14,6 +24,7 @@ def timeraw_import_spatialdata(): """ +@_timeraw def timeraw_import_SpatialData(): """Time importing the top-level ``SpatialData`` class.""" return """ @@ -21,6 +32,7 @@ def timeraw_import_SpatialData(): """ +@_timeraw def timeraw_import_read_zarr(): """Time importing ``read_zarr`` from the top-level namespace.""" return """ @@ -28,6 +40,7 @@ def timeraw_import_read_zarr(): """ +@_timeraw def timeraw_import_models_elements(): """Time importing the main element model classes.""" return """ @@ -35,6 +48,7 @@ def timeraw_import_models_elements(): """ +@_timeraw def timeraw_import_transformations(): """Time importing the ``spatialdata.transformations`` submodule.""" return """ From d3e46491543b745f4983fa4c0c775c3e11cc7b5c Mon Sep 17 00:00:00 2001 From: Luca Marconato Date: Wed, 18 Feb 2026 18:23:46 +0100 Subject: [PATCH 4/6] add profimp to optional requirements --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index b1b0e679..9c3f6b97 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -79,6 +79,7 @@ docs = [ benchmark = [ "asv", "memray", + "profimp", ] torch = [ "torch" From ce54b02c473a815201df20e6a51b130590928825 Mon Sep 17 00:00:00 2001 From: Luca Marconato Date: Wed, 18 Feb 2026 18:29:58 +0100 Subject: [PATCH 5/6] relax pre-commit ignore; cleanup optional deps --- benchmarks/benchmark_imports.py | 14 +++++++------- pyproject.toml | 1 - 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/benchmarks/benchmark_imports.py b/benchmarks/benchmark_imports.py index 61325f71..e1a47ea8 100644 --- a/benchmarks/benchmark_imports.py +++ b/benchmarks/benchmark_imports.py @@ -1,4 +1,3 @@ -# type: ignore """Benchmarks for import times of the spatialdata package and its submodules. Each ``timeraw_*`` function returns a snippet of Python code that asv runs in @@ -7,9 +6,10 @@ """ from collections.abc import Callable +from typing import Any -def _timeraw(func: Callable) -> Callable: +def _timeraw(func: Any) -> Any: """Set asv benchmark attributes for a cold-import timeraw function.""" func.repeat = 5 # number of independent subprocess measurements func.number = 1 # must be 1: second import in same process hits module cache @@ -17,7 +17,7 @@ def _timeraw(func: Callable) -> Callable: @_timeraw -def timeraw_import_spatialdata(): +def timeraw_import_spatialdata() -> str: """Time a bare ``import spatialdata``.""" return """ import spatialdata @@ -25,7 +25,7 @@ def timeraw_import_spatialdata(): @_timeraw -def timeraw_import_SpatialData(): +def timeraw_import_SpatialData() -> str: """Time importing the top-level ``SpatialData`` class.""" return """ from spatialdata import SpatialData @@ -33,7 +33,7 @@ def timeraw_import_SpatialData(): @_timeraw -def timeraw_import_read_zarr(): +def timeraw_import_read_zarr() -> str: """Time importing ``read_zarr`` from the top-level namespace.""" return """ from spatialdata import read_zarr @@ -41,7 +41,7 @@ def timeraw_import_read_zarr(): @_timeraw -def timeraw_import_models_elements(): +def timeraw_import_models_elements() -> str: """Time importing the main element model classes.""" return """ from spatialdata.models import Image2DModel, Labels2DModel, PointsModel, ShapesModel, TableModel @@ -49,7 +49,7 @@ def timeraw_import_models_elements(): @_timeraw -def timeraw_import_transformations(): +def timeraw_import_transformations() -> str: """Time importing the ``spatialdata.transformations`` submodule.""" return """ from spatialdata.transformations import Affine, Scale, Translation, Sequence diff --git a/pyproject.toml b/pyproject.toml index 9c3f6b97..545af43e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -55,7 +55,6 @@ dependencies = [ [project.optional-dependencies] dev = [ "bump2version", - "profimp", ] test = [ "pytest", From 0dce65b5dc6de5205c129eaad987eb867472318a Mon Sep 17 00:00:00 2001 From: "Philipp A." Date: Thu, 19 Feb 2026 14:14:10 +0100 Subject: [PATCH 6/6] tests: force string annotations --- docs/conf.py | 2 ++ docs/extensions/typed_returns.py | 2 ++ pyproject.toml | 3 +++ src/spatialdata/__main__.py | 2 ++ src/spatialdata/_core/_deepcopy.py | 2 ++ src/spatialdata/_core/_elements.py | 2 ++ src/spatialdata/_core/_utils.py | 2 ++ src/spatialdata/_core/centroids.py | 2 ++ src/spatialdata/_core/concatenate.py | 2 ++ src/spatialdata/_core/data_extent.py | 2 ++ src/spatialdata/_core/operations/aggregate.py | 2 ++ src/spatialdata/_core/operations/map.py | 2 ++ src/spatialdata/_core/query/_utils.py | 2 ++ src/spatialdata/_core/query/relational_query.py | 2 ++ src/spatialdata/_core/query/spatial_query.py | 2 ++ src/spatialdata/_docs.py | 2 ++ src/spatialdata/_io/__init__.py | 2 ++ src/spatialdata/_io/_utils.py | 2 ++ src/spatialdata/_io/format.py | 2 ++ src/spatialdata/_io/io_points.py | 2 ++ src/spatialdata/_io/io_raster.py | 2 ++ src/spatialdata/_io/io_shapes.py | 2 ++ src/spatialdata/_io/io_table.py | 2 ++ src/spatialdata/_io/io_zarr.py | 2 ++ src/spatialdata/_logging.py | 4 +++- src/spatialdata/_types.py | 2 ++ src/spatialdata/_utils.py | 2 ++ src/spatialdata/config.py | 2 ++ src/spatialdata/dataloader/datasets.py | 2 ++ src/spatialdata/datasets.py | 2 ++ src/spatialdata/io/__init__.py | 2 ++ src/spatialdata/models/__init__.py | 2 ++ src/spatialdata/models/_accessor.py | 2 ++ src/spatialdata/models/chunks_utils.py | 2 ++ src/spatialdata/models/models.py | 2 ++ src/spatialdata/models/pyramids_utils.py | 2 ++ src/spatialdata/testing.py | 2 ++ src/spatialdata/transformations/__init__.py | 2 ++ src/spatialdata/transformations/ngff/_utils.py | 2 ++ .../transformations/ngff/ngff_transformations.py | 16 +++++++++------- tests/conftest.py | 2 ++ tests/core/operations/test_aggregations.py | 2 ++ tests/core/operations/test_map.py | 2 ++ tests/core/operations/test_rasterize.py | 2 ++ tests/core/operations/test_rasterize_bins.py | 2 ++ .../operations/test_spatialdata_operations.py | 2 ++ tests/core/operations/test_transform.py | 2 ++ tests/core/operations/test_vectorize.py | 2 ++ tests/core/query/test_relational_query.py | 2 ++ ...test_relational_query_match_sdata_to_table.py | 2 ++ tests/core/query/test_spatial_query.py | 2 ++ tests/core/test_centroids.py | 2 ++ tests/core/test_data_extent.py | 2 ++ tests/core/test_deepcopy.py | 2 ++ tests/core/test_get_attrs.py | 2 ++ tests/core/test_validation.py | 2 ++ tests/dataloader/__init__.py | 2 ++ tests/dataloader/test_datasets.py | 2 ++ tests/datasets/test_datasets.py | 2 ++ tests/io/test_attrs_io.py | 2 ++ tests/io/test_format.py | 2 ++ tests/io/test_metadata.py | 2 ++ tests/io/test_multi_table.py | 2 ++ tests/io/test_pyramids_performance.py | 4 +++- tests/io/test_readwrite.py | 2 ++ tests/io/test_utils.py | 2 ++ tests/models/test_accessor.py | 2 ++ tests/models/test_chunks_utils.py | 2 ++ tests/models/test_models.py | 2 ++ tests/models/test_pyramids_utils.py | 2 ++ tests/test_init.py | 2 ++ tests/transformations/ngff/conftest.py | 2 ++ .../ngff/test_ngff_coordinate_system.py | 2 ++ .../ngff/test_ngff_transformations.py | 2 ++ tests/transformations/test_transformations.py | 2 ++ .../test_transformations_utils.py | 2 ++ tests/utils/test_element_utils.py | 2 ++ tests/utils/test_sanitize.py | 2 ++ tests/utils/test_testing.py | 2 ++ 79 files changed, 168 insertions(+), 9 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 4ea29943..6bd7b243 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -4,6 +4,8 @@ # list see the documentation: # https://www.sphinx-doc.org/en/master/usage/configuration.html +from __future__ import annotations + # -- Path setup -------------------------------------------------------------- import sys from datetime import datetime diff --git a/docs/extensions/typed_returns.py b/docs/extensions/typed_returns.py index d044c698..5eefc02a 100644 --- a/docs/extensions/typed_returns.py +++ b/docs/extensions/typed_returns.py @@ -1,5 +1,7 @@ # code from https://github.com/theislab/scanpy/blob/master/docs/extensions/typed_returns.py # with some minor adjustment +from __future__ import annotations + import re from sphinx.application import Sphinx diff --git a/pyproject.toml b/pyproject.toml index 545af43e..b3e53915 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -183,6 +183,9 @@ select = [ ] unfixable = ["B", "C4", "UP", "BLE", "T20", "RET"] +[tool.ruff.lint.isort] +required-imports = ["from __future__ import annotations"] + [tool.ruff.lint.pydocstyle] convention = "numpy" diff --git a/src/spatialdata/__main__.py b/src/spatialdata/__main__.py index 22b62cc1..6d15de39 100644 --- a/src/spatialdata/__main__.py +++ b/src/spatialdata/__main__.py @@ -6,6 +6,8 @@ the contents of a SpatialData .zarr dataset. Additional CLI functionalities will be implemented in the future. """ +from __future__ import annotations + from typing import Literal import click diff --git a/src/spatialdata/_core/_deepcopy.py b/src/spatialdata/_core/_deepcopy.py index 67d709f0..9e2e7f00 100644 --- a/src/spatialdata/_core/_deepcopy.py +++ b/src/spatialdata/_core/_deepcopy.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from copy import deepcopy as _deepcopy from functools import singledispatch diff --git a/src/spatialdata/_core/_elements.py b/src/spatialdata/_core/_elements.py index 88205c7e..a3267a70 100644 --- a/src/spatialdata/_core/_elements.py +++ b/src/spatialdata/_core/_elements.py @@ -1,5 +1,7 @@ """SpatialData elements.""" +from __future__ import annotations + from collections import UserDict from collections.abc import Iterable, KeysView, ValuesView from typing import TypeVar diff --git a/src/spatialdata/_core/_utils.py b/src/spatialdata/_core/_utils.py index a6806334..a5581565 100644 --- a/src/spatialdata/_core/_utils.py +++ b/src/spatialdata/_core/_utils.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from collections.abc import Iterable from anndata import AnnData diff --git a/src/spatialdata/_core/centroids.py b/src/spatialdata/_core/centroids.py index cd43fb2f..24ea616c 100644 --- a/src/spatialdata/_core/centroids.py +++ b/src/spatialdata/_core/centroids.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from collections import defaultdict from functools import singledispatch diff --git a/src/spatialdata/_core/concatenate.py b/src/spatialdata/_core/concatenate.py index f9004360..b0639eac 100644 --- a/src/spatialdata/_core/concatenate.py +++ b/src/spatialdata/_core/concatenate.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from collections import defaultdict from collections.abc import Callable, Iterable from copy import copy # Should probably go up at the top diff --git a/src/spatialdata/_core/data_extent.py b/src/spatialdata/_core/data_extent.py index 6abd46ca..6504bb08 100644 --- a/src/spatialdata/_core/data_extent.py +++ b/src/spatialdata/_core/data_extent.py @@ -1,4 +1,6 @@ # Functions to compute the bounding box describing the extent of a SpatialElement or SpatialData object +from __future__ import annotations + from collections import defaultdict from functools import singledispatch diff --git a/src/spatialdata/_core/operations/aggregate.py b/src/spatialdata/_core/operations/aggregate.py index 5fdbfd61..d0c4741e 100644 --- a/src/spatialdata/_core/operations/aggregate.py +++ b/src/spatialdata/_core/operations/aggregate.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import warnings from typing import Any diff --git a/src/spatialdata/_core/operations/map.py b/src/spatialdata/_core/operations/map.py index 823295e1..0f5a380a 100644 --- a/src/spatialdata/_core/operations/map.py +++ b/src/spatialdata/_core/operations/map.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import math import operator from collections.abc import Callable, Iterable, Mapping diff --git a/src/spatialdata/_core/query/_utils.py b/src/spatialdata/_core/query/_utils.py index 5ab904d9..f66679ca 100644 --- a/src/spatialdata/_core/query/_utils.py +++ b/src/spatialdata/_core/query/_utils.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from typing import Any import numba as nb diff --git a/src/spatialdata/_core/query/relational_query.py b/src/spatialdata/_core/query/relational_query.py index 01cfb745..c83cd770 100644 --- a/src/spatialdata/_core/query/relational_query.py +++ b/src/spatialdata/_core/query/relational_query.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import math import warnings from collections import defaultdict diff --git a/src/spatialdata/_core/query/spatial_query.py b/src/spatialdata/_core/query/spatial_query.py index b112b68c..c29e3bc0 100644 --- a/src/spatialdata/_core/query/spatial_query.py +++ b/src/spatialdata/_core/query/spatial_query.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import warnings from abc import abstractmethod from collections.abc import Callable, Mapping diff --git a/src/spatialdata/_docs.py b/src/spatialdata/_docs.py index b8e13bc7..691ed765 100644 --- a/src/spatialdata/_docs.py +++ b/src/spatialdata/_docs.py @@ -1,4 +1,6 @@ # from https://stackoverflow.com/questions/10307696/how-to-put-a-variable-into-python-docstring +from __future__ import annotations + from collections.abc import Callable from typing import Any, TypeVar diff --git a/src/spatialdata/_io/__init__.py b/src/spatialdata/_io/__init__.py index b0dc914e..38ff8c6b 100644 --- a/src/spatialdata/_io/__init__.py +++ b/src/spatialdata/_io/__init__.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from spatialdata._io._utils import get_dask_backing_files from spatialdata._io.format import SpatialDataFormatType from spatialdata._io.io_points import write_points diff --git a/src/spatialdata/_io/_utils.py b/src/spatialdata/_io/_utils.py index b58d6744..6690d111 100644 --- a/src/spatialdata/_io/_utils.py +++ b/src/spatialdata/_io/_utils.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import filecmp import os.path import re diff --git a/src/spatialdata/_io/format.py b/src/spatialdata/_io/format.py index cce6654e..cfb5be1b 100644 --- a/src/spatialdata/_io/format.py +++ b/src/spatialdata/_io/format.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from collections.abc import Iterator from typing import Any diff --git a/src/spatialdata/_io/io_points.py b/src/spatialdata/_io/io_points.py index 5f42497c..b47fc418 100644 --- a/src/spatialdata/_io/io_points.py +++ b/src/spatialdata/_io/io_points.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from pathlib import Path import zarr diff --git a/src/spatialdata/_io/io_raster.py b/src/spatialdata/_io/io_raster.py index 86b537d0..df7e1cb8 100644 --- a/src/spatialdata/_io/io_raster.py +++ b/src/spatialdata/_io/io_raster.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from pathlib import Path from typing import Any, Literal diff --git a/src/spatialdata/_io/io_shapes.py b/src/spatialdata/_io/io_shapes.py index 65cb099a..b0725627 100644 --- a/src/spatialdata/_io/io_shapes.py +++ b/src/spatialdata/_io/io_shapes.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from pathlib import Path from typing import Any, Literal diff --git a/src/spatialdata/_io/io_table.py b/src/spatialdata/_io/io_table.py index d2e5c3cc..8cd7b838 100644 --- a/src/spatialdata/_io/io_table.py +++ b/src/spatialdata/_io/io_table.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from pathlib import Path import numpy as np diff --git a/src/spatialdata/_io/io_zarr.py b/src/spatialdata/_io/io_zarr.py index 0312dc96..4c410fab 100644 --- a/src/spatialdata/_io/io_zarr.py +++ b/src/spatialdata/_io/io_zarr.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import os import warnings from collections.abc import Callable diff --git a/src/spatialdata/_logging.py b/src/spatialdata/_logging.py index c49c1c88..0b117a7a 100644 --- a/src/spatialdata/_logging.py +++ b/src/spatialdata/_logging.py @@ -1,8 +1,10 @@ +from __future__ import annotations + import logging import os -def _setup_logger() -> "logging.Logger": +def _setup_logger() -> logging.Logger: from rich.console import Console from rich.logging import RichHandler diff --git a/src/spatialdata/_types.py b/src/spatialdata/_types.py index e1415f74..c5bd76de 100644 --- a/src/spatialdata/_types.py +++ b/src/spatialdata/_types.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from typing import Any, TypeAlias import numpy as np diff --git a/src/spatialdata/_utils.py b/src/spatialdata/_utils.py index 64dd7638..3df13385 100644 --- a/src/spatialdata/_utils.py +++ b/src/spatialdata/_utils.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import functools import re import warnings diff --git a/src/spatialdata/config.py b/src/spatialdata/config.py index dab848b3..35b96e5f 100644 --- a/src/spatialdata/config.py +++ b/src/spatialdata/config.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from dataclasses import dataclass from typing import Literal diff --git a/src/spatialdata/dataloader/datasets.py b/src/spatialdata/dataloader/datasets.py index 4bf14a91..6a105b68 100644 --- a/src/spatialdata/dataloader/datasets.py +++ b/src/spatialdata/dataloader/datasets.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import warnings from collections.abc import Callable, Mapping from functools import partial diff --git a/src/spatialdata/datasets.py b/src/spatialdata/datasets.py index 85c1c142..cb7a7b55 100644 --- a/src/spatialdata/datasets.py +++ b/src/spatialdata/datasets.py @@ -1,5 +1,7 @@ """SpatialData datasets.""" +from __future__ import annotations + import warnings from typing import Any, Literal diff --git a/src/spatialdata/io/__init__.py b/src/spatialdata/io/__init__.py index a23181ca..eba096e3 100644 --- a/src/spatialdata/io/__init__.py +++ b/src/spatialdata/io/__init__.py @@ -1,5 +1,7 @@ """Experimental bridge to the spatialdata_io package.""" +from __future__ import annotations + try: from spatialdata_io import * # noqa: F403 except ImportError as e: diff --git a/src/spatialdata/models/__init__.py b/src/spatialdata/models/__init__.py index ba064e0a..2f25b4f3 100644 --- a/src/spatialdata/models/__init__.py +++ b/src/spatialdata/models/__init__.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from spatialdata._core.validation import check_target_region_column_symmetry from spatialdata.models._utils import ( C, diff --git a/src/spatialdata/models/_accessor.py b/src/spatialdata/models/_accessor.py index a8b19653..1b635427 100644 --- a/src/spatialdata/models/_accessor.py +++ b/src/spatialdata/models/_accessor.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from collections.abc import Iterator, MutableMapping from typing import Any, Literal diff --git a/src/spatialdata/models/chunks_utils.py b/src/spatialdata/models/chunks_utils.py index 14ac208e..47406701 100644 --- a/src/spatialdata/models/chunks_utils.py +++ b/src/spatialdata/models/chunks_utils.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from collections.abc import Mapping, Sequence from typing import Any, TypeAlias diff --git a/src/spatialdata/models/models.py b/src/spatialdata/models/models.py index c8001a84..16883c6a 100644 --- a/src/spatialdata/models/models.py +++ b/src/spatialdata/models/models.py @@ -1,5 +1,7 @@ """Models and schema for SpatialData.""" +from __future__ import annotations + import warnings from collections.abc import Mapping, Sequence from functools import singledispatchmethod diff --git a/src/spatialdata/models/pyramids_utils.py b/src/spatialdata/models/pyramids_utils.py index 8a0a0ba1..bef7dfb3 100644 --- a/src/spatialdata/models/pyramids_utils.py +++ b/src/spatialdata/models/pyramids_utils.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from collections.abc import Sequence from typing import Any diff --git a/src/spatialdata/testing.py b/src/spatialdata/testing.py index 1d145547..17945818 100644 --- a/src/spatialdata/testing.py +++ b/src/spatialdata/testing.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from anndata import AnnData from anndata.tests.helpers import assert_equal as assert_anndata_equal from dask.dataframe import DataFrame as DaskDataFrame diff --git a/src/spatialdata/transformations/__init__.py b/src/spatialdata/transformations/__init__.py index f6affcaf..e95a9276 100644 --- a/src/spatialdata/transformations/__init__.py +++ b/src/spatialdata/transformations/__init__.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from spatialdata.transformations.operations import ( align_elements_using_landmarks, get_transformation, diff --git a/src/spatialdata/transformations/ngff/_utils.py b/src/spatialdata/transformations/ngff/_utils.py index 4d0eaf21..07dff152 100644 --- a/src/spatialdata/transformations/ngff/_utils.py +++ b/src/spatialdata/transformations/ngff/_utils.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import copy from spatialdata.models import C, X, Y, Z diff --git a/src/spatialdata/transformations/ngff/ngff_transformations.py b/src/spatialdata/transformations/ngff/ngff_transformations.py index 6e25646f..1758159b 100644 --- a/src/spatialdata/transformations/ngff/ngff_transformations.py +++ b/src/spatialdata/transformations/ngff/ngff_transformations.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import math from abc import ABC, abstractmethod from numbers import Number @@ -71,11 +73,11 @@ def __repr__(self) -> str: @classmethod @abstractmethod - def _from_dict(cls, d: Transformation_t) -> "NgffBaseTransformation": + def _from_dict(cls, d: Transformation_t) -> NgffBaseTransformation: pass @classmethod - def from_dict(cls, d: Transformation_t) -> "NgffBaseTransformation": + def from_dict(cls, d: Transformation_t) -> NgffBaseTransformation: """ Initialize a transformation from the Python dict of its json representation. @@ -134,7 +136,7 @@ def _update_dict_with_input_output_cs(self, d: Transformation_t) -> None: d["output"] = d["output"].to_dict() @abstractmethod - def inverse(self) -> "NgffBaseTransformation": + def inverse(self) -> NgffBaseTransformation: """Return the inverse of the transformation.""" @abstractmethod @@ -156,7 +158,7 @@ def transform_points(self, points: ArrayLike) -> ArrayLike: """ @abstractmethod - def to_affine(self) -> "NgffAffine": + def to_affine(self) -> NgffAffine: """Convert the transformation to an affine transformation, whenever the conversion can be made.""" def _validate_transform_points_shapes(self, input_size: int, points_shape: tuple[int, ...]) -> None: @@ -177,7 +179,7 @@ def _validate_transform_points_shapes(self, input_size: int, points_shape: tuple ) # order of the composition: self is applied first, then the transformation passed as argument - def compose_with(self, transformation: "NgffBaseTransformation") -> "NgffBaseTransformation": + def compose_with(self, transformation: NgffBaseTransformation) -> NgffBaseTransformation: """ Compose the transfomation object with another transformation @@ -343,7 +345,7 @@ def transform_points(self, points: ArrayLike) -> ArrayLike: assert isinstance(res, np.ndarray) return res - def to_affine(self) -> "NgffAffine": + def to_affine(self) -> NgffAffine: return NgffAffine( self.affine, input_coordinate_system=self.input_coordinate_system, @@ -381,7 +383,7 @@ def from_input_output_coordinate_systems( cls, input_coordinate_system: NgffCoordinateSystem, output_coordinate_system: NgffCoordinateSystem, - ) -> "NgffAffine": + ) -> NgffAffine: input_axes = input_coordinate_system.axes_names output_axes = output_coordinate_system.axes_names m = cls._affine_matrix_from_input_and_output_axes(input_axes, output_axes) diff --git a/tests/conftest.py b/tests/conftest.py index 7149b4e7..c9793912 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from collections.abc import Sequence from pathlib import Path from typing import Any diff --git a/tests/core/operations/test_aggregations.py b/tests/core/operations/test_aggregations.py index eb2ed089..d471b79c 100644 --- a/tests/core/operations/test_aggregations.py +++ b/tests/core/operations/test_aggregations.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import geopandas import numpy as np import pandas as pd diff --git a/tests/core/operations/test_map.py b/tests/core/operations/test_map.py index a032d381..cde00fd3 100644 --- a/tests/core/operations/test_map.py +++ b/tests/core/operations/test_map.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import math import re diff --git a/tests/core/operations/test_rasterize.py b/tests/core/operations/test_rasterize.py index a2ffde3d..53d362d1 100644 --- a/tests/core/operations/test_rasterize.py +++ b/tests/core/operations/test_rasterize.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import dask.dataframe as dd import numpy as np import pandas as pd diff --git a/tests/core/operations/test_rasterize_bins.py b/tests/core/operations/test_rasterize_bins.py index b99508ef..2918855d 100644 --- a/tests/core/operations/test_rasterize_bins.py +++ b/tests/core/operations/test_rasterize_bins.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import logging import re diff --git a/tests/core/operations/test_spatialdata_operations.py b/tests/core/operations/test_spatialdata_operations.py index 6ea4661d..68b538e0 100644 --- a/tests/core/operations/test_spatialdata_operations.py +++ b/tests/core/operations/test_spatialdata_operations.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import math import numpy as np diff --git a/tests/core/operations/test_transform.py b/tests/core/operations/test_transform.py index 1bb494fb..e006cf28 100644 --- a/tests/core/operations/test_transform.py +++ b/tests/core/operations/test_transform.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import math import tempfile from pathlib import Path diff --git a/tests/core/operations/test_vectorize.py b/tests/core/operations/test_vectorize.py index cf5e2794..ae83f6c9 100644 --- a/tests/core/operations/test_vectorize.py +++ b/tests/core/operations/test_vectorize.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import math import numpy as np diff --git a/tests/core/query/test_relational_query.py b/tests/core/query/test_relational_query.py index 3267b1e5..63e7a6f1 100644 --- a/tests/core/query/test_relational_query.py +++ b/tests/core/query/test_relational_query.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import annsel as an import numpy as np import pandas as pd diff --git a/tests/core/query/test_relational_query_match_sdata_to_table.py b/tests/core/query/test_relational_query_match_sdata_to_table.py index 6d4fcadf..0c47ca91 100644 --- a/tests/core/query/test_relational_query_match_sdata_to_table.py +++ b/tests/core/query/test_relational_query_match_sdata_to_table.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import pytest from spatialdata import SpatialData, concatenate, match_sdata_to_table diff --git a/tests/core/query/test_spatial_query.py b/tests/core/query/test_spatial_query.py index d7147dbf..7dadc6f6 100644 --- a/tests/core/query/test_spatial_query.py +++ b/tests/core/query/test_spatial_query.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from dataclasses import FrozenInstanceError import dask.dataframe as dd diff --git a/tests/core/test_centroids.py b/tests/core/test_centroids.py index aa332f9d..94cd40b0 100644 --- a/tests/core/test_centroids.py +++ b/tests/core/test_centroids.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import math import numpy as np diff --git a/tests/core/test_data_extent.py b/tests/core/test_data_extent.py index de5875cf..4d2316ac 100644 --- a/tests/core/test_data_extent.py +++ b/tests/core/test_data_extent.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import math import numpy as np diff --git a/tests/core/test_deepcopy.py b/tests/core/test_deepcopy.py index b21cc925..b63b4361 100644 --- a/tests/core/test_deepcopy.py +++ b/tests/core/test_deepcopy.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from pandas.testing import assert_frame_equal from spatialdata import SpatialData diff --git a/tests/core/test_get_attrs.py b/tests/core/test_get_attrs.py index aedf3271..a6b467fa 100644 --- a/tests/core/test_get_attrs.py +++ b/tests/core/test_get_attrs.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import pandas as pd import pytest diff --git a/tests/core/test_validation.py b/tests/core/test_validation.py index 258c1471..bb612a4e 100644 --- a/tests/core/test_validation.py +++ b/tests/core/test_validation.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import pytest from spatialdata._core.validation import ValidationError, raise_validation_errors diff --git a/tests/dataloader/__init__.py b/tests/dataloader/__init__.py index 328f7403..c2a5fdd8 100644 --- a/tests/dataloader/__init__.py +++ b/tests/dataloader/__init__.py @@ -1,3 +1,5 @@ +from __future__ import annotations + try: from spatialdata.dataloader.datasets import ImageTilesDataset except ImportError as e: diff --git a/tests/dataloader/test_datasets.py b/tests/dataloader/test_datasets.py index 48881b0b..6b6607e8 100644 --- a/tests/dataloader/test_datasets.py +++ b/tests/dataloader/test_datasets.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import numpy as np import pytest diff --git a/tests/datasets/test_datasets.py b/tests/datasets/test_datasets.py index e22182b4..2237e253 100644 --- a/tests/datasets/test_datasets.py +++ b/tests/datasets/test_datasets.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from spatialdata.datasets import blobs, raccoon diff --git a/tests/io/test_attrs_io.py b/tests/io/test_attrs_io.py index 4dda6f0d..1f9895ea 100644 --- a/tests/io/test_attrs_io.py +++ b/tests/io/test_attrs_io.py @@ -1,5 +1,7 @@ """Test attrs read/write for all SpatialData container formats.""" +from __future__ import annotations + import tempfile from pathlib import Path diff --git a/tests/io/test_format.py b/tests/io/test_format.py index c8d9f04c..3ef1319c 100644 --- a/tests/io/test_format.py +++ b/tests/io/test_format.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import json import tempfile from pathlib import Path diff --git a/tests/io/test_metadata.py b/tests/io/test_metadata.py index bb993b00..dd0ae704 100644 --- a/tests/io/test_metadata.py +++ b/tests/io/test_metadata.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import logging import os import tempfile diff --git a/tests/io/test_multi_table.py b/tests/io/test_multi_table.py index 5c90109d..abaaea8d 100644 --- a/tests/io/test_multi_table.py +++ b/tests/io/test_multi_table.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from pathlib import Path import pandas as pd diff --git a/tests/io/test_pyramids_performance.py b/tests/io/test_pyramids_performance.py index 87587954..82501ae8 100644 --- a/tests/io/test_pyramids_performance.py +++ b/tests/io/test_pyramids_performance.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from pathlib import Path from typing import TYPE_CHECKING @@ -18,7 +20,7 @@ @pytest.fixture -def sdata_with_image(request: "_pytest.fixtures.SubRequest", tmp_path: Path) -> SpatialData: +def sdata_with_image(request: _pytest.fixtures.SubRequest, tmp_path: Path) -> SpatialData: params = request.param if request.param is not None else {} width = params.get("width", 2048) chunksize = params.get("chunk_size", 1024) diff --git a/tests/io/test_readwrite.py b/tests/io/test_readwrite.py index af028d29..b46620f4 100644 --- a/tests/io/test_readwrite.py +++ b/tests/io/test_readwrite.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import json import os import tempfile diff --git a/tests/io/test_utils.py b/tests/io/test_utils.py index 57bfe6e4..1f7be358 100644 --- a/tests/io/test_utils.py +++ b/tests/io/test_utils.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import os import tempfile from contextlib import nullcontext diff --git a/tests/models/test_accessor.py b/tests/models/test_accessor.py index 7356f52c..6f21a190 100644 --- a/tests/models/test_accessor.py +++ b/tests/models/test_accessor.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import dask.dataframe as dd import pandas as pd import pytest diff --git a/tests/models/test_chunks_utils.py b/tests/models/test_chunks_utils.py index 5bfcfa81..a450ec66 100644 --- a/tests/models/test_chunks_utils.py +++ b/tests/models/test_chunks_utils.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import pytest from spatialdata.models.chunks_utils import Chunks_t, normalize_chunks diff --git a/tests/models/test_models.py b/tests/models/test_models.py index e2087ace..348db1b2 100644 --- a/tests/models/test_models.py +++ b/tests/models/test_models.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import os import re import tempfile diff --git a/tests/models/test_pyramids_utils.py b/tests/models/test_pyramids_utils.py index da0a3354..5d5e9608 100644 --- a/tests/models/test_pyramids_utils.py +++ b/tests/models/test_pyramids_utils.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import numpy as np import pytest from multiscale_spatial_image.to_multiscale.to_multiscale import Methods diff --git a/tests/test_init.py b/tests/test_init.py index a0063f14..94107c93 100644 --- a/tests/test_init.py +++ b/tests/test_init.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import spatialdata diff --git a/tests/transformations/ngff/conftest.py b/tests/transformations/ngff/conftest.py index cc0089cd..a5ce9024 100644 --- a/tests/transformations/ngff/conftest.py +++ b/tests/transformations/ngff/conftest.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from spatialdata.transformations.ngff.ngff_coordinate_system import ( NgffAxis, NgffCoordinateSystem, diff --git a/tests/transformations/ngff/test_ngff_coordinate_system.py b/tests/transformations/ngff/test_ngff_coordinate_system.py index 6a8474b7..ae1412fc 100644 --- a/tests/transformations/ngff/test_ngff_coordinate_system.py +++ b/tests/transformations/ngff/test_ngff_coordinate_system.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import copy import json diff --git a/tests/transformations/ngff/test_ngff_transformations.py b/tests/transformations/ngff/test_ngff_transformations.py index 8e1ea0f3..7051f364 100644 --- a/tests/transformations/ngff/test_ngff_transformations.py +++ b/tests/transformations/ngff/test_ngff_transformations.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import contextlib import copy import json diff --git a/tests/transformations/test_transformations.py b/tests/transformations/test_transformations.py index b8571dc9..a8de25f4 100644 --- a/tests/transformations/test_transformations.py +++ b/tests/transformations/test_transformations.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from contextlib import nullcontext from copy import deepcopy diff --git a/tests/transformations/test_transformations_utils.py b/tests/transformations/test_transformations_utils.py index 2c69d96e..dc5f884b 100644 --- a/tests/transformations/test_transformations_utils.py +++ b/tests/transformations/test_transformations_utils.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from spatialdata.transformations._utils import convert_transformations_to_affine from spatialdata.transformations.operations import get_transformation, set_transformation from spatialdata.transformations.transformations import Affine, Scale, Sequence, Translation diff --git a/tests/utils/test_element_utils.py b/tests/utils/test_element_utils.py index 1bfd20aa..341f6687 100644 --- a/tests/utils/test_element_utils.py +++ b/tests/utils/test_element_utils.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import itertools import dask_image.ndinterp diff --git a/tests/utils/test_sanitize.py b/tests/utils/test_sanitize.py index b61f1908..6b2fd315 100644 --- a/tests/utils/test_sanitize.py +++ b/tests/utils/test_sanitize.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import numpy as np import pandas as pd import pytest diff --git a/tests/utils/test_testing.py b/tests/utils/test_testing.py index a181c87f..322b6ad5 100644 --- a/tests/utils/test_testing.py +++ b/tests/utils/test_testing.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import copy import numpy as np