From 4c9cbcbfd5a60e4a427828c5f7135049eedb20c0 Mon Sep 17 00:00:00 2001 From: Jayaram Kancherla Date: Sat, 3 Jan 2026 17:20:35 -0800 Subject: [PATCH 1/3] changes to adapt to sce --- src/spatialexperiment/spatialexperiment.py | 46 ++++++++++--------- src/spatialexperiment/spatialimage.py | 52 +--------------------- 2 files changed, 27 insertions(+), 71 deletions(-) diff --git a/src/spatialexperiment/spatialexperiment.py b/src/spatialexperiment/spatialexperiment.py index 48c0be2..5c805fa 100644 --- a/src/spatialexperiment/spatialexperiment.py +++ b/src/spatialexperiment/spatialexperiment.py @@ -1,5 +1,7 @@ +from __future__ import annotations + from pathlib import Path -from typing import Any, Dict, List, Optional, Sequence, Union, Tuple +from typing import Any, Dict, List, Optional, Sequence, Tuple, Union from urllib.parse import urlparse from warnings import warn @@ -30,10 +32,10 @@ _validate_spatial_coords_names, ) from .spatialimage import ( - VirtualSpatialImage, - StoredSpatialImage, - RemoteSpatialImage, LoadedSpatialImage, + RemoteSpatialImage, + StoredSpatialImage, + VirtualSpatialImage, construct_spatial_image_class, ) @@ -60,7 +62,7 @@ def __init__( column_data: Optional[BiocFrame] = None, row_names: Optional[List[str]] = None, column_names: Optional[List[str]] = None, - metadata: Optional[dict] = None, + metadata: Optional[Union[Dict[str, Any], ut.NamedList]] = None, reduced_dims: Optional[Dict[str, Any]] = None, main_experiment_name: Optional[str] = None, alternative_experiments: Optional[Dict[str, Any]] = None, @@ -69,7 +71,7 @@ def __init__( column_pairs: Optional[Any] = None, spatial_coords: Optional[Union[BiocFrame, np.ndarray]] = None, img_data: Optional[BiocFrame] = None, - validate: bool = True, + _validate: bool = True, **kwargs, ) -> None: """Initialize a spatial experiment. @@ -179,7 +181,7 @@ def __init__( Image data are coerced to a :py:class:`~biocframe.BiocFrame.BiocFrame`. Defaults to None. - validate: + _validate: Internal use only. """ super().__init__( @@ -196,7 +198,7 @@ def __init__( row_pairs=row_pairs, column_pairs=column_pairs, alternative_experiment_check_dim_names=alternative_experiment_check_dim_names, - validate=validate, + _validate=_validate, **kwargs, ) @@ -217,7 +219,7 @@ def __init__( self._cols = column_data self._spatial_coords = spatial_coords - if validate: + if _validate: _validate_column_data(column_data=column_data) _validate_img_data(img_data=img_data) _validate_sample_ids(column_data=column_data, img_data=img_data) @@ -265,6 +267,7 @@ def __deepcopy__(self, memo=None, _nil=[]): column_pairs=_col_pair_copy, spatial_coords=_spatial_coords_copy, img_data=_img_data_copy, + _validate=False, ) def __copy__(self): @@ -288,6 +291,7 @@ def __copy__(self): column_pairs=self._column_pairs, spatial_coords=self._spatial_coords, img_data=self._img_data, + _validate=False, ) def copy(self): @@ -399,7 +403,7 @@ def set_spatial_coordinates( self, spatial_coords: Optional[Union[BiocFrame, np.ndarray]], in_place: bool = False, - ) -> "SpatialExperiment": + ) -> SpatialExperiment: """Set new spatial coordinates. Args: @@ -434,7 +438,7 @@ def set_spatial_coords( self, spatial_coords: Optional[Union[BiocFrame, np.ndarray]], in_place: bool = False, - ) -> "SpatialExperiment": + ) -> SpatialExperiment: """Alias for :py:meth:`~set_spatial_coordinates`.""" return self.set_spatial_coordinates(spatial_coords=spatial_coords, in_place=in_place) @@ -487,7 +491,7 @@ def get_spatial_coords_names(self) -> List[str]: def set_spatial_coordinates_names( self, spatial_coords_names: List[str], in_place: bool = False - ) -> "SpatialExperiment": + ) -> SpatialExperiment: """Set new spatial coordinates names. Args: @@ -514,7 +518,7 @@ def set_spatial_coordinates_names( output._spatial_coords = new_spatial_coords return output - def set_spatial_coords_names(self, spatial_coords_names: List[str], in_place: bool = False) -> "SpatialExperiment": + def set_spatial_coords_names(self, spatial_coords_names: List[str], in_place: bool = False) -> SpatialExperiment: """Alias for :py:meth:`~set_spatial_coordinates_names`.""" return self.set_spatial_coordinates_names(spatial_coords_names=spatial_coords_names, in_place=in_place) @@ -562,7 +566,7 @@ def get_img_data(self) -> BiocFrame: """Alias for :py:meth:`~get_image_data`.""" return self.get_image_data() - def set_image_data(self, img_data: Optional[BiocFrame], in_place: bool = False) -> "SpatialExperiment": + def set_image_data(self, img_data: Optional[BiocFrame], in_place: bool = False) -> SpatialExperiment: """Set new image data. Args: @@ -591,7 +595,7 @@ def set_image_data(self, img_data: Optional[BiocFrame], in_place: bool = False) output._img_data = img_data return output - def set_img_data(self, img_data: BiocFrame, in_place: bool = False) -> "SpatialExperiment": + def set_img_data(self, img_data: BiocFrame, in_place: bool = False) -> SpatialExperiment: """Alias for :py:meth:`~set_image_data`.""" return self.set_image_data(img_data=img_data, in_place=in_place) @@ -666,7 +670,7 @@ def set_column_data( cols: Optional[BiocFrame], replace_column_names: bool = False, in_place: bool = False, - ) -> "SpatialExperiment": + ) -> SpatialExperiment: """Override: Set sample data. Args: @@ -710,7 +714,7 @@ def get_slice( self, rows: Optional[Union[str, int, bool, Sequence]], columns: Optional[Union[str, int, bool, Sequence]], - ) -> "SpatialExperiment": + ) -> SpatialExperiment: """Alias for :py:attr:`~__getitem__`.""" spe = super().get_slice(rows=rows, columns=columns) @@ -820,7 +824,7 @@ def add_img( image_id: Union[str, bool, None], load: bool = True, in_place: bool = False, - ) -> "SpatialExperiment": + ) -> SpatialExperiment: """Add a new image entry. Args: @@ -883,7 +887,7 @@ def add_img( def remove_img( self, sample_id: Union[str, bool, None] = None, image_id: Union[str, bool, None] = None, in_place: bool = False - ) -> "SpatialExperiment": + ) -> SpatialExperiment: """Remove an image entry. Args: @@ -1083,11 +1087,11 @@ def to_anndata( #######>> combine ops <<######## ################################ - def relaxed_combine_columns(self, *other) -> "SpatialExperiment": + def relaxed_combine_columns(self, *other) -> SpatialExperiment: """Wrapper around :py:func:`~relaxed_combine_columns`.""" return relaxed_combine_columns(self, *other) - def combine_columns(self, *other) -> "SpatialExperiment": + def combine_columns(self, *other) -> SpatialExperiment: """Wrapper around :py:func:`~combine_columns`.""" return combine_columns(self, *other) diff --git a/src/spatialexperiment/spatialimage.py b/src/spatialexperiment/spatialimage.py index 337d89c..22f47d6 100644 --- a/src/spatialexperiment/spatialimage.py +++ b/src/spatialexperiment/spatialimage.py @@ -19,11 +19,11 @@ # Keeping the same names as the R classes -class VirtualSpatialImage(ABC): +class VirtualSpatialImage(ut.BiocObject): """Base class for spatial images.""" def __init__(self, metadata: Optional[dict] = None): - self._metadata = metadata if metadata is not None else {} + super().__init__(metadata=metadata) ######################### ######>> Equality <<##### @@ -40,54 +40,6 @@ def __hash__(self): # Generally, these classes are mutable and shouldn't be used as dict keys or in sets. return hash(frozenset(self._metadata.items())) - ########################### - ######>> metadata <<####### - ########################### - - def get_metadata(self) -> dict: - """ - Returns: - Dictionary of metadata for this object. - """ - return self._metadata - - def set_metadata(self, metadata: dict, in_place: bool = False) -> "VirtualSpatialImage": - """Set additional metadata. - - Args: - metadata: - New metadata for this object. - - in_place: - Whether to modify the ``VirtualSpatialImage`` in place. - - Returns: - A modified ``VirtualSpatialImage`` object, either as a copy of the original - or as a reference to the (in-place-modified) original. - """ - if not isinstance(metadata, dict): - raise TypeError(f"`metadata` must be a dictionary, provided {type(metadata)}.") - output = self._define_output(in_place) - output._metadata = metadata - return output - - @property - def metadata(self) -> dict: - """Alias for :py:attr:`~get_metadata`.""" - return self.get_metadata() - - @metadata.setter - def metadata(self, metadata: dict): - """Alias for :py:attr:`~set_metadata` with ``in_place = True``. - - As this mutates the original object, a warning is raised. - """ - warn( - "Setting property 'metadata' is an in-place operation, use 'set_metadata' instead", - UserWarning, - ) - self.set_metadata(metadata, in_place=True) - ################################## ######>> Spatial Props <<######### ################################## From f195c76833c1073dd294a29e75d8a2cb6d7ef71a Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 4 Jan 2026 01:21:11 +0000 Subject: [PATCH 2/3] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/spatialexperiment/spatialimage.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/spatialexperiment/spatialimage.py b/src/spatialexperiment/spatialimage.py index 22f47d6..a4a3f0b 100644 --- a/src/spatialexperiment/spatialimage.py +++ b/src/spatialexperiment/spatialimage.py @@ -1,6 +1,6 @@ import shutil import tempfile -from abc import ABC, abstractmethod +from abc import abstractmethod from functools import lru_cache from pathlib import Path from typing import Optional, Tuple, Union From bbe8d905469eee5cf14560279a9957d00c86e226 Mon Sep 17 00:00:00 2001 From: Jayaram Kancherla Date: Sat, 3 Jan 2026 17:25:55 -0800 Subject: [PATCH 3/3] update changelog and package versions --- CHANGELOG.md | 21 +++++++++++++-------- setup.cfg | 6 +++--- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 759fb14..0fc2537 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,47 +1,52 @@ # Changelog +## Version 0.1.0 + +- Migrating changes from SCE, replace `validate` with `_validate`. + ## Version 0.0.11-0.0.13 + - BUGFIX: `to_anndata()` only populates `obsm` with spatial coordinates if the original `SpatialExperiment` has spatial coordinates (PR #53, #54) - Enhance docstring for `to_anndata()` to describe the structure of returned AnnData object (PR #55) - ## Version 0.0.10 + - Add an affine function that computes a `rasterio.Affine` object given a `scale_factor`. This assumes a simple scaling where the origin is (0,0) in the spatial coordinate system corresponding to the top-left pixel (0,0). More complex alignments would require explicit affine transforms. - Ensure img_raster() consistently returns a PIL.Image.Image. - Add to_numpy() method. - Changes to how caching works in remote images. - ## Version 0.0.9 -- Added `to_anndata()` in main `SpatialExperiment` class (PR #50) +- Added `to_anndata()` in main `SpatialExperiment` class (PR #50) ## Version 0.0.8 -- Set the expected column names for image data slot (PR #46) +- Set the expected column names for image data slot (PR #46) ## Version 0.0.7 + - Added `img_source` function in main SpatialExperiment class and child classes of VirtualSpatialExperiment (PR #36) - Added `remove_img` function (PR #34) - Refactored `get_img_idx` for improved maintainability - Disambiguated `get_img_data` between `_imgutils.py` and `SpatialExperiment.py` - Moved `SpatialFeatureExperiment` into its own package - ## Version 0.0.6 + - Added `read_tenx_visium()` function to load 10x Visium data as SpatialExperiment - Added `combine_columns` function - Implemented `__eq__` override for `SpatialImage` subclasses - ## Version 0.0.5 -- Implementing a placeholder `SpatialFeatureExperiment` class. This version only implements the data structure to hold various geometries but none of the methods except for slicing. +- Implementing a placeholder `SpatialFeatureExperiment` class. This version only implements the data structure to hold various geometries but none of the methods except for slicing. ## Version 0.0.3 - 0.0.4 -- Streamlining the `SpatialImage` class implementations. +- Streamlining the `SpatialImage` class implementations. ## Version 0.0.1 - 0.0.2 + - Initial version of the SpatialExperiment class with the additional slots. - Allow spatial coordinates to be a numpy array diff --git a/setup.cfg b/setup.cfg index 2776665..756d4b3 100644 --- a/setup.cfg +++ b/setup.cfg @@ -49,9 +49,9 @@ python_requires = >=3.9 # For more information, check out https://semver.org/. install_requires = importlib-metadata; python_version<"3.8" - biocframe>=0.6.3 - biocutils>=0.2 - singlecellexperiment>=0.5.7 + biocframe>=0.7.2 + biocutils>=0.3.3 + singlecellexperiment>=0.6.0 pillow>=11.0 requests scipy~=1.13