Skip to content
Open

Iss1152 #1170

Show file tree
Hide file tree
Changes from all commits
Commits
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
26 changes: 26 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ extra = [
[dependency-groups]
dev = [
"bump2version",
"prek>=0.4.11",
"ruff>=0.16.0",
]
test = [
"pytest",
Expand Down Expand Up @@ -244,3 +246,27 @@ memray-flame = "memray flamegraph --temporal"

[tool.pixi.environments]
profiling = { features = ["profiling"], solve-group = "default" }

[tool.hatch.envs.test]
dependency-groups = ["test"]

[tool.hatch.envs.test-anndata-pandas]
template = "test"
extra-dependencies = ["zarr>=3"]
scripts.test = ["pip list|grep anndata && pip list|grep pandas && pytest {args}"]
scripts.test-readwrite = ["pip list|grep anndata && pip list|grep pandas && pytest tests/io/test_readwrite.py"]
scripts.test-all = ["pip list|grep anndata && pip list|grep pandas && pytest ."]

[[tool.hatch.envs.test-anndata-pandas.matrix]]
anndata-pandas = ["0.13-2", "0.13-3", "0.12-2"]

[tool.hatch.envs.test-anndata-pandas.overrides]
matrix.anndata-pandas.extra-dependencies = [
# every option where the if-condition is True gets included
{value="anndata~=0.13", if = ["0.13-2"]},
{value="pandas>=2.3,<3", if = ["0.13-2"]},
{value="anndata~=0.13", if = ["0.13-3"]},
{value="pandas~=3.0", if = ["0.13-3"]},
{value="anndata>=0.12,<0.13", if = ["0.12-2"]},
{value="pandas>=2.3,<3", if = ["0.12-2"]},
]
26 changes: 26 additions & 0 deletions src/spatialdata/_io/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from __future__ import annotations

from ome_zarr.format import Format


class FormatVersionUnknownError(ValueError):
"""Exception raised when an unknown element format is encountered."""

def __init__(self, element_type: str, version_encountered: Format):
self.element_type = element_type
self.version_encountered = version_encountered
self.message = (
f"Encountered unknown element format version "
f"`{self.version_encountered}` for element of type `{self.element_type}`"
)
super().__init__(self.message)


class WritingToZarrV2DeprecationWarning(DeprecationWarning):
"""Warning raised when writing to zarr v2 format."""

message = (
"Writing to zarr v2 format is currently deprecated in spatialdata "
"and will be removed in a future version. "
"Please consider writing to zarr v3."
)
6 changes: 6 additions & 0 deletions src/spatialdata/_io/io_points.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations

import warnings
from pathlib import Path

import zarr
Expand All @@ -12,6 +13,7 @@
_write_metadata,
overwrite_coordinate_transformations_non_raster,
)
from spatialdata._io.exceptions import WritingToZarrV2DeprecationWarning
from spatialdata._io.format import CurrentPointsFormat, PointsFormats, _parse_version
from spatialdata.models import get_axes_names
from spatialdata.transformations._utils import (
Expand Down Expand Up @@ -65,6 +67,10 @@ def write_points(
element_format
The format of the points element used to store it.
"""
if element_format.zarr_format == 2:
warnings.warn(
message=WritingToZarrV2DeprecationWarning.message, category=WritingToZarrV2DeprecationWarning, stacklevel=2
)
axes = get_axes_names(points)
transformations = _get_transformations(points)
assert transformations is not None # mypy: validate_element() in _write_element guarantees this
Expand Down
12 changes: 12 additions & 0 deletions src/spatialdata/_io/io_raster.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations

import warnings
from collections.abc import Sequence
from pathlib import Path
from typing import Any, Literal, TypeGuard, cast
Expand All @@ -23,6 +24,7 @@
overwrite_channel_names,
overwrite_coordinate_transformations_raster,
)
from spatialdata._io.exceptions import WritingToZarrV2DeprecationWarning
from spatialdata._io.format import (
CurrentRasterFormat,
RasterFormatType,
Expand Down Expand Up @@ -581,6 +583,11 @@ def write_image(
raster_compressor: dict[Literal["lz4", "zstd"], int] | None = None,
**metadata: str | JSONDict | list[JSONDict],
) -> None:
if element_format.zarr_format == 2:
warnings.warn(
message=WritingToZarrV2DeprecationWarning.message, category=WritingToZarrV2DeprecationWarning, stacklevel=2
)

_write_raster(
raster_type="image",
raster_data=image,
Expand All @@ -603,6 +610,11 @@ def write_labels(
raster_compressor: dict[Literal["lz4", "zstd"], int] | None = None,
**metadata: JSONDict,
) -> None:
if element_format.zarr_format == 2:
warnings.warn(
message=WritingToZarrV2DeprecationWarning.message, category=WritingToZarrV2DeprecationWarning, stacklevel=2
)

_write_raster(
raster_type="labels",
raster_data=labels,
Expand Down
7 changes: 7 additions & 0 deletions src/spatialdata/_io/io_shapes.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations

import warnings
from pathlib import Path
from typing import Any, Literal

Expand All @@ -15,6 +16,7 @@
_write_metadata,
overwrite_coordinate_transformations_non_raster,
)
from spatialdata._io.exceptions import WritingToZarrV2DeprecationWarning
from spatialdata._io.format import (
CurrentShapesFormat,
ShapesFormats,
Expand Down Expand Up @@ -93,6 +95,11 @@ def write_shapes(
Whether to use the WKB or geoarrow encoding for GeoParquet. See :meth:`geopandas.GeoDataFrame.to_parquet` for
details. If None, uses the value from :attr:`spatialdata.settings.shapes_geometry_encoding`.
"""
if element_format.zarr_format == 2:
warnings.warn(
message=WritingToZarrV2DeprecationWarning.message, category=WritingToZarrV2DeprecationWarning, stacklevel=2
)

from spatialdata.config import settings

if geometry_encoding is None:
Expand Down
45 changes: 38 additions & 7 deletions src/spatialdata/_io/io_table.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from __future__ import annotations

import warnings
from importlib.metadata import version
from pathlib import Path

import numpy as np
Expand All @@ -9,6 +11,8 @@
from anndata._io.specs import write_elem as write_adata
from ome_zarr.format import Format

from spatialdata._io._utils import _resolve_zarr_store
from spatialdata._io.exceptions import FormatVersionUnknownError, WritingToZarrV2DeprecationWarning
from spatialdata._io.format import (
CurrentTablesFormat,
TablesFormats,
Expand Down Expand Up @@ -56,16 +60,43 @@ def write_table(
group_type: str = "ngff:regions_table",
element_format: Format = CurrentTablesFormat(),
) -> None:
if element_format.zarr_format == 2:
warnings.warn(
message=WritingToZarrV2DeprecationWarning.message, category=WritingToZarrV2DeprecationWarning, stacklevel=2
)

if TableModel.ATTRS_KEY in table.uns:
region, region_key, instance_key = get_table_keys(table)
TableModel.validate(table)
else:
region, region_key, instance_key = (None, None, None)

write_adata(group, name, table)
tables_group = group[name]
tables_group.attrs["spatialdata-encoding-type"] = group_type
tables_group.attrs["region"] = region
tables_group.attrs["region_key"] = region_key
tables_group.attrs["instance_key"] = instance_key
tables_group.attrs["version"] = element_format.spatialdata_format_version
# Ensure the table group exists
table_group = group.require_group(name=name)

assert element_format in TablesFormats.values(), FormatVersionUnknownError(
element_type="table", version_encountered=element_format
)

if element_format.zarr_format == 3 and version("anndata") >= "0.13":
# `write_zarr` in anndata v0.13 and above can only write to zarr v3
# solution of passing resolved store directly roughly based on:
# https://github.com/scverse/anndata/issues/1548#issuecomment-2199801855

# resolve the store from the group
resolved_store = _resolve_zarr_store(table_group)

# Write the table to the path of the table group
table.write_zarr(store=resolved_store, consolidate_metadata=False)

table_group = group[name]
else:
table.strings_to_categoricals()
write_adata(group, name, table)
table_group = group[name]

table_group.attrs["spatialdata-encoding-type"] = group_type
table_group.attrs["region"] = region
table_group.attrs["region_key"] = region_key
table_group.attrs["instance_key"] = instance_key
table_group.attrs["version"] = element_format.spatialdata_format_version
9 changes: 2 additions & 7 deletions tests/io/test_readwrite.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
import zarr
from anndata import AnnData
from numpy.random import default_rng
from packaging.version import Version
from shapely import MultiPolygon, Polygon
from upath import UPath
from xarray import DataArray
Expand Down Expand Up @@ -1294,8 +1293,7 @@ def test_sdata_with_nan_in_obs(tmp_path: Path) -> None:

Regression test for https://github.com/scverse/spatialdata/issues/399
Previously this raised TypeError: expected unicode string, found nan.
Now the write succeeds, though NaN values in object-dtype columns are
converted to the string "nan" after round-trip.
Now the write succeeds, and NaN values are preserved round trip
"""
from spatialdata.models import TableModel

Expand Down Expand Up @@ -1329,8 +1327,5 @@ def test_sdata_with_nan_in_obs(tmp_path: Path) -> None:

assert r1.iloc[0] == "string"
assert r2.iloc[1] == 3
if Version(pd.__version__) >= Version("3"):
assert pd.isna(r1.iloc[1])
else: # After round-trip, NaN in object-dtype column becomes string "nan" on pandas 2
assert r1.iloc[1] == "nan"
assert pd.isna(r1.iloc[1])
assert np.isnan(r2.iloc[0])