From 0a41aada4db859d5f9655e967f0126e6a602961f Mon Sep 17 00:00:00 2001 From: Luca Marconato Date: Fri, 30 Jan 2026 14:33:10 +0100 Subject: [PATCH] add spatialdata-io version and reader name to attrs --- src/spatialdata_io/readers/_utils/_utils.py | 10 ++++++++++ src/spatialdata_io/readers/codex.py | 3 ++- src/spatialdata_io/readers/cosmx.py | 4 +++- src/spatialdata_io/readers/curio.py | 4 +++- src/spatialdata_io/readers/dbit.py | 3 ++- src/spatialdata_io/readers/iss.py | 4 +++- src/spatialdata_io/readers/macsima.py | 3 ++- src/spatialdata_io/readers/mcmicro.py | 4 +++- src/spatialdata_io/readers/merscope.py | 4 +++- src/spatialdata_io/readers/seqfish.py | 3 ++- src/spatialdata_io/readers/steinbock.py | 4 +++- src/spatialdata_io/readers/stereoseq.py | 4 ++-- src/spatialdata_io/readers/visium.py | 5 +++-- src/spatialdata_io/readers/visium_hd.py | 3 ++- src/spatialdata_io/readers/xenium.py | 4 ++-- 15 files changed, 45 insertions(+), 17 deletions(-) diff --git a/src/spatialdata_io/readers/_utils/_utils.py b/src/spatialdata_io/readers/_utils/_utils.py index e4156d36..1dbd496f 100644 --- a/src/spatialdata_io/readers/_utils/_utils.py +++ b/src/spatialdata_io/readers/_utils/_utils.py @@ -17,6 +17,7 @@ from collections.abc import Mapping from anndata import AnnData + from spatialdata import SpatialData PathLike = os.PathLike | str # type:ignore[type-arg] @@ -109,6 +110,15 @@ def parse_channels(path: Path) -> list[str]: return names +def _set_reader_metadata(sdata: SpatialData, reader: str) -> SpatialData: + """Set spatialdata-io provenance metadata on a SpatialData object.""" + from spatialdata_io import __version__ + + sdata.attrs["spatialdata_io_software_version"] = __version__ + sdata.attrs["spatialdata_io_reader"] = reader + return sdata + + def parse_physical_size(path: Path | None = None, ome_pixels: Pixels | None = None) -> float: """Parse physical size from OME-TIFF to micrometer.""" pixels = ome_pixels or from_tiff(path).images[0].pixels diff --git a/src/spatialdata_io/readers/codex.py b/src/spatialdata_io/readers/codex.py index ef36b01c..fb0a65dd 100644 --- a/src/spatialdata_io/readers/codex.py +++ b/src/spatialdata_io/readers/codex.py @@ -16,6 +16,7 @@ from spatialdata_io._constants._constants import CodexKeys from spatialdata_io._docs import inject_docs +from spatialdata_io.readers._utils._utils import _set_reader_metadata if TYPE_CHECKING: from collections.abc import Mapping @@ -89,7 +90,7 @@ def codex( logger.warning("Cannot find .tif file. Will build spatialdata with shapes and table only.") sdata = SpatialData(shapes={str(region): shapes}, table=table) - return sdata + return _set_reader_metadata(sdata, "codex") def _codex_df_to_anndata(df: pd.DataFrame) -> ad.AnnData: diff --git a/src/spatialdata_io/readers/cosmx.py b/src/spatialdata_io/readers/cosmx.py index fcccc02d..35d3d0d2 100644 --- a/src/spatialdata_io/readers/cosmx.py +++ b/src/spatialdata_io/readers/cosmx.py @@ -21,6 +21,7 @@ from spatialdata_io._constants._constants import CosmxKeys from spatialdata_io._docs import inject_docs +from spatialdata_io.readers._utils._utils import _set_reader_metadata if TYPE_CHECKING: from collections.abc import Mapping @@ -289,4 +290,5 @@ def cosmx( # logg.warning(f"FOV `{str(fov)}` does not exist, skipping it.") # continue - return SpatialData(images=images, labels=labels, points=points, tables={"table": table}) + sdata = SpatialData(images=images, labels=labels, points=points, tables={"table": table}) + return _set_reader_metadata(sdata, "cosmx") diff --git a/src/spatialdata_io/readers/curio.py b/src/spatialdata_io/readers/curio.py index 55b0b00e..e01de8cc 100644 --- a/src/spatialdata_io/readers/curio.py +++ b/src/spatialdata_io/readers/curio.py @@ -11,6 +11,7 @@ from spatialdata_io._constants._constants import CurioKeys from spatialdata_io._docs import inject_docs +from spatialdata_io.readers._utils._utils import _set_reader_metadata __all__ = ["curio"] @@ -97,4 +98,5 @@ def curio( shapes = ShapesModel.parse(xy, geometry=0, radius=10, index=adata.obs[CurioKeys.INSTANCE_KEY]) - return SpatialData(table=table, shapes={CurioKeys.REGION.value: shapes}) + sdata = SpatialData(table=table, shapes={CurioKeys.REGION.value: shapes}) + return _set_reader_metadata(sdata, "curio") diff --git a/src/spatialdata_io/readers/dbit.py b/src/spatialdata_io/readers/dbit.py index bd4d20d2..03ffb73b 100644 --- a/src/spatialdata_io/readers/dbit.py +++ b/src/spatialdata_io/readers/dbit.py @@ -18,6 +18,7 @@ from spatialdata_io._constants._constants import DbitKeys from spatialdata_io._docs import inject_docs +from spatialdata_io.readers._utils._utils import _set_reader_metadata if TYPE_CHECKING: from numpy.typing import NDArray @@ -363,4 +364,4 @@ def dbit( if hasimage: imgname = dataset_id + "_image" sdata.images[imgname] = image_sd - return sdata + return _set_reader_metadata(sdata, "dbit") diff --git a/src/spatialdata_io/readers/iss.py b/src/spatialdata_io/readers/iss.py index 63c86571..b439da20 100644 --- a/src/spatialdata_io/readers/iss.py +++ b/src/spatialdata_io/readers/iss.py @@ -12,6 +12,7 @@ from xarray import DataArray from spatialdata_io._docs import inject_docs +from spatialdata_io.readers._utils._utils import _set_reader_metadata if TYPE_CHECKING: from collections.abc import Mapping @@ -106,8 +107,9 @@ def iss( **image_models_kwargs, ) - return SpatialData( + sdata = SpatialData( images={f"{dataset_id}_raw_image": raw_image_parsed}, labels={REGION: labels_image_parsed}, table=table, ) + return _set_reader_metadata(sdata, "iss") diff --git a/src/spatialdata_io/readers/macsima.py b/src/spatialdata_io/readers/macsima.py index 23a131ba..2b4493f4 100644 --- a/src/spatialdata_io/readers/macsima.py +++ b/src/spatialdata_io/readers/macsima.py @@ -21,6 +21,7 @@ from spatialdata_io._constants._enum import ModeEnum from spatialdata_io.readers._utils._utils import ( + _set_reader_metadata, calc_scale_factors, parse_channels, parse_physical_size, @@ -745,7 +746,7 @@ def create_sdata( sdata.images[f"{filtered_name}_nuclei_image"] = nuclei_image_element sdata.tables[f"{filtered_name}_nuclei_table"] = table_nuclei - return sdata + return _set_reader_metadata(sdata, "macsima") def create_table(mci: MultiChannelImage) -> ad.AnnData: diff --git a/src/spatialdata_io/readers/mcmicro.py b/src/spatialdata_io/readers/mcmicro.py index f6cca509..68171da1 100644 --- a/src/spatialdata_io/readers/mcmicro.py +++ b/src/spatialdata_io/readers/mcmicro.py @@ -17,6 +17,7 @@ from yaml.loader import SafeLoader from spatialdata_io._constants._constants import McmicroKeys +from spatialdata_io.readers._utils._utils import _set_reader_metadata if TYPE_CHECKING: from collections.abc import Mapping @@ -189,7 +190,8 @@ def mcmicro( tables_dict = _get_tables(path, markers, tma) - return SpatialData(images=images, labels=labels, tables=tables_dict) + sdata = SpatialData(images=images, labels=labels, tables=tables_dict) + return _set_reader_metadata(sdata, "mcmicro") def _load_params(path: Path) -> Any: diff --git a/src/spatialdata_io/readers/merscope.py b/src/spatialdata_io/readers/merscope.py index 90b0a362..0a360700 100644 --- a/src/spatialdata_io/readers/merscope.py +++ b/src/spatialdata_io/readers/merscope.py @@ -22,6 +22,7 @@ from spatialdata_io._constants._constants import MerscopeKeys from spatialdata_io._docs import inject_docs +from spatialdata_io.readers._utils._utils import _set_reader_metadata if TYPE_CHECKING: from collections.abc import Callable, Mapping @@ -227,7 +228,8 @@ def merscope( f"At least one of the following files does not exist: {count_path}, {obs_path}. The table is not loaded." ) - return SpatialData(shapes=shapes, points=points, images=images, tables=tables) + sdata = SpatialData(shapes=shapes, points=points, images=images, tables=tables) + return _set_reader_metadata(sdata, "merscope") def _get_reader(backend: str | None) -> Callable: # type: ignore[type-arg] diff --git a/src/spatialdata_io/readers/seqfish.py b/src/spatialdata_io/readers/seqfish.py index e0f26d04..d990c38d 100644 --- a/src/spatialdata_io/readers/seqfish.py +++ b/src/spatialdata_io/readers/seqfish.py @@ -28,6 +28,7 @@ from spatialdata_io._constants._constants import SeqfishKeys as SK from spatialdata_io._docs import inject_docs +from spatialdata_io.readers._utils._utils import _set_reader_metadata if TYPE_CHECKING: from collections.abc import Mapping @@ -270,7 +271,7 @@ def _get_scale_factors(raster_path: Path, raster_models_scale_factors: list[int] sdata = SpatialData(images=images, labels=labels, points=points, tables=tables, shapes=shapes) - return sdata + return _set_reader_metadata(sdata, "seqfish") def _is_ome_tiff_multiscale(ome_tiff_file: Path) -> bool: diff --git a/src/spatialdata_io/readers/steinbock.py b/src/spatialdata_io/readers/steinbock.py index 8c8d50a7..0b15c022 100644 --- a/src/spatialdata_io/readers/steinbock.py +++ b/src/spatialdata_io/readers/steinbock.py @@ -13,6 +13,7 @@ from spatialdata.transformations.transformations import Identity from spatialdata_io._constants._constants import SteinbockKeys +from spatialdata_io.readers._utils._utils import _set_reader_metadata if TYPE_CHECKING: from collections.abc import Mapping @@ -101,7 +102,8 @@ def steinbock( raise ValueError("Samples in table and images are inconsistent, please check.") table = TableModel.parse(adata, region=regions.unique().tolist(), region_key="region", instance_key="cell_id") - return SpatialData(images=images, labels=labels, tables={"table": table}) + sdata = SpatialData(images=images, labels=labels, tables={"table": table}) + return _set_reader_metadata(sdata, "steinbock") def _get_images( diff --git a/src/spatialdata_io/readers/stereoseq.py b/src/spatialdata_io/readers/stereoseq.py index e4c83af4..73f556c1 100644 --- a/src/spatialdata_io/readers/stereoseq.py +++ b/src/spatialdata_io/readers/stereoseq.py @@ -20,7 +20,7 @@ from spatialdata_io._constants._constants import StereoseqKeys as SK from spatialdata_io._docs import inject_docs -from spatialdata_io.readers._utils._utils import _initialize_raster_models_kwargs +from spatialdata_io.readers._utils._utils import _initialize_raster_models_kwargs, _set_reader_metadata if TYPE_CHECKING: from collections.abc import Mapping @@ -339,4 +339,4 @@ def stereoseq( images[Path(cell_mask_name).stem] = masks sdata = SpatialData(images=images, tables=tables, shapes=shapes, points=points) - return sdata + return _set_reader_metadata(sdata, "stereoseq") diff --git a/src/spatialdata_io/readers/visium.py b/src/spatialdata_io/readers/visium.py index a2f84ede..bf3690e5 100644 --- a/src/spatialdata_io/readers/visium.py +++ b/src/spatialdata_io/readers/visium.py @@ -19,7 +19,7 @@ from spatialdata_io._constants._constants import VisiumKeys from spatialdata_io._docs import inject_docs -from spatialdata_io.readers._utils._utils import _read_counts +from spatialdata_io.readers._utils._utils import _read_counts, _set_reader_metadata if TYPE_CHECKING: from collections.abc import Mapping @@ -253,7 +253,8 @@ def visium( rgb=None, ) - return SpatialData(images=images, shapes=shapes, tables={"table": table}) + sdata = SpatialData(images=images, shapes=shapes, tables={"table": table}) + return _set_reader_metadata(sdata, "visium") def _read_image(image_file: Path, imread_kwargs: dict[str, Any]) -> Any: diff --git a/src/spatialdata_io/readers/visium_hd.py b/src/spatialdata_io/readers/visium_hd.py index 1efd8ca3..d6513147 100644 --- a/src/spatialdata_io/readers/visium_hd.py +++ b/src/spatialdata_io/readers/visium_hd.py @@ -31,6 +31,7 @@ from spatialdata_io._constants._constants import VisiumHDKeys from spatialdata_io._docs import inject_docs +from spatialdata_io.readers._utils._utils import _set_reader_metadata if TYPE_CHECKING: from collections.abc import Mapping @@ -576,7 +577,7 @@ def _get_bins(path_bins: Path) -> list[str]: sdata=sdata, table_name=bin_size_str, rasterized_labels_name=labels_name ) - return sdata + return _set_reader_metadata(sdata, "visium_hd") def _infer_dataset_id(path: Path) -> str: diff --git a/src/spatialdata_io/readers/xenium.py b/src/spatialdata_io/readers/xenium.py index 5c2be682..e764aa7f 100644 --- a/src/spatialdata_io/readers/xenium.py +++ b/src/spatialdata_io/readers/xenium.py @@ -39,7 +39,7 @@ from spatialdata_io._docs import inject_docs from spatialdata_io._utils import deprecation_alias, zarr_open from spatialdata_io.readers._utils._read_10x_h5 import _read_10x_h5 -from spatialdata_io.readers._utils._utils import _initialize_raster_models_kwargs +from spatialdata_io.readers._utils._utils import _initialize_raster_models_kwargs, _set_reader_metadata if TYPE_CHECKING: from collections.abc import Mapping @@ -400,7 +400,7 @@ def filter(self, record: logging.LogRecord) -> bool: for key, value in extra_images.items(): sdata.images[key] = value - return sdata + return _set_reader_metadata(sdata, "xenium") def _decode_cell_id_column(cell_id_column: pd.Series) -> pd.Series: