Skip to content
Draft
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
2 changes: 1 addition & 1 deletion .github/workflows/test.yaml
Original file line numberDiff line numberDiff line change
Expand Up@@ -59,7 +59,7 @@ jobs:
PLATFORM: ${{ matrix.os }}
DISPLAY: :42
run: |
uv run pytest --cov --color=yes --cov-report=xml -n auto --dist worksteal
uv run pytest --run-network --cov --color=yes --cov-report=xml -n auto --dist worksteal
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v6
with:
Expand Down
2 changes: 1 addition & 1 deletion .gitmodules
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
[submodule "docs/tutorials/notebooks"]
path = docs/tutorials/notebooks
url = https://github.com/scverse/spatialdata-notebooks
url = https://github.com/scverse/spatialdata-tutorials
6 changes: 3 additions & 3 deletions .pre-commit-config.yaml
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,18 +9,18 @@ ci:
skip: []
repos:
- repo: https://github.com/rbubley/mirrors-prettier
rev: v3.9.4
rev: v3.9.6
hooks:
- id: prettier
exclude: ^.github/workflows/test.yaml
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v2.1.0
rev: v2.3.1
hooks:
- id: mypy
additional_dependencies: [numpy, types-requests]
exclude: tests/|docs/
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.15.20
rev: v0.16.3
hooks:
- id: ruff
args: [--fix, --exit-non-zero-on-fix]
Expand Down
1 change: 1 addition & 0 deletions docs/api/datasets.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,5 +7,6 @@ Convenience small datasets

.. autofunction:: blobs
.. autofunction:: blobs_annotating_element
.. autofunction:: cells
.. autofunction:: raccoon
```
5 changes: 2 additions & 3 deletions docs/contributing.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -13,8 +13,7 @@ In addition to the packages needed to _use_ this package, you need additional py
the documentation_. It's easy to install them using `pip`:

```bash
cd spatialdata-io
pip install -e ".[dev,test,doc]"
pip install -e . --group dev --group test --group docs
```

## Code-style
Expand DownExpand Up@@ -51,7 +50,7 @@ and [prettier][prettier-editors].
## Writing tests

```{note}
Remember to first install the package with `pip install '-e[dev,test]'`
Remember to first install the package with `pip install -e . --group dev --group test`
```

This package uses [pytest][] for automated testing. Please [write tests][scanpy-test-docs] for every function added to the package.
Expand Down
7 changes: 4 additions & 3 deletions docs/index.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,8 +12,8 @@ This page provides documentation on how to install, use, and extend the core `sp

- `spatialdata-io`: load data from common spatial omics technologies into `spatialdata` ([repository][spatialdata-io-repo], [documentation][spatialdata-io-docs]).
- `spatialdata-plot`: Static plotting library for `spatialdata` ([repository][spatialdata-plot-repo], [documentation][spatialdata-plot-docs]).
- `napari-spatialdata-repo`: napari plugin for interactive exploration and annotation of `spatialdata` ([repository][napari-spatialdata-repo], [documentation][napari-spatialdata-docs]).
- `SpatialData` (R): R implementation of the SpatialData framework ([repository][spatialdata-r-repo]).
- `napari-spatialdata`: napari plugin for interactive exploration and annotation of `spatialdata` ([repository][napari-spatialdata-repo], [documentation][napari-spatialdata-docs]).
- `spatialdataR`: R implementation of the SpatialData framework ([repository][spatialdata-r-repo]).
- `SpatialData.js`: JavaScript/TypeScript implementation of the SpatialData framework ([repository][spatialdata-js-repo]).

Please see our publication {cite}`marconatoSpatialDataOpenUniversal2024` for citation and to learn more.
Expand DownExpand Up@@ -110,6 +110,7 @@ tutorials/notebooks/notebooks.md
tutorials/notebooks/datasets/README.md
glossary.md
design_doc.md
interoperability.md
contributing.md
changelog.md
references.md
Expand All@@ -123,5 +124,5 @@ references.md
[napari-spatialdata-docs]: https://spatialdata.scverse.org/projects/napari/en/stable/notebooks/spatialdata.html
[spatialdata-io-docs]: https://spatialdata.scverse.org/projects/io/en/stable/
[spatialdata-plot-docs]: https://spatialdata.scverse.org/projects/plot/en/stable/api.html
[spatialdata-r-repo]: https://github.com/HelenaLC/SpatialData
[spatialdata-r-repo]: https://github.com/HelenaLC/spatialdataR
[spatialdata-js-repo]: https://github.com/Taylor-CCB-Group/SpatialData.js
16 changes: 16 additions & 0 deletions docs/interoperability.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
# Interoperability

The on-disk representation of SpatialData can be read from other languages. Here we list interfaces for working with SpatialData from your language of choice:

## R

- [spatialdataR](https://helenalc.github.io/spatialdataR/) provides an R implementation of the `SpatialData` object, with out-of-memory images and labels, `duckdb`-backed points and shapes, and tables represented as `SingleCellExperiment` objects.

## JavaScript and TypeScript

- [SpatialData.js](https://github.com/Taylor-CCB-Group/SpatialData.js) provides a TypeScript and JavaScript library for interfacing with SpatialData stores.
- [Vitessce](https://vitessce.io/docs/data-file-types/#spatialdatazarr) reads `spatialdata.zarr` stores directly and uses them for interactive visualization.

## File format

The SpatialData on-disk format builds on [OME-NGFF](https://ngff.openmicroscopy.org/latest/). See the [design document](design_doc.md) for details of the current on-disk layout.
37 changes: 37 additions & 0 deletions pyproject.toml
Original file line numberDiff line numberDiff line change
Expand Up@@ -45,6 +45,7 @@ dependencies = [
"spatial_image>=1.2.3",
"scikit-image",
"scipy!=1.17.0",
"scverse-misc[datasets]>=0.1.0",
"typing_extensions>=4.8.0",
"universal_pathlib>=0.2.6",
"xarray>=2024.10.0",
Expand DownExpand Up@@ -108,6 +109,7 @@ addopts = [
# These are all markers coming from xarray, dask or anndata. Added here to silence warnings.
markers = [
"slow: marks tests as slow (deselect with '-m \"not slow\"')",
"network: marks tests that require network access; skipped by default, run with '--run-network'",
"gpu: run test on GPU using CuPY.",
"array_api: used by anndata.tests.helpers, not us",
"skip_with_pyarrow_strings: skipwhen pyarrow string conversion is turned on",
Expand DownExpand Up@@ -244,3 +246,38 @@ 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"
# no "0.12-3": support for pandas>=3 is available only in anndata>=0.13
]

[tool.hatch.envs.test-anndata-pandas.overrides]
matrix.anndata-pandas.extra-dependencies = [
# every option where the if-condition is True gets included

# anndata 0.13
{value="anndata~=0.13", if = ["0.13-2", "0.13-3"]},

# anndata 0.12
{value="anndata>=0.12,<0.13", if = ["0.12-2"]},

# pandas 2
{value="pandas>=2.3,<3", if = ["0.13-2", "0.12-2"]},

# pandas 3
{value="pandas~=3.0", if = ["0.13-3"]},
]
17 changes: 15 additions & 2 deletions src/spatialdata/_core/spatialdata.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -107,7 +107,7 @@ class SpatialData:
- the table are stored as :class:`anndata.AnnData` objects, with the spatial coordinates stored in the obsm
slot.

The table can annotate regions (shapesor labels) and can be used to store additional information.
The table can annotate regions (shapes or labels) and can be used to store additional information.
Points are not regions but 0-dimensional locations. They can't be annotated by a table, but they can store
annotation directly.
"""
Expand DownExpand Up@@ -1114,6 +1114,7 @@ def write(
sdata_formats: SpatialDataFormatType | list[SpatialDataFormatType] | None = None,
shapes_geometry_encoding: Literal["WKB", "geoarrow"] | None = None,
raster_compressor: dict[Literal["lz4", "zstd"], int] | None = None,
convert_table_strings_to_categoricals: bool = False,
) -> None:
"""
Write the `SpatialData` object to a Zarr store.
Expand DownExpand Up@@ -1166,6 +1167,9 @@ def write(
compression level which should be inclusive between 0 and 9. For compression, `lz4` and `zstd` are
supported. If not specified, the compression will be `lz4` with compression level 5. Bytes are automatically
ordered for more efficient compression.
convert_table_strings_to_categoricals
If True, convert string columns of all tables to categoricals before writing.
Note that this will have a side effect of modifying string columns into categoricals in place.
"""
from spatialdata._io._utils import _resolve_zarr_store, _validate_compressor_args
from spatialdata._io.format import _parse_formats
Expand DownExpand Up@@ -1194,6 +1198,7 @@ def write(
parsed_formats=parsed,
shapes_geometry_encoding=shapes_geometry_encoding,
raster_compressor=raster_compressor,
convert_table_strings_to_categoricals=convert_table_strings_to_categoricals,
)

if self.path != file_path and update_sdata_path:
Expand All@@ -1212,6 +1217,7 @@ def _write_element(
parsed_formats: dict[str, SpatialDataFormatType] | None = None,
shapes_geometry_encoding: Literal["WKB", "geoarrow"] | None = None,
raster_compressor: dict[Literal["lz4", "zstd"], int] | None = None,
convert_table_strings_to_categoricals: bool = False,
) -> None:
from spatialdata._io.io_zarr import _get_groups_for_element

Expand DownExpand Up@@ -1279,6 +1285,7 @@ def _write_element(
group=element_type_group,
name=element_name,
element_format=parsed_formats["tables"],
convert_strings_to_categoricals=convert_table_strings_to_categoricals,
)
else:
raise ValueError(f"Unknown element type: {element_type}")
Expand All@@ -1290,6 +1297,7 @@ def write_element(
sdata_formats: SpatialDataFormatType | list[SpatialDataFormatType] | None = None,
shapes_geometry_encoding: Literal["WKB", "geoarrow"] | None = None,
raster_compressor: dict[Literal["lz4", "zstd"], int] | None = None,
convert_table_strings_to_categoricals: bool = False,
) -> None:
"""
Write a single element, or a list of elements, to the Zarr store used for backing.
Expand All@@ -1308,11 +1316,14 @@ def write_element(
shapes_geometry_encoding
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`.
raster_compressor
raster_compressor
A lenght-1 dictionary with as key the type of compression to use for images and labels and as value the
compression level which should be inclusive between 0 and 9. For compression, `lz4` and `zstd` are
supported. If not specified, the compression will be `lz4` with compression level 5. Bytes are automatically
ordered for more efficient compression.
convert_table_strings_to_categoricals
If True, and if element to be written is a table, convert string columns to categoricals before writing.
Note that this will have a side effect of modifying string columns into categoricals in place.

Notes
-----
Expand All@@ -1332,6 +1343,7 @@ def write_element(
sdata_formats=sdata_formats,
shapes_geometry_encoding=shapes_geometry_encoding,
raster_compressor=raster_compressor,
convert_table_strings_to_categoricals=convert_table_strings_to_categoricals,
)
return

Expand DownExpand Up@@ -1368,6 +1380,7 @@ def write_element(
parsed_formats=parsed_formats,
shapes_geometry_encoding=shapes_geometry_encoding,
raster_compressor=raster_compressor,
convert_table_strings_to_categoricals=convert_table_strings_to_categoricals,
)
# After every write, metadata should be consolidated, otherwise this can lead to IO problems like when deleting.
if self.has_consolidated_metadata():
Expand Down
2 changes: 1 addition & 1 deletion src/spatialdata/_core/validation.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -388,7 +388,7 @@ def __exit__(
# Exceptions were collected that we want to raise as a combined validation error.
if self._collector.errors:
raise ValidationError(
title=self._message + "\nTo fix, run `spatialdata.utils.sanitize_table(adata)`.",
title=self._message + "\nTo fix, run `spatialdata.sanitize_table(adata)`.",
errors=self._collector.errors,
)
return True
26 changes: 26 additions & 0 deletions src/spatialdata/_io/exceptions.py
Original file line numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
fix: point table validation to public sanitizer by stanbot8 · Pull Request #1 · stanbot8/spatialdata · GitHub
Skip to content
Draft
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
2 changes: 1 addition & 1 deletion .github/workflows/test.yaml
Original file line numberDiff line numberDiff line change
Expand Up@@ -59,7 +59,7 @@ jobs:
PLATFORM: ${{ matrix.os }}
DISPLAY: :42
run: |
uv run pytest --cov --color=yes --cov-report=xml -n auto --dist worksteal
uv run pytest --run-network --cov --color=yes --cov-report=xml -n auto --dist worksteal
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v6
with:
Expand Down
2 changes: 1 addition & 1 deletion .gitmodules
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
[submodule "docs/tutorials/notebooks"]
path = docs/tutorials/notebooks
url = https://github.com/scverse/spatialdata-notebooks
url = https://github.com/scverse/spatialdata-tutorials
6 changes: 3 additions & 3 deletions .pre-commit-config.yaml
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,18 +9,18 @@ ci:
skip: []
repos:
- repo: https://github.com/rbubley/mirrors-prettier
rev: v3.9.4
rev: v3.9.6
hooks:
- id: prettier
exclude: ^.github/workflows/test.yaml
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v2.1.0
rev: v2.3.1
hooks:
- id: mypy
additional_dependencies: [numpy, types-requests]
exclude: tests/|docs/
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.15.20
rev: v0.16.3
hooks:
- id: ruff
args: [--fix, --exit-non-zero-on-fix]
Expand Down
1 change: 1 addition & 0 deletions docs/api/datasets.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,5 +7,6 @@ Convenience small datasets

.. autofunction:: blobs
.. autofunction:: blobs_annotating_element
.. autofunction:: cells
.. autofunction:: raccoon
```
5 changes: 2 additions & 3 deletions docs/contributing.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -13,8 +13,7 @@ In addition to the packages needed to _use_ this package, you need additional py
the documentation_. It's easy to install them using `pip`:

```bash
cd spatialdata-io
pip install -e ".[dev,test,doc]"
pip install -e . --group dev --group test --group docs
```

## Code-style
Expand DownExpand Up@@ -51,7 +50,7 @@ and [prettier][prettier-editors].
## Writing tests

```{note}
Remember to first install the package with `pip install '-e[dev,test]'`
Remember to first install the package with `pip install -e . --group dev --group test`
```

This package uses [pytest][] for automated testing. Please [write tests][scanpy-test-docs] for every function added to the package.
Expand Down
7 changes: 4 additions & 3 deletions docs/index.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,8 +12,8 @@ This page provides documentation on how to install, use, and extend the core `sp

- `spatialdata-io`: load data from common spatial omics technologies into `spatialdata` ([repository][spatialdata-io-repo], [documentation][spatialdata-io-docs]).
- `spatialdata-plot`: Static plotting library for `spatialdata` ([repository][spatialdata-plot-repo], [documentation][spatialdata-plot-docs]).
- `napari-spatialdata-repo`: napari plugin for interactive exploration and annotation of `spatialdata` ([repository][napari-spatialdata-repo], [documentation][napari-spatialdata-docs]).
- `SpatialData` (R): R implementation of the SpatialData framework ([repository][spatialdata-r-repo]).
- `napari-spatialdata`: napari plugin for interactive exploration and annotation of `spatialdata` ([repository][napari-spatialdata-repo], [documentation][napari-spatialdata-docs]).
- `spatialdataR`: R implementation of the SpatialData framework ([repository][spatialdata-r-repo]).
- `SpatialData.js`: JavaScript/TypeScript implementation of the SpatialData framework ([repository][spatialdata-js-repo]).

Please see our publication {cite}`marconatoSpatialDataOpenUniversal2024` for citation and to learn more.
Expand DownExpand Up@@ -110,6 +110,7 @@ tutorials/notebooks/notebooks.md
tutorials/notebooks/datasets/README.md
glossary.md
design_doc.md
interoperability.md
contributing.md
changelog.md
references.md
Expand All@@ -123,5 +124,5 @@ references.md
[napari-spatialdata-docs]: https://spatialdata.scverse.org/projects/napari/en/stable/notebooks/spatialdata.html
[spatialdata-io-docs]: https://spatialdata.scverse.org/projects/io/en/stable/
[spatialdata-plot-docs]: https://spatialdata.scverse.org/projects/plot/en/stable/api.html
[spatialdata-r-repo]: https://github.com/HelenaLC/SpatialData
[spatialdata-r-repo]: https://github.com/HelenaLC/spatialdataR
[spatialdata-js-repo]: https://github.com/Taylor-CCB-Group/SpatialData.js
16 changes: 16 additions & 0 deletions docs/interoperability.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
# Interoperability

The on-disk representation of SpatialData can be read from other languages. Here we list interfaces for working with SpatialData from your language of choice:

## R

- [spatialdataR](https://helenalc.github.io/spatialdataR/) provides an R implementation of the `SpatialData` object, with out-of-memory images and labels, `duckdb`-backed points and shapes, and tables represented as `SingleCellExperiment` objects.

## JavaScript and TypeScript

- [SpatialData.js](https://github.com/Taylor-CCB-Group/SpatialData.js) provides a TypeScript and JavaScript library for interfacing with SpatialData stores.
- [Vitessce](https://vitessce.io/docs/data-file-types/#spatialdatazarr) reads `spatialdata.zarr` stores directly and uses them for interactive visualization.

## File format

The SpatialData on-disk format builds on [OME-NGFF](https://ngff.openmicroscopy.org/latest/). See the [design document](design_doc.md) for details of the current on-disk layout.
37 changes: 37 additions & 0 deletions pyproject.toml
Original file line numberDiff line numberDiff line change
Expand Up@@ -45,6 +45,7 @@ dependencies = [
"spatial_image>=1.2.3",
"scikit-image",
"scipy!=1.17.0",
"scverse-misc[datasets]>=0.1.0",
"typing_extensions>=4.8.0",
"universal_pathlib>=0.2.6",
"xarray>=2024.10.0",
Expand DownExpand Up@@ -108,6 +109,7 @@ addopts = [
# These are all markers coming from xarray, dask or anndata. Added here to silence warnings.
markers = [
"slow: marks tests as slow (deselect with '-m \"not slow\"')",
"network: marks tests that require network access; skipped by default, run with '--run-network'",
"gpu: run test on GPU using CuPY.",
"array_api: used by anndata.tests.helpers, not us",
"skip_with_pyarrow_strings: skipwhen pyarrow string conversion is turned on",
Expand DownExpand Up@@ -244,3 +246,38 @@ 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"
# no "0.12-3": support for pandas>=3 is available only in anndata>=0.13
]

[tool.hatch.envs.test-anndata-pandas.overrides]
matrix.anndata-pandas.extra-dependencies = [
# every option where the if-condition is True gets included

# anndata 0.13
{value="anndata~=0.13", if = ["0.13-2", "0.13-3"]},

# anndata 0.12
{value="anndata>=0.12,<0.13", if = ["0.12-2"]},

# pandas 2
{value="pandas>=2.3,<3", if = ["0.13-2", "0.12-2"]},

# pandas 3
{value="pandas~=3.0", if = ["0.13-3"]},
]
17 changes: 15 additions & 2 deletions src/spatialdata/_core/spatialdata.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -107,7 +107,7 @@ class SpatialData:
- the table are stored as :class:`anndata.AnnData` objects, with the spatial coordinates stored in the obsm
slot.

The table can annotate regions (shapesor labels) and can be used to store additional information.
The table can annotate regions (shapes or labels) and can be used to store additional information.
Points are not regions but 0-dimensional locations. They can't be annotated by a table, but they can store
annotation directly.
"""
Expand DownExpand Up@@ -1114,6 +1114,7 @@ def write(
sdata_formats: SpatialDataFormatType | list[SpatialDataFormatType] | None = None,
shapes_geometry_encoding: Literal["WKB", "geoarrow"] | None = None,
raster_compressor: dict[Literal["lz4", "zstd"], int] | None = None,
convert_table_strings_to_categoricals: bool = False,
) -> None:
"""
Write the `SpatialData` object to a Zarr store.
Expand DownExpand Up@@ -1166,6 +1167,9 @@ def write(
compression level which should be inclusive between 0 and 9. For compression, `lz4` and `zstd` are
supported. If not specified, the compression will be `lz4` with compression level 5. Bytes are automatically
ordered for more efficient compression.
convert_table_strings_to_categoricals
If True, convert string columns of all tables to categoricals before writing.
Note that this will have a side effect of modifying string columns into categoricals in place.
"""
from spatialdata._io._utils import _resolve_zarr_store, _validate_compressor_args
from spatialdata._io.format import _parse_formats
Expand DownExpand Up@@ -1194,6 +1198,7 @@ def write(
parsed_formats=parsed,
shapes_geometry_encoding=shapes_geometry_encoding,
raster_compressor=raster_compressor,
convert_table_strings_to_categoricals=convert_table_strings_to_categoricals,
)

if self.path != file_path and update_sdata_path:
Expand All@@ -1212,6 +1217,7 @@ def _write_element(
parsed_formats: dict[str, SpatialDataFormatType] | None = None,
shapes_geometry_encoding: Literal["WKB", "geoarrow"] | None = None,
raster_compressor: dict[Literal["lz4", "zstd"], int] | None = None,
convert_table_strings_to_categoricals: bool = False,
) -> None:
from spatialdata._io.io_zarr import _get_groups_for_element

Expand DownExpand Up@@ -1279,6 +1285,7 @@ def _write_element(
group=element_type_group,
name=element_name,
element_format=parsed_formats["tables"],
convert_strings_to_categoricals=convert_table_strings_to_categoricals,
)
else:
raise ValueError(f"Unknown element type: {element_type}")
Expand All@@ -1290,6 +1297,7 @@ def write_element(
sdata_formats: SpatialDataFormatType | list[SpatialDataFormatType] | None = None,
shapes_geometry_encoding: Literal["WKB", "geoarrow"] | None = None,
raster_compressor: dict[Literal["lz4", "zstd"], int] | None = None,
convert_table_strings_to_categoricals: bool = False,
) -> None:
"""
Write a single element, or a list of elements, to the Zarr store used for backing.
Expand All@@ -1308,11 +1316,14 @@ def write_element(
shapes_geometry_encoding
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`.
raster_compressor
raster_compressor
A lenght-1 dictionary with as key the type of compression to use for images and labels and as value the
compression level which should be inclusive between 0 and 9. For compression, `lz4` and `zstd` are
supported. If not specified, the compression will be `lz4` with compression level 5. Bytes are automatically
ordered for more efficient compression.
convert_table_strings_to_categoricals
If True, and if element to be written is a table, convert string columns to categoricals before writing.
Note that this will have a side effect of modifying string columns into categoricals in place.

Notes
-----
Expand All@@ -1332,6 +1343,7 @@ def write_element(
sdata_formats=sdata_formats,
shapes_geometry_encoding=shapes_geometry_encoding,
raster_compressor=raster_compressor,
convert_table_strings_to_categoricals=convert_table_strings_to_categoricals,
)
return

Expand DownExpand Up@@ -1368,6 +1380,7 @@ def write_element(
parsed_formats=parsed_formats,
shapes_geometry_encoding=shapes_geometry_encoding,
raster_compressor=raster_compressor,
convert_table_strings_to_categoricals=convert_table_strings_to_categoricals,
)
# After every write, metadata should be consolidated, otherwise this can lead to IO problems like when deleting.
if self.has_consolidated_metadata():
Expand Down
2 changes: 1 addition & 1 deletion src/spatialdata/_core/validation.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -388,7 +388,7 @@ def __exit__(
# Exceptions were collected that we want to raise as a combined validation error.
if self._collector.errors:
raise ValidationError(
title=self._message + "\nTo fix, run `spatialdata.utils.sanitize_table(adata)`.",
title=self._message + "\nTo fix, run `spatialdata.sanitize_table(adata)`.",
errors=self._collector.errors,
)
return True
26 changes: 26 additions & 0 deletions src/spatialdata/_io/exceptions.py
Original file line numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' fix: point table validation to public sanitizer by stanbot8 · Pull Request #1 · stanbot8/spatialdata · GitHub
Skip to content
Draft
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
2 changes: 1 addition & 1 deletion .github/workflows/test.yaml
Original file line numberDiff line numberDiff line change
Expand Up@@ -59,7 +59,7 @@ jobs:
PLATFORM: ${{ matrix.os }}
DISPLAY: :42
run: |
uv run pytest --cov --color=yes --cov-report=xml -n auto --dist worksteal
uv run pytest --run-network --cov --color=yes --cov-report=xml -n auto --dist worksteal
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v6
with:
Expand Down
2 changes: 1 addition & 1 deletion .gitmodules
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
[submodule "docs/tutorials/notebooks"]
path = docs/tutorials/notebooks
url = https://github.com/scverse/spatialdata-notebooks
url = https://github.com/scverse/spatialdata-tutorials
6 changes: 3 additions & 3 deletions .pre-commit-config.yaml
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,18 +9,18 @@ ci:
skip: []
repos:
- repo: https://github.com/rbubley/mirrors-prettier
rev: v3.9.4
rev: v3.9.6
hooks:
- id: prettier
exclude: ^.github/workflows/test.yaml
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v2.1.0
rev: v2.3.1
hooks:
- id: mypy
additional_dependencies: [numpy, types-requests]
exclude: tests/|docs/
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.15.20
rev: v0.16.3
hooks:
- id: ruff
args: [--fix, --exit-non-zero-on-fix]
Expand Down
1 change: 1 addition & 0 deletions docs/api/datasets.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,5 +7,6 @@ Convenience small datasets

.. autofunction:: blobs
.. autofunction:: blobs_annotating_element
.. autofunction:: cells
.. autofunction:: raccoon
```
5 changes: 2 additions & 3 deletions docs/contributing.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -13,8 +13,7 @@ In addition to the packages needed to _use_ this package, you need additional py
the documentation_. It's easy to install them using `pip`:

```bash
cd spatialdata-io
pip install -e ".[dev,test,doc]"
pip install -e . --group dev --group test --group docs
```

## Code-style
Expand DownExpand Up@@ -51,7 +50,7 @@ and [prettier][prettier-editors].
## Writing tests

```{note}
Remember to first install the package with `pip install '-e[dev,test]'`
Remember to first install the package with `pip install -e . --group dev --group test`
```

This package uses [pytest][] for automated testing. Please [write tests][scanpy-test-docs] for every function added to the package.
Expand Down
7 changes: 4 additions & 3 deletions docs/index.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,8 +12,8 @@ This page provides documentation on how to install, use, and extend the core `sp

- `spatialdata-io`: load data from common spatial omics technologies into `spatialdata` ([repository][spatialdata-io-repo], [documentation][spatialdata-io-docs]).
- `spatialdata-plot`: Static plotting library for `spatialdata` ([repository][spatialdata-plot-repo], [documentation][spatialdata-plot-docs]).
- `napari-spatialdata-repo`: napari plugin for interactive exploration and annotation of `spatialdata` ([repository][napari-spatialdata-repo], [documentation][napari-spatialdata-docs]).
- `SpatialData` (R): R implementation of the SpatialData framework ([repository][spatialdata-r-repo]).
- `napari-spatialdata`: napari plugin for interactive exploration and annotation of `spatialdata` ([repository][napari-spatialdata-repo], [documentation][napari-spatialdata-docs]).
- `spatialdataR`: R implementation of the SpatialData framework ([repository][spatialdata-r-repo]).
- `SpatialData.js`: JavaScript/TypeScript implementation of the SpatialData framework ([repository][spatialdata-js-repo]).

Please see our publication {cite}`marconatoSpatialDataOpenUniversal2024` for citation and to learn more.
Expand DownExpand Up@@ -110,6 +110,7 @@ tutorials/notebooks/notebooks.md
tutorials/notebooks/datasets/README.md
glossary.md
design_doc.md
interoperability.md
contributing.md
changelog.md
references.md
Expand All@@ -123,5 +124,5 @@ references.md
[napari-spatialdata-docs]: https://spatialdata.scverse.org/projects/napari/en/stable/notebooks/spatialdata.html
[spatialdata-io-docs]: https://spatialdata.scverse.org/projects/io/en/stable/
[spatialdata-plot-docs]: https://spatialdata.scverse.org/projects/plot/en/stable/api.html
[spatialdata-r-repo]: https://github.com/HelenaLC/SpatialData
[spatialdata-r-repo]: https://github.com/HelenaLC/spatialdataR
[spatialdata-js-repo]: https://github.com/Taylor-CCB-Group/SpatialData.js
16 changes: 16 additions & 0 deletions docs/interoperability.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
# Interoperability

The on-disk representation of SpatialData can be read from other languages. Here we list interfaces for working with SpatialData from your language of choice:

## R

- [spatialdataR](https://helenalc.github.io/spatialdataR/) provides an R implementation of the `SpatialData` object, with out-of-memory images and labels, `duckdb`-backed points and shapes, and tables represented as `SingleCellExperiment` objects.

## JavaScript and TypeScript

- [SpatialData.js](https://github.com/Taylor-CCB-Group/SpatialData.js) provides a TypeScript and JavaScript library for interfacing with SpatialData stores.
- [Vitessce](https://vitessce.io/docs/data-file-types/#spatialdatazarr) reads `spatialdata.zarr` stores directly and uses them for interactive visualization.

## File format

The SpatialData on-disk format builds on [OME-NGFF](https://ngff.openmicroscopy.org/latest/). See the [design document](design_doc.md) for details of the current on-disk layout.
37 changes: 37 additions & 0 deletions pyproject.toml
Original file line numberDiff line numberDiff line change
Expand Up@@ -45,6 +45,7 @@ dependencies = [
"spatial_image>=1.2.3",
"scikit-image",
"scipy!=1.17.0",
"scverse-misc[datasets]>=0.1.0",
"typing_extensions>=4.8.0",
"universal_pathlib>=0.2.6",
"xarray>=2024.10.0",
Expand DownExpand Up@@ -108,6 +109,7 @@ addopts = [
# These are all markers coming from xarray, dask or anndata. Added here to silence warnings.
markers = [
"slow: marks tests as slow (deselect with '-m \"not slow\"')",
"network: marks tests that require network access; skipped by default, run with '--run-network'",
"gpu: run test on GPU using CuPY.",
"array_api: used by anndata.tests.helpers, not us",
"skip_with_pyarrow_strings: skipwhen pyarrow string conversion is turned on",
Expand DownExpand Up@@ -244,3 +246,38 @@ 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"
# no "0.12-3": support for pandas>=3 is available only in anndata>=0.13
]

[tool.hatch.envs.test-anndata-pandas.overrides]
matrix.anndata-pandas.extra-dependencies = [
# every option where the if-condition is True gets included

# anndata 0.13
{value="anndata~=0.13", if = ["0.13-2", "0.13-3"]},

# anndata 0.12
{value="anndata>=0.12,<0.13", if = ["0.12-2"]},

# pandas 2
{value="pandas>=2.3,<3", if = ["0.13-2", "0.12-2"]},

# pandas 3
{value="pandas~=3.0", if = ["0.13-3"]},
]
17 changes: 15 additions & 2 deletions src/spatialdata/_core/spatialdata.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -107,7 +107,7 @@ class SpatialData:
- the table are stored as :class:`anndata.AnnData` objects, with the spatial coordinates stored in the obsm
slot.

The table can annotate regions (shapesor labels) and can be used to store additional information.
The table can annotate regions (shapes or labels) and can be used to store additional information.
Points are not regions but 0-dimensional locations. They can't be annotated by a table, but they can store
annotation directly.
"""
Expand DownExpand Up@@ -1114,6 +1114,7 @@ def write(
sdata_formats: SpatialDataFormatType | list[SpatialDataFormatType] | None = None,
shapes_geometry_encoding: Literal["WKB", "geoarrow"] | None = None,
raster_compressor: dict[Literal["lz4", "zstd"], int] | None = None,
convert_table_strings_to_categoricals: bool = False,
) -> None:
"""
Write the `SpatialData` object to a Zarr store.
Expand DownExpand Up@@ -1166,6 +1167,9 @@ def write(
compression level which should be inclusive between 0 and 9. For compression, `lz4` and `zstd` are
supported. If not specified, the compression will be `lz4` with compression level 5. Bytes are automatically
ordered for more efficient compression.
convert_table_strings_to_categoricals
If True, convert string columns of all tables to categoricals before writing.
Note that this will have a side effect of modifying string columns into categoricals in place.
"""
from spatialdata._io._utils import _resolve_zarr_store, _validate_compressor_args
from spatialdata._io.format import _parse_formats
Expand DownExpand Up@@ -1194,6 +1198,7 @@ def write(
parsed_formats=parsed,
shapes_geometry_encoding=shapes_geometry_encoding,
raster_compressor=raster_compressor,
convert_table_strings_to_categoricals=convert_table_strings_to_categoricals,
)

if self.path != file_path and update_sdata_path:
Expand All@@ -1212,6 +1217,7 @@ def _write_element(
parsed_formats: dict[str, SpatialDataFormatType] | None = None,
shapes_geometry_encoding: Literal["WKB", "geoarrow"] | None = None,
raster_compressor: dict[Literal["lz4", "zstd"], int] | None = None,
convert_table_strings_to_categoricals: bool = False,
) -> None:
from spatialdata._io.io_zarr import _get_groups_for_element

Expand DownExpand Up@@ -1279,6 +1285,7 @@ def _write_element(
group=element_type_group,
name=element_name,
element_format=parsed_formats["tables"],
convert_strings_to_categoricals=convert_table_strings_to_categoricals,
)
else:
raise ValueError(f"Unknown element type: {element_type}")
Expand All@@ -1290,6 +1297,7 @@ def write_element(
sdata_formats: SpatialDataFormatType | list[SpatialDataFormatType] | None = None,
shapes_geometry_encoding: Literal["WKB", "geoarrow"] | None = None,
raster_compressor: dict[Literal["lz4", "zstd"], int] | None = None,
convert_table_strings_to_categoricals: bool = False,
) -> None:
"""
Write a single element, or a list of elements, to the Zarr store used for backing.
Expand All@@ -1308,11 +1316,14 @@ def write_element(
shapes_geometry_encoding
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`.
raster_compressor
raster_compressor
A lenght-1 dictionary with as key the type of compression to use for images and labels and as value the
compression level which should be inclusive between 0 and 9. For compression, `lz4` and `zstd` are
supported. If not specified, the compression will be `lz4` with compression level 5. Bytes are automatically
ordered for more efficient compression.
convert_table_strings_to_categoricals
If True, and if element to be written is a table, convert string columns to categoricals before writing.
Note that this will have a side effect of modifying string columns into categoricals in place.

Notes
-----
Expand All@@ -1332,6 +1343,7 @@ def write_element(
sdata_formats=sdata_formats,
shapes_geometry_encoding=shapes_geometry_encoding,
raster_compressor=raster_compressor,
convert_table_strings_to_categoricals=convert_table_strings_to_categoricals,
)
return

Expand DownExpand Up@@ -1368,6 +1380,7 @@ def write_element(
parsed_formats=parsed_formats,
shapes_geometry_encoding=shapes_geometry_encoding,
raster_compressor=raster_compressor,
convert_table_strings_to_categoricals=convert_table_strings_to_categoricals,
)
# After every write, metadata should be consolidated, otherwise this can lead to IO problems like when deleting.
if self.has_consolidated_metadata():
Expand Down
2 changes: 1 addition & 1 deletion src/spatialdata/_core/validation.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -388,7 +388,7 @@ def __exit__(
# Exceptions were collected that we want to raise as a combined validation error.
if self._collector.errors:
raise ValidationError(
title=self._message + "\nTo fix, run `spatialdata.utils.sanitize_table(adata)`.",
title=self._message + "\nTo fix, run `spatialdata.sanitize_table(adata)`.",
errors=self._collector.errors,
)
return True
26 changes: 26 additions & 0 deletions src/spatialdata/_io/exceptions.py
Original file line numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' fix: point table validation to public sanitizer by stanbot8 · Pull Request #1 · stanbot8/spatialdata · GitHub
Skip to content
Draft
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
2 changes: 1 addition & 1 deletion .github/workflows/test.yaml
Original file line numberDiff line numberDiff line change
Expand Up@@ -59,7 +59,7 @@ jobs:
PLATFORM: ${{ matrix.os }}
DISPLAY: :42
run: |
uv run pytest --cov --color=yes --cov-report=xml -n auto --dist worksteal
uv run pytest --run-network --cov --color=yes --cov-report=xml -n auto --dist worksteal
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v6
with:
Expand Down
2 changes: 1 addition & 1 deletion .gitmodules
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
[submodule "docs/tutorials/notebooks"]
path = docs/tutorials/notebooks
url = https://github.com/scverse/spatialdata-notebooks
url = https://github.com/scverse/spatialdata-tutorials
6 changes: 3 additions & 3 deletions .pre-commit-config.yaml
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,18 +9,18 @@ ci:
skip: []
repos:
- repo: https://github.com/rbubley/mirrors-prettier
rev: v3.9.4
rev: v3.9.6
hooks:
- id: prettier
exclude: ^.github/workflows/test.yaml
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v2.1.0
rev: v2.3.1
hooks:
- id: mypy
additional_dependencies: [numpy, types-requests]
exclude: tests/|docs/
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.15.20
rev: v0.16.3
hooks:
- id: ruff
args: [--fix, --exit-non-zero-on-fix]
Expand Down
1 change: 1 addition & 0 deletions docs/api/datasets.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,5 +7,6 @@ Convenience small datasets

.. autofunction:: blobs
.. autofunction:: blobs_annotating_element
.. autofunction:: cells
.. autofunction:: raccoon
```
5 changes: 2 additions & 3 deletions docs/contributing.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -13,8 +13,7 @@ In addition to the packages needed to _use_ this package, you need additional py
the documentation_. It's easy to install them using `pip`:

```bash
cd spatialdata-io
pip install -e ".[dev,test,doc]"
pip install -e . --group dev --group test --group docs
```

## Code-style
Expand DownExpand Up@@ -51,7 +50,7 @@ and [prettier][prettier-editors].
## Writing tests

```{note}
Remember to first install the package with `pip install '-e[dev,test]'`
Remember to first install the package with `pip install -e . --group dev --group test`
```

This package uses [pytest][] for automated testing. Please [write tests][scanpy-test-docs] for every function added to the package.
Expand Down
7 changes: 4 additions & 3 deletions docs/index.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,8 +12,8 @@ This page provides documentation on how to install, use, and extend the core `sp

- `spatialdata-io`: load data from common spatial omics technologies into `spatialdata` ([repository][spatialdata-io-repo], [documentation][spatialdata-io-docs]).
- `spatialdata-plot`: Static plotting library for `spatialdata` ([repository][spatialdata-plot-repo], [documentation][spatialdata-plot-docs]).
- `napari-spatialdata-repo`: napari plugin for interactive exploration and annotation of `spatialdata` ([repository][napari-spatialdata-repo], [documentation][napari-spatialdata-docs]).
- `SpatialData` (R): R implementation of the SpatialData framework ([repository][spatialdata-r-repo]).
- `napari-spatialdata`: napari plugin for interactive exploration and annotation of `spatialdata` ([repository][napari-spatialdata-repo], [documentation][napari-spatialdata-docs]).
- `spatialdataR`: R implementation of the SpatialData framework ([repository][spatialdata-r-repo]).
- `SpatialData.js`: JavaScript/TypeScript implementation of the SpatialData framework ([repository][spatialdata-js-repo]).

Please see our publication {cite}`marconatoSpatialDataOpenUniversal2024` for citation and to learn more.
Expand DownExpand Up@@ -110,6 +110,7 @@ tutorials/notebooks/notebooks.md
tutorials/notebooks/datasets/README.md
glossary.md
design_doc.md
interoperability.md
contributing.md
changelog.md
references.md
Expand All@@ -123,5 +124,5 @@ references.md
[napari-spatialdata-docs]: https://spatialdata.scverse.org/projects/napari/en/stable/notebooks/spatialdata.html
[spatialdata-io-docs]: https://spatialdata.scverse.org/projects/io/en/stable/
[spatialdata-plot-docs]: https://spatialdata.scverse.org/projects/plot/en/stable/api.html
[spatialdata-r-repo]: https://github.com/HelenaLC/SpatialData
[spatialdata-r-repo]: https://github.com/HelenaLC/spatialdataR
[spatialdata-js-repo]: https://github.com/Taylor-CCB-Group/SpatialData.js
16 changes: 16 additions & 0 deletions docs/interoperability.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
# Interoperability

The on-disk representation of SpatialData can be read from other languages. Here we list interfaces for working with SpatialData from your language of choice:

## R

- [spatialdataR](https://helenalc.github.io/spatialdataR/) provides an R implementation of the `SpatialData` object, with out-of-memory images and labels, `duckdb`-backed points and shapes, and tables represented as `SingleCellExperiment` objects.

## JavaScript and TypeScript

- [SpatialData.js](https://github.com/Taylor-CCB-Group/SpatialData.js) provides a TypeScript and JavaScript library for interfacing with SpatialData stores.
- [Vitessce](https://vitessce.io/docs/data-file-types/#spatialdatazarr) reads `spatialdata.zarr` stores directly and uses them for interactive visualization.

## File format

The SpatialData on-disk format builds on [OME-NGFF](https://ngff.openmicroscopy.org/latest/). See the [design document](design_doc.md) for details of the current on-disk layout.
37 changes: 37 additions & 0 deletions pyproject.toml
Original file line numberDiff line numberDiff line change
Expand Up@@ -45,6 +45,7 @@ dependencies = [
"spatial_image>=1.2.3",
"scikit-image",
"scipy!=1.17.0",
"scverse-misc[datasets]>=0.1.0",
"typing_extensions>=4.8.0",
"universal_pathlib>=0.2.6",
"xarray>=2024.10.0",
Expand DownExpand Up@@ -108,6 +109,7 @@ addopts = [
# These are all markers coming from xarray, dask or anndata. Added here to silence warnings.
markers = [
"slow: marks tests as slow (deselect with '-m \"not slow\"')",
"network: marks tests that require network access; skipped by default, run with '--run-network'",
"gpu: run test on GPU using CuPY.",
"array_api: used by anndata.tests.helpers, not us",
"skip_with_pyarrow_strings: skipwhen pyarrow string conversion is turned on",
Expand DownExpand Up@@ -244,3 +246,38 @@ 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"
# no "0.12-3": support for pandas>=3 is available only in anndata>=0.13
]

[tool.hatch.envs.test-anndata-pandas.overrides]
matrix.anndata-pandas.extra-dependencies = [
# every option where the if-condition is True gets included

# anndata 0.13
{value="anndata~=0.13", if = ["0.13-2", "0.13-3"]},

# anndata 0.12
{value="anndata>=0.12,<0.13", if = ["0.12-2"]},

# pandas 2
{value="pandas>=2.3,<3", if = ["0.13-2", "0.12-2"]},

# pandas 3
{value="pandas~=3.0", if = ["0.13-3"]},
]
17 changes: 15 additions & 2 deletions src/spatialdata/_core/spatialdata.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -107,7 +107,7 @@ class SpatialData:
- the table are stored as :class:`anndata.AnnData` objects, with the spatial coordinates stored in the obsm
slot.

The table can annotate regions (shapesor labels) and can be used to store additional information.
The table can annotate regions (shapes or labels) and can be used to store additional information.
Points are not regions but 0-dimensional locations. They can't be annotated by a table, but they can store
annotation directly.
"""
Expand DownExpand Up@@ -1114,6 +1114,7 @@ def write(
sdata_formats: SpatialDataFormatType | list[SpatialDataFormatType] | None = None,
shapes_geometry_encoding: Literal["WKB", "geoarrow"] | None = None,
raster_compressor: dict[Literal["lz4", "zstd"], int] | None = None,
convert_table_strings_to_categoricals: bool = False,
) -> None:
"""
Write the `SpatialData` object to a Zarr store.
Expand DownExpand Up@@ -1166,6 +1167,9 @@ def write(
compression level which should be inclusive between 0 and 9. For compression, `lz4` and `zstd` are
supported. If not specified, the compression will be `lz4` with compression level 5. Bytes are automatically
ordered for more efficient compression.
convert_table_strings_to_categoricals
If True, convert string columns of all tables to categoricals before writing.
Note that this will have a side effect of modifying string columns into categoricals in place.
"""
from spatialdata._io._utils import _resolve_zarr_store, _validate_compressor_args
from spatialdata._io.format import _parse_formats
Expand DownExpand Up@@ -1194,6 +1198,7 @@ def write(
parsed_formats=parsed,
shapes_geometry_encoding=shapes_geometry_encoding,
raster_compressor=raster_compressor,
convert_table_strings_to_categoricals=convert_table_strings_to_categoricals,
)

if self.path != file_path and update_sdata_path:
Expand All@@ -1212,6 +1217,7 @@ def _write_element(
parsed_formats: dict[str, SpatialDataFormatType] | None = None,
shapes_geometry_encoding: Literal["WKB", "geoarrow"] | None = None,
raster_compressor: dict[Literal["lz4", "zstd"], int] | None = None,
convert_table_strings_to_categoricals: bool = False,
) -> None:
from spatialdata._io.io_zarr import _get_groups_for_element

Expand DownExpand Up@@ -1279,6 +1285,7 @@ def _write_element(
group=element_type_group,
name=element_name,
element_format=parsed_formats["tables"],
convert_strings_to_categoricals=convert_table_strings_to_categoricals,
)
else:
raise ValueError(f"Unknown element type: {element_type}")
Expand All@@ -1290,6 +1297,7 @@ def write_element(
sdata_formats: SpatialDataFormatType | list[SpatialDataFormatType] | None = None,
shapes_geometry_encoding: Literal["WKB", "geoarrow"] | None = None,
raster_compressor: dict[Literal["lz4", "zstd"], int] | None = None,
convert_table_strings_to_categoricals: bool = False,
) -> None:
"""
Write a single element, or a list of elements, to the Zarr store used for backing.
Expand All@@ -1308,11 +1316,14 @@ def write_element(
shapes_geometry_encoding
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`.
raster_compressor
raster_compressor
A lenght-1 dictionary with as key the type of compression to use for images and labels and as value the
compression level which should be inclusive between 0 and 9. For compression, `lz4` and `zstd` are
supported. If not specified, the compression will be `lz4` with compression level 5. Bytes are automatically
ordered for more efficient compression.
convert_table_strings_to_categoricals
If True, and if element to be written is a table, convert string columns to categoricals before writing.
Note that this will have a side effect of modifying string columns into categoricals in place.

Notes
-----
Expand All@@ -1332,6 +1343,7 @@ def write_element(
sdata_formats=sdata_formats,
shapes_geometry_encoding=shapes_geometry_encoding,
raster_compressor=raster_compressor,
convert_table_strings_to_categoricals=convert_table_strings_to_categoricals,
)
return

Expand DownExpand Up@@ -1368,6 +1380,7 @@ def write_element(
parsed_formats=parsed_formats,
shapes_geometry_encoding=shapes_geometry_encoding,
raster_compressor=raster_compressor,
convert_table_strings_to_categoricals=convert_table_strings_to_categoricals,
)
# After every write, metadata should be consolidated, otherwise this can lead to IO problems like when deleting.
if self.has_consolidated_metadata():
Expand Down
2 changes: 1 addition & 1 deletion src/spatialdata/_core/validation.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -388,7 +388,7 @@ def __exit__(
# Exceptions were collected that we want to raise as a combined validation error.
if self._collector.errors:
raise ValidationError(
title=self._message + "\nTo fix, run `spatialdata.utils.sanitize_table(adata)`.",
title=self._message + "\nTo fix, run `spatialdata.sanitize_table(adata)`.",
errors=self._collector.errors,
)
return True
26 changes: 26 additions & 0 deletions src/spatialdata/_io/exceptions.py
Original file line numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' fix: point table validation to public sanitizer by stanbot8 · Pull Request #1 · stanbot8/spatialdata · GitHub
Skip to content
Draft
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
2 changes: 1 addition & 1 deletion .github/workflows/test.yaml
Original file line numberDiff line numberDiff line change
Expand Up@@ -59,7 +59,7 @@ jobs:
PLATFORM: ${{ matrix.os }}
DISPLAY: :42
run: |
uv run pytest --cov --color=yes --cov-report=xml -n auto --dist worksteal
uv run pytest --run-network --cov --color=yes --cov-report=xml -n auto --dist worksteal
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v6
with:
Expand Down
2 changes: 1 addition & 1 deletion .gitmodules
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
[submodule "docs/tutorials/notebooks"]
path = docs/tutorials/notebooks
url = https://github.com/scverse/spatialdata-notebooks
url = https://github.com/scverse/spatialdata-tutorials
6 changes: 3 additions & 3 deletions .pre-commit-config.yaml
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,18 +9,18 @@ ci:
skip: []
repos:
- repo: https://github.com/rbubley/mirrors-prettier
rev: v3.9.4
rev: v3.9.6
hooks:
- id: prettier
exclude: ^.github/workflows/test.yaml
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v2.1.0
rev: v2.3.1
hooks:
- id: mypy
additional_dependencies: [numpy, types-requests]
exclude: tests/|docs/
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.15.20
rev: v0.16.3
hooks:
- id: ruff
args: [--fix, --exit-non-zero-on-fix]
Expand Down
1 change: 1 addition & 0 deletions docs/api/datasets.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,5 +7,6 @@ Convenience small datasets

.. autofunction:: blobs
.. autofunction:: blobs_annotating_element
.. autofunction:: cells
.. autofunction:: raccoon
```
5 changes: 2 additions & 3 deletions docs/contributing.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -13,8 +13,7 @@ In addition to the packages needed to _use_ this package, you need additional py
the documentation_. It's easy to install them using `pip`:

```bash
cd spatialdata-io
pip install -e ".[dev,test,doc]"
pip install -e . --group dev --group test --group docs
```

## Code-style
Expand DownExpand Up@@ -51,7 +50,7 @@ and [prettier][prettier-editors].
## Writing tests

```{note}
Remember to first install the package with `pip install '-e[dev,test]'`
Remember to first install the package with `pip install -e . --group dev --group test`
```

This package uses [pytest][] for automated testing. Please [write tests][scanpy-test-docs] for every function added to the package.
Expand Down
7 changes: 4 additions & 3 deletions docs/index.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,8 +12,8 @@ This page provides documentation on how to install, use, and extend the core `sp

- `spatialdata-io`: load data from common spatial omics technologies into `spatialdata` ([repository][spatialdata-io-repo], [documentation][spatialdata-io-docs]).
- `spatialdata-plot`: Static plotting library for `spatialdata` ([repository][spatialdata-plot-repo], [documentation][spatialdata-plot-docs]).
- `napari-spatialdata-repo`: napari plugin for interactive exploration and annotation of `spatialdata` ([repository][napari-spatialdata-repo], [documentation][napari-spatialdata-docs]).
- `SpatialData` (R): R implementation of the SpatialData framework ([repository][spatialdata-r-repo]).
- `napari-spatialdata`: napari plugin for interactive exploration and annotation of `spatialdata` ([repository][napari-spatialdata-repo], [documentation][napari-spatialdata-docs]).
- `spatialdataR`: R implementation of the SpatialData framework ([repository][spatialdata-r-repo]).
- `SpatialData.js`: JavaScript/TypeScript implementation of the SpatialData framework ([repository][spatialdata-js-repo]).

Please see our publication {cite}`marconatoSpatialDataOpenUniversal2024` for citation and to learn more.
Expand DownExpand Up@@ -110,6 +110,7 @@ tutorials/notebooks/notebooks.md
tutorials/notebooks/datasets/README.md
glossary.md
design_doc.md
interoperability.md
contributing.md
changelog.md
references.md
Expand All@@ -123,5 +124,5 @@ references.md
[napari-spatialdata-docs]: https://spatialdata.scverse.org/projects/napari/en/stable/notebooks/spatialdata.html
[spatialdata-io-docs]: https://spatialdata.scverse.org/projects/io/en/stable/
[spatialdata-plot-docs]: https://spatialdata.scverse.org/projects/plot/en/stable/api.html
[spatialdata-r-repo]: https://github.com/HelenaLC/SpatialData
[spatialdata-r-repo]: https://github.com/HelenaLC/spatialdataR
[spatialdata-js-repo]: https://github.com/Taylor-CCB-Group/SpatialData.js
16 changes: 16 additions & 0 deletions docs/interoperability.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
# Interoperability

The on-disk representation of SpatialData can be read from other languages. Here we list interfaces for working with SpatialData from your language of choice:

## R

- [spatialdataR](https://helenalc.github.io/spatialdataR/) provides an R implementation of the `SpatialData` object, with out-of-memory images and labels, `duckdb`-backed points and shapes, and tables represented as `SingleCellExperiment` objects.

## JavaScript and TypeScript

- [SpatialData.js](https://github.com/Taylor-CCB-Group/SpatialData.js) provides a TypeScript and JavaScript library for interfacing with SpatialData stores.
- [Vitessce](https://vitessce.io/docs/data-file-types/#spatialdatazarr) reads `spatialdata.zarr` stores directly and uses them for interactive visualization.

## File format

The SpatialData on-disk format builds on [OME-NGFF](https://ngff.openmicroscopy.org/latest/). See the [design document](design_doc.md) for details of the current on-disk layout.
37 changes: 37 additions & 0 deletions pyproject.toml
Original file line numberDiff line numberDiff line change
Expand Up@@ -45,6 +45,7 @@ dependencies = [
"spatial_image>=1.2.3",
"scikit-image",
"scipy!=1.17.0",
"scverse-misc[datasets]>=0.1.0",
"typing_extensions>=4.8.0",
"universal_pathlib>=0.2.6",
"xarray>=2024.10.0",
Expand DownExpand Up@@ -108,6 +109,7 @@ addopts = [
# These are all markers coming from xarray, dask or anndata. Added here to silence warnings.
markers = [
"slow: marks tests as slow (deselect with '-m \"not slow\"')",
"network: marks tests that require network access; skipped by default, run with '--run-network'",
"gpu: run test on GPU using CuPY.",
"array_api: used by anndata.tests.helpers, not us",
"skip_with_pyarrow_strings: skipwhen pyarrow string conversion is turned on",
Expand DownExpand Up@@ -244,3 +246,38 @@ 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"
# no "0.12-3": support for pandas>=3 is available only in anndata>=0.13
]

[tool.hatch.envs.test-anndata-pandas.overrides]
matrix.anndata-pandas.extra-dependencies = [
# every option where the if-condition is True gets included

# anndata 0.13
{value="anndata~=0.13", if = ["0.13-2", "0.13-3"]},

# anndata 0.12
{value="anndata>=0.12,<0.13", if = ["0.12-2"]},

# pandas 2
{value="pandas>=2.3,<3", if = ["0.13-2", "0.12-2"]},

# pandas 3
{value="pandas~=3.0", if = ["0.13-3"]},
]
17 changes: 15 additions & 2 deletions src/spatialdata/_core/spatialdata.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -107,7 +107,7 @@ class SpatialData:
- the table are stored as :class:`anndata.AnnData` objects, with the spatial coordinates stored in the obsm
slot.

The table can annotate regions (shapesor labels) and can be used to store additional information.
The table can annotate regions (shapes or labels) and can be used to store additional information.
Points are not regions but 0-dimensional locations. They can't be annotated by a table, but they can store
annotation directly.
"""
Expand DownExpand Up@@ -1114,6 +1114,7 @@ def write(
sdata_formats: SpatialDataFormatType | list[SpatialDataFormatType] | None = None,
shapes_geometry_encoding: Literal["WKB", "geoarrow"] | None = None,
raster_compressor: dict[Literal["lz4", "zstd"], int] | None = None,
convert_table_strings_to_categoricals: bool = False,
) -> None:
"""
Write the `SpatialData` object to a Zarr store.
Expand DownExpand Up@@ -1166,6 +1167,9 @@ def write(
compression level which should be inclusive between 0 and 9. For compression, `lz4` and `zstd` are
supported. If not specified, the compression will be `lz4` with compression level 5. Bytes are automatically
ordered for more efficient compression.
convert_table_strings_to_categoricals
If True, convert string columns of all tables to categoricals before writing.
Note that this will have a side effect of modifying string columns into categoricals in place.
"""
from spatialdata._io._utils import _resolve_zarr_store, _validate_compressor_args
from spatialdata._io.format import _parse_formats
Expand DownExpand Up@@ -1194,6 +1198,7 @@ def write(
parsed_formats=parsed,
shapes_geometry_encoding=shapes_geometry_encoding,
raster_compressor=raster_compressor,
convert_table_strings_to_categoricals=convert_table_strings_to_categoricals,
)

if self.path != file_path and update_sdata_path:
Expand All@@ -1212,6 +1217,7 @@ def _write_element(
parsed_formats: dict[str, SpatialDataFormatType] | None = None,
shapes_geometry_encoding: Literal["WKB", "geoarrow"] | None = None,
raster_compressor: dict[Literal["lz4", "zstd"], int] | None = None,
convert_table_strings_to_categoricals: bool = False,
) -> None:
from spatialdata._io.io_zarr import _get_groups_for_element

Expand DownExpand Up@@ -1279,6 +1285,7 @@ def _write_element(
group=element_type_group,
name=element_name,
element_format=parsed_formats["tables"],
convert_strings_to_categoricals=convert_table_strings_to_categoricals,
)
else:
raise ValueError(f"Unknown element type: {element_type}")
Expand All@@ -1290,6 +1297,7 @@ def write_element(
sdata_formats: SpatialDataFormatType | list[SpatialDataFormatType] | None = None,
shapes_geometry_encoding: Literal["WKB", "geoarrow"] | None = None,
raster_compressor: dict[Literal["lz4", "zstd"], int] | None = None,
convert_table_strings_to_categoricals: bool = False,
) -> None:
"""
Write a single element, or a list of elements, to the Zarr store used for backing.
Expand All@@ -1308,11 +1316,14 @@ def write_element(
shapes_geometry_encoding
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`.
raster_compressor
raster_compressor
A lenght-1 dictionary with as key the type of compression to use for images and labels and as value the
compression level which should be inclusive between 0 and 9. For compression, `lz4` and `zstd` are
supported. If not specified, the compression will be `lz4` with compression level 5. Bytes are automatically
ordered for more efficient compression.
convert_table_strings_to_categoricals
If True, and if element to be written is a table, convert string columns to categoricals before writing.
Note that this will have a side effect of modifying string columns into categoricals in place.

Notes
-----
Expand All@@ -1332,6 +1343,7 @@ def write_element(
sdata_formats=sdata_formats,
shapes_geometry_encoding=shapes_geometry_encoding,
raster_compressor=raster_compressor,
convert_table_strings_to_categoricals=convert_table_strings_to_categoricals,
)
return

Expand DownExpand Up@@ -1368,6 +1380,7 @@ def write_element(
parsed_formats=parsed_formats,
shapes_geometry_encoding=shapes_geometry_encoding,
raster_compressor=raster_compressor,
convert_table_strings_to_categoricals=convert_table_strings_to_categoricals,
)
# After every write, metadata should be consolidated, otherwise this can lead to IO problems like when deleting.
if self.has_consolidated_metadata():
Expand Down
2 changes: 1 addition & 1 deletion src/spatialdata/_core/validation.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -388,7 +388,7 @@ def __exit__(
# Exceptions were collected that we want to raise as a combined validation error.
if self._collector.errors:
raise ValidationError(
title=self._message + "\nTo fix, run `spatialdata.utils.sanitize_table(adata)`.",
title=self._message + "\nTo fix, run `spatialdata.sanitize_table(adata)`.",
errors=self._collector.errors,
)
return True
26 changes: 26 additions & 0 deletions src/spatialdata/_io/exceptions.py
Original file line numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' fix: point table validation to public sanitizer by stanbot8 · Pull Request #1 · stanbot8/spatialdata · GitHub
Skip to content
Draft
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
2 changes: 1 addition & 1 deletion .github/workflows/test.yaml
Original file line numberDiff line numberDiff line change
Expand Up@@ -59,7 +59,7 @@ jobs:
PLATFORM: ${{ matrix.os }}
DISPLAY: :42
run: |
uv run pytest --cov --color=yes --cov-report=xml -n auto --dist worksteal
uv run pytest --run-network --cov --color=yes --cov-report=xml -n auto --dist worksteal
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v6
with:
Expand Down
2 changes: 1 addition & 1 deletion .gitmodules
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
[submodule "docs/tutorials/notebooks"]
path = docs/tutorials/notebooks
url = https://github.com/scverse/spatialdata-notebooks
url = https://github.com/scverse/spatialdata-tutorials
6 changes: 3 additions & 3 deletions .pre-commit-config.yaml
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,18 +9,18 @@ ci:
skip: []
repos:
- repo: https://github.com/rbubley/mirrors-prettier
rev: v3.9.4
rev: v3.9.6
hooks:
- id: prettier
exclude: ^.github/workflows/test.yaml
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v2.1.0
rev: v2.3.1
hooks:
- id: mypy
additional_dependencies: [numpy, types-requests]
exclude: tests/|docs/
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.15.20
rev: v0.16.3
hooks:
- id: ruff
args: [--fix, --exit-non-zero-on-fix]
Expand Down
1 change: 1 addition & 0 deletions docs/api/datasets.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,5 +7,6 @@ Convenience small datasets

.. autofunction:: blobs
.. autofunction:: blobs_annotating_element
.. autofunction:: cells
.. autofunction:: raccoon
```
5 changes: 2 additions & 3 deletions docs/contributing.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -13,8 +13,7 @@ In addition to the packages needed to _use_ this package, you need additional py
the documentation_. It's easy to install them using `pip`:

```bash
cd spatialdata-io
pip install -e ".[dev,test,doc]"
pip install -e . --group dev --group test --group docs
```

## Code-style
Expand DownExpand Up@@ -51,7 +50,7 @@ and [prettier][prettier-editors].
## Writing tests

```{note}
Remember to first install the package with `pip install '-e[dev,test]'`
Remember to first install the package with `pip install -e . --group dev --group test`
```

This package uses [pytest][] for automated testing. Please [write tests][scanpy-test-docs] for every function added to the package.
Expand Down
7 changes: 4 additions & 3 deletions docs/index.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,8 +12,8 @@ This page provides documentation on how to install, use, and extend the core `sp

- `spatialdata-io`: load data from common spatial omics technologies into `spatialdata` ([repository][spatialdata-io-repo], [documentation][spatialdata-io-docs]).
- `spatialdata-plot`: Static plotting library for `spatialdata` ([repository][spatialdata-plot-repo], [documentation][spatialdata-plot-docs]).
- `napari-spatialdata-repo`: napari plugin for interactive exploration and annotation of `spatialdata` ([repository][napari-spatialdata-repo], [documentation][napari-spatialdata-docs]).
- `SpatialData` (R): R implementation of the SpatialData framework ([repository][spatialdata-r-repo]).
- `napari-spatialdata`: napari plugin for interactive exploration and annotation of `spatialdata` ([repository][napari-spatialdata-repo], [documentation][napari-spatialdata-docs]).
- `spatialdataR`: R implementation of the SpatialData framework ([repository][spatialdata-r-repo]).
- `SpatialData.js`: JavaScript/TypeScript implementation of the SpatialData framework ([repository][spatialdata-js-repo]).

Please see our publication {cite}`marconatoSpatialDataOpenUniversal2024` for citation and to learn more.
Expand DownExpand Up@@ -110,6 +110,7 @@ tutorials/notebooks/notebooks.md
tutorials/notebooks/datasets/README.md
glossary.md
design_doc.md
interoperability.md
contributing.md
changelog.md
references.md
Expand All@@ -123,5 +124,5 @@ references.md
[napari-spatialdata-docs]: https://spatialdata.scverse.org/projects/napari/en/stable/notebooks/spatialdata.html
[spatialdata-io-docs]: https://spatialdata.scverse.org/projects/io/en/stable/
[spatialdata-plot-docs]: https://spatialdata.scverse.org/projects/plot/en/stable/api.html
[spatialdata-r-repo]: https://github.com/HelenaLC/SpatialData
[spatialdata-r-repo]: https://github.com/HelenaLC/spatialdataR
[spatialdata-js-repo]: https://github.com/Taylor-CCB-Group/SpatialData.js
16 changes: 16 additions & 0 deletions docs/interoperability.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
# Interoperability

The on-disk representation of SpatialData can be read from other languages. Here we list interfaces for working with SpatialData from your language of choice:

## R

- [spatialdataR](https://helenalc.github.io/spatialdataR/) provides an R implementation of the `SpatialData` object, with out-of-memory images and labels, `duckdb`-backed points and shapes, and tables represented as `SingleCellExperiment` objects.

## JavaScript and TypeScript

- [SpatialData.js](https://github.com/Taylor-CCB-Group/SpatialData.js) provides a TypeScript and JavaScript library for interfacing with SpatialData stores.
- [Vitessce](https://vitessce.io/docs/data-file-types/#spatialdatazarr) reads `spatialdata.zarr` stores directly and uses them for interactive visualization.

## File format

The SpatialData on-disk format builds on [OME-NGFF](https://ngff.openmicroscopy.org/latest/). See the [design document](design_doc.md) for details of the current on-disk layout.
37 changes: 37 additions & 0 deletions pyproject.toml
Original file line numberDiff line numberDiff line change
Expand Up@@ -45,6 +45,7 @@ dependencies = [
"spatial_image>=1.2.3",
"scikit-image",
"scipy!=1.17.0",
"scverse-misc[datasets]>=0.1.0",
"typing_extensions>=4.8.0",
"universal_pathlib>=0.2.6",
"xarray>=2024.10.0",
Expand DownExpand Up@@ -108,6 +109,7 @@ addopts = [
# These are all markers coming from xarray, dask or anndata. Added here to silence warnings.
markers = [
"slow: marks tests as slow (deselect with '-m \"not slow\"')",
"network: marks tests that require network access; skipped by default, run with '--run-network'",
"gpu: run test on GPU using CuPY.",
"array_api: used by anndata.tests.helpers, not us",
"skip_with_pyarrow_strings: skipwhen pyarrow string conversion is turned on",
Expand DownExpand Up@@ -244,3 +246,38 @@ 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"
# no "0.12-3": support for pandas>=3 is available only in anndata>=0.13
]

[tool.hatch.envs.test-anndata-pandas.overrides]
matrix.anndata-pandas.extra-dependencies = [
# every option where the if-condition is True gets included

# anndata 0.13
{value="anndata~=0.13", if = ["0.13-2", "0.13-3"]},

# anndata 0.12
{value="anndata>=0.12,<0.13", if = ["0.12-2"]},

# pandas 2
{value="pandas>=2.3,<3", if = ["0.13-2", "0.12-2"]},

# pandas 3
{value="pandas~=3.0", if = ["0.13-3"]},
]
17 changes: 15 additions & 2 deletions src/spatialdata/_core/spatialdata.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -107,7 +107,7 @@ class SpatialData:
- the table are stored as :class:`anndata.AnnData` objects, with the spatial coordinates stored in the obsm
slot.

The table can annotate regions (shapesor labels) and can be used to store additional information.
The table can annotate regions (shapes or labels) and can be used to store additional information.
Points are not regions but 0-dimensional locations. They can't be annotated by a table, but they can store
annotation directly.
"""
Expand DownExpand Up@@ -1114,6 +1114,7 @@ def write(
sdata_formats: SpatialDataFormatType | list[SpatialDataFormatType] | None = None,
shapes_geometry_encoding: Literal["WKB", "geoarrow"] | None = None,
raster_compressor: dict[Literal["lz4", "zstd"], int] | None = None,
convert_table_strings_to_categoricals: bool = False,
) -> None:
"""
Write the `SpatialData` object to a Zarr store.
Expand DownExpand Up@@ -1166,6 +1167,9 @@ def write(
compression level which should be inclusive between 0 and 9. For compression, `lz4` and `zstd` are
supported. If not specified, the compression will be `lz4` with compression level 5. Bytes are automatically
ordered for more efficient compression.
convert_table_strings_to_categoricals
If True, convert string columns of all tables to categoricals before writing.
Note that this will have a side effect of modifying string columns into categoricals in place.
"""
from spatialdata._io._utils import _resolve_zarr_store, _validate_compressor_args
from spatialdata._io.format import _parse_formats
Expand DownExpand Up@@ -1194,6 +1198,7 @@ def write(
parsed_formats=parsed,
shapes_geometry_encoding=shapes_geometry_encoding,
raster_compressor=raster_compressor,
convert_table_strings_to_categoricals=convert_table_strings_to_categoricals,
)

if self.path != file_path and update_sdata_path:
Expand All@@ -1212,6 +1217,7 @@ def _write_element(
parsed_formats: dict[str, SpatialDataFormatType] | None = None,
shapes_geometry_encoding: Literal["WKB", "geoarrow"] | None = None,
raster_compressor: dict[Literal["lz4", "zstd"], int] | None = None,
convert_table_strings_to_categoricals: bool = False,
) -> None:
from spatialdata._io.io_zarr import _get_groups_for_element

Expand DownExpand Up@@ -1279,6 +1285,7 @@ def _write_element(
group=element_type_group,
name=element_name,
element_format=parsed_formats["tables"],
convert_strings_to_categoricals=convert_table_strings_to_categoricals,
)
else:
raise ValueError(f"Unknown element type: {element_type}")
Expand All@@ -1290,6 +1297,7 @@ def write_element(
sdata_formats: SpatialDataFormatType | list[SpatialDataFormatType] | None = None,
shapes_geometry_encoding: Literal["WKB", "geoarrow"] | None = None,
raster_compressor: dict[Literal["lz4", "zstd"], int] | None = None,
convert_table_strings_to_categoricals: bool = False,
) -> None:
"""
Write a single element, or a list of elements, to the Zarr store used for backing.
Expand All@@ -1308,11 +1316,14 @@ def write_element(
shapes_geometry_encoding
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`.
raster_compressor
raster_compressor
A lenght-1 dictionary with as key the type of compression to use for images and labels and as value the
compression level which should be inclusive between 0 and 9. For compression, `lz4` and `zstd` are
supported. If not specified, the compression will be `lz4` with compression level 5. Bytes are automatically
ordered for more efficient compression.
convert_table_strings_to_categoricals
If True, and if element to be written is a table, convert string columns to categoricals before writing.
Note that this will have a side effect of modifying string columns into categoricals in place.

Notes
-----
Expand All@@ -1332,6 +1343,7 @@ def write_element(
sdata_formats=sdata_formats,
shapes_geometry_encoding=shapes_geometry_encoding,
raster_compressor=raster_compressor,
convert_table_strings_to_categoricals=convert_table_strings_to_categoricals,
)
return

Expand DownExpand Up@@ -1368,6 +1380,7 @@ def write_element(
parsed_formats=parsed_formats,
shapes_geometry_encoding=shapes_geometry_encoding,
raster_compressor=raster_compressor,
convert_table_strings_to_categoricals=convert_table_strings_to_categoricals,
)
# After every write, metadata should be consolidated, otherwise this can lead to IO problems like when deleting.
if self.has_consolidated_metadata():
Expand Down
2 changes: 1 addition & 1 deletion src/spatialdata/_core/validation.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -388,7 +388,7 @@ def __exit__(
# Exceptions were collected that we want to raise as a combined validation error.
if self._collector.errors:
raise ValidationError(
title=self._message + "\nTo fix, run `spatialdata.utils.sanitize_table(adata)`.",
title=self._message + "\nTo fix, run `spatialdata.sanitize_table(adata)`.",
errors=self._collector.errors,
)
return True
26 changes: 26 additions & 0 deletions src/spatialdata/_io/exceptions.py
Original file line numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' fix: point table validation to public sanitizer by stanbot8 · Pull Request #1 · stanbot8/spatialdata · GitHub
Skip to content
Draft
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
2 changes: 1 addition & 1 deletion .github/workflows/test.yaml
Original file line numberDiff line numberDiff line change
Expand Up@@ -59,7 +59,7 @@ jobs:
PLATFORM: ${{ matrix.os }}
DISPLAY: :42
run: |
uv run pytest --cov --color=yes --cov-report=xml -n auto --dist worksteal
uv run pytest --run-network --cov --color=yes --cov-report=xml -n auto --dist worksteal
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v6
with:
Expand Down
2 changes: 1 addition & 1 deletion .gitmodules
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
[submodule "docs/tutorials/notebooks"]
path = docs/tutorials/notebooks
url = https://github.com/scverse/spatialdata-notebooks
url = https://github.com/scverse/spatialdata-tutorials
6 changes: 3 additions & 3 deletions .pre-commit-config.yaml
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,18 +9,18 @@ ci:
skip: []
repos:
- repo: https://github.com/rbubley/mirrors-prettier
rev: v3.9.4
rev: v3.9.6
hooks:
- id: prettier
exclude: ^.github/workflows/test.yaml
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v2.1.0
rev: v2.3.1
hooks:
- id: mypy
additional_dependencies: [numpy, types-requests]
exclude: tests/|docs/
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.15.20
rev: v0.16.3
hooks:
- id: ruff
args: [--fix, --exit-non-zero-on-fix]
Expand Down
1 change: 1 addition & 0 deletions docs/api/datasets.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,5 +7,6 @@ Convenience small datasets

.. autofunction:: blobs
.. autofunction:: blobs_annotating_element
.. autofunction:: cells
.. autofunction:: raccoon
```
5 changes: 2 additions & 3 deletions docs/contributing.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -13,8 +13,7 @@ In addition to the packages needed to _use_ this package, you need additional py
the documentation_. It's easy to install them using `pip`:

```bash
cd spatialdata-io
pip install -e ".[dev,test,doc]"
pip install -e . --group dev --group test --group docs
```

## Code-style
Expand DownExpand Up@@ -51,7 +50,7 @@ and [prettier][prettier-editors].
## Writing tests

```{note}
Remember to first install the package with `pip install '-e[dev,test]'`
Remember to first install the package with `pip install -e . --group dev --group test`
```

This package uses [pytest][] for automated testing. Please [write tests][scanpy-test-docs] for every function added to the package.
Expand Down
7 changes: 4 additions & 3 deletions docs/index.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,8 +12,8 @@ This page provides documentation on how to install, use, and extend the core `sp

- `spatialdata-io`: load data from common spatial omics technologies into `spatialdata` ([repository][spatialdata-io-repo], [documentation][spatialdata-io-docs]).
- `spatialdata-plot`: Static plotting library for `spatialdata` ([repository][spatialdata-plot-repo], [documentation][spatialdata-plot-docs]).
- `napari-spatialdata-repo`: napari plugin for interactive exploration and annotation of `spatialdata` ([repository][napari-spatialdata-repo], [documentation][napari-spatialdata-docs]).
- `SpatialData` (R): R implementation of the SpatialData framework ([repository][spatialdata-r-repo]).
- `napari-spatialdata`: napari plugin for interactive exploration and annotation of `spatialdata` ([repository][napari-spatialdata-repo], [documentation][napari-spatialdata-docs]).
- `spatialdataR`: R implementation of the SpatialData framework ([repository][spatialdata-r-repo]).
- `SpatialData.js`: JavaScript/TypeScript implementation of the SpatialData framework ([repository][spatialdata-js-repo]).

Please see our publication {cite}`marconatoSpatialDataOpenUniversal2024` for citation and to learn more.
Expand DownExpand Up@@ -110,6 +110,7 @@ tutorials/notebooks/notebooks.md
tutorials/notebooks/datasets/README.md
glossary.md
design_doc.md
interoperability.md
contributing.md
changelog.md
references.md
Expand All@@ -123,5 +124,5 @@ references.md
[napari-spatialdata-docs]: https://spatialdata.scverse.org/projects/napari/en/stable/notebooks/spatialdata.html
[spatialdata-io-docs]: https://spatialdata.scverse.org/projects/io/en/stable/
[spatialdata-plot-docs]: https://spatialdata.scverse.org/projects/plot/en/stable/api.html
[spatialdata-r-repo]: https://github.com/HelenaLC/SpatialData
[spatialdata-r-repo]: https://github.com/HelenaLC/spatialdataR
[spatialdata-js-repo]: https://github.com/Taylor-CCB-Group/SpatialData.js
16 changes: 16 additions & 0 deletions docs/interoperability.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
# Interoperability

The on-disk representation of SpatialData can be read from other languages. Here we list interfaces for working with SpatialData from your language of choice:

## R

- [spatialdataR](https://helenalc.github.io/spatialdataR/) provides an R implementation of the `SpatialData` object, with out-of-memory images and labels, `duckdb`-backed points and shapes, and tables represented as `SingleCellExperiment` objects.

## JavaScript and TypeScript

- [SpatialData.js](https://github.com/Taylor-CCB-Group/SpatialData.js) provides a TypeScript and JavaScript library for interfacing with SpatialData stores.
- [Vitessce](https://vitessce.io/docs/data-file-types/#spatialdatazarr) reads `spatialdata.zarr` stores directly and uses them for interactive visualization.

## File format

The SpatialData on-disk format builds on [OME-NGFF](https://ngff.openmicroscopy.org/latest/). See the [design document](design_doc.md) for details of the current on-disk layout.
37 changes: 37 additions & 0 deletions pyproject.toml
Original file line numberDiff line numberDiff line change
Expand Up@@ -45,6 +45,7 @@ dependencies = [
"spatial_image>=1.2.3",
"scikit-image",
"scipy!=1.17.0",
"scverse-misc[datasets]>=0.1.0",
"typing_extensions>=4.8.0",
"universal_pathlib>=0.2.6",
"xarray>=2024.10.0",
Expand DownExpand Up@@ -108,6 +109,7 @@ addopts = [
# These are all markers coming from xarray, dask or anndata. Added here to silence warnings.
markers = [
"slow: marks tests as slow (deselect with '-m \"not slow\"')",
"network: marks tests that require network access; skipped by default, run with '--run-network'",
"gpu: run test on GPU using CuPY.",
"array_api: used by anndata.tests.helpers, not us",
"skip_with_pyarrow_strings: skipwhen pyarrow string conversion is turned on",
Expand DownExpand Up@@ -244,3 +246,38 @@ 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"
# no "0.12-3": support for pandas>=3 is available only in anndata>=0.13
]

[tool.hatch.envs.test-anndata-pandas.overrides]
matrix.anndata-pandas.extra-dependencies = [
# every option where the if-condition is True gets included

# anndata 0.13
{value="anndata~=0.13", if = ["0.13-2", "0.13-3"]},

# anndata 0.12
{value="anndata>=0.12,<0.13", if = ["0.12-2"]},

# pandas 2
{value="pandas>=2.3,<3", if = ["0.13-2", "0.12-2"]},

# pandas 3
{value="pandas~=3.0", if = ["0.13-3"]},
]
17 changes: 15 additions & 2 deletions src/spatialdata/_core/spatialdata.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -107,7 +107,7 @@ class SpatialData:
- the table are stored as :class:`anndata.AnnData` objects, with the spatial coordinates stored in the obsm
slot.

The table can annotate regions (shapesor labels) and can be used to store additional information.
The table can annotate regions (shapes or labels) and can be used to store additional information.
Points are not regions but 0-dimensional locations. They can't be annotated by a table, but they can store
annotation directly.
"""
Expand DownExpand Up@@ -1114,6 +1114,7 @@ def write(
sdata_formats: SpatialDataFormatType | list[SpatialDataFormatType] | None = None,
shapes_geometry_encoding: Literal["WKB", "geoarrow"] | None = None,
raster_compressor: dict[Literal["lz4", "zstd"], int] | None = None,
convert_table_strings_to_categoricals: bool = False,
) -> None:
"""
Write the `SpatialData` object to a Zarr store.
Expand DownExpand Up@@ -1166,6 +1167,9 @@ def write(
compression level which should be inclusive between 0 and 9. For compression, `lz4` and `zstd` are
supported. If not specified, the compression will be `lz4` with compression level 5. Bytes are automatically
ordered for more efficient compression.
convert_table_strings_to_categoricals
If True, convert string columns of all tables to categoricals before writing.
Note that this will have a side effect of modifying string columns into categoricals in place.
"""
from spatialdata._io._utils import _resolve_zarr_store, _validate_compressor_args
from spatialdata._io.format import _parse_formats
Expand DownExpand Up@@ -1194,6 +1198,7 @@ def write(
parsed_formats=parsed,
shapes_geometry_encoding=shapes_geometry_encoding,
raster_compressor=raster_compressor,
convert_table_strings_to_categoricals=convert_table_strings_to_categoricals,
)

if self.path != file_path and update_sdata_path:
Expand All@@ -1212,6 +1217,7 @@ def _write_element(
parsed_formats: dict[str, SpatialDataFormatType] | None = None,
shapes_geometry_encoding: Literal["WKB", "geoarrow"] | None = None,
raster_compressor: dict[Literal["lz4", "zstd"], int] | None = None,
convert_table_strings_to_categoricals: bool = False,
) -> None:
from spatialdata._io.io_zarr import _get_groups_for_element

Expand DownExpand Up@@ -1279,6 +1285,7 @@ def _write_element(
group=element_type_group,
name=element_name,
element_format=parsed_formats["tables"],
convert_strings_to_categoricals=convert_table_strings_to_categoricals,
)
else:
raise ValueError(f"Unknown element type: {element_type}")
Expand All@@ -1290,6 +1297,7 @@ def write_element(
sdata_formats: SpatialDataFormatType | list[SpatialDataFormatType] | None = None,
shapes_geometry_encoding: Literal["WKB", "geoarrow"] | None = None,
raster_compressor: dict[Literal["lz4", "zstd"], int] | None = None,
convert_table_strings_to_categoricals: bool = False,
) -> None:
"""
Write a single element, or a list of elements, to the Zarr store used for backing.
Expand All@@ -1308,11 +1316,14 @@ def write_element(
shapes_geometry_encoding
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`.
raster_compressor
raster_compressor
A lenght-1 dictionary with as key the type of compression to use for images and labels and as value the
compression level which should be inclusive between 0 and 9. For compression, `lz4` and `zstd` are
supported. If not specified, the compression will be `lz4` with compression level 5. Bytes are automatically
ordered for more efficient compression.
convert_table_strings_to_categoricals
If True, and if element to be written is a table, convert string columns to categoricals before writing.
Note that this will have a side effect of modifying string columns into categoricals in place.

Notes
-----
Expand All@@ -1332,6 +1343,7 @@ def write_element(
sdata_formats=sdata_formats,
shapes_geometry_encoding=shapes_geometry_encoding,
raster_compressor=raster_compressor,
convert_table_strings_to_categoricals=convert_table_strings_to_categoricals,
)
return

Expand DownExpand Up@@ -1368,6 +1380,7 @@ def write_element(
parsed_formats=parsed_formats,
shapes_geometry_encoding=shapes_geometry_encoding,
raster_compressor=raster_compressor,
convert_table_strings_to_categoricals=convert_table_strings_to_categoricals,
)
# After every write, metadata should be consolidated, otherwise this can lead to IO problems like when deleting.
if self.has_consolidated_metadata():
Expand Down
2 changes: 1 addition & 1 deletion src/spatialdata/_core/validation.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -388,7 +388,7 @@ def __exit__(
# Exceptions were collected that we want to raise as a combined validation error.
if self._collector.errors:
raise ValidationError(
title=self._message + "\nTo fix, run `spatialdata.utils.sanitize_table(adata)`.",
title=self._message + "\nTo fix, run `spatialdata.sanitize_table(adata)`.",
errors=self._collector.errors,
)
return True
26 changes: 26 additions & 0 deletions src/spatialdata/_io/exceptions.py
Original file line numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); fix: point table validation to public sanitizer by stanbot8 · Pull Request #1 · stanbot8/spatialdata · GitHub
Skip to content
Draft
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
2 changes: 1 addition & 1 deletion .github/workflows/test.yaml
Original file line numberDiff line numberDiff line change
Expand Up@@ -59,7 +59,7 @@ jobs:
PLATFORM: ${{ matrix.os }}
DISPLAY: :42
run: |
uv run pytest --cov --color=yes --cov-report=xml -n auto --dist worksteal
uv run pytest --run-network --cov --color=yes --cov-report=xml -n auto --dist worksteal
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v6
with:
Expand Down
2 changes: 1 addition & 1 deletion .gitmodules
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
[submodule "docs/tutorials/notebooks"]
path = docs/tutorials/notebooks
url = https://github.com/scverse/spatialdata-notebooks
url = https://github.com/scverse/spatialdata-tutorials
6 changes: 3 additions & 3 deletions .pre-commit-config.yaml
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,18 +9,18 @@ ci:
skip: []
repos:
- repo: https://github.com/rbubley/mirrors-prettier
rev: v3.9.4
rev: v3.9.6
hooks:
- id: prettier
exclude: ^.github/workflows/test.yaml
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v2.1.0
rev: v2.3.1
hooks:
- id: mypy
additional_dependencies: [numpy, types-requests]
exclude: tests/|docs/
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.15.20
rev: v0.16.3
hooks:
- id: ruff
args: [--fix, --exit-non-zero-on-fix]
Expand Down
1 change: 1 addition & 0 deletions docs/api/datasets.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,5 +7,6 @@ Convenience small datasets

.. autofunction:: blobs
.. autofunction:: blobs_annotating_element
.. autofunction:: cells
.. autofunction:: raccoon
```
5 changes: 2 additions & 3 deletions docs/contributing.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -13,8 +13,7 @@ In addition to the packages needed to _use_ this package, you need additional py
the documentation_. It's easy to install them using `pip`:

```bash
cd spatialdata-io
pip install -e ".[dev,test,doc]"
pip install -e . --group dev --group test --group docs
```

## Code-style
Expand DownExpand Up@@ -51,7 +50,7 @@ and [prettier][prettier-editors].
## Writing tests

```{note}
Remember to first install the package with `pip install '-e[dev,test]'`
Remember to first install the package with `pip install -e . --group dev --group test`
```

This package uses [pytest][] for automated testing. Please [write tests][scanpy-test-docs] for every function added to the package.
Expand Down
7 changes: 4 additions & 3 deletions docs/index.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,8 +12,8 @@ This page provides documentation on how to install, use, and extend the core `sp

- `spatialdata-io`: load data from common spatial omics technologies into `spatialdata` ([repository][spatialdata-io-repo], [documentation][spatialdata-io-docs]).
- `spatialdata-plot`: Static plotting library for `spatialdata` ([repository][spatialdata-plot-repo], [documentation][spatialdata-plot-docs]).
- `napari-spatialdata-repo`: napari plugin for interactive exploration and annotation of `spatialdata` ([repository][napari-spatialdata-repo], [documentation][napari-spatialdata-docs]).
- `SpatialData` (R): R implementation of the SpatialData framework ([repository][spatialdata-r-repo]).
- `napari-spatialdata`: napari plugin for interactive exploration and annotation of `spatialdata` ([repository][napari-spatialdata-repo], [documentation][napari-spatialdata-docs]).
- `spatialdataR`: R implementation of the SpatialData framework ([repository][spatialdata-r-repo]).
- `SpatialData.js`: JavaScript/TypeScript implementation of the SpatialData framework ([repository][spatialdata-js-repo]).

Please see our publication {cite}`marconatoSpatialDataOpenUniversal2024` for citation and to learn more.
Expand DownExpand Up@@ -110,6 +110,7 @@ tutorials/notebooks/notebooks.md
tutorials/notebooks/datasets/README.md
glossary.md
design_doc.md
interoperability.md
contributing.md
changelog.md
references.md
Expand All@@ -123,5 +124,5 @@ references.md
[napari-spatialdata-docs]: https://spatialdata.scverse.org/projects/napari/en/stable/notebooks/spatialdata.html
[spatialdata-io-docs]: https://spatialdata.scverse.org/projects/io/en/stable/
[spatialdata-plot-docs]: https://spatialdata.scverse.org/projects/plot/en/stable/api.html
[spatialdata-r-repo]: https://github.com/HelenaLC/SpatialData
[spatialdata-r-repo]: https://github.com/HelenaLC/spatialdataR
[spatialdata-js-repo]: https://github.com/Taylor-CCB-Group/SpatialData.js
16 changes: 16 additions & 0 deletions docs/interoperability.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
# Interoperability

The on-disk representation of SpatialData can be read from other languages. Here we list interfaces for working with SpatialData from your language of choice:

## R

- [spatialdataR](https://helenalc.github.io/spatialdataR/) provides an R implementation of the `SpatialData` object, with out-of-memory images and labels, `duckdb`-backed points and shapes, and tables represented as `SingleCellExperiment` objects.

## JavaScript and TypeScript

- [SpatialData.js](https://github.com/Taylor-CCB-Group/SpatialData.js) provides a TypeScript and JavaScript library for interfacing with SpatialData stores.
- [Vitessce](https://vitessce.io/docs/data-file-types/#spatialdatazarr) reads `spatialdata.zarr` stores directly and uses them for interactive visualization.

## File format

The SpatialData on-disk format builds on [OME-NGFF](https://ngff.openmicroscopy.org/latest/). See the [design document](design_doc.md) for details of the current on-disk layout.
37 changes: 37 additions & 0 deletions pyproject.toml
Original file line numberDiff line numberDiff line change
Expand Up@@ -45,6 +45,7 @@ dependencies = [
"spatial_image>=1.2.3",
"scikit-image",
"scipy!=1.17.0",
"scverse-misc[datasets]>=0.1.0",
"typing_extensions>=4.8.0",
"universal_pathlib>=0.2.6",
"xarray>=2024.10.0",
Expand DownExpand Up@@ -108,6 +109,7 @@ addopts = [
# These are all markers coming from xarray, dask or anndata. Added here to silence warnings.
markers = [
"slow: marks tests as slow (deselect with '-m \"not slow\"')",
"network: marks tests that require network access; skipped by default, run with '--run-network'",
"gpu: run test on GPU using CuPY.",
"array_api: used by anndata.tests.helpers, not us",
"skip_with_pyarrow_strings: skipwhen pyarrow string conversion is turned on",
Expand DownExpand Up@@ -244,3 +246,38 @@ 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"
# no "0.12-3": support for pandas>=3 is available only in anndata>=0.13
]

[tool.hatch.envs.test-anndata-pandas.overrides]
matrix.anndata-pandas.extra-dependencies = [
# every option where the if-condition is True gets included

# anndata 0.13
{value="anndata~=0.13", if = ["0.13-2", "0.13-3"]},

# anndata 0.12
{value="anndata>=0.12,<0.13", if = ["0.12-2"]},

# pandas 2
{value="pandas>=2.3,<3", if = ["0.13-2", "0.12-2"]},

# pandas 3
{value="pandas~=3.0", if = ["0.13-3"]},
]
17 changes: 15 additions & 2 deletions src/spatialdata/_core/spatialdata.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -107,7 +107,7 @@ class SpatialData:
- the table are stored as :class:`anndata.AnnData` objects, with the spatial coordinates stored in the obsm
slot.

The table can annotate regions (shapesor labels) and can be used to store additional information.
The table can annotate regions (shapes or labels) and can be used to store additional information.
Points are not regions but 0-dimensional locations. They can't be annotated by a table, but they can store
annotation directly.
"""
Expand DownExpand Up@@ -1114,6 +1114,7 @@ def write(
sdata_formats: SpatialDataFormatType | list[SpatialDataFormatType] | None = None,
shapes_geometry_encoding: Literal["WKB", "geoarrow"] | None = None,
raster_compressor: dict[Literal["lz4", "zstd"], int] | None = None,
convert_table_strings_to_categoricals: bool = False,
) -> None:
"""
Write the `SpatialData` object to a Zarr store.
Expand DownExpand Up@@ -1166,6 +1167,9 @@ def write(
compression level which should be inclusive between 0 and 9. For compression, `lz4` and `zstd` are
supported. If not specified, the compression will be `lz4` with compression level 5. Bytes are automatically
ordered for more efficient compression.
convert_table_strings_to_categoricals
If True, convert string columns of all tables to categoricals before writing.
Note that this will have a side effect of modifying string columns into categoricals in place.
"""
from spatialdata._io._utils import _resolve_zarr_store, _validate_compressor_args
from spatialdata._io.format import _parse_formats
Expand DownExpand Up@@ -1194,6 +1198,7 @@ def write(
parsed_formats=parsed,
shapes_geometry_encoding=shapes_geometry_encoding,
raster_compressor=raster_compressor,
convert_table_strings_to_categoricals=convert_table_strings_to_categoricals,
)

if self.path != file_path and update_sdata_path:
Expand All@@ -1212,6 +1217,7 @@ def _write_element(
parsed_formats: dict[str, SpatialDataFormatType] | None = None,
shapes_geometry_encoding: Literal["WKB", "geoarrow"] | None = None,
raster_compressor: dict[Literal["lz4", "zstd"], int] | None = None,
convert_table_strings_to_categoricals: bool = False,
) -> None:
from spatialdata._io.io_zarr import _get_groups_for_element

Expand DownExpand Up@@ -1279,6 +1285,7 @@ def _write_element(
group=element_type_group,
name=element_name,
element_format=parsed_formats["tables"],
convert_strings_to_categoricals=convert_table_strings_to_categoricals,
)
else:
raise ValueError(f"Unknown element type: {element_type}")
Expand All@@ -1290,6 +1297,7 @@ def write_element(
sdata_formats: SpatialDataFormatType | list[SpatialDataFormatType] | None = None,
shapes_geometry_encoding: Literal["WKB", "geoarrow"] | None = None,
raster_compressor: dict[Literal["lz4", "zstd"], int] | None = None,
convert_table_strings_to_categoricals: bool = False,
) -> None:
"""
Write a single element, or a list of elements, to the Zarr store used for backing.
Expand All@@ -1308,11 +1316,14 @@ def write_element(
shapes_geometry_encoding
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`.
raster_compressor
raster_compressor
A lenght-1 dictionary with as key the type of compression to use for images and labels and as value the
compression level which should be inclusive between 0 and 9. For compression, `lz4` and `zstd` are
supported. If not specified, the compression will be `lz4` with compression level 5. Bytes are automatically
ordered for more efficient compression.
convert_table_strings_to_categoricals
If True, and if element to be written is a table, convert string columns to categoricals before writing.
Note that this will have a side effect of modifying string columns into categoricals in place.

Notes
-----
Expand All@@ -1332,6 +1343,7 @@ def write_element(
sdata_formats=sdata_formats,
shapes_geometry_encoding=shapes_geometry_encoding,
raster_compressor=raster_compressor,
convert_table_strings_to_categoricals=convert_table_strings_to_categoricals,
)
return

Expand DownExpand Up@@ -1368,6 +1380,7 @@ def write_element(
parsed_formats=parsed_formats,
shapes_geometry_encoding=shapes_geometry_encoding,
raster_compressor=raster_compressor,
convert_table_strings_to_categoricals=convert_table_strings_to_categoricals,
)
# After every write, metadata should be consolidated, otherwise this can lead to IO problems like when deleting.
if self.has_consolidated_metadata():
Expand Down
2 changes: 1 addition & 1 deletion src/spatialdata/_core/validation.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -388,7 +388,7 @@ def __exit__(
# Exceptions were collected that we want to raise as a combined validation error.
if self._collector.errors:
raise ValidationError(
title=self._message + "\nTo fix, run `spatialdata.utils.sanitize_table(adata)`.",
title=self._message + "\nTo fix, run `spatialdata.sanitize_table(adata)`.",
errors=self._collector.errors,
)
return True
26 changes: 26 additions & 0 deletions src/spatialdata/_io/exceptions.py
Original file line numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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
Loading
Loading