diff --git a/.bumpversion.cfg b/.bumpversion.cfg
deleted file mode 100644
index fdba9e4f..00000000
--- a/.bumpversion.cfg
+++ /dev/null
@@ -1,8 +0,0 @@
-[bumpversion]
-current_version = 0.0.1
-tag = True
-commit = True
-
-[bumpversion:file:./pyproject.toml]
-search = version = "{current_version}"
-replace = version = "{new_version}"
diff --git a/.codecov.yaml b/.codecov.yaml
index 829e56c0..d0c0e291 100644
--- a/.codecov.yaml
+++ b/.codecov.yaml
@@ -1,17 +1,17 @@
# Based on pydata/xarray
codecov:
- require_ci_to_pass: no
+ require_ci_to_pass: no
coverage:
- status:
- project:
- default:
- # Require 1% coverage, i.e., always succeed
- target: 1
- patch: false
- changes: false
+ status:
+ project:
+ default:
+ # Require 1% coverage, i.e., always succeed
+ target: 1
+ patch: false
+ changes: false
comment:
- layout: diff, flags, files
- behavior: once
- require_base: no
+ layout: diff, flags, files
+ behavior: once
+ require_base: no
diff --git a/.cruft.json b/.cruft.json
new file mode 100644
index 00000000..3e39041c
--- /dev/null
+++ b/.cruft.json
@@ -0,0 +1,44 @@
+{
+ "template": "https://github.com/scverse/cookiecutter-scverse",
+ "commit": "707f3faab836f9272912f92188b63b31da70c064",
+ "checkout": null,
+ "context": {
+ "cookiecutter": {
+ "project_name": "spatialdata-io",
+ "package_name": "spatialdata_io",
+ "project_description": "SpatialData IO for common techs",
+ "author_full_name": "scverse",
+ "author_email": "scverse@scverse.scverse",
+ "github_user": "scverse",
+ "github_repo": "spatialdata-io",
+ "license": "BSD 3-Clause License",
+ "ide_integration": false,
+ "issue_categorization": "labels",
+ "_copy_without_render": [
+ ".github/workflows/build.yaml",
+ ".github/workflows/test.yaml",
+ "docs/_templates/autosummary/**.rst"
+ ],
+ "_exclude_on_template_update": [
+ "CHANGELOG.md",
+ "LICENSE",
+ "README.md",
+ "docs/api.md",
+ "docs/index.md",
+ "docs/notebooks/example.ipynb",
+ "docs/references.bib",
+ "docs/references.md",
+ "src/**",
+ "tests/**"
+ ],
+ "_render_devdocs": false,
+ "_jinja2_env_vars": {
+ "lstrip_blocks": true,
+ "trim_blocks": true
+ },
+ "_template": "https://github.com/scverse/cookiecutter-scverse",
+ "_commit": "707f3faab836f9272912f92188b63b31da70c064"
+ }
+ },
+ "directory": null
+}
diff --git a/.editorconfig b/.editorconfig
index 2fe0ce08..66678e37 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -8,5 +8,8 @@ charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
+[{*.{yml,yaml,toml},.cruft.json}]
+indent_size = 2
+
[Makefile]
indent_style = tab
diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml
new file mode 100644
index 00000000..cc32eda8
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.yml
@@ -0,0 +1,93 @@
+name: Bug report
+description: Report something that is broken or incorrect
+labels: bug
+body:
+ - type: markdown
+ attributes:
+ value: |
+ **Note**: Please read [this guide](https://matthewrocklin.com/blog/work/2018/02/28/minimal-bug-reports)
+ detailing how to provide the necessary information for us to reproduce your bug. In brief:
+ * Please provide exact steps how to reproduce the bug in a clean Python environment.
+ * In case it's not clear what's causing this bug, please provide the data or the data generation procedure.
+ * Replicate problems on public datasets or share data subsets when full sharing isn't possible.
+
+ - type: textarea
+ id: report
+ attributes:
+ label: Report
+ description: A clear and concise description of what the bug is.
+ validations:
+ required: true
+
+ - type: textarea
+ id: versions
+ attributes:
+ label: Versions
+ description: |
+ Which version of packages.
+
+ Please install `session-info2`, run the following command in a notebook,
+ click the “Copy as Markdown” button, then paste the results into the text box below.
+
+ ```python
+ In[1]: import session_info2; session_info2.session_info(dependencies=True)
+ ```
+
+ Alternatively, run this in a console:
+
+ ```python
+ >>> import session_info2; print(session_info2.session_info(dependencies=True)._repr_mimebundle_()["text/markdown"])
+ ```
+ render: python
+ placeholder: |
+ anndata 0.11.3
+ ---- ----
+ charset-normalizer 3.4.1
+ coverage 7.7.0
+ psutil 7.0.0
+ dask 2024.7.1
+ jaraco.context 5.3.0
+ numcodecs 0.15.1
+ jaraco.functools 4.0.1
+ Jinja2 3.1.6
+ sphinxcontrib-jsmath 1.0.1
+ sphinxcontrib-htmlhelp 2.1.0
+ toolz 1.0.0
+ session-info2 0.1.2
+ PyYAML 6.0.2
+ llvmlite 0.44.0
+ scipy 1.15.2
+ pandas 2.2.3
+ sphinxcontrib-devhelp 2.0.0
+ h5py 3.13.0
+ tblib 3.0.0
+ setuptools-scm 8.2.0
+ more-itertools 10.3.0
+ msgpack 1.1.0
+ sparse 0.15.5
+ wrapt 1.17.2
+ jaraco.collections 5.1.0
+ numba 0.61.0
+ pyarrow 19.0.1
+ pytz 2025.1
+ MarkupSafe 3.0.2
+ crc32c 2.7.1
+ sphinxcontrib-qthelp 2.0.0
+ sphinxcontrib-serializinghtml 2.0.0
+ zarr 2.18.4
+ asciitree 0.3.3
+ six 1.17.0
+ sphinxcontrib-applehelp 2.0.0
+ numpy 2.1.3
+ cloudpickle 3.1.1
+ sphinxcontrib-bibtex 2.6.3
+ natsort 8.4.0
+ jaraco.text 3.12.1
+ setuptools 76.1.0
+ Deprecated 1.2.18
+ packaging 24.2
+ python-dateutil 2.9.0.post0
+ ---- ----
+ Python 3.13.2 | packaged by conda-forge | (main, Feb 17 2025, 14:10:22) [GCC 13.3.0]
+ OS Linux-6.11.0-109019-tuxedo-x86_64-with-glibc2.39
+ Updated 2025-03-18 15:47
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
new file mode 100644
index 00000000..5b62547f
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -0,0 +1,5 @@
+blank_issues_enabled: false
+contact_links:
+ - name: Scverse Community Forum
+ url: https://discourse.scverse.org/
+ about: If you have questions about “How to do X”, please ask them here.
diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml
new file mode 100644
index 00000000..da9231c7
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.yml
@@ -0,0 +1,11 @@
+name: Feature request
+description: Propose a new feature for spatialdata-io
+labels: enhancement
+body:
+ - type: textarea
+ id: description
+ attributes:
+ label: Description of feature
+ description: Please describe your suggestion for a new feature. It might help to describe a problem or use case, plus any alternatives that you have considered.
+ validations:
+ required: true
diff --git a/.github/codecov.yml b/.github/codecov.yml
deleted file mode 100644
index 872442c7..00000000
--- a/.github/codecov.yml
+++ /dev/null
@@ -1,17 +0,0 @@
-# Based on pydata/xarray
-codecov:
- require_ci_to_pass: false
-
-coverage:
- status:
- project:
- default:
- # Require 1% coverage, i.e., always succeed
- target: 1
- patch: false
- changes: false
-
-comment:
- layout: "diff, flags, files"
- behavior: once
- require_base: false
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 00000000..32279304
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,21 @@
+version: 2
+updates:
+ - package-ecosystem: pre-commit
+ directory: /
+ schedule:
+ interval: weekly
+ cooldown:
+ default-days: 7
+ groups:
+ pre-commit:
+ patterns: ["*"]
+ - package-ecosystem: github-actions
+ directory: /
+ schedule:
+ interval: weekly
+ cooldown:
+ default-days: 7
+ groups:
+ actions-deps:
+ patterns:
+ - "*"
diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml
new file mode 100644
index 00000000..6cca7d4d
--- /dev/null
+++ b/.github/workflows/build.yaml
@@ -0,0 +1,30 @@
+name: Check Build
+
+on:
+ push:
+ branches: [main]
+ pull_request:
+ branches: [main]
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
+permissions:
+ contents: read
+
+jobs:
+ package:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
+ with:
+ filter: blob:none
+ fetch-depth: 0
+ persist-credentials: false
+ - name: Install uv
+ uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
+ - name: Build package
+ run: uv build
+ - name: Check package
+ run: uvx twine check --strict dist/*.whl
diff --git a/.github/workflows/prepare_test_data.yaml b/.github/workflows/prepare_test_data.yaml
index 34d15cca..14475c66 100644
--- a/.github/workflows/prepare_test_data.yaml
+++ b/.github/workflows/prepare_test_data.yaml
@@ -1,83 +1,89 @@
name: Prepare test data
on:
- schedule:
- - cron: "0 0 1 * *" # run once a month to prevent artifact expiration
- workflow_dispatch:
- # Uncomment and adjust the branch name if you need to add new datasets to the artifact.
- # It needs to be a branch in the spatialdata-io origin repository, not from a fork.
-# push:
-# branches:
-# - main
+ schedule:
+ - cron: "0 0 1 * *" # run once a month to prevent artifact expiration
+ workflow_dispatch:
+ # Uncomment and adjust the branch name if you need to add new datasets to the artifact.
+ # It needs to be a branch in the spatialdata-io origin repository, not from a fork.
+# push:
+# branches:
+# - main
+
+permissions:
+ contents: read
jobs:
- prepare-data:
- runs-on: ubuntu-latest
-
- steps:
- - uses: actions/checkout@v4
-
- - name: Download test datasets
- run: |
- mkdir -p ./data
- cd ./data
-
- # -------
- # the Xenium datasets are licensed as CC BY 4.0, as shown here
- # https://www.10xgenomics.com/support/software/xenium-onboard-analysis/latest/resources/xenium-example-data
-
- # 10x Genomics Xenium 2.0.0
- curl -O https://cf.10xgenomics.com/samples/xenium/2.0.0/Xenium_V1_human_Breast_2fov/Xenium_V1_human_Breast_2fov_outs.zip
- curl -O https://cf.10xgenomics.com/samples/xenium/2.0.0/Xenium_V1_human_Lung_2fov/Xenium_V1_human_Lung_2fov_outs.zip
-
- # 10x Genomics Xenium 3.0.0 (5K) Mouse ileum, multimodal cell segmentation
- # this file seems to be corrupted; skipping it for now
- # curl -O https://cf.10xgenomics.com/samples/xenium/3.0.0/Xenium_Prime_MultiCellSeg_Mouse_Ileum_tiny/Xenium_Prime_MultiCellSeg_Mouse_Ileum_tiny.zip
-
- # 10x Genomics Xenium 3.0.0 (5K) Mouse ileum, nuclear expansion
- curl -O https://cf.10xgenomics.com/samples/xenium/3.0.0/Xenium_Prime_Mouse_Ileum_tiny/Xenium_Prime_Mouse_Ileum_tiny_outs.zip
-
- # 10x Genomics Xenium 4.0.0 (v1) Human ovary, nuclear expansion
- curl -O https://cf.10xgenomics.com/samples/xenium/4.0.0/Xenium_V1_Human_Ovary_tiny/Xenium_V1_Human_Ovary_tiny_outs.zip
-
- # 10x Genomics Xenium 4.0.0 (v1) Human ovary, multimodal cell segmentation
- curl -O https://cf.10xgenomics.com/samples/xenium/4.0.0/Xenium_V1_MultiCellSeg_Human_Ovary_tiny/Xenium_V1_MultiCellSeg_Human_Ovary_tiny_outs.zip
-
- # 10x Genomics Xenium 4.0.0 (v1+Protein) Human kidney, multimodal cell segmentation
- curl -O https://cf.10xgenomics.com/samples/xenium/4.0.0/Xenium_V1_Protein_Human_Kidney_tiny/Xenium_V1_Protein_Human_Kidney_tiny_outs.zip
-
- # -------
- # the Visium HD dataset is licensed as CC BY 4.0, as shown here
- # https://www.10xgenomics.com/support/software/space-ranger/latest/resources/visium-hd-example-data
-
- # 10x Genomics Visium HD 4.0.1 3' Mouse Brain Chunk
- curl -O https://cf.10xgenomics.com/samples/spatial-exp/4.0.1/Visium_HD_Tiny_3prime_Dataset/Visium_HD_Tiny_3prime_Dataset_outs.zip
-
- # -------
- # we received written permission to make the following dataset public and integrate it in the CI system of spatialdata-io
- # Spatial Genomics seqFISH v2
- curl -O https://s3.embl.de/spatialdata/raw_data/seqfish-2-test-dataset.zip
-
- # -------
- # MACSima OMAP datasets are licensed as CC BY 4.0
- # OMAP23 for format v1.x.x
- curl -o OMAP23_small.zip "https://zenodo.org/api/records/18196452/files-archive"
-
- # OMAP10 for format v0.x.x
- curl -o OMAP10_small.zip "https://zenodo.org/api/records/18196366/files-archive"
-
- - name: Unzip files
- run: |
- cd ./data
- for file in *.zip; do
- dir="${file%.zip}"
- mkdir -p "$dir"
- unzip "$file" -d "$dir"
- rm "$file"
- done
-
- - name: Upload artifacts
- uses: actions/upload-artifact@v4
- with:
- name: data
- path: ./data
+ prepare-data:
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
+ with:
+ filter: blob:none
+ persist-credentials: false
+
+ - name: Download test datasets
+ run: |
+ mkdir -p ./data
+ cd ./data
+
+ # -------
+ # the Xenium datasets are licensed as CC BY 4.0, as shown here
+ # https://www.10xgenomics.com/support/software/xenium-onboard-analysis/latest/resources/xenium-example-data
+
+ # 10x Genomics Xenium 2.0.0
+ curl -O https://cf.10xgenomics.com/samples/xenium/2.0.0/Xenium_V1_human_Breast_2fov/Xenium_V1_human_Breast_2fov_outs.zip
+ curl -O https://cf.10xgenomics.com/samples/xenium/2.0.0/Xenium_V1_human_Lung_2fov/Xenium_V1_human_Lung_2fov_outs.zip
+
+ # 10x Genomics Xenium 3.0.0 (5K) Mouse ileum, multimodal cell segmentation
+ # this file seems to be corrupted; skipping it for now
+ # curl -O https://cf.10xgenomics.com/samples/xenium/3.0.0/Xenium_Prime_MultiCellSeg_Mouse_Ileum_tiny/Xenium_Prime_MultiCellSeg_Mouse_Ileum_tiny.zip
+
+ # 10x Genomics Xenium 3.0.0 (5K) Mouse ileum, nuclear expansion
+ curl -O https://cf.10xgenomics.com/samples/xenium/3.0.0/Xenium_Prime_Mouse_Ileum_tiny/Xenium_Prime_Mouse_Ileum_tiny_outs.zip
+
+ # 10x Genomics Xenium 4.0.0 (v1) Human ovary, nuclear expansion
+ curl -O https://cf.10xgenomics.com/samples/xenium/4.0.0/Xenium_V1_Human_Ovary_tiny/Xenium_V1_Human_Ovary_tiny_outs.zip
+
+ # 10x Genomics Xenium 4.0.0 (v1) Human ovary, multimodal cell segmentation
+ curl -O https://cf.10xgenomics.com/samples/xenium/4.0.0/Xenium_V1_MultiCellSeg_Human_Ovary_tiny/Xenium_V1_MultiCellSeg_Human_Ovary_tiny_outs.zip
+
+ # 10x Genomics Xenium 4.0.0 (v1+Protein) Human kidney, multimodal cell segmentation
+ curl -O https://cf.10xgenomics.com/samples/xenium/4.0.0/Xenium_V1_Protein_Human_Kidney_tiny/Xenium_V1_Protein_Human_Kidney_tiny_outs.zip
+
+ # -------
+ # the Visium HD dataset is licensed as CC BY 4.0, as shown here
+ # https://www.10xgenomics.com/support/software/space-ranger/latest/resources/visium-hd-example-data
+
+ # 10x Genomics Visium HD 4.0.1 3' Mouse Brain Chunk
+ curl -O https://cf.10xgenomics.com/samples/spatial-exp/4.0.1/Visium_HD_Tiny_3prime_Dataset/Visium_HD_Tiny_3prime_Dataset_outs.zip
+
+ # -------
+ # we received written permission to make the following dataset public and integrate it in the CI system of spatialdata-io
+ # Spatial Genomics seqFISH v2
+ curl -O https://s3.embl.de/spatialdata/raw_data/seqfish-2-test-dataset.zip
+
+ # -------
+ # MACSima OMAP datasets are licensed as CC BY 4.0
+ # OMAP23 for format v1.x.x
+ curl -o OMAP23_small.zip "https://zenodo.org/api/records/18196452/files-archive"
+
+ # OMAP10 for format v0.x.x
+ curl -o OMAP10_small.zip "https://zenodo.org/api/records/18196366/files-archive"
+
+ - name: Unzip files
+ run: |
+ cd ./data
+ for file in *.zip; do
+ dir="${file%.zip}"
+ mkdir -p "$dir"
+ unzip "$file" -d "$dir"
+ rm "$file"
+ done
+
+ - name: Upload artifacts
+ uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
+ with:
+ name: data
+ path: ./data
diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml
index 17cea589..ae1441d4 100644
--- a/.github/workflows/release.yaml
+++ b/.github/workflows/release.yaml
@@ -1,31 +1,33 @@
name: Release
on:
- release:
- types: [published]
+ release:
+ types: [published]
+
+# Use "trusted publishing", see https://docs.pypi.org/trusted-publishers/
+permissions: {}
jobs:
- package_and_release:
- runs-on: ubuntu-latest
- if: startsWith(github.ref, 'refs/tags/v')
- steps:
- - uses: actions/checkout@v4
- - name: Set up Python 3.12
- uses: actions/setup-python@v5
- with:
- python-version: "3.13"
- cache: pip
- - name: Install build dependencies
- run: python -m pip install --upgrade pip wheel twine build
- - name: Build package
- run: python -m build
- - name: Check package
- run: twine check --strict dist/*.whl
- - name: Install hatch
- run: pip install hatch
- - name: Build project for distribution
- run: hatch build
- - name: Publish a Python distribution to PyPI
- uses: pypa/gh-action-pypi-publish@release/v1
- with:
- password: ${{ secrets.PYPI_API_TOKEN }}
+ release:
+ name: Upload release to PyPI
+ runs-on: ubuntu-latest
+ environment:
+ name: pypi
+ url: https://pypi.org/p/spatialdata_io
+ permissions:
+ contents: read
+ id-token: write # IMPORTANT: this permission is mandatory for trusted publishing
+ steps:
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
+ with:
+ filter: blob:none
+ fetch-depth: 0
+ persist-credentials: false
+ - name: Install uv
+ uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
+ with:
+ enable-cache: false
+ - name: Build package
+ run: uv build
+ - name: Publish package distributions to PyPI
+ uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0
diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml
index 4ee1a57d..236e253d 100644
--- a/.github/workflows/test.yaml
+++ b/.github/workflows/test.yaml
@@ -1,75 +1,123 @@
name: Test
on:
- push:
- branches: [main]
- tags:
- - "v*" # Push events to matching v*, i.e. v1.0, v20.15.10, v1.0-rc.1, v2.1-alpha, v3.0-beta.3
- pull_request:
- branches: [main]
+ push:
+ branches: [main]
+ pull_request:
+ branches: [main]
+ schedule:
+ - cron: "0 5 1,15 * *"
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
+permissions:
+ contents: read
jobs:
- test:
- runs-on: ${{ matrix.os }}
- defaults:
- run:
- shell: bash -e {0} # -e to fail on error
+ # Get the test environment from hatch as defined in pyproject.toml.
+ # This ensures that the pyproject.toml is the single point of truth for test definitions and the same tests are
+ # run locally and on continuous integration.
+ # Check [[tool.hatch.envs.hatch-test.matrix]] in pyproject.toml and https://hatch.pypa.io/latest/environment/ for
+ # more details.
+ get-environments:
+ runs-on: ubuntu-slim
+ outputs:
+ envs: ${{ steps.get-envs.outputs.envs }}
+ steps:
+ - &clone
+ uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
+ with:
+ filter: blob:none
+ fetch-depth: 0
+ persist-credentials: false
+ - &setup-uv
+ uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
+ - name: Get test environments
+ id: get-envs
+ run: |
+ ENVS_JSON=$(uvx hatch env show --json | jq -c 'to_entries
+ | map(
+ select(.key | startswith("hatch-test"))
+ | {
+ name: .key,
+ label: (if (.key | contains("pre")) then .key + " (PRE-RELEASE DEPENDENCIES)" else .key end),
+ python: .value.python
+ }
+ )')
+ echo "envs=${ENVS_JSON}" | tee $GITHUB_OUTPUT
- strategy:
- fail-fast: false
- matrix:
- python: ["3.11", "3.12", "3.13"]
- os: [ubuntu-latest]
+ # Run tests through hatch. Spawns a separate runner for each environment defined in the hatch matrix obtained above.
+ test:
+ needs: get-environments
+ permissions:
+ id-token: write # for codecov OIDC
+ contents: read
+ actions: read # to download the test data artifact
- env:
- OS: ${{ matrix.os }}
- PYTHON: ${{ matrix.python }}
+ strategy:
+ fail-fast: false
+ matrix:
+ os: [ubuntu-latest]
+ env: ${{ fromJSON(needs.get-environments.outputs.envs) }}
- steps:
- - uses: actions/checkout@v4
- - name: Set up Python ${{ matrix.python }}
- uses: actions/setup-python@v5
- with:
- python-version: ${{ matrix.python }}
+ name: ${{ matrix.env.label }}
+ runs-on: ${{ matrix.os }}
+ continue-on-error: ${{ contains(matrix.env.name, 'pre') }} # make "all-green" pass even if pre-release job fails
+ env:
+ UV_PYTHON: ${{ matrix.env.python }}
- - name: Get pip cache dir
- id: pip-cache-dir
- run: |
- echo "::set-output name=dir::$(pip cache dir)"
- - name: Restore pip cache
- uses: actions/cache@v4
- with:
- path: ${{ steps.pip-cache-dir.outputs.dir }}
- key: pip-${{ runner.os }}-${{ env.pythonLocation }}-${{ hashFiles('**/pyproject.toml') }}
- restore-keys: |
- pip-${{ runner.os }}-${{ env.pythonLocation }}-
- - name: Install test dependencies
- run: |
- python -m pip install --upgrade pip wheel
- pip install pytest-cov
- - name: Install dependencies
- run: |
- pip install --pre -e ".[dev,test]"
+ steps:
+ - *clone
+ - *setup-uv
+ - name: Download artifact of test data
+ uses: dawidd6/action-download-artifact@07ab29fd4a977ae4d2b275087cf67563dfdf0295 # v9
+ with:
+ workflow: prepare_test_data.yaml
+ name: data
+ path: ./data
+ - name: create hatch environment
+ run: uvx hatch env create ${{ matrix.env.name }}
+ - name: list all all installed package versions
+ run: uvx hatch run ${{ matrix.env.name }}:uv pip list
+ - name: run tests using hatch
+ env:
+ MPLBACKEND: agg
+ PLATFORM: ${{ matrix.os }}
+ DISPLAY: :42
+ run: uvx hatch run ${{ matrix.env.name }}:run-cov -v --color=yes -n auto
+ - name: generate coverage report
+ run: |
+ # See https://coverage.readthedocs.io/page/config.html#run-patch
+ test -f .coverage || uvx hatch run ${{ matrix.env.name }}:cov-combine
+ uvx hatch run ${{ matrix.env.name }}:cov-report # report visibly
+ uvx hatch run ${{ matrix.env.name }}:coverage xml # create report for upload
+ - name: Upload coverage
+ uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
+ with:
+ fail_ci_if_error: true
+ use_oidc: true
- - name: Download artifact of test data
- if: matrix.python == '3.13'
- uses: dawidd6/action-download-artifact@v9
- with:
- workflow: prepare_test_data.yaml
- name: data
- path: ./data
+ lint:
+ name: Pre-commit checks
+ runs-on: ubuntu-latest
+ steps:
+ - *clone
+ - *setup-uv
+ - run: uvx prek run --all-files --show-diff-on-failure --color=always
- - name: Test
- env:
- MPLBACKEND: agg
- PLATFORM: ${{ matrix.os }}
- DISPLAY: :42
- run: |
- pytest -v --cov --color=yes --cov-report=xml
- - name: Upload coverage to Codecov
- uses: codecov/codecov-action@v4
- with:
- name: coverage
- verbose: true
- env:
- CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
+ # Check that all tests defined above pass. This makes it easy to set a single "required" test in branch
+ # protection instead of having to update it frequently. See https://github.com/re-actors/alls-green#why.
+ check:
+ name: Tests pass in all hatch environments
+ if: always()
+ needs:
+ - get-environments
+ - test
+ - lint
+ runs-on: ubuntu-latest
+ steps:
+ - uses: re-actors/alls-green@05ac9388f0aebcb5727afa17fcccfecd6f8ec5fe # v1.2.2
+ with:
+ jobs: ${{ toJSON(needs) }}
diff --git a/.gitignore b/.gitignore
index 20ab3635..3da189e4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,51 +1,36 @@
# Temp files
.DS_Store
*~
-*.tmp
-temp/
+buck-out/
+
+# IDEs
+/.idea/
+/.vscode/
+/.spyproject/
+*.code-workspace
# Compiled files
+.venv/
__pycache__/
+.*cache/
.ipynb_checkpoints/
# Distribution / packaging
-/build/
/dist/
-/*.egg-info/
+_version.py
+uv.lock
# Tests and coverage
-/.pytest_cache/
-/.cache/
/data/
+/node_modules/
+/.coverage*
+/tests/data/
# docs
/docs/generated/
/docs/_build/
-# IDEs
-/.idea/
-/.vscode/
-/.spyproject/
-
-# docs
-/docs/generated/
-/docs/_build/
-
-# other
-_version.py
-node_modules/
-.code-workspace
-.asv/
-uv.lock
-
-# memray report
+# benchmarks
+/.asv/
*.bin
-
-# speedscope report
profile.speedscope.json
-
-# test datasets (e.g. Xenium ones)
-# data folder
-data/
-tests/data
-.venv
diff --git a/.mypy.ini b/.mypy.ini
deleted file mode 100644
index 1e3bcb54..00000000
--- a/.mypy.ini
+++ /dev/null
@@ -1,27 +0,0 @@
-[mypy]
-python_version = 3.10
-
-ignore_errors = False
-warn_redundant_casts = True
-warn_unused_configs = True
-warn_unused_ignores = True
-
-disallow_untyped_calls = False
-disallow_untyped_defs = True
-disallow_incomplete_defs = True
-disallow_any_generics = True
-
-strict_optional = True
-strict_equality = True
-warn_return_any = True
-warn_unreachable = False
-check_untyped_defs = True
-; because of docrep
-allow_untyped_decorators = True
-no_implicit_optional = True
-no_implicit_reexport = True
-no_warn_no_return = True
-
-show_error_codes = True
-show_column_numbers = True
-error_summary = True
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index c2681a9f..c6d9c514 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,43 +1,54 @@
fail_fast: false
default_language_version:
- python: python3
+ python: python3
default_stages:
- - pre-commit
- - pre-push
+ - pre-commit
+ - pre-push
minimum_pre_commit_version: 2.16.0
repos:
- - repo: https://github.com/rbubley/mirrors-prettier
- rev: v3.8.3
- hooks:
- - id: prettier
- - repo: https://github.com/astral-sh/ruff-pre-commit
- rev: v0.15.12
- hooks:
- - id: ruff
- args: [--fix, --exit-non-zero-on-fix, --unsafe-fixes]
- - id: ruff-format
- - repo: https://github.com/pre-commit/mirrors-mypy
- rev: v1.20.2
- hooks:
- - id: mypy
- additional_dependencies: [numpy, types-PyYAML]
- exclude: docs/
- - repo: https://github.com/pre-commit/pre-commit-hooks
- rev: v6.0.0
- hooks:
- - id: detect-private-key
- - id: check-ast
- - id: end-of-file-fixer
- - id: mixed-line-ending
- args: [--fix=lf]
- - id: trailing-whitespace
- - id: check-case-conflict
- - repo: local
- hooks:
- - id: forbid-to-commit
- name: Don't commit rej files
- entry: |
- Cannot commit .rej files. These indicate merge conflicts that arise during automated template updates.
- Fix the merge conflicts manually and remove the .rej files.
- language: fail
- files: '.*\.rej$'
+ - repo: https://github.com/biomejs/pre-commit
+ rev: v2.5.5
+ hooks:
+ - id: biome-format
+ exclude: ^\.cruft\.json$ # inconsistent indentation with cruft - file never to be modified manually.
+ - repo: https://github.com/tox-dev/pyproject-fmt
+ rev: v2.26.0
+ hooks:
+ - id: pyproject-fmt
+ - repo: https://github.com/astral-sh/ruff-pre-commit
+ rev: v0.16.0
+ hooks:
+ - id: ruff-check
+ types_or: [python, pyi, jupyter]
+ args: [--fix, --exit-non-zero-on-fix]
+ - id: ruff-format
+ types_or: [python, pyi, jupyter]
+ - repo: local
+ hooks:
+ - id: mypy
+ name: mypy
+ entry: uv run --group test mypy src tests
+ language: system
+ types: [python]
+ files: ^(src|tests)/
+ require_serial: true
+ pass_filenames: false
+ - repo: https://github.com/pre-commit/pre-commit-hooks
+ rev: v6.0.0
+ hooks:
+ - id: detect-private-key
+ - id: check-ast
+ - id: end-of-file-fixer
+ - id: mixed-line-ending
+ args: [--fix=lf]
+ - id: trailing-whitespace
+ - id: check-case-conflict
+ # Check that there are no merge conflicts (could be generated by template sync)
+ - id: check-merge-conflict
+ args: [--assume-in-merge]
+
+ - repo: https://github.com/zizmorcore/zizmor-pre-commit
+ rev: v1.28.0
+ hooks:
+ - id: zizmor
+ args: [--no-progress, --fix]
diff --git a/.readthedocs.yaml b/.readthedocs.yaml
index 31d8fef4..67eb948d 100644
--- a/.readthedocs.yaml
+++ b/.readthedocs.yaml
@@ -1,15 +1,16 @@
+# https://docs.readthedocs.io/page/config-file/v2.html
version: 2
build:
- os: ubuntu-24.04
- tools:
- python: "3.12"
-sphinx:
- configuration: docs/conf.py
- fail_on_warning: true
-python:
- install:
- - method: pip
- path: .
- extra_requirements:
- - doc
- - pre
+ os: ubuntu-24.04
+ tools:
+ python: "3.14"
+ nodejs: latest
+ jobs:
+ create_environment:
+ - asdf plugin add uv
+ - asdf install uv latest
+ - asdf global uv latest
+ build:
+ html:
+ - uvx hatch run docs:build
+ - mv docs/_build $READTHEDOCS_OUTPUT
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 00000000..6521a5dc
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,28 @@
+# Changelog
+
+All notable changes to this project will be documented in this file.
+
+The format is based on [Keep a Changelog][],
+and this project adheres to [Semantic Versioning][].
+
+Release notes for `v0.7.1` and earlier are available on the [Releases][] page.
+
+[keep a changelog]: https://keepachangelog.com/
+[semantic versioning]: https://semver.org/
+[releases]: https://github.com/scverse/spatialdata-io/releases
+
+## [Unreleased]
+
+### Added
+
+- `spatialdata_io` ships a `py.typed` marker, so downstream type checkers use its annotations.
+
+### Changed
+
+- Adopted the current `cookiecutter-scverse` template: `hatch`-managed test environments, `uv`-based CI and
+ documentation builds, `mypy` type checking of `src` and `tests`, `biome`/`pyproject-fmt`/`zizmor` pre-commit hooks,
+ and Dependabot updates.
+
+### Removed
+
+- Support for Python 3.11.
diff --git a/README.md b/README.md
index c674420b..84ae7f70 100644
--- a/README.md
+++ b/README.md
@@ -57,7 +57,7 @@ Please refer to the [documentation][link-docs]. In particular, the
## Installation
-You need to have Python 3.8 or newer installed on your system. If you don't have
+You need to have Python 3.12 or newer installed on your system. If you don't have
Python installed, we recommend installing [Miniconda](https://docs.conda.io/en/latest/miniconda.html).
There are several alternative options to install spatialdata-io:
diff --git a/asv.conf.json b/asv.conf.json
index c3005b58..c87b97b0 100644
--- a/asv.conf.json
+++ b/asv.conf.json
@@ -8,7 +8,7 @@
"environment_type": "virtualenv",
"pythons": ["3.13"],
"build_command": [],
- "install_command": ["python -m pip install {build_dir}[test]"],
+ "install_command": ["python -m pip install {build_dir}"],
"uninstall_command": ["python -m pip uninstall -y {project}"],
"env_dir": ".asv/env",
"results_dir": ".asv/results",
diff --git a/benchmarks/benchmark_xenium.py b/benchmarks/benchmark_xenium.py
index 58ac614f..5252ad33 100644
--- a/benchmarks/benchmark_xenium.py
+++ b/benchmarks/benchmark_xenium.py
@@ -62,6 +62,8 @@ def get_paths() -> tuple[Path, Path]:
class IOBenchmarkXenium:
+ """Benchmark reading Xenium data and writing it to the SpatialData Zarr format."""
+
timeout = 3600
repeat = 3
number = 1
diff --git a/biome.jsonc b/biome.jsonc
new file mode 100644
index 00000000..9f8f2208
--- /dev/null
+++ b/biome.jsonc
@@ -0,0 +1,17 @@
+{
+ "$schema": "https://biomejs.dev/schemas/2.2.0/schema.json",
+ "vcs": { "enabled": true, "clientKind": "git", "useIgnoreFile": true },
+ "formatter": { "useEditorconfig": true },
+ "overrides": [
+ {
+ "includes": ["./.vscode/*.json", "**/*.jsonc"],
+ "json": {
+ "formatter": { "trailingCommas": "all" },
+ "parser": {
+ "allowComments": true,
+ "allowTrailingCommas": true,
+ },
+ },
+ },
+ ],
+}
diff --git a/docs/_static/css/custom.css b/docs/_static/css/custom.css
new file mode 100644
index 00000000..b8c8d47f
--- /dev/null
+++ b/docs/_static/css/custom.css
@@ -0,0 +1,4 @@
+/* Reduce the font size in data frames - See https://github.com/scverse/cookiecutter-scverse/issues/193 */
+div.cell_output table.dataframe {
+ font-size: 0.8em;
+}
diff --git a/docs/_templates/autosummary/class.rst b/docs/_templates/autosummary/class.rst
deleted file mode 100644
index ee6d05f5..00000000
--- a/docs/_templates/autosummary/class.rst
+++ /dev/null
@@ -1,75 +0,0 @@
-{{ fullname | escape | underline}}
-
-.. currentmodule:: {{ module }}
-
-.. add toctree option to make autodoc generate the pages
-
-.. autoclass:: {{ objname }}
-
-{% block attributes %}
-{% if attributes %}
-Attributes table
-~~~~~~~~~~~~~~~~~~
-
-.. autosummary::
-
-{% for item in attributes %}
-
- ~{{ fullname }}.{{ item }}
-
-{%- endfor %}
-{% endif %}
-{% endblock %}
-
-{% block methods %}
-{% if methods %}
-Methods table
-~~~~~~~~~~~~~
-
-.. autosummary::
-
-{% for item in methods %}
-
- {%- if item != '__init__' %}
- ~{{ fullname }}.{{ item }}
- {%- endif -%}
-
-{%- endfor %}
-{% endif %}
-{% endblock %}
-
-{% block attributes_documentation %}
-{% if attributes %}
-Attributes
-~~~~~~~~~~~
-
-{% for item in attributes %}
-
-{{ item }}
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-
-.. autoattribute:: {{ [objname, item] | join(".") }}
-
-{%- endfor %}
-
-{% endif %}
-{% endblock %}
-
-{% block methods_documentation %}
-{% if methods %}
-Methods
-~~~~~~~
-
-{% for item in methods %}
-{%- if item != '__init__' %}
-
-{{ item }}
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-
-.. automethod:: {{ [objname, item] | join(".") }}
-
-{%- endif -%}
-{%- endfor %}
-
-{% endif %}
-{% endblock %}
diff --git a/docs/changelog.md b/docs/changelog.md
index c37a2194..d9e79ba6 100644
--- a/docs/changelog.md
+++ b/docs/changelog.md
@@ -1,4 +1,3 @@
-# Changelog
+```{include} ../CHANGELOG.md
-Please refer directly to the [Releases](https://github.com/scverse/spatialdata-io/releases) section on GitHub, where you can find curated release notes for each release.
-For developers, please consult the [contributing guide](https://github.com/scverse/spatialdata/blob/main/docs/contributing.md), which explains how to keep release notes are up-to-date at each release.
+```
diff --git a/docs/conf.py b/docs/conf.py
index c2eeea45..26bdfa28 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -1,15 +1,18 @@
# Configuration file for the Sphinx documentation builder.
-#
+
# This file only contains a selection of the most common options. For a full
# list see the documentation:
-# https://www.sphinx-doc.org/en/master/usage/configuration.html
+# https://www.sphinx-doc.org/page/usage/configuration.html
# -- Path setup --------------------------------------------------------------
+import shutil
import sys
from datetime import datetime
from importlib.metadata import metadata
from pathlib import Path
+from sphinxcontrib import katex
+
import spatialdata_io.experimental
_ = spatialdata_io.experimental
@@ -20,12 +23,15 @@
# -- Project information -----------------------------------------------------
+# NOTE: If you installed your project in editable mode, this might be stale.
+# If this is the case, reinstall it to refresh the metadata
info = metadata("spatialdata-io")
-project_name = info["Name"]
+project = info["Name"]
author = info["Author"]
-copyright = f"{datetime.now():%Y}, {author}"
+copyright = f"{datetime.now():%Y}, {author}."
version = info["Version"]
-repository_url = f"https://github.com/scverse/{project_name}"
+urls = dict(pu.split(", ") for pu in info.get_all("Project-URL"))
+repository_url = urls["Source"]
# The full version, including alpha/beta/rc tags
release = info["Version"]
@@ -37,10 +43,10 @@
html_context = {
"display_github": True, # Integrate GitHub
- "github_user": "scverse", # Username
- "github_repo": project_name, # Repo name
- "github_version": "main", # Version
- "conf_py_path": "/docs/", # Path in the checkout to the docs root
+ "github_user": "scverse",
+ "github_repo": project,
+ "github_version": "main",
+ "conf_py_path": "/docs/",
}
# -- General configuration ---------------------------------------------------
@@ -55,10 +61,15 @@
"sphinx.ext.autosummary",
"sphinx.ext.napoleon",
"sphinxcontrib.bibtex",
+ "sphinxcontrib.katex",
+ # must precede sphinx_autodoc_typehints, which shadows `sphinx.ext.autodoc.mock`
+ # with the submodule of the same name that sphinx_click imports as a callable
+ "sphinx_click",
"sphinx_autodoc_typehints",
- "sphinx.ext.mathjax",
+ "sphinx_design",
"IPython.sphinxext.ipython_console_highlighting",
- "sphinx_click",
+ "sphinxext.opengraph",
+ "scverse_misc.sphinx_ext",
*[p.stem for p in (HERE / "extensions").glob("*.py")],
]
@@ -70,7 +81,7 @@
napoleon_include_init_with_doc = False
napoleon_use_rtype = True # having a separate entry generally helps readability
napoleon_use_param = True
-myst_heading_anchors = 3 # create anchors for h1-h3
+myst_heading_anchors = 6 # create anchors for h1-h6
myst_enable_extensions = [
"amsmath",
"colon_fence",
@@ -84,6 +95,7 @@
nb_execution_mode = "off"
nb_merge_streams = True
typehints_defaults = "braces"
+always_use_bars_union = True # use `|` instead of `Union` in types even when building with Python ≤3.14
source_suffix = {
".rst": "restructuredtext",
@@ -93,13 +105,13 @@
intersphinx_mapping = {
"python": ("https://docs.python.org/3", None),
- "anndata": ("https://anndata.readthedocs.io/en/stable/", None),
- "spatialdata": ("https://scverse-spatialdata.readthedocs.io/en/latest/", None),
+ "anndata": ("https://anndata.scverse.org/en/stable/", None),
+ "scanpy": ("https://scanpy.scverse.org/en/stable/", None),
"numpy": ("https://numpy.org/doc/stable/", None),
+ "spatialdata": ("https://spatialdata.scverse.org/en/stable/", None),
"dask-image": ("https://image.dask.org/en/latest/", None),
"geopandas": ("https://geopandas.org/en/stable/", None),
"xarray": ("https://docs.xarray.dev/en/stable/", None),
- "datatree": ("https://datatree.readthedocs.io/en/latest/", None),
"shapely": ("https://shapely.readthedocs.io/en/stable/", None),
"imageio": ("https://imageio.readthedocs.io/en/stable/", None),
}
@@ -117,33 +129,22 @@
#
html_theme = "sphinx_book_theme"
html_static_path = ["_static"]
-html_title = project_name
+html_css_files = ["css/custom.css"]
-html_theme_options = {"repository_url": repository_url, "use_repository_button": True, "navigation_with_keys": True}
+html_title = project
+
+html_theme_options = {
+ "repository_url": repository_url,
+ "use_repository_button": True,
+ "path_to_docs": "docs/",
+ "navigation_with_keys": False,
+}
pygments_style = "default"
+katex_prerender = shutil.which(katex.NODEJS_BINARY) is not None
nitpick_ignore = [
# If building the documentation fails because of a missing link that is outside your control,
# you can add an exception to this list.
- ("py:class", "Path"),
- ("py:class", "pathlib._local.Path"),
- ("py:class", "AnnData"),
- ("py:class", "SpatialData"),
- ("py:func", "imageio.imread"), # maybe this can be fixed
+ # ("py:class", "igraph.Graph"),
]
-
-
-def setup(app):
- """App setup hook."""
- app.add_config_value(
- "recommonmark_config",
- {
- "auto_toc_tree_section": "Contents",
- "enable_auto_toc_tree": True,
- "enable_math": True,
- "enable_inline_math": False,
- "enable_eval_rst": True,
- },
- True,
- )
diff --git a/docs/contributing.md b/docs/contributing.md
index ebfda601..28bac260 100644
--- a/docs/contributing.md
+++ b/docs/contributing.md
@@ -1,6 +1,345 @@
-# Contribution guide
+# Contributing guide
-This guide is intended for:
+This document aims at summarizing the most important information for getting you started on contributing to this project.
+We assume that you are already familiar with git and with making pull requests on GitHub.
+
+For more extensive tutorials, that also cover the absolute basics,
+please refer to other resources such as the [pyopensci tutorials][],
+the [scientific Python tutorials][], or the [scanpy developer guide][].
+
+[pyopensci tutorials]: https://www.pyopensci.org/learn.html
+[scientific Python tutorials]: https://learn.scientific-python.org/development/tutorials/
+[scanpy developer guide]: https://scanpy.scverse.org/page/dev/
+
+:::{tip} The *hatch* project manager
+
+We highly recommend to familiarize yourself with [`hatch`][hatch].
+Hatch is a Python project manager that
+
+- manages virtual environments, separately for development, testing and building the documentation.
+ Separating the environments is useful to avoid dependency conflicts.
+- allows to run tests locally in different environments (e.g. different python versions)
+- allows to run tasks defined in `pyproject.toml`, e.g. to build documentation.
+
+While the project is setup with `hatch` in mind,
+it is still possible to use different tools to manage dependencies, such as `uv` or `pip`.
+
+:::
+
+[hatch]: https://hatch.pypa.io/latest/
+
+## Installing dev dependencies
+
+In addition to the packages needed to _use_ this package,
+you need additional python packages to [run tests](#writing-tests) and [build the documentation](#docs-building).
+
+:::::{tab-set}
+::::{tab-item} Hatch
+:sync: hatch
+
+On the command line, you typically interact with hatch through its command line interface (CLI).
+Running one of the following commands will automatically resolve the environments for testing and
+building the documentation in the background:
+
+```bash
+hatch test # defined in the table [tool.hatch.envs.hatch-test] in pyproject.toml
+hatch run docs:build # defined in the table [tool.hatch.envs.docs]
+```
+
+### VS Code
+
+If you are using VS code, install the [hatch-code][] extension.
+Additionally, make sure that the `vscode-python-environments` extension is installed (should be by default)
+and `"python.useEnvironmentsExtension": true` is activated in your `settings.json`.
+
+Next, open the "Python Environment Managers" sidebar.
+You can do so by opening the command palette (Ctrl+Shift+P) and searching for `Python: Focus on Environment Managers View`.
+It will show a collapsible list where you can expand "Hatch"
+and activate an environment by clicking on the checkmark next to it.
+As the main development environment, we recommend to use `hatch-test` with the latest supported Python version.
+
+### Other IDEs
+
+For other IDEs, you’ll have to point the editor at the paths to the virtual environments manually.
+To get a list of all environments for your projects, run
+
+```bash
+hatch env show -i
+```
+
+This will list “Standalone” environments and a table of “Matrix” environments like the following:
+
+```
++------------+---------+--------------------------+----------+---------------------------------+-------------+
+| Name | Type | Envs | Features | Dependencies | Scripts |
++------------+---------+--------------------------+----------+---------------------------------+-------------+
+| hatch-test | virtual | hatch-test.py3.12-stable | dev | coverage-enable-subprocess==1.0 | cov-combine |
+| | | hatch-test.py3.14-stable | test | coverage[toml]~=7.4 | cov-report |
+| | | hatch-test.py3.14-pre | | pytest-mock~=3.12 | run |
+| | | | | pytest-randomly~=3.15 | run-cov |
+| | | | | pytest-rerunfailures~=14.0 | |
+| | | | | pytest-xdist[psutil]~=3.5 | |
+| | | | | pytest~=8.1 | |
++------------+---------+--------------------------+----------+---------------------------------+-------------+
+```
+
+From the `Envs` column, select the environment name you want to use for development.
+As the main development environment, we recommend to use `hatch-test` with the latest supported Python version.
+In this example, it would be `hatch-test.py3.14-stable`.
+
+Next, create the environment with
+
+```bash
+hatch env create hatch-test.py3.14-stable
+```
+
+Then, obtain the path to the environment using
+
+```bash
+hatch env find hatch-test.py3.14-stable
+```
+
+and manually point it to the python binary.
+
+
+::::
+
+::::{tab-item} uv
+:sync: uv
+
+A popular choice for managing virtual environments is [uv][].
+The main disadvantage compared to hatch is that it supports only a single environment per project at a time,
+which requires you to mix the dependencies for running tests and building docs.
+This can have undesired side-effects,
+such as requiring to install a lower version of a library your project depends on,
+only because an outdated sphinx plugin pins an older version.
+
+To initialize a virtual environment in the `.venv` directory of your project, simply run
+
+```bash
+uv sync --all-extras
+```
+
+The `.venv` directory is typically automatically discovered by IDEs such as VS Code.
+
+::::
+
+::::{tab-item} Pip
+:sync: pip
+
+Pip is nowadays mostly superseded by environment manager such as [hatch][].
+However, for the sake of completeness, and since it’s ubiquitously available,
+we describe how you can manage environments manually using `pip`:
+
+```bash
+python3 -m venv .venv
+source .venv/bin/activate
+pip install -e ".[dev,test,doc]"
+```
+
+The `.venv` directory is typically automatically discovered by IDEs such as VS Code.
+
+::::
+:::::
+
+[hatch environments]: https://hatch.pypa.io/latest/tutorials/environment/basic-usage/
+[hatch-code]: https://marketplace.visualstudio.com/items?itemName=PyPA.hatch
+[uv]: https://docs.astral.sh/uv/
+
+## Code-style
+
+This package uses [pre-commit][]-style hooks to enforce consistent code-styles.
+We recommend running them with [prek][], a fast, drop-in replacement for `pre-commit` that reads the same `.pre-commit-config.yaml`.
+On every commit, the checks will either automatically fix issues with the code, or raise an error message.
+
+To enable the checks locally, install [prek][] (e.g. with `uv tool install prek`) and run
+
+```bash
+prek install
+```
+
+in the root of the repository.
+prek will automatically download all dependencies when it is run for the first time.
+
+If you didn’t run the checks locally, the `Pre-commit checks` job of the GitHub Actions CI runs them on your pull request and reports any failures.
+We strongly encourage installing and running the checks locally first to understand their usage.
+
+Finally, most editors have an _autoformat on save_ feature.
+Consider enabling this option for [ruff][ruff-editors] and [biome][biome-editors].
+
+[pre-commit]: https://pre-commit.com/
+[prek]: https://prek.j178.dev/
+[ruff-editors]: https://docs.astral.sh/ruff/integrations/
+[biome-editors]: https://biomejs.dev/guides/integrate-in-editor/
+
+(writing-tests)=
+
+## Writing tests
+
+This package uses [pytest][] for automated testing.
+Please write {doc}`scanpy:dev/testing` for every function added to the package.
+
+Most IDEs integrate with pytest and provide a GUI to run tests.
+If you set up your virtual environments as described in [installing dev dependencies](#installing-dev-dependencies),
+test cases should be automatically discovered by your IDE.
+
+Alternatively, you can run all tests from the command line by executing
+
+:::::{tab-set}
+::::{tab-item} Hatch
+:sync: hatch
+
+```bash
+hatch test # test with the highest supported Python version
+# or
+hatch test --all # test with all supported Python versions
+```
+
+::::
+
+::::{tab-item} uv
+:sync: uv
+
+```bash
+uv run pytest
+```
+
+::::
+
+::::{tab-item} Pip
+:sync: pip
+
+```bash
+source .venv/bin/activate
+pytest
+```
+
+::::
+:::::
+
+in the root of the repository.
+
+[pytest]: https://docs.pytest.org/
+
+### Continuous integration
+
+Continuous integration via GitHub actions will automatically run the tests on all pull requests and test
+against the minimum and maximum supported Python version.
+
+Additionally, there’s a CI job that tests against pre-releases of all dependencies (if there are any).
+The purpose of this check is to detect incompatibilities of new package versions early on and
+gives you time to fix the issue or reach out to the developers of the dependency before the package
+is released to a wider audience.
+
+The CI job is defined in `.github/workflows/test.yaml`,
+however the single point of truth for CI jobs is the Hatch test matrix defined in `pyproject.toml`.
+This means that local testing via hatch and remote testing on CI tests against the same python versions and uses the same environments.
+
+## Publishing a release
+
+### Updating the version number
+
+The version number is derived from the git tag by [hatch-vcs][], so there is nothing to edit in `pyproject.toml`.
+Add the release notes to `CHANGELOG.md` and pick the tag according to [Semantic Versioning][semver], in brief
+
+> Given a version number MAJOR.MINOR.PATCH, increment the:
+>
+> 1. MAJOR version when you make incompatible API changes,
+> 2. MINOR version when you add functionality in a backwards compatible manner, and
+> 3. PATCH version when you make backwards compatible bug fixes.
+>
+> Additional labels for pre-release and build metadata are available as extensions to the MAJOR.MINOR.PATCH format.
+
+Once you are done, commit and push your changes and navigate to the "Releases" page of this project on GitHub.
+Specify `vX.X.X` as a tag name and create a release.
+For more information, see [managing GitHub releases][].
+This will automatically create a git tag and trigger a Github workflow that creates a release on [PyPI][].
+
+[hatch-vcs]: https://github.com/ofek/hatch-vcs
+[semver]: https://semver.org/
+[managing GitHub releases]: https://docs.github.com/en/repositories/releasing-projects-on-github/managing-releases-in-a-repository
+[pypi]: https://pypi.org/
+
+## Writing documentation
+
+Please write documentation for new or changed features and use-cases.
+This project uses [sphinx][] with the following features:
+
+- The [myst][] extension allows to write documentation in markdown/Markedly Structured Text
+- [Numpy-style docstrings][numpydoc] (through the [napoloen][numpydoc-napoleon] extension).
+- Jupyter notebooks as tutorials through [myst-nb][] (See [Tutorials with myst-nb](#tutorials-with-myst-nb-and-jupyter-notebooks))
+- [sphinx-autodoc-typehints][], to automatically reference annotated input and output types
+- Citations (like {cite:p}`Virshup_2023`) can be included with [sphinxcontrib-bibtex](https://sphinxcontrib-bibtex.readthedocs.io/)
+
+See scanpy’s {doc}`scanpy:dev/documentation` for more information on how to write your own.
+
+[sphinx]: https://www.sphinx-doc.org/
+[myst]: https://myst-parser.readthedocs.io/page/intro.html
+[myst-nb]: https://myst-nb.readthedocs.io/
+[numpydoc-napoleon]: https://www.sphinx-doc.org/page/usage/extensions/napoleon.html
+[numpydoc]: https://numpydoc.readthedocs.io/page/format.html
+[sphinx-autodoc-typehints]: https://github.com/tox-dev/sphinx-autodoc-typehints
+
+### Tutorials with myst-nb and jupyter notebooks
+
+The documentation is set-up to render jupyter notebooks stored in the `docs/notebooks` directory using [myst-nb][].
+Currently, only notebooks in `.ipynb` format are supported that will be included with both their input and output cells.
+It is your responsibility to update and re-run the notebook whenever necessary.
+
+If you are interested in automatically running notebooks as part of the continuous integration,
+please check out [this feature request][issue-render-notebooks] in the `cookiecutter-scverse` repository.
+
+[issue-render-notebooks]: https://github.com/scverse/cookiecutter-scverse/issues/40
+
+#### Hints
+
+- If you refer to objects from other packages, please add an entry to `intersphinx_mapping` in `docs/conf.py`.
+ Only if you do so can sphinx automatically create a link to the external documentation.
+- If building the documentation fails because of a missing link that is outside your control,
+ you can add an entry to the `nitpick_ignore` list in `docs/conf.py`
+
+(docs-building)=
+
+### Building the docs locally
+
+:::::{tab-set}
+::::{tab-item} Hatch
+:sync: hatch
+
+```bash
+hatch run docs:build
+hatch run docs:open
+```
+
+::::
+
+::::{tab-item} uv
+:sync: uv
+
+```bash
+cd docs
+uv run sphinx-build -M html . _build -W
+(xdg-)open _build/html/index.html
+```
+
+::::
+
+::::{tab-item} Pip
+:sync: pip
+
+```bash
+source .venv/bin/activate
+cd docs
+sphinx-build -M html . _build -W
+(xdg-)open _build/html/index.html
+```
+
+::::
+:::::
+
+## Contributing readers and converters
+
+This section is intended for:
- **Spatial technology companies** that would like to add a reader (or converter) for a new product, support a new version of an existing technology, or contribute with edits/bugfixes to one of the available readers.
- **Academic labs** that would like to contribute a reader (or converter) for in-house data generation pipelines.
@@ -9,9 +348,9 @@ This guide is intended for:
The `spatialdata-io` repository inherits the technical stack of `spatialdata`. Therefore, please also read the [contribution guide from the `spatialdata` repository](https://github.com/scverse/spatialdata/blob/main/docs/contributing.md).
The remainder of this document is specific to `spatialdata-io`. Feedback on this document is highly welcomed: you are encouraged to open a PR to improve this guide.
-**A summary checklist is provided at the end of the document.**
+**A summary checklist is provided at the end of this document.**
-## Adding a new reader
+### Adding a new reader
To contribute a reader and make code review efficient, we kindly ask for:
@@ -20,7 +359,7 @@ To contribute a reader and make code review efficient, we kindly ask for:
- example **data**: preferably one or more (very) small _public_ datasets, with a permissive license, to represent the data across various versions and cover potential edge cases of the raw data format. Alternatively (or preferably, in addition), scripts to easily download _public_ and licensed data.
- **test** functions: to ensure that the reader parses the data correctly. If helper functions are used, they should also be tested.
-### The specification
+#### The specification
In your issue or PR, please include a link to a _public_ specification and/or changelog describing how the raw data is organized on disk and what changes across various versions.
@@ -28,7 +367,7 @@ If the specification cannot (yet) be made public, you can reach out to scverse v
The advantage of a public specification is that it makes contributions and bugfixes from the community easier.
-### The reader file
+#### The reader file
The reader files are located under [src/spatialdata_io/readers](https://github.com/scverse/spatialdata-io/tree/main/src/spatialdata_io/readers). For each supported technology, the reader file is a single function, named after the technology, that takes as input the path to the raw data and extra arguments, and returns a `SpatialData` object:
@@ -53,23 +392,23 @@ We recommend studying existing readers and reusing code from them. A few technic
- When the raw data has multiple samples, we recommend adding a coordinate system for each sample, and if the samples are aligned in space, one common coordinate system. A single table containing the annotation for all samples is preferred. See an example in the [`cosmx()`](https://github.com/scverse/spatialdata-io/blob/main/src/spatialdata_io/readers/cosmx.py) reader.
- Small images should be represented as single-scale images (`xarray.DataArray`), large images as multiscale images (`xarray.DataTree`). The scale factors and chunk shape (`chunks`) should lead to chunks that fit in memory. See an example in [`visium()`](https://github.com/scverse/spatialdata-io/blob/main/src/spatialdata_io/readers/visium.py).
-#### Experimental readers
+##### Experimental readers
If a specification is not available, a reader has less guarantee to work out-of-the-box. This is, for instance, the case for spatial omics datasets generated with in-house technologies. Even in this case, users may find a reader beneficial: they may still have to adapt the reader for their use cases, but it could provide a good starting point to parse their data. We list these readers under the [`experimental`](https://github.com/scverse/spatialdata-io/blob/main/src/spatialdata_io/experimental/__init__.py) module. An example of this is the [`iss()`](https://github.com/scverse/spatialdata-io/blob/main/src/spatialdata_io/readers/iss.py) reader for In-Situ Sequencing data.
-### Separating the constants from the reader logic
+#### Separating the constants from the reader logic
Each raw data format introduces specific naming schemes. To reduce the risk of typos and maintain an overview of all the names used in a reader, we list all the string constants in the file [src/spatialdata_io.\_constants/\_constants.py](https://github.com/scverse/spatialdata-io/blob/main/src/spatialdata_io/_constants/_constants.py).
-#### Technical notes
+##### Technical notes
By using the decorator `inject_docs`, you can include constants in the docstring of reader functions. [See an example here](https://github.com/scverse/spatialdata-io/blob/d2fe0bc18349093ad2cafce752590117729baee8/src/spatialdata_io/readers/xenium.py#L55).
-## Adding tests: small data vs real data
+### Adding tests: small data vs real data
An effective way to make a specification easier to understand and to improve the code review process is to provide test data.
-### When small public test datasets are available
+#### When small public test datasets are available
Our preferred way to test a reader is by using (very) small test datasets (e.g. 100kB-10MB). By being small, we can afford to use the datasets in continuous integration tests that are run via GitHub actions. See here the [GitHub workflow](https://github.com/scverse/spatialdata-io/blob/main/.github/workflows/prepare_test_data.yaml) that we use to download the test datasets, and here an [example of a test](https://github.com/LucaMarconato/spatialdata-io/blob/d2fe0bc18349093ad2cafce752590117729baee8/tests/test_xenium.py#L47) using the small datasets.
@@ -80,13 +419,13 @@ Notes:
- The data should be licensed with a permissive format, e.g. [CC BY 4.0 license](https://creativecommons.org/licenses/by/4.0/).
- If the dataset is not accessible from the web, we invite you to submit it to the ["SpatialData Submissions" Zenodo community](https://zenodo.org/communities/scverse-spatialdata-submissions/) to make it available to the public.
-### When only real (large) public data is available
+#### When only real (large) public data is available
Even when a small dataset is not available, we encourage you to produce one. If this is not possible, a public real dataset (with a permissive license) can be used. It is also possible to use both: small test datasets for CI testing, and a real dataset for double-checking real-world usability and performance.
To streamline the usage of datasets, we kindly ask you to provide two scripts via a PR to the [`spatialdata-sandbox`](https://github.com/giovp/spatialdata-sandbox) repository (not `spatialdata-io`): a script to download the data and a script to convert the downloaded data to the SpatialData Zarr format.
-#### Folder structure
+##### Folder structure
Please create a folder in the [root directory of `spatialdata-sandbox`](https://github.com/giovp/spatialdata-sandbox/tree/main) named after your technology. Please include a format version if available. We also add the suffix `_io`: we use this to distinguish between scripts that reuse `spatialdata_io` for converting the data to SpatialData Zarr and scripts that parse the data from scratch.
@@ -94,24 +433,24 @@ An example is [`visium_hd_3.1.1_io`](https://github.com/giovp/spatialdata-sandbo
A note: `spatialdata-sandbox` is primarily used by scverse developers and, as the name suggests, we mainly use it as a sandbox for experimenting and for small scripts. We do not have pre-commit installed nor CI set up, so feel free not to worry about the code style.
-#### The `download.py` script
+##### The `download.py` script
The `download.py` script simply downloads the raw data inside the subfolder `data` and, if it is zipped, unzips it. Example [`download.py`](https://github.com/giovp/spatialdata-sandbox/blob/main/visium_hd_3.1.1_io/download.py) for Visium HD data.
When the data does not follow any specification (for instance, for in-house data), it sometimes also performs some manipulation on the data and, in such cases, stores the result in any convenient way. Example [`download.py`](https://github.com/giovp/spatialdata-sandbox/blob/main/merfish/download.py) for in-house MERFISH data.
-#### The `to_zarr.py` script
+##### The `to_zarr.py` script
The `to_zarr.py` script: (1) imports a suitable reader from `spatialdata_io`; (2) reads the raw data from the folder `data` to obtain a `SpatialData` object; (3) saves the object to disk to a new Zarr store called `data.zarr`; (4) as a consistency check, reads the store back into memory to a new `SpatialData` object.
-### The importance of public data and permissive licenses
+#### The importance of public data and permissive licenses
There are several reasons to prefer public datasets provided with a permissive license (e.g. 10x public datasets often use the [CC BY 4.0 license](https://creativecommons.org/licenses/by/4.0/)):
- It allows the community to use the `download.py` script to download the data. An example of data accessible to the public but not easily downloadable is when the data can be downloaded only after completing a questionnaire. With private data, collaborations and community contributions are difficult.
- It allows the community to modify (e.g. subset, to make download faster) and rehost the data. This makes it easier to share results and collaborate effectively.
-### When only private data is available
+#### When only private data is available
Due to the open-source, collaborative nature of our work, developing readers for private datasets is out of our scope. Therefore, we will not be able to consider private datasets. An exception is when developing a reader for a technology whose launch is scheduled and imminent, and for which the data will soon become public. If you find yourself in this scenario, please reach out to scverse via [Zulip](https://scverse.zulipchat.com/#narrow/channel/443514-spatialdata-dev) via private message, or via email. In such cases, we will ask you to give us access to a private repository where you can upload a `download.py` and `to_zarr.py` script working as described above. Alternatively, you can also upload the data privately to the ["SpatialData Submissions" Zenodo community](https://zenodo.org/communities/scverse-spatialdata-submissions/), instead of providing a `download.py` script.
@@ -119,15 +458,15 @@ Due to the open-source, collaborative nature of our work, developing readers for
If the `download.py` and `to_zarr.py` scripts require Python imports for packages that are not available in an environment where `spatialdata` is installed, please also provide a `requirements.txt` file (see for instance a [legacy one here](https://github.com/giovp/spatialdata-sandbox/blob/main/merfish/requirements.txt)), or specify the dependencies in the scripts as [inline script metadata (PEP 723)](https://docs.astral.sh/uv/guides/scripts/#declaring-script-dependencies).
-## What to test
+### What to test
We encourage testing the reader function and any helper function.
-### Testing multiple versions
+#### Testing multiple versions
When multiple versions of the raw data format are present, we encourage testing the reader on all of them to ensure backward compatibility. This task is greatly simplified if small test datasets are used for the CI tests. If this is not available, we suggest running the tests locally on multiple versions of the data before the PR is ready for review.
-### Testing that the visualization and table annotations are correct
+#### Testing that the visualization and table annotations are correct
Using `spatialdata-plot` and/or `napari-spatialdata` to visualize the data is a quick way to easily spot issues with spatial alignment or table annotations.
@@ -140,18 +479,18 @@ In `napari-spatialdata` and `spatialdata-plot`, we have tests that visually comp
Note: writing these tests requires some manual work to explore the data. If you find this too time-consuming, we could consider adding some external scripts to create these tests automatically: if you are interested, please reach out to us.
-### Testing auxiliary functions
+#### Testing auxiliary functions
Some technologies make use of auxiliary functions to parse the data. We encourage testing these functions as well. Here is an [example of such an auxiliary function for Xenium data](https://github.com/LucaMarconato/spatialdata-io/blob/4ee33da99781ccb2ea284be0614bdaaf69bfb2ed/src/spatialdata_io/readers/xenium.py#L771) and the [corresponding test](https://github.com/LucaMarconato/spatialdata-io/blob/4ee33da99781ccb2ea284be0614bdaaf69bfb2ed/tests/test_xenium.py#L19).
-## Adding a new converter
+### Adding a new converter
Readers are developed to parse raw data from spatial omics technologies. We also provide converters to convert data from/to other general data formats, i.e. data formats that are not specific to a single (or a small set) of spatial omics technologies.
An example of this is the legacy `AnnData` spatial format, used in early versions of `squidpy`.
Converters are not the primary scope of `spatialdata-io`, so we will just give some general indications: if you are interested in contributing a converter, please adapt and follow the guidelines as for readers (in particular, link to a specification, provide test data, and write extensive tests). Feedback is welcome: if you find it useful, we can expand this section of the contribution guide.
-## Updating the CLI for readers and converters
+### Updating the CLI for readers and converters
The readers and converters from `spatialdata-io` can be invoked via the command line (see the [CLI documentation](https://spatialdata.scverse.org/projects/io/en/stable/cli.html)). This Python file defines the CLI: [src/spatialdata_io/**main**.py](https://github.com/scverse/spatialdata-io/blob/main/src/spatialdata_io/__main__.py). Please, if you add or modify a reader or converter, update the CLI accordingly.
@@ -160,7 +499,7 @@ The readers and converters from `spatialdata-io` can be invoked via the command
- In the future, we may consider automatically generating the CLI from the readers and converters, [see more here](https://github.com/scverse/spatialdata-io/pull/239#issuecomment-2588005228).
- Keeping the CLI code up-to-date could be a good task for the GitHub Copilot code agent. We will experiment with this in the future.
-# Bug tracking
+## Bug tracking
To see current open [issues](https://github.com/scverse/spatialdata-io/issues?q=sort%3Aupdated-desc+is%3Aissue+is%3Aopen) and [PRs](https://github.com/scverse/spatialdata-io/pulls?q=sort%3Aupdated-desc+is%3Apr+is%3Aopen) for a particular technology, please filter the issues/PRs by label. Not all the issues have been labeled yet, so additional issues/PRs for a particular technology may be present.
@@ -168,31 +507,31 @@ To provide an overview of the open bugs and to help prioritize them, we set up a
The issues in the project board need to be added manually, so the board may not be complete and we invite you to consult also the issues/PRs list. Due to the redundant nature of the project board, we are still experimenting with it, so please give us feedback if you find it useful or not.
-# Limitations and possible improvements
+## Limitations and possible improvements
We are aware of some limitations of the current implementation of `spatialdata-io`.
-## Unsupported technologies
+### Unsupported technologies
Certain highly-used technologies, or certain versions for a particular technology, are not supported yet, and we encourage community contributions to help with implementations.
-## Naming consistency across readers
+### Naming consistency across readers
The naming of the arguments of the readers is not fully consistent across technologies. An improvement would be to standardize the naming of the arguments across readers, for instance always using `path` for the path to the raw data, `sample_id` for the sample identifier, etc.
-## Pixel space vs physical space
+### Pixel space vs physical space
For certain use cases, the "pixel" space is more convenient, for others the "physical" space. Thanks to the flexibility of coordinate transformations, one can easily handle the data in both spaces. However, for some technologies, the first coordinate system (i.e. the default one) is the "pixel" space, for others the "physical" space. An improvement would be to standardize this across technologies.
-## Transformations between coordinate systems
+### Transformations between coordinate systems
Coordinate transformations are currently only available from elements to coordinate systems. We are working (initially in a separate repository) on a large refactoring of coordinate transformations to allow transformations also between coordinate systems. This will make it easier to work with multiple samples and multiple coordinate systems and simplify the process of dealing with transformations in `spatialdata-io` readers.
-## Summary table of supported technologies and specifications
+### Summary table of supported technologies and specifications
In our README, we list the supported technologies but do not list the supported versions for each technology. An overview table could be helpful.
-# Wrap-up: checklist
+## Wrap-up: checklist
As a wrap-up, here is a checklist of the main points to consider when contributing a reader.
diff --git a/docs/extensions/typed_returns.py b/docs/extensions/typed_returns.py
deleted file mode 100644
index d044c698..00000000
--- a/docs/extensions/typed_returns.py
+++ /dev/null
@@ -1,29 +0,0 @@
-# code from https://github.com/theislab/scanpy/blob/master/docs/extensions/typed_returns.py
-# with some minor adjustment
-import re
-
-from sphinx.application import Sphinx
-from sphinx.ext.napoleon import NumpyDocstring
-
-
-def _process_return(lines):
- for line in lines:
- m = re.fullmatch(r"(?P\w+)\s+:\s+(?P[\w.]+)", line)
- if m:
- # Once this is in scanpydoc, we can use the fancy hover stuff
- yield f"-{m['param']} (:class:`~{m['type']}`)"
- else:
- yield line
-
-
-def _parse_returns_section(self, section):
- lines_raw = list(_process_return(self._dedent(self._consume_to_next_section())))
- lines = self._format_block(":returns: ", lines_raw)
- if lines and lines[-1]:
- lines.append("")
- return lines
-
-
-def setup(app: Sphinx):
- """Set app."""
- NumpyDocstring._parse_returns_section = _parse_returns_section
diff --git a/docs/references.bib b/docs/references.bib
index cd413861..372f533c 100644
--- a/docs/references.bib
+++ b/docs/references.bib
@@ -15,3 +15,13 @@ @article{Wolf2018
doi = {10.1186/s13059-017-1382-0},
url = {https://doi.org/10.1186/s13059-017-1382-0}
}
+@article{Virshup_2023,
+ doi = {10.1038/s41587-023-01733-8},
+ url = {https://doi.org/10.1038%2Fs41587-023-01733-8},
+ year = 2023,
+ month = {apr},
+ publisher = {Springer Science and Business Media {LLC}},
+ author = {Isaac Virshup and Danila Bredikhin and Lukas Heumos and Giovanni Palla and Gregor Sturm and Adam Gayoso and Ilia Kats and Mikaela Koutrouli and Philipp Angerer and Volker Bergen and Pierre Boyeau and Maren Büttner and Gokcen Eraslan and David Fischer and Max Frank and Justin Hong and Michal Klein and Marius Lange and Romain Lopez and Mohammad Lotfollahi and Malte D. Luecken and Fidel Ramirez and Jeffrey Regier and Sergei Rybakov and Anna C. Schaar and Valeh Valiollah Pour Amiri and Philipp Weiler and Galen Xing and Bonnie Berger and Dana Pe'er and Aviv Regev and Sarah A. Teichmann and Francesca Finotello and F. Alexander Wolf and Nir Yosef and Oliver Stegle and Fabian J. Theis and},
+ title = {The scverse project provides a computational ecosystem for single-cell omics data analysis},
+ journal = {Nature Biotechnology}
+}
diff --git a/pyproject.toml b/pyproject.toml
index 05c5baa2..41f06caf 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,205 +1,200 @@
[build-system]
build-backend = "hatchling.build"
-requires = ["hatchling", "hatch-vcs"]
-
+requires = [ "hatch-vcs", "hatchling" ]
[project]
name = "spatialdata-io"
-dynamic = [
- "version" # allow version to be set by git tags
-]
description = "SpatialData IO for common techs"
readme = "README.md"
-requires-python = ">=3.11"
license = { file = "LICENSE" }
+maintainers = [
+ { name = "scverse", email = "scverse@scverse.scverse" },
+]
authors = [
- { name = "scverse" },
+ { name = "scverse" },
]
-maintainers = [
- { name = "scverse", email = "scverse@scverse.scverse" },
+requires-python = ">=3.12"
+classifiers = [
+ "Programming Language :: Python :: 3 :: Only",
+ "Programming Language :: Python :: 3.12",
+ "Programming Language :: Python :: 3.13",
+ "Programming Language :: Python :: 3.14",
+ "Typing :: Typed",
+]
+dynamic = [
+ "version", # allow version to be set by git tags
]
-urls.Documentation = "https://spatialdata-io.readthedocs.io/"
-urls.Source = "https://github.com/scverse/spatialdata-io"
-urls.Home-page = "https://github.com/scverse/spatialdata-io"
dependencies = [
- "anndata",
- "click",
- "numpy",
- "scanpy",
- "spatialdata>=0.7.3a0",
- "scikit-image",
- "h5py",
- "joblib",
- "imagecodecs",
- "dask-image",
- "pyarrow",
- "readfcs",
- "tifffile>=2023.8.12",
- "ome-types",
- "xmltodict",
+ "anndata",
+ "click",
+ "dask-image",
+ "h5py",
+ "imagecodecs",
+ "joblib",
+ "numpy",
+ "ome-types",
+ "pyarrow",
+ "readfcs",
+ "scanpy",
+ "scikit-image",
+ # for debug logging (referenced from the issue template)
+ "session-info2",
+ "spatialdata>=0.7.3a0",
+ "tifffile>=2023.8.12",
+ "xmltodict",
]
+# https://docs.pypi.org/project_metadata/#project-urls
+urls.Documentation = "https://spatialdata-io.readthedocs.io/"
+urls.Homepage = "https://github.com/scverse/spatialdata-io"
+urls.Source = "https://github.com/scverse/spatialdata-io"
-[project.optional-dependencies]
+[dependency-groups]
dev = [
- # CLI for bumping the version number
- "bump2version",
- "pre-commit"
-]
-doc = [
- "sphinx>=4.5,<9",
- "sphinx-book-theme>=1.0.0",
- "myst-nb",
- "sphinxcontrib-bibtex>=1.0.0",
- "sphinx-autodoc-typehints",
- "sphinx-design",
- "sphinx-click",
- # For notebooks
- "ipython>=8.6.0",
- "sphinx-copybutton",
+ "mypy",
+ "pandas-stubs",
+ "prek",
+ "scipy-stubs",
+ "twine>=4.0.2",
+ "types-geopandas",
+ "types-pyyaml",
+ "types-shapely",
+ "types-tqdm",
+ "types-xmltodict",
]
test = [
- "pytest",
- "pytest-cov",
- # https://github.com/scverse/spatialdata-io/issues/334
- "pyarrow!=22",
- "pytest-mock"
-]
-# this will be used by readthedocs and will make pip also look for pre-releases, generally installing the latest available version
-# update: readthedocs doens't seem to try to install pre-releases even if when trying to install the pre optional-dependency. For
-# the moment, if needed, let's add the latest pre-release explicitly here.
-pre = [
- "spatialdata>=0.7.3a0"
+ "coverage>=7.10",
+ # https://github.com/scverse/spatialdata-io/issues/334
+ "pyarrow!=22",
+ "pytest",
+ "pytest-mock",
]
-
-[tool.coverage.run]
-source = ["spatialdata_io"]
-omit = [
- "**/test_*.py",
+doc = [
+ "ipykernel",
+ "ipython",
+ "myst-nb>=1.1",
+ "pandas",
+ "scverse-misc[sphinx]>=0.1.2",
+ "sphinx>=8.1",
+ "sphinx-autodoc-typehints",
+ "sphinx-book-theme>=1",
+ "sphinx-click",
+ "sphinx-copybutton",
+ "sphinx-design",
+ "sphinxcontrib-bibtex>=1",
+ "sphinxcontrib-katex",
+ "sphinxext-opengraph",
]
-[tool.pytest.ini_options]
-testpaths = ["tests"]
-xfail_strict = true
-addopts = [
- "--import-mode=importlib", # allow using test files with same name
+[tool.hatch]
+version.source = "vcs"
+build.hooks.vcs.version-file = "_version.py"
+build.targets.wheel.packages = [ "src/spatialdata_io" ]
+envs.default.installer = "uv"
+envs.default.dependency-groups = [ "dev" ]
+envs.docs.scripts.build = "sphinx-build -M html docs docs/_build -W {args}"
+envs.docs.scripts.clean = "git clean -fdX -- {args:docs}"
+envs.docs.scripts.open = "python -m webbrowser -t docs/_build/html/index.html"
+envs.docs.dependency-groups = [ "doc" ]
+envs.hatch-test.matrix = [
+ # Test the lowest and highest supported Python versions with normal deps
+ { deps = [ "stable" ], python = [ "3.12", "3.14" ] },
+ # Test the newest supported Python version also with pre-release deps
+ { deps = [ "pre" ], python = [ "3.14" ] },
+]
+# If the matrix variable `deps` is set to "pre",
+# set the environment variable `UV_PRERELEASE` to "allow".
+envs.hatch-test.overrides.matrix.deps.env-vars = [
+ { value = "allow", key = "UV_PRERELEASE", if = [ "pre" ] },
]
+envs.hatch-test.dependency-groups = [ "dev", "test" ]
[tool.ruff]
line-length = 120
-exclude = [
- ".git",
- ".tox",
- "__pycache__",
- "build",
- "docs/_build",
- "dist",
- "setup.py",
-]
+src = [ "src" ]
+extend-include = [ "*.ipynb" ]
+format.docstring-code-format = true
lint.select = [
- "F", # Errors detected by Pyflakes
- "E", # Error detected by Pycodestyle
- "W", # Warning detected by Pycodestyle
- "I", # isort
- "D", # pydocstyle
- "B", # flake8-bugbear
- "TID", # flake8-tidy-imports
- "C4", # flake8-comprehensions
- "BLE", # flake8-blind-except
- "UP", # pyupgrade
- "RUF100", # Report unused noqa directives
- "TCH", # Typing imports
- "NPY", # Numpy specific rules
- # "PTH", # Use pathlib
- # "S" # Security
+ "B", # flake8-bugbear
+ "BLE", # flake8-blind-except
+ "C4", # flake8-comprehensions
+ "D", # pydocstyle
+ "E", # Error detected by Pycodestyle
+ "F", # Errors detected by Pyflakes
+ "I", # isort
+ "RUF100", # Report unused noqa directives
+ "TID", # flake8-tidy-imports
+ "UP", # pyupgrade
+ "W", # Warning detected by Pycodestyle
]
lint.ignore = [
- # Do not catch blind exception: `Exception`
- "BLE001",
- # Errors from function calls in argument defaults. These are fine when the result is immutable.
- "B008",
- # line too long -> we accept long comment lines; black gets rid of long code lines
- "E501",
- # Do not assign a lambda expression, use a def -> lambda expression assignments are convenient
- "E731",
- # allow I, O, l as variable names -> I is the identity matrix
- "E741",
- # Missing docstring in public module
- "D100",
- # undocumented-public-class
- "D101",
- # Missing docstring in public method
- "D102",
- # Missing docstring in public function
- "D103",
- # Missing docstring in public package
- "D104",
- # __magic__ methods are are often self-explanatory, allow missing docstrings
- "D105",
- # Missing docstring in public nested class
- "D106",
- # Missing docstring in __init__
- "D107",
- ## Disable one in each pair of mutually incompatible rules
- # We don’t want a blank line before a class docstring
- "D203",
- # 1 blank line required after class docstring
- "D204",
- # first line should end with a period [Bug: doesn't work with single-line docstrings]
- # We want docstrings to start immediately after the opening triple quote
- "D213",
- # Section underline is over-indented ("{name}")
- "D215",
- # First line should be in imperative mood; try rephrasing
- "D401",
- # First word of the first line should be capitalized: {} -> {}
- "D403",
- # First word of the docstring should not be "This"
- "D404",
- # Section name should end with a newline ("{name}")
- "D406",
- # Missing dashed underline after section ("{name}")
- "D407",
- # Section underline should be in the line following the section's name ("{name}")
- "D408",
- # Section underline should match the length of its name ("{name}")
- "D409",
- # No blank lines allowed between a section header and its content ("{name}")
- "D412",
- # Missing blank line after last section ("{name}")
- "D413",
- # Missing argument description in the docstring
- "D417",
- # camcelcase imported as lowercase
- "N813",
- # module import not at top level of file
- "E402",
- # open()` should be replaced by `Path.open()
- "PTH123",
- # subprocess` call: check for execution of untrusted input - https://github.com/PyCQA/bandit/issues/333
- "S603",
- # Starting a process with a partial executable path
- "S607",
- # Prefer absolute imports over relative imports from parent modules
- "TID252",
- # Standard pseudo-random generators are not suitable for cryptographic purposes
- "S311",
- # Unused imports
- "F401",
-]
-[tool.ruff.lint.per-file-ignores]
-"src/spatialdata_io/__init__.py" = ["I001"]
-
-[tool.jupytext]
-formats = "ipynb,md"
+ "B008", # Errors from function calls in argument defaults. These are fine when the result is immutable.
+ "D100", # Missing docstring in public module
+ "D104", # Missing docstring in public package
+ "D105", # __magic__ methods are often self-explanatory, allow missing docstrings
+ "D107", # Missing docstring in __init__
+ # Disable one in each pair of mutually incompatible rules
+ "D203", # We don’t want a blank line before a class docstring
+ "D213", # <> We want docstrings to start immediately after the opening triple quote
+ "D400", # first line should end with a period [Bug: doesn’t work with single-line docstrings]
+ "D401", # First line should be in imperative mood; try rephrasing
+ "E501", # line too long -> we accept long comment lines; formatter gets rid of long code lines
+ "E731", # Do not assign a lambda expression, use a def -> lambda expression assignments are convenient
+ "E741", # allow I, O, l as variable names -> I is the identity matrix
+]
+lint.per-file-ignores."*/__init__.py" = [ "F401" ]
+lint.per-file-ignores."docs/*" = [ "I" ]
+lint.per-file-ignores."tests/*" = [ "D" ]
+lint.pydocstyle.convention = "numpy"
-[tool.hatch.metadata]
-allow-direct-references = true
+# Dependencies that ship neither inline types nor stubs. Listed explicitly rather than
+# globally, so that a newly added untyped dependency is still reported.
+[[tool.mypy.overrides]]
+module = [
+ "dask_image.*",
+ "h5py.*",
+ "multiscale_spatial_image.*",
+ "pyarrow.*",
+ "rasterio.*",
+ "readfcs.*",
+ "rioxarray.*",
+ "scanpy.*",
+ "spatial_image.*",
+ "spatialdata.*",
+]
+ignore_missing_imports = true
-[tool.hatch.build.targets.wheel]
-packages = ['src/spatialdata_io']
+[tool.pytest]
+addopts = [
+ "--import-mode=importlib", # allow using test files with same name
+]
+# used by the third-party test modules that `anndata.tests.helpers` and
+# `spatialdata.testing` pull in, which strict marker checking would otherwise reject
+markers = [
+ "array_api",
+ "gpu",
+ "skip_with_pyarrow_strings",
+ "slow",
+]
+strict = true
+testpaths = [ "tests" ]
+xfail_strict = true
-[tool.hatch.version]
-source = "vcs"
+[tool.coverage]
+run.omit = [
+ "**/test_*.py",
+]
+run.patch = [ "subprocess" ]
+run.source = [ "spatialdata_io" ]
-[tool.hatch.build.hooks.vcs]
-version-file = "_version.py"
+[tool.cruft]
+skip = [
+ ".git",
+ "tests",
+ "src/**/__init__.py",
+ "src/**/basic.py",
+ "docs/api.md",
+ "docs/changelog.md",
+ "docs/references.bib",
+ "docs/references.md",
+ "docs/notebooks/example.ipynb",
+]
diff --git a/src/spatialdata_io/__init__.py b/src/spatialdata_io/__init__.py
index 8badb295..e2d129ce 100644
--- a/src/spatialdata_io/__init__.py
+++ b/src/spatialdata_io/__init__.py
@@ -1,6 +1,6 @@
from importlib import import_module
from importlib.metadata import version
-from typing import Any, TYPE_CHECKING
+from typing import TYPE_CHECKING, Any
__version__ = version("spatialdata-io")
@@ -74,11 +74,12 @@ def __dir__() -> list[str]:
if TYPE_CHECKING:
- # readers
+ from spatialdata_io.converters.generic_to_zarr import generic_to_zarr
from spatialdata_io.readers.codex import codex
from spatialdata_io.readers.cosmx import cosmx
from spatialdata_io.readers.curio import curio
from spatialdata_io.readers.dbit import dbit
+ from spatialdata_io.readers.generic import generic, geojson, image
from spatialdata_io.readers.macsima import macsima
from spatialdata_io.readers.mcmicro import mcmicro
from spatialdata_io.readers.merscope import merscope
@@ -92,9 +93,3 @@ def __dir__() -> list[str]:
xenium_aligned_image,
xenium_explorer_selection,
)
-
- # readers file types
- from spatialdata_io.readers.generic import generic, geojson, image
-
- # converters
- from spatialdata_io.converters.generic_to_zarr import generic_to_zarr
diff --git a/src/spatialdata_io/converters/legacy_anndata.py b/src/spatialdata_io/converters/legacy_anndata.py
index fbf32aa1..04e4c452 100644
--- a/src/spatialdata_io/converters/legacy_anndata.py
+++ b/src/spatialdata_io/converters/legacy_anndata.py
@@ -4,6 +4,7 @@
from typing import TYPE_CHECKING
import numpy as np
+import pandas as pd
from spatialdata import (
SpatialData,
get_centroids,
@@ -332,6 +333,7 @@ def from_legacy_anndata(adata: AnnData, rgb: bool | None = None) -> SpatialData:
shapes_transformations[transform_name] = Scale([scalefactor, scalefactor], axes=("x", "y"))
# validate the spot_diameter_fullres value
+ spot_diameter_fullres: float
if len(spot_diameter_fullres_list) > 0:
d = np.array(spot_diameter_fullres_list)
if not np.allclose(d, d[0]):
@@ -357,6 +359,7 @@ def from_legacy_anndata(adata: AnnData, rgb: bool | None = None) -> SpatialData:
new_table = adata.copy()
if TableModel.ATTRS_KEY in new_table.uns:
del new_table.uns[TableModel.ATTRS_KEY]
+ assert isinstance(new_table.obs, pd.DataFrame)
new_table.obs[REGION_KEY] = REGION
new_table.obs[REGION_KEY] = new_table.obs[REGION_KEY].astype("category")
new_table.obs[INSTANCE_KEY] = shapes[REGION].index.values
diff --git a/docs/extensions/.gitkeep b/src/spatialdata_io/py.typed
similarity index 100%
rename from docs/extensions/.gitkeep
rename to src/spatialdata_io/py.typed
diff --git a/src/spatialdata_io/readers/_utils/_image.py b/src/spatialdata_io/readers/_utils/_image.py
index deba6bf6..d95e5af0 100644
--- a/src/spatialdata_io/readers/_utils/_image.py
+++ b/src/spatialdata_io/readers/_utils/_image.py
@@ -1,6 +1,5 @@
from __future__ import annotations
-from collections.abc import Mapping, Sequence
from typing import TYPE_CHECKING, Any
import dask.array as da
diff --git a/src/spatialdata_io/readers/_utils/_utils.py b/src/spatialdata_io/readers/_utils/_utils.py
index a94ff108..428ac900 100644
--- a/src/spatialdata_io/readers/_utils/_utils.py
+++ b/src/spatialdata_io/readers/_utils/_utils.py
@@ -123,7 +123,12 @@ def parse_physical_size(path: Path | None = None, ome_pixels: Pixels | None = No
from ome_types import from_tiff
from ome_types.model import UnitsLength
- pixels = ome_pixels or from_tiff(path).images[0].pixels
+ if ome_pixels is not None:
+ pixels = ome_pixels
+ elif path is not None:
+ pixels = from_tiff(path).images[0].pixels
+ else:
+ raise ValueError("Either `path` or `ome_pixels` must be provided.")
logger.debug(pixels)
if pixels.physical_size_x_unit != pixels.physical_size_y_unit:
logger.error("Physical units for x and y dimensions are not the same.")
@@ -131,6 +136,9 @@ def parse_physical_size(path: Path | None = None, ome_pixels: Pixels | None = No
if pixels.physical_size_x != pixels.physical_size_y:
logger.error("Physical sizes for x and y dimensions are not the same.")
raise NotImplementedError
+ if pixels.physical_size_x is None:
+ logger.error("Physical size for the x dimension is not set.")
+ raise ValueError("The OME-TIFF metadata does not define a physical size for the x dimension.")
# convert to micrometer if needed
if pixels.physical_size_x_unit == UnitsLength.NANOMETER:
physical_size = pixels.physical_size_x / 1000
diff --git a/src/spatialdata_io/readers/codex.py b/src/spatialdata_io/readers/codex.py
index 38da357f..925ed077 100644
--- a/src/spatialdata_io/readers/codex.py
+++ b/src/spatialdata_io/readers/codex.py
@@ -58,7 +58,7 @@ def codex(
patt = re.compile(".*.fcs") if fcs else re.compile(".*.csv")
path_files = [i for i in os.listdir(path) if patt.match(i)]
if path_files and CodexKeys.FCS_FILE or CodexKeys.FCS_FILE_CSV in patt.pattern:
- fcs = (
+ df = (
readfcs.ReadFCS(path / path_files[0]).data
if CodexKeys.FCS_FILE in path_files[0]
else pd.read_csv(path_files[0], header=0, index_col=None)
@@ -66,7 +66,8 @@ def codex(
else:
raise ValueError("Cannot determine data set. Expecting a file with format .fcs or .csv")
- adata = _codex_df_to_anndata(fcs)
+ adata = _codex_df_to_anndata(df)
+ assert isinstance(adata.obs, pd.DataFrame)
xy = adata.obsm[CodexKeys.SPATIAL_KEY]
shapes = ShapesModel.parse(xy, geometry=0, radius=1, index=adata.obs[CodexKeys.INSTANCE_KEY])
diff --git a/src/spatialdata_io/readers/cosmx.py b/src/spatialdata_io/readers/cosmx.py
index 4c918a4f..382b0f63 100644
--- a/src/spatialdata_io/readers/cosmx.py
+++ b/src/spatialdata_io/readers/cosmx.py
@@ -106,23 +106,22 @@ def cosmx(
raise FileNotFoundError(f"Labels directory not found: {labels_dir}.")
counts = pd.read_csv(counts_file, header=0, index_col=CosmxKeys.INSTANCE_KEY)
- counts.index = counts.index.astype(str).str.cat(counts.pop(CosmxKeys.FOV).astype(str).values, sep="_")
+ counts.index = counts.index.astype(str).str.cat(counts.pop(CosmxKeys.FOV).astype(str).tolist(), sep="_")
obs = pd.read_csv(meta_file, header=0, index_col=CosmxKeys.INSTANCE_KEY)
obs[CosmxKeys.FOV] = pd.Categorical(obs[CosmxKeys.FOV].astype(str))
obs[CosmxKeys.REGION_KEY] = pd.Categorical(obs[CosmxKeys.FOV].astype(str).apply(lambda s: s + "_labels"))
obs[CosmxKeys.INSTANCE_KEY] = obs.index.astype(np.int64)
obs.rename_axis(None, inplace=True)
- obs.index = obs.index.astype(str).str.cat(obs[CosmxKeys.FOV].values, sep="_")
+ obs.index = obs.index.astype(str).str.cat(obs[CosmxKeys.FOV].astype(str).tolist(), sep="_")
common_index = obs.index.intersection(counts.index)
adata = AnnData(
csr_matrix(counts.loc[common_index, :].values),
- dtype=counts.values.dtype,
obs=obs.loc[common_index, :],
)
- adata.var_names = counts.columns
+ adata.var_names = list(counts.columns)
table = TableModel.parse(
adata,
@@ -131,7 +130,9 @@ def cosmx(
instance_key=CosmxKeys.INSTANCE_KEY.value,
)
- fovs_counts = list(map(str, adata.obs.fov.astype(int).unique()))
+ adata_obs = adata.obs
+ assert isinstance(adata_obs, pd.DataFrame)
+ fovs_counts = list(map(str, adata_obs.fov.astype(int).unique()))
affine_transforms_to_global = {}
diff --git a/src/spatialdata_io/readers/curio.py b/src/spatialdata_io/readers/curio.py
index e01de8cc..65531804 100644
--- a/src/spatialdata_io/readers/curio.py
+++ b/src/spatialdata_io/readers/curio.py
@@ -65,9 +65,11 @@ def curio(
var_features_moransi = pd.read_csv(path / file_names[CurioKeys.VAR_FEATURES_MORANSI], sep="\t", header=0)
# adding cluster information in adata.obs
- assert np.array_equal(cluster_assign[0].to_numpy(), adata.obs.index.to_numpy())
- adata.obs = adata.obs.assign(cluster=cluster_assign[1].values)
- adata.obs["cluster"] = adata.obs["cluster"].astype("category")
+ obs = adata.obs
+ assert isinstance(obs, pd.DataFrame)
+ assert np.array_equal(cluster_assign[0].to_numpy(), obs.index.to_numpy())
+ obs = obs.assign(cluster=cluster_assign[1].values)
+ obs["cluster"] = obs["cluster"].astype("category")
# adding metrics information in adata.uns
categories = metrics[CurioKeys.CATEGORY].unique()
@@ -77,12 +79,15 @@ def curio(
adata.uns[CurioKeys.TOP_CLUSTER_DEFINING_FEATURES] = var_features_clusters
# adding Moran's I information in adata.var, for the variable for which it is available
+ var = adata.var
+ assert isinstance(var, pd.DataFrame)
assert set(adata.var_names).issuperset(var_features_moransi.index)
- adata.var.join(var_features_moransi, how="outer")
+ var.join(var_features_moransi, how="outer")
- adata.obs[CurioKeys.REGION_KEY] = CurioKeys.REGION
- adata.obs[CurioKeys.REGION_KEY] = adata.obs[CurioKeys.REGION_KEY].astype("category")
- adata.obs[CurioKeys.INSTANCE_KEY] = adata.obs.index
+ obs[CurioKeys.REGION_KEY] = CurioKeys.REGION
+ obs[CurioKeys.REGION_KEY] = obs[CurioKeys.REGION_KEY].astype("category")
+ obs[CurioKeys.INSTANCE_KEY] = obs.index
+ adata.obs = obs
table = TableModel.parse(
adata,
@@ -92,8 +97,10 @@ def curio(
)
# adding geometry information in a shapes element (we redundantly leave it in obsm['spatial'])
- assert np.array_equal(adata.obsm["spatial"], adata.obsm["X_spatial"])
xy = adata.obsm["spatial"]
+ x_spatial = adata.obsm["X_spatial"]
+ assert isinstance(xy, np.ndarray) and isinstance(x_spatial, np.ndarray)
+ assert np.array_equal(xy, x_spatial)
del adata.obsm["X_spatial"]
shapes = ShapesModel.parse(xy, geometry=0, radius=10, index=adata.obs[CurioKeys.INSTANCE_KEY])
diff --git a/src/spatialdata_io/readers/dbit.py b/src/spatialdata_io/readers/dbit.py
index 03ffb73b..51f96bd8 100644
--- a/src/spatialdata_io/readers/dbit.py
+++ b/src/spatialdata_io/readers/dbit.py
@@ -177,7 +177,9 @@ def _barcode_check(barcode_file: Path) -> pd.DataFrame:
return pd.DataFrame(bc_positions).transpose()
-def _xy2edges(xy: list[int], scale: float = 1.0, border: bool = True, border_scale: float = 1) -> NDArray[np.double]:
+def _xy2edges(
+ xy: NDArray[np.int_], scale: float = 1.0, border: bool = True, border_scale: float = 1
+) -> NDArray[np.double]:
"""Construct vertex coordinate of a square from the barcode coordinates.
The constructed square has a scalable border.
@@ -264,13 +266,12 @@ def dbit(
-------
:class:`spatialdata.SpatialData`.
"""
- if path is not None:
- path = Path(path)
- # if path is invalid, raise error
- if not os.path.isdir(path):
- raise FileNotFoundError(
- f"The path you have passed: {path} has not been found. A correct path to the data directory is needed."
- )
+ path = Path() if path is None else Path(path)
+ # if path is invalid, raise error
+ if not os.path.isdir(path):
+ raise FileNotFoundError(
+ f"The path you have passed: {path} has not been found. A correct path to the data directory is needed."
+ )
# compile regex pattern to find file name in path, according to _constants.DbitKeys()
patt_h5ad = re.compile(f".*{DbitKeys.COUNTS_FILE}")
@@ -279,29 +280,33 @@ def dbit(
# search for files paths. Gives priority to files matching the pattern found in path.
anndata_path_checked = _check_path(
- path=path, # type: ignore
+ path=path,
path_specific=anndata_path,
pattern=patt_h5ad,
key=DbitKeys.COUNTS_FILE,
)[0]
barcode_position_checked = _check_path(
- path=path, # type: ignore
+ path=path,
path_specific=barcode_position,
pattern=patt_barcode,
key=DbitKeys.BARCODE_POSITION,
)[0]
image_path_checked, hasimage = _check_path(
- path=path, # type: ignore
+ path=path,
path_specific=image_path,
pattern=patt_lowres,
key=DbitKeys.IMAGE_LOWRES_FILE,
optional_arg=True,
)
+ # `_check_path` raises unless `optional_arg=True`, so the two mandatory files are always found
+ assert anndata_path_checked is not None
+ assert barcode_position_checked is not None
# read annData.
adata = ad.read_h5ad(anndata_path_checked)
+ assert isinstance(adata.obs, pd.DataFrame)
# Read barcode.
- bc_df = _barcode_check(barcode_file=barcode_position_checked) # type: ignore
+ bc_df = _barcode_check(barcode_file=barcode_position_checked)
# add barcode positions to annData.
# A and B naming follow original publication and protocol
@@ -310,13 +315,13 @@ def dbit(
# sort annData by barcode position. Barcode A first, then Barcode B
idx = adata.obs.sort_values(by=["array_A", "array_B"]).index
adata = adata[idx]
+ assert isinstance(adata.obs, pd.DataFrame)
# populate annData
if dataset_id is None: # if no dataset_id, use file name as id.
logger.warning("No dataset_id received as input.")
- dataset_id = ".".join(
- anndata_path_checked.name.split(".")[:-1] # type: ignore
- ) # this is the filename stripped from the file extension
+ # this is the filename stripped from the file extension
+ dataset_id = ".".join(anndata_path_checked.name.split(".")[:-1])
logger.warning(f"{dataset_id} is used as dataset_id.")
adata.obs["region"] = dataset_id
diff --git a/src/spatialdata_io/readers/generic.py b/src/spatialdata_io/readers/generic.py
index c0d9bc3e..ac2e7e03 100644
--- a/src/spatialdata_io/readers/generic.py
+++ b/src/spatialdata_io/readers/generic.py
@@ -14,11 +14,11 @@
from spatialdata.transformations import Identity
if TYPE_CHECKING:
- from collections.abc import Sequence
+ from collections.abc import Mapping, Sequence
+ from typing import Any
from geopandas import GeoDataFrame
from numpy.typing import NDArray
- from spatialdata.models.models import Chunks_t
from xarray import DataArray
@@ -113,10 +113,10 @@ def _tiff_to_chunks(
2D list of dask arrays representing spatial tiles, each with shape (n_channels, height, width).
"""
# Lazy file reader
- slide = tifffile.memmap(input)
+ memmapped_slide = tifffile.memmap(input)
# Transpose to cyx order
- slide = np.transpose(slide, (axes_dim_mapping["c"], axes_dim_mapping["y"], axes_dim_mapping["x"]))
+ slide = np.transpose(memmapped_slide, (axes_dim_mapping["c"], axes_dim_mapping["y"], axes_dim_mapping["x"]))
# Get dimensions in (y, x)
slide_dimensions = slide.shape[1], slide.shape[2]
@@ -171,7 +171,11 @@ def image(
data_axes: Sequence[str],
coordinate_system: str,
use_tiff_memmap: bool = True,
- chunks: Chunks_t | None = None,
+ chunks: int
+ | tuple[int, ...]
+ | tuple[tuple[int, ...], ...]
+ | Mapping[Any, int | tuple[int, ...] | None]
+ | None = None,
scale_factors: Sequence[int] | None = None,
) -> DataArray:
"""Read an image file and returns a parsed Image2D spatial element.
diff --git a/src/spatialdata_io/readers/macsima.py b/src/spatialdata_io/readers/macsima.py
index 3ccc4137..cb7b3e75 100644
--- a/src/spatialdata_io/readers/macsima.py
+++ b/src/spatialdata_io/readers/macsima.py
@@ -1,7 +1,5 @@
from __future__ import annotations
-import os
-import re
import warnings
from collections import defaultdict
from copy import deepcopy
@@ -23,7 +21,6 @@
from spatialdata_io.readers._utils._utils import (
_set_reader_metadata,
calc_scale_factors,
- parse_channels,
parse_physical_size,
)
@@ -490,14 +487,14 @@ def _get_software_major_version(version: str) -> int:
def _get_translations(ome: OME) -> dict[str, int]:
try:
- translations = {
- "translation_x": ome.images[0].pixels.planes[0].position_x,
- "translation_y": ome.images[0].pixels.planes[0].position_y,
- }
+ plane = ome.images[0].pixels.planes[0]
+ position_x, position_y = plane.position_x, plane.position_y
# If the position attributes are not present the values will be None and we default to (0,0)
- if any(v is None for v in translations.values()):
+ if position_x is None or position_y is None:
logger.debug(f"No translation found for {ome.images[0].name}, defaulting to (0, 0)")
translations = {"translation_x": 0, "translation_y": 0}
+ else:
+ translations = {"translation_x": int(position_x), "translation_y": int(position_y)}
# In case the ome is faulty, also default to (0,0)
except AttributeError:
@@ -801,7 +798,7 @@ def create_sdata(
for p in path_files:
try:
pixels_to_microns = parse_physical_size(p)
- except Exception:
+ except (OSError, ValueError, IndexError, NotImplementedError):
logger.debug(f"Could not parse physical size from {p}. Trying next file.")
continue
if pixels_to_microns is None:
diff --git a/src/spatialdata_io/readers/mcmicro.py b/src/spatialdata_io/readers/mcmicro.py
index 68171da1..66272e60 100644
--- a/src/spatialdata_io/readers/mcmicro.py
+++ b/src/spatialdata_io/readers/mcmicro.py
@@ -260,11 +260,10 @@ def _create_anndata(
region_value = "core_" + sample_id + "_" + labels_basename
table[McmicroKeys.INSTANCE_KEY] = table[McmicroKeys.INSTANCE_KEY]
adata = AnnData(
- table[var].to_numpy(),
+ table[var].to_numpy(dtype=float),
obs=table.drop(columns=var + coords),
var=markers,
- obsm={"spatial": table[coords].to_numpy()},
- dtype=float,
)
+ adata.obsm["spatial"] = table[coords].to_numpy()
adata.obs["region"] = pd.Categorical([region_value] * len(adata))
return adata, region_value
diff --git a/src/spatialdata_io/readers/merscope.py b/src/spatialdata_io/readers/merscope.py
index 0a360700..aa703c35 100644
--- a/src/spatialdata_io/readers/merscope.py
+++ b/src/spatialdata_io/readers/merscope.py
@@ -1,5 +1,6 @@
from __future__ import annotations
+import importlib.util
import re
import warnings
from pathlib import Path
@@ -232,15 +233,12 @@ def merscope(
return _set_reader_metadata(sdata, "merscope")
-def _get_reader(backend: str | None) -> Callable: # type: ignore[type-arg]
+def _get_reader(backend: str | None) -> Callable[..., Image2DModel]:
if backend is not None:
return _rioxarray_load_merscope if backend == "rioxarray" else _dask_image_load_merscope
- try:
- import rioxarray
-
+ if importlib.util.find_spec("rioxarray") is not None:
return _rioxarray_load_merscope
- except ModuleNotFoundError:
- return _dask_image_load_merscope
+ return _dask_image_load_merscope
def _rioxarray_load_merscope(
@@ -335,8 +333,9 @@ def _get_table(
obs = pd.read_csv(obs_path, index_col=0, dtype={MerscopeKeys.METADATA_CELL_KEY: str})
is_gene = ~data.columns.str.lower().str.contains("blank")
- adata = anndata.AnnData(data.loc[:, is_gene], dtype=data.values.dtype, obs=obs)
+ adata = anndata.AnnData(data.loc[:, is_gene], obs=obs)
+ assert isinstance(adata.obs, pd.DataFrame)
adata.obsm["blank"] = data.loc[:, ~is_gene] # blank fields are excluded from adata.X
adata.obsm["spatial"] = adata.obs[[MerscopeKeys.CELL_X, MerscopeKeys.CELL_Y]].values
adata.obs["region"] = pd.Series(vizgen_region, index=adata.obs_names, dtype="category")
diff --git a/src/spatialdata_io/readers/seqfish.py b/src/spatialdata_io/readers/seqfish.py
index d990c38d..31482b7b 100644
--- a/src/spatialdata_io/readers/seqfish.py
+++ b/src/spatialdata_io/readers/seqfish.py
@@ -3,7 +3,6 @@
import os
import re
import warnings
-import xml.etree.ElementTree as ET
from collections.abc import Mapping
from pathlib import Path
from types import MappingProxyType
@@ -169,6 +168,7 @@ def get_transcript_file(roi: str) -> str:
# map tables to cell labels (defined later)
region = os.path.splitext(get_cell_segmentation_labels_file(roi_str))[0]
+ assert isinstance(adata.obs, pd.DataFrame)
adata.obs[SK.REGION_KEY] = region
adata.obs[SK.REGION_KEY] = adata.obs[SK.REGION_KEY].astype("category")
adata.obs[SK.INSTANCE_KEY_TABLE] = instance_id.to_numpy().astype(np.uint16)
@@ -289,8 +289,8 @@ def _is_ome_tiff_multiscale(ome_tiff_file: Path) -> bool:
# for some image files we couldn't find the multiscale information in the omexml metadata, and this method proves to
# be more robust
try:
- zarr_tiff_store = tifffile.imread(ome_tiff_file, is_ome=True, level=1, aszarr=True)
- zarr_tiff_store.close()
+ with tifffile.TiffFile(ome_tiff_file, is_ome=True) as tif:
+ tif.aszarr(level=1).close()
except IndexError:
return False
return True
@@ -307,6 +307,8 @@ def _get_n_pixels(ome_tiff_file: Path) -> int:
def _get_scale_factors_scale0(DAPI_path: Path) -> list[float]:
with tifffile.TiffFile(DAPI_path, is_ome=True) as tif:
+ if tif.ome_metadata is None:
+ raise ValueError(f"Expected OME-XML metadata in {DAPI_path}, found none.")
ome_metadata = xmltodict.parse(tif.ome_metadata)
scalefactor_x = ome_metadata["OME"]["Image"]["Pixels"]["@PhysicalSizeX"]
scalefactor_y = ome_metadata["OME"]["Image"]["Pixels"]["@PhysicalSizeY"]
diff --git a/src/spatialdata_io/readers/steinbock.py b/src/spatialdata_io/readers/steinbock.py
index 786f6509..a4caca8a 100644
--- a/src/spatialdata_io/readers/steinbock.py
+++ b/src/spatialdata_io/readers/steinbock.py
@@ -6,6 +6,7 @@
from typing import TYPE_CHECKING, Any, Literal
import anndata as ad
+import pandas as pd
from dask_image.imread import imread
from spatialdata import SpatialData
from spatialdata._logging import logger
@@ -102,6 +103,7 @@ def steinbock(
)
adata = ad.read_h5ad(path / SteinbockKeys.CELLS_FILE)
+ assert isinstance(adata.obs, pd.DataFrame) and isinstance(adata.var, pd.DataFrame)
idx = adata.obs.index.str.split(" ").map(lambda x: int(x[1]))
regions = adata.obs.image.str.replace(".tiff", "", regex=False)
regions = regions.apply(lambda x: f"{x}_labels")
diff --git a/src/spatialdata_io/readers/stereoseq.py b/src/spatialdata_io/readers/stereoseq.py
index 73f556c1..58673ae9 100644
--- a/src/spatialdata_io/readers/stereoseq.py
+++ b/src/spatialdata_io/readers/stereoseq.py
@@ -59,7 +59,7 @@ def stereoseq(
:class:`spatialdata.SpatialData`
Notes
- _____
+ -----
The cell segmentation, which encodes the background as 0 and the cells as 1, is parsed as an image (i.e. (c, y, x))
object and not as labels object (i.e. (y, x)). If you want to visualize this binary image with napari you will
have to adjust the color limit to be able to see the cells.
@@ -151,6 +151,7 @@ def stereoseq(
var[SK.GENE_EXON] = cellbin_gef[SK.CELL_BIN][SK.GENE_EXON][:]
# merge columns of obs and var to adata.obs and adata.var
+ assert isinstance(adata.obs, pd.DataFrame) and isinstance(adata.var, pd.DataFrame)
obs.index = adata.obs.index
adata.obs = pd.merge(adata.obs, obs, left_index=True, right_index=True)
var.index = adata.var.index
@@ -269,8 +270,11 @@ def stereoseq(
expression = coo_matrix(
(
- df_points[SK.COUNT],
- (index_to_bin_id.loc[df_points.index]["bin_id"].to_numpy(), df_points[SK.FEATURE_KEY].cat.codes),
+ df_points[SK.COUNT].to_numpy(),
+ (
+ index_to_bin_id.loc[df_points.index]["bin_id"].to_numpy(),
+ df_points[SK.FEATURE_KEY].cat.codes.to_numpy(),
+ ),
),
shape=(len(points_coords), len(df_points[SK.FEATURE_KEY].cat.categories)),
).tocsr()
diff --git a/src/spatialdata_io/readers/visium.py b/src/spatialdata_io/readers/visium.py
index bf3690e5..5d9fad58 100644
--- a/src/spatialdata_io/readers/visium.py
+++ b/src/spatialdata_io/readers/visium.py
@@ -172,9 +172,9 @@ def visium(
assert tissue_positions_file.name == VisiumKeys.SPOTS_FILE_2
coords = pd.read_csv(tissue_positions_file, header=0, index_col=0)
+ assert isinstance(adata.obs, pd.DataFrame)
adata.obs = pd.merge(adata.obs, coords, how="left", left_index=True, right_index=True)
- coords = adata.obs[[VisiumKeys.SPOTS_X, VisiumKeys.SPOTS_Y]].values
- adata.obsm["spatial"] = coords
+ adata.obsm["spatial"] = adata.obs[[VisiumKeys.SPOTS_X, VisiumKeys.SPOTS_Y]].values
adata.obs = pd.DataFrame(adata.obs)
adata.obs.drop(columns=[VisiumKeys.SPOTS_X, VisiumKeys.SPOTS_Y], inplace=True)
adata.obs["spot_id"] = np.arange(len(adata))
diff --git a/src/spatialdata_io/readers/visium_hd.py b/src/spatialdata_io/readers/visium_hd.py
index 153eb05d..c0adc433 100644
--- a/src/spatialdata_io/readers/visium_hd.py
+++ b/src/spatialdata_io/readers/visium_hd.py
@@ -103,7 +103,7 @@ def visium_hd(
var_names_make_unique
If `True`, call `.var_names_make_unique()` on each `AnnData` table.
imread_kwargs
- Keyword arguments for :func:`imageio.imread`.
+ Keyword arguments for :func:`imageio.v2.imread`.
image_models_kwargs
Keyword arguments for :class:`spatialdata.models.Image2DModel`.
The ``scale_factors`` key, when provided, overrides the scale factors used to downscale the full-resolution
@@ -395,6 +395,7 @@ def _get_bins(path_bins: Path) -> list[str]:
)
SHAPES_KEY_HD = f"{dataset_id}_{VisiumHDKeys.NUCLEUS_SEG_KEY_HD}"
+ assert isinstance(nucleus_adata_hd.obs, pd.DataFrame)
nucleus_adata_hd.obs["cell_id"] = nucleus_adata_hd.obs.index
nucleus_adata_hd.obs["region"] = SHAPES_KEY_HD
nucleus_adata_hd.obs["region"] = nucleus_adata_hd.obs["region"].astype("category")
@@ -777,8 +778,8 @@ def _make_filtered_nucleus_adata(
and aggregates the data based on specified bin into cell IDs which only contain
the 2um square data under segmented nuclei.
- Parameters:
- -----------
+ Parameters
+ ----------
filtered_matrix_h5_path
Path to the 10x Genomics HDF5 matrix file.
barcode_mappings_parquet_path
@@ -790,8 +791,8 @@ def _make_filtered_nucleus_adata(
gex_only
If `True`, only the gene expression (GEX) data will be loaded.
- Returns:
- --------
+ Returns
+ -------
AnnData
An AnnData object where the observations correspond to filtered cell IDs
and the variables correspond to the original features from the input data.
diff --git a/src/spatialdata_io/readers/xenium.py b/src/spatialdata_io/readers/xenium.py
index fcf2b6e1..9bc5c31a 100644
--- a/src/spatialdata_io/readers/xenium.py
+++ b/src/spatialdata_io/readers/xenium.py
@@ -50,6 +50,13 @@
__all__ = ["xenium", "xenium_aligned_image", "xenium_explorer_selection"]
+def _zarr_array(group: zarr.Group, key: str) -> zarr.Array:
+ node = group[key]
+ if not isinstance(node, zarr.Array):
+ raise TypeError(f"Expected {key!r} to be a zarr array, found {type(node).__name__}.")
+ return node
+
+
@dataclass
class _XeniumCells:
"""Centralised cell-data context for a Xenium output folder.
@@ -134,14 +141,14 @@ def open(cls, path: Path, version: packaging.version.Version | None) -> _XeniumC
XOA version parsed from ``experiment.xenium``, or ``None`` if unavailable.
"""
store = zarr.storage.ZipStore(path / XeniumKeys.CELLS_ZARR, read_only=True)
- group = zarr.open(store, mode="r")
+ group = zarr.open_group(store, mode="r")
# For v < 1.3.0 the zarr cell_id array is 1-D plain integers with no prefix/suffix
# encoding, so no string representation is produced here; cell_id_str stays None and
# downstream code reads cell IDs directly from the parquet files.
cell_id_str = None
if version is not None and version >= packaging.version.parse("1.3.0"):
- cell_id_raw = group["cell_id"][...]
+ cell_id_raw = np.asarray(_zarr_array(group, "cell_id")[...])
cell_id_prefix, dataset_suffix = cell_id_raw[:, 0], cell_id_raw[:, 1]
cell_id_str = cell_id_str_from_prefix_suffix_uint32(cell_id_prefix, dataset_suffix)
@@ -180,7 +187,7 @@ def get_indices_mapping(self, mask_index: int) -> pd.DataFrame | None:
in the cells.zarr.zip structure.
Notes
- ----------
+ -----
For v2.0+ (polygon_sets): uses polygon_sets/{mask_index}/cell_index.
For v1.3.0–v1.x (seg_mask_value): only mask_index=1 (cells) returns a mapping;
mask_index=0 (nuclei) returns None. The nucleus label integers are identical to
@@ -189,12 +196,12 @@ def get_indices_mapping(self, mask_index: int) -> pd.DataFrame | None:
For v < 1.3.0: returns None (no mapping available).
"""
if self.cell_id_str is not None and self.has_polygon_sets:
- cell_index = self.group[f"polygon_sets/{mask_index}/cell_index"][...]
+ cell_index = np.asarray(_zarr_array(self.group, f"polygon_sets/{mask_index}/cell_index")[...])
label_index = np.arange(1, len(cell_index) + 1, dtype=np.int64)
cell_id = self.cell_id_str[cell_index]
return pd.DataFrame({"cell_id": cell_id, "label_index": label_index})
if self.cell_id_str is not None and self.has_seg_mask_value and mask_index == 1:
- label_index = self.group["seg_mask_value"][...]
+ label_index = np.asarray(_zarr_array(self.group, "seg_mask_value")[...])
expected = np.arange(1, len(label_index) + 1, dtype=label_index.dtype)
if not np.array_equal(label_index, expected):
warnings.warn(
@@ -246,9 +253,11 @@ def get_cell_summary(self) -> pd.DataFrame | None:
"""
if self.cell_id_str is None:
return None
- x = self.group["cell_summary"][...]
- column_names = self.group["cell_summary"].attrs["column_names"]
- df = pd.DataFrame(x, columns=column_names)
+ cell_summary = _zarr_array(self.group, "cell_summary")
+ column_names = cell_summary.attrs["column_names"]
+ if not isinstance(column_names, list):
+ raise TypeError(f"Expected 'column_names' to be a list, found {type(column_names).__name__}.")
+ df = pd.DataFrame(np.asarray(cell_summary[...]), columns=column_names)
df[XeniumKeys.CELL_ID] = self.cell_id_str
return df
@@ -438,12 +447,14 @@ def xenium(
if nuc_polys is not None:
polygons["nucleus_boundaries"] = nuc_polys
if cells_boundaries:
- polygons["cell_boundaries"] = _get_polygons(
+ cell_polys = _get_polygons(
path,
XeniumKeys.CELL_BOUNDARIES_FILE,
specs,
indices_mapping=cells_zarr_ctx.cell_indices_mapping,
)
+ if cell_polys is not None:
+ polygons["cell_boundaries"] = cell_polys
if transcripts:
points["transcripts"] = _get_points(path, specs)
@@ -632,7 +643,7 @@ def _get_labels(
"""Read the labels raster from cells.zarr.zip masks/{mask_index}."""
if mask_index not in [0, 1]:
raise ValueError(f"mask_index must be 0 or 1, found {mask_index}.")
- masks = da.from_array(cells_zarr["masks"][f"{mask_index}"])
+ masks = da.from_array(_zarr_array(cells_zarr, f"masks/{mask_index}"))
return Labels2DModel.parse(masks, dims=("y", "x"), transformations={"global": Identity()}, **labels_models_kwargs)
@@ -802,7 +813,10 @@ def _get_morphology_focus(
f"chNNNN_.ome.tif starting with {XeniumKeys.MORPHOLOGY_FOCUS_V4_DAPI_FILENAME.value}"
)
first_tiff_path = morphology_focus_dir / XeniumKeys.MORPHOLOGY_FOCUS_V4_DAPI_FILENAME.value
- ome = from_xml(tifffile.tiffcomment(first_tiff_path), validate=False)
+ ome_xml = tifffile.tiffcomment(first_tiff_path)
+ if ome_xml is None:
+ raise ValueError(f"Expected OME-XML metadata in {first_tiff_path}, found none.")
+ ome = from_xml(ome_xml, validate=False)
# Get channel names from the OME XML
ome_channels = ome.images[0].pixels.channels
diff --git a/tests/_utils.py b/tests/_utils.py
index 5087b900..266581e5 100644
--- a/tests/_utils.py
+++ b/tests/_utils.py
@@ -3,7 +3,7 @@
import pytest
-def skip_if_below_python_version() -> pytest.mark.skipif:
+def skip_if_below_python_version() -> pytest.MarkDecorator:
"""Decorator to skip tests if the Python version is below a specified version.
This decorator prevents running tests on unsupported Python versions. Update the `MIN_VERSION`
@@ -11,12 +11,12 @@ def skip_if_below_python_version() -> pytest.mark.skipif:
Returns
-------
- pytest.mark.skipif
+ pytest.MarkDecorator
A pytest marker that skips the test if the current Python version is below the specified `MIN_VERSION`.
Notes
-----
- The current minimum version is set to Python 3.10. Adjust the `MIN_VERSION` constant as needed
+ The current minimum version is set to Python 3.13. Adjust the `MIN_VERSION` constant as needed
to accommodate newer Python versions.
Examples
diff --git a/tests/test_cli_alignment.py b/tests/test_cli_alignment.py
index cab2c96a..f2d6e6b1 100644
--- a/tests/test_cli_alignment.py
+++ b/tests/test_cli_alignment.py
@@ -30,7 +30,6 @@
"""
import inspect
-from typing import Any
import pytest
diff --git a/tests/test_generic.py b/tests/test_generic.py
index dd46e15d..122f5354 100644
--- a/tests/test_generic.py
+++ b/tests/test_generic.py
@@ -112,11 +112,10 @@ def test_read_generic_image(runner: CliRunner, cli: bool, element_name: str | No
read_generic_wrapper,
[
"--input",
- image_path,
+ str(image_path),
"--output",
- output_zarr_path,
- "--name",
- element_name,
+ str(output_zarr_path),
+ *(() if element_name is None else ("--name", element_name)),
"--data-axes",
"cyx",
"--coordinate-system",
@@ -148,14 +147,15 @@ def test_cli_read_generic_image_invalid_data_axes(runner: CliRunner) -> None:
read_generic_wrapper,
[
"--input",
- image_path,
+ str(image_path),
"--output",
- output_zarr_path,
+ str(output_zarr_path),
"--data-axes",
"invalid_axes",
],
)
assert result.exit_code != 0, result.output
+ assert result.exc_info is not None
assert "data_axes must be a permutation of 'cyx' or 'czyx'." in result.exc_info[1].args[0]
@@ -169,9 +169,9 @@ def test_read_generic_geojson(runner: CliRunner, cli: bool) -> None:
read_generic_wrapper,
[
"--input",
- geojson_path,
+ str(geojson_path),
"--output",
- output_zarr_path,
+ str(output_zarr_path),
"--coordinate-system",
"global",
],
diff --git a/tests/test_macsima.py b/tests/test_macsima.py
index 9be7a3c6..f557f8f4 100644
--- a/tests/test_macsima.py
+++ b/tests/test_macsima.py
@@ -15,6 +15,7 @@
from ome_types import OME
from ome_types.model import (
Image,
+ Map,
MapAnnotation,
Pixels,
Pixels_DimensionOrder,
@@ -484,15 +485,15 @@ def test_cli_macsima(runner: CliRunner, dataset: str) -> None:
macsima_wrapper,
[
"--input",
- f,
+ str(f),
"--output",
- output_zarr,
+ str(output_zarr),
"--subset",
- 500,
+ "500",
"--c-subset",
- 1,
+ "1",
"--multiscale",
- False,
+ "False",
],
)
assert result.exit_code == 0, result.output
@@ -503,8 +504,8 @@ def test_collect_map_annotation_values_with_no_duplicate_keys() -> None:
ome = OME(
structured_annotations=StructuredAnnotations(
map_annotations=[
- MapAnnotation(value={"a": "1", "b": "2"}),
- MapAnnotation(value={"c": "3"}),
+ MapAnnotation(value=Map.model_validate({"a": "1", "b": "2"})),
+ MapAnnotation(value=Map.model_validate({"c": "3"})),
]
)
)
@@ -518,8 +519,8 @@ def test_collect_map_annotations_values_with_duplicate_keys_identical_values() -
ome = OME(
structured_annotations=StructuredAnnotations(
map_annotations=[
- MapAnnotation(value={"a": "1", "b": "2"}),
- MapAnnotation(value={"b": "2", "c": "3"}),
+ MapAnnotation(value=Map.model_validate({"a": "1", "b": "2"})),
+ MapAnnotation(value=Map.model_validate({"b": "2", "c": "3"})),
]
)
)
@@ -533,12 +534,11 @@ def test_collect_map_annotations_values_with_duplicate_keys_different_values() -
ome = OME(
structured_annotations=StructuredAnnotations(
map_annotations=[
- MapAnnotation(value={"a": "1", "b": "2"}),
- MapAnnotation(value={"b": "99", "c": "3"}),
+ MapAnnotation(value=Map.model_validate({"a": "1", "b": "2"})),
+ MapAnnotation(value=Map.model_validate({"b": "99", "c": "3"})),
]
)
)
- import re
result = _collect_map_annotation_values(ome)
@@ -606,13 +606,15 @@ def test_parse_v0_ome_metadata_basic_extraction_and_conversions() -> None:
structured_annotations=StructuredAnnotations(
map_annotations=[
MapAnnotation(
- value={
- "Fluorochrome": "AF488",
- "Exposure time": "123.4",
- "Cycle": "5",
- "ROI ID": "7",
- "MICS cycle type": "AntigenCycle",
- }
+ value=Map.model_validate(
+ {
+ "Fluorochrome": "AF488",
+ "Exposure time": "123.4",
+ "Cycle": "5",
+ "ROI ID": "7",
+ "MICS cycle type": "AntigenCycle",
+ }
+ )
)
]
),
@@ -637,11 +639,13 @@ def test_parse_v0_ome_metadata_handles_missing_or_invalid_numeric_fields() -> No
structured_annotations=StructuredAnnotations(
map_annotations=[
MapAnnotation(
- value={
- "Exposure time": "not-a-number",
- "Cycle": "NaN",
- "ROI ID": "x",
- }
+ value=Map.model_validate(
+ {
+ "Exposure time": "not-a-number",
+ "Cycle": "NaN",
+ "ROI ID": "x",
+ }
+ )
)
]
),
@@ -666,9 +670,11 @@ def test_parse_v0_ome_metadata_falls_back_to_MICScycleID_if_no_cycle_keyword() -
structured_annotations=StructuredAnnotations(
map_annotations=[
MapAnnotation(
- value={
- "MICS cycle ID": "5",
- }
+ value=Map.model_validate(
+ {
+ "MICS cycle ID": "5",
+ }
+ )
)
]
),
@@ -681,7 +687,7 @@ def test_parse_v0_ome_metadata_falls_back_to_MICScycleID_if_no_cycle_keyword() -
def test_parse_v0_ome_metadata_prefers_Cycle_over_MICScycleID_keyword() -> None:
ome = OME(
structured_annotations=StructuredAnnotations(
- map_annotations=[MapAnnotation(value={"MICS cycle ID": "5", "Cycle": "1"})]
+ map_annotations=[MapAnnotation(value=Map.model_validate({"MICS cycle ID": "5", "Cycle": "1"}))]
),
)
@@ -694,9 +700,11 @@ def test_parse_v0_ome_metadata_bleach_cycle_appends_background() -> None:
structured_annotations=StructuredAnnotations(
map_annotations=[
MapAnnotation(
- value={
- "MICS cycle type": "BleachCycle",
- }
+ value=Map.model_validate(
+ {
+ "MICS cycle type": "BleachCycle",
+ }
+ )
)
]
),
@@ -714,9 +722,11 @@ def test_parse_v0_ome_metadata_handles_unknown_imagetypes() -> None:
structured_annotations=StructuredAnnotations(
map_annotations=[
MapAnnotation(
- value={
- "MICS cycle type": "NOT A VALID TYPE",
- }
+ value=Map.model_validate(
+ {
+ "MICS cycle type": "NOT A VALID TYPE",
+ }
+ )
)
]
),
@@ -734,15 +744,17 @@ def test_parse_v1_ome_metadata_basic_extraction_and_conversions() -> None:
structured_annotations=StructuredAnnotations(
map_annotations=[
MapAnnotation(
- value={
- "Clone": "OKT3",
- "Biomarker": "CD3",
- "Fluorochrome": "AF488",
- "ExposureTime": "45.6",
- "Cycle": "3",
- "RoiId": "10",
- "ScanType": "S",
- }
+ value=Map.model_validate(
+ {
+ "Clone": "OKT3",
+ "Biomarker": "CD3",
+ "Fluorochrome": "AF488",
+ "ExposureTime": "45.6",
+ "Cycle": "3",
+ "RoiId": "10",
+ "ScanType": "S",
+ }
+ )
)
]
),
@@ -766,11 +778,13 @@ def test_parse_v1_ome_metadata_invalid_numerics_become_none() -> None:
structured_annotations=StructuredAnnotations(
map_annotations=[
MapAnnotation(
- value={
- "ExposureTime": "x",
- "Cycle": "NaN",
- "RoiId": "ABC",
- }
+ value=Map.model_validate(
+ {
+ "ExposureTime": "x",
+ "Cycle": "NaN",
+ "RoiId": "ABC",
+ }
+ )
)
]
),
@@ -788,9 +802,11 @@ def test_parse_v1_ome_metadata_handles_unknown_imagetypes() -> None:
structured_annotations=StructuredAnnotations(
map_annotations=[
MapAnnotation(
- value={
- "ScanType": "NOT A VALID TYPE",
- }
+ value=Map.model_validate(
+ {
+ "ScanType": "NOT A VALID TYPE",
+ }
+ )
)
]
),
@@ -868,7 +884,7 @@ def make_ome(extra_ma: dict[str, Any] | None = None) -> OME:
)
)
],
- structured_annotations=StructuredAnnotations(map_annotations=[MapAnnotation(value=base)]),
+ structured_annotations=StructuredAnnotations(map_annotations=[MapAnnotation(value=Map.model_validate(base))]),
)
diff --git a/tests/test_seqfish.py b/tests/test_seqfish.py
index b71bfa6a..76fe3ec3 100644
--- a/tests/test_seqfish.py
+++ b/tests/test_seqfish.py
@@ -45,9 +45,9 @@ def test_cli_seqfish(runner: CliRunner, dataset: str) -> None:
seqfish_wrapper,
[
"--input",
- f,
+ str(f),
"--output",
- output_zarr,
+ str(output_zarr),
],
)
assert result.exit_code == 0, result.output
diff --git a/tests/test_xenium.py b/tests/test_xenium.py
index 17903888..53f09961 100644
--- a/tests/test_xenium.py
+++ b/tests/test_xenium.py
@@ -7,7 +7,7 @@
from click.testing import CliRunner
from pytest_mock import MockerFixture
from spatialdata import match_table_to_element, read_zarr
-from spatialdata.models import TableModel, get_table_keys
+from spatialdata.models import get_table_keys
from spatialdata_io.__main__ import xenium_wrapper
from spatialdata_io.readers.xenium import (
@@ -202,9 +202,9 @@ def test_cli_xenium(runner: CliRunner, dataset: str) -> None:
xenium_wrapper,
[
"--input",
- f,
+ str(f),
"--output",
- output_zarr,
+ str(output_zarr),
],
)
assert result.exit_code == 0, result.output