From 756874e81c9acf277006d873635030978e948603 Mon Sep 17 00:00:00 2001 From: "Philipp A." Date: Thu, 12 Feb 2026 09:14:32 +0100 Subject: [PATCH 01/14] test: make pre-release tests work --- .github/workflows/release.yaml | 4 ++-- .github/workflows/test.yaml | 17 ++++++----------- .readthedocs.yaml | 29 ++++++++++++++++------------- pyproject.toml | 23 ++++++++++++----------- tests/core/test_centroids.py | 2 +- tests/io/test_readwrite.py | 23 +++++++++++------------ 6 files changed, 48 insertions(+), 50 deletions(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 299ed867b..18493bca3 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -9,9 +9,9 @@ jobs: runs-on: ubuntu-latest if: startsWith(github.ref, 'refs/tags/v') steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v6 - name: Set up Python 3.12 - uses: actions/setup-python@v4 + uses: actions/setup-python@v6 with: python-version: "3.12" cache: pip diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 127986287..3ced8b3ce 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -11,9 +11,6 @@ on: jobs: test: runs-on: ${{ matrix.os }} - defaults: - run: - shell: bash -e {0} strategy: fail-fast: false @@ -32,8 +29,8 @@ jobs: PRERELEASE: ${{ matrix.prerelease }} steps: - - uses: actions/checkout@v2 - - uses: astral-sh/setup-uv@v5 + - uses: actions/checkout@v6 + - uses: astral-sh/setup-uv@v7 id: setup-uv with: version: "latest" @@ -41,12 +38,10 @@ jobs: - name: Install dependencies run: | if [[ "${PRERELEASE}" == "allow" ]]; then - uv sync --extra test - : # uv sync --extra test --prerelease ${PRERELEASE} - uv pip install git+https://github.com/scverse/anndata.git - uv pip install --prerelease allow pandas + uv sync --group=test # --prerelease=${PRERELEASE} + uv pip install git+https://github.com/scverse/anndata.git pandas>=3.dev0 else - uv sync --extra test + uv sync --group=test fi if [[ -n "${DASK_VERSION}" ]]; then if [[ "${DASK_VERSION}" == "latest" ]]; then @@ -63,7 +58,7 @@ jobs: run: | uv run pytest --cov --color=yes --cov-report=xml - name: Upload coverage to Codecov - uses: codecov/codecov-action@v4 + uses: codecov/codecov-action@v5 with: name: coverage verbose: true diff --git a/.readthedocs.yaml b/.readthedocs.yaml index acecf90e6..bea845657 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -1,19 +1,22 @@ # https://docs.readthedocs.io/en/stable/config-file/v2.html version: 2 build: - os: ubuntu-20.04 - tools: - python: "3.11" -sphinx: - configuration: docs/conf.py - fail_on_warning: true -python: - install: - - method: pip - path: . - extra_requirements: - - docs - - torch + os: ubuntu-24.04 + tools: + python: '3.13' + jobs: + post_checkout: + # unshallow so version can be derived from tag + - git fetch --unshallow || true + create_environment: + - asdf plugin add uv + - asdf install uv latest + - asdf global uv latest + build: + html: + - uv sync --group=docs --group=torch + - make --directory=docs build + - mv docs/_build $READTHEDOCS_OUTPUT submodules: include: - "docs/tutorials/notebooks" diff --git a/pyproject.toml b/pyproject.toml index 89e9e0235..77b913b91 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -51,8 +51,17 @@ dependencies = [ "xarray-spatial>=0.3.5", "zarr>=3.0.0", ] - [project.optional-dependencies] +torch = [ + "torch" +] +extra = [ + "napari-spatialdata[all]", + "spatialdata-plot", + "spatialdata-io", +] + +[dependency-groups] dev = [ "bump2version", "sentry-prevent-cli", @@ -80,14 +89,6 @@ benchmark = [ "asv", "memray", ] -torch = [ - "torch" -] -extra = [ - "napari-spatialdata[all]", - "spatialdata-plot", - "spatialdata-io", -] [tool.coverage.run] source = ["spatialdata"] @@ -95,9 +96,9 @@ omit = [ "**/test_*.py", ] -[tool.pytest.ini_options] +[tool.pytest] testpaths = ["tests"] -xfail_strict = true +strict = true addopts = [ # "-Werror", # if 3rd party libs raise DeprecationWarnings, just use filterwarnings below "--import-mode=importlib", # allow using test files with same name diff --git a/tests/core/test_centroids.py b/tests/core/test_centroids.py index aa332f9da..9679c3ff1 100644 --- a/tests/core/test_centroids.py +++ b/tests/core/test_centroids.py @@ -183,7 +183,7 @@ def test_get_centroids_invalid_element(images): region_key="region", instance_key="instance_id", ) - with pytest.raises(ValueError, match="The object type is not supported."): + with pytest.raises(ValueError, match=r"The object type is not supported"): get_centroids(adata) diff --git a/tests/io/test_readwrite.py b/tests/io/test_readwrite.py index af028d29c..9d191f0d9 100644 --- a/tests/io/test_readwrite.py +++ b/tests/io/test_readwrite.py @@ -1067,7 +1067,7 @@ def test_read_sdata(tmp_path: Path, points: SpatialData) -> None: assert_spatial_data_objects_are_identical(sdata_from_path, sdata_from_zarr_group) -def test_sdata_with_nan_in_obs() -> None: +def test_sdata_with_nan_in_obs(tmp_path: Path) -> None: """Test writing SpatialData with mixed string/NaN values in obs works correctly. Regression test for https://github.com/scverse/spatialdata/issues/399 @@ -1096,14 +1096,13 @@ def test_sdata_with_nan_in_obs() -> None: assert sdata["table"].obs["column_only_region1"].iloc[1] is np.nan assert np.isnan(sdata["table"].obs["column_only_region2"].iloc[0]) - with tempfile.TemporaryDirectory() as tmpdir: - path = os.path.join(tmpdir, "data.zarr") - sdata.write(path) - - sdata2 = SpatialData.read(path) - assert "column_only_region1" in sdata2["table"].obs.columns - assert sdata2["table"].obs["column_only_region1"].iloc[0] == "string" - assert sdata2["table"].obs["column_only_region2"].iloc[1] == 3 - # After round-trip, NaN in object-dtype column becomes string "nan" - assert sdata2["table"].obs["column_only_region1"].iloc[1] == "nan" - assert np.isnan(sdata2["table"].obs["column_only_region2"].iloc[0]) + path = tmp_path / "data.zarr" + sdata.write(path) + + sdata2 = SpatialData.read(path) + assert "column_only_region1" in sdata2["table"].obs.columns + assert sdata2["table"].obs["column_only_region1"].iloc[0] == "string" + assert sdata2["table"].obs["column_only_region2"].iloc[1] == 3 + # After round-trip, NaN in object-dtype column becomes string "nan" + assert sdata2["table"].obs["column_only_region1"].iloc[1] == "nan" + assert np.isnan(sdata2["table"].obs["column_only_region2"].iloc[0]) From 42d99d5faaf7900d788932de3e6f6bef00245f8f Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 12 Feb 2026 08:14:58 +0000 Subject: [PATCH 02/14] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- .readthedocs.yaml | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/.readthedocs.yaml b/.readthedocs.yaml index bea845657..98bcede76 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -1,22 +1,22 @@ # https://docs.readthedocs.io/en/stable/config-file/v2.html version: 2 build: - os: ubuntu-24.04 - tools: - python: '3.13' - jobs: - post_checkout: - # unshallow so version can be derived from tag - - git fetch --unshallow || true - create_environment: - - asdf plugin add uv - - asdf install uv latest - - asdf global uv latest - build: - html: - - uv sync --group=docs --group=torch - - make --directory=docs build - - mv docs/_build $READTHEDOCS_OUTPUT + os: ubuntu-24.04 + tools: + python: "3.13" + jobs: + post_checkout: + # unshallow so version can be derived from tag + - git fetch --unshallow || true + create_environment: + - asdf plugin add uv + - asdf install uv latest + - asdf global uv latest + build: + html: + - uv sync --group=docs --group=torch + - make --directory=docs build + - mv docs/_build $READTHEDOCS_OUTPUT submodules: include: - "docs/tutorials/notebooks" From 9626940f901020aebf34bbae184fe4865ff4fbcb Mon Sep 17 00:00:00 2001 From: "Philipp A." Date: Thu, 12 Feb 2026 09:16:59 +0100 Subject: [PATCH 03/14] restore bash --- .github/workflows/test.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 3ced8b3ce..b0d579c29 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -11,6 +11,9 @@ on: jobs: test: runs-on: ${{ matrix.os }} + defaults: + run: + shell: bash # bash also on windows strategy: fail-fast: false From 8361a43d411b5dc2753dd286d2e50ac471fcc415 Mon Sep 17 00:00:00 2001 From: "Philipp A." Date: Thu, 12 Feb 2026 09:23:47 +0100 Subject: [PATCH 04/14] fix test ids --- tests/models/test_models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/models/test_models.py b/tests/models/test_models.py index e2087ace0..f5f7c6751 100644 --- a/tests/models/test_models.py +++ b/tests/models/test_models.py @@ -311,7 +311,7 @@ def test_shapes_model(self, model: ShapesModel, path: Path) -> None: @pytest.mark.parametrize("model", [PointsModel]) @pytest.mark.parametrize("instance_key", [None, "cell_id"]) @pytest.mark.parametrize("feature_key", [None, "target"]) - @pytest.mark.parametrize("typ", [np.ndarray, pd.DataFrame, dd.DataFrame]) + @pytest.mark.parametrize("typ", [np.ndarray, pd.DataFrame, dd.DataFrame], ids=["numpy", "pandas", "dask"]) @pytest.mark.parametrize("is_annotation", [True, False]) @pytest.mark.parametrize("is_3d", [True, False]) @pytest.mark.parametrize("coordinates", [None, {"x": "A", "y": "B", "z": "C"}]) From 775115b560e7ec28daf214865362a6f44d068bc9 Mon Sep 17 00:00:00 2001 From: "Philipp A." Date: Thu, 12 Feb 2026 09:26:36 +0100 Subject: [PATCH 05/14] add marker --- pyproject.toml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 77b913b91..fb06b8611 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -100,7 +100,6 @@ omit = [ testpaths = ["tests"] strict = true addopts = [ -# "-Werror", # if 3rd party libs raise DeprecationWarnings, just use filterwarnings below "--import-mode=importlib", # allow using test files with same name "-s", # print output from tests ] @@ -108,11 +107,13 @@ addopts = [ markers = [ "slow: marks tests as slow (deselect with '-m \"not slow\"')", "gpu: run test on GPU using CuPY.", + "array_api: used by anndata.tests.helpers, not us", "skip_with_pyarrow_strings: skipwhen pyarrow string conversion is turned on", ] # info on how to use this https://stackoverflow.com/questions/57925071/how-do-i-avoid-getting-deprecationwarning-from-inside-dependencies-with-pytest filterwarnings = [ - # "ignore:.*U.*mode is deprecated:DeprecationWarning", + # "error", # if 3rd party libs raise DeprecationWarnings, TODO: filter them individually below + # "ignore:.*U.*mode is deprecated:DeprecationWarning", ] [tool.jupytext] From c95ecbfd5fab11d892632f6c0ee0e893d418f04c Mon Sep 17 00:00:00 2001 From: "Philipp A." Date: Thu, 12 Feb 2026 09:28:21 +0100 Subject: [PATCH 06/14] fix docs --- .readthedocs.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 98bcede76..4bbc59371 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -14,7 +14,7 @@ build: - asdf global uv latest build: html: - - uv sync --group=docs --group=torch + - uv sync --group=docs --extra=torch - make --directory=docs build - mv docs/_build $READTHEDOCS_OUTPUT submodules: From 15f819338439fa9d78f0c733cc20ddc0c9630be3 Mon Sep 17 00:00:00 2001 From: "Philipp A." Date: Thu, 12 Feb 2026 09:30:40 +0100 Subject: [PATCH 07/14] pandas 3 this time? --- .github/workflows/test.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index b0d579c29..9489adf0a 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -42,7 +42,7 @@ jobs: run: | if [[ "${PRERELEASE}" == "allow" ]]; then uv sync --group=test # --prerelease=${PRERELEASE} - uv pip install git+https://github.com/scverse/anndata.git pandas>=3.dev0 + uv pip install --prerelease=allow git+https://github.com/scverse/anndata.git pandas>=3 else uv sync --group=test fi From ae6a5699baeb81edb7528bcbaa1d99a441178433 Mon Sep 17 00:00:00 2001 From: "Philipp A." Date: Thu, 12 Feb 2026 09:37:29 +0100 Subject: [PATCH 08/14] use separate command because wtf? --- .github/workflows/test.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 9489adf0a..b13c0ace9 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -42,7 +42,8 @@ jobs: run: | if [[ "${PRERELEASE}" == "allow" ]]; then uv sync --group=test # --prerelease=${PRERELEASE} - uv pip install --prerelease=allow git+https://github.com/scverse/anndata.git pandas>=3 + uv pip install git+https://github.com/scverse/anndata.git + uv pip install --prerelease=allow pandas>=3 else uv sync --group=test fi From b92940a53c2a253fe811ce9bc9ae996e4db90a04 Mon Sep 17 00:00:00 2001 From: "Philipp A." Date: Thu, 12 Feb 2026 09:41:14 +0100 Subject: [PATCH 09/14] use `add` --- .github/workflows/test.yaml | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index b13c0ace9..5060616dd 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -41,19 +41,17 @@ jobs: - name: Install dependencies run: | if [[ "${PRERELEASE}" == "allow" ]]; then - uv sync --group=test # --prerelease=${PRERELEASE} - uv pip install git+https://github.com/scverse/anndata.git - uv pip install --prerelease=allow pandas>=3 - else - uv sync --group=test + uv add git+https://github.com/scverse/anndata.git + uv add pandas>=3.dev0 fi if [[ -n "${DASK_VERSION}" ]]; then if [[ "${DASK_VERSION}" == "latest" ]]; then - uv pip install --upgrade dask + uv add dask else - uv pip install dask==${DASK_VERSION} + uv add dask==${DASK_VERSION} fi fi + uv sync --group=test - name: Test env: MPLBACKEND: agg From 09ebe00fdee5fa31be1d552aa6536b7e7cae6974 Mon Sep 17 00:00:00 2001 From: "Philipp A." Date: Thu, 12 Feb 2026 09:47:17 +0100 Subject: [PATCH 10/14] shut up uv --- .github/workflows/test.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 5060616dd..2885c84bf 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -41,6 +41,7 @@ jobs: - name: Install dependencies run: | if [[ "${PRERELEASE}" == "allow" ]]; then + sed -i 's/requires-python.*//' uv add git+https://github.com/scverse/anndata.git uv add pandas>=3.dev0 fi From 6c5a7e0071afe7b910bff03c4a5317ba432ce2c0 Mon Sep 17 00:00:00 2001 From: "Philipp A." Date: Thu, 12 Feb 2026 09:48:43 +0100 Subject: [PATCH 11/14] ugh --- .github/workflows/test.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 2885c84bf..739c8654b 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -41,7 +41,7 @@ jobs: - name: Install dependencies run: | if [[ "${PRERELEASE}" == "allow" ]]; then - sed -i 's/requires-python.*//' + sed -i 's/requires-python.*//' pyproject.toml # otherwise uv complains that anndata requires python>=3.12 and we only do >=3.11 😱 uv add git+https://github.com/scverse/anndata.git uv add pandas>=3.dev0 fi From cd3727208c5b38da2e47463287a1416ea735b14f Mon Sep 17 00:00:00 2001 From: "Philipp A." Date: Thu, 12 Feb 2026 09:49:59 +0100 Subject: [PATCH 12/14] macos bullshit --- .github/workflows/test.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 739c8654b..efd31ef3f 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -41,7 +41,7 @@ jobs: - name: Install dependencies run: | if [[ "${PRERELEASE}" == "allow" ]]; then - sed -i 's/requires-python.*//' pyproject.toml # otherwise uv complains that anndata requires python>=3.12 and we only do >=3.11 😱 + sed -i '' 's/requires-python.*//' pyproject.toml # otherwise uv complains that anndata requires python>=3.12 and we only do >=3.11 😱 uv add git+https://github.com/scverse/anndata.git uv add pandas>=3.dev0 fi From f5b659e33be37e41cbc5d0eccf6656478fed74e9 Mon Sep 17 00:00:00 2001 From: "Philipp A." Date: Thu, 12 Feb 2026 10:05:50 +0100 Subject: [PATCH 13/14] quote things --- .github/workflows/test.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index efd31ef3f..196c6d8cb 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -42,8 +42,8 @@ jobs: run: | if [[ "${PRERELEASE}" == "allow" ]]; then sed -i '' 's/requires-python.*//' pyproject.toml # otherwise uv complains that anndata requires python>=3.12 and we only do >=3.11 😱 - uv add git+https://github.com/scverse/anndata.git - uv add pandas>=3.dev0 + uv add 'git+https://github.com/scverse/anndata.git' + uv add --prerelease=allow 'pandas>=3' fi if [[ -n "${DASK_VERSION}" ]]; then if [[ "${DASK_VERSION}" == "latest" ]]; then From 862eb64e3723d842cbe1d85f69309a541d95d06c Mon Sep 17 00:00:00 2001 From: "Philipp A." Date: Thu, 12 Feb 2026 10:11:19 +0100 Subject: [PATCH 14/14] fix tests --- tests/io/test_readwrite.py | 16 +++++++++++----- tests/models/test_models.py | 11 ++++++----- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/tests/io/test_readwrite.py b/tests/io/test_readwrite.py index 9d191f0d9..e6d23eee2 100644 --- a/tests/io/test_readwrite.py +++ b/tests/io/test_readwrite.py @@ -13,6 +13,7 @@ import zarr from anndata import AnnData from numpy.random import default_rng +from packaging.version import Version from shapely import MultiPolygon, Polygon from upath import UPath from zarr.errors import GroupNotFoundError @@ -1101,8 +1102,13 @@ def test_sdata_with_nan_in_obs(tmp_path: Path) -> None: sdata2 = SpatialData.read(path) assert "column_only_region1" in sdata2["table"].obs.columns - assert sdata2["table"].obs["column_only_region1"].iloc[0] == "string" - assert sdata2["table"].obs["column_only_region2"].iloc[1] == 3 - # After round-trip, NaN in object-dtype column becomes string "nan" - assert sdata2["table"].obs["column_only_region1"].iloc[1] == "nan" - assert np.isnan(sdata2["table"].obs["column_only_region2"].iloc[0]) + r1 = sdata2["table"].obs["column_only_region1"] + r2 = sdata2["table"].obs["column_only_region2"] + + assert r1.iloc[0] == "string" + assert r2.iloc[1] == 3 + if Version(pd.__version__) >= Version("3"): + assert pd.isna(r1.iloc[1]) + else: # After round-trip, NaN in object-dtype column becomes string "nan" on pandas 2 + assert r1.iloc[1] == "nan" + assert np.isnan(r2.iloc[0]) diff --git a/tests/models/test_models.py b/tests/models/test_models.py index f5f7c6751..1e61c33ac 100644 --- a/tests/models/test_models.py +++ b/tests/models/test_models.py @@ -18,6 +18,7 @@ from dask.dataframe import DataFrame as DaskDataFrame from geopandas import GeoDataFrame from numpy.random import default_rng +from packaging.version import Version from shapely.geometry import MultiPolygon, Point, Polygon from shapely.io import to_ragged_array from spatial_image import to_spatial_image @@ -880,12 +881,12 @@ def test_categories_on_partitioned_dataframe(sdata_blobs: SpatialData): assert np.array_equal(df["genes"].to_numpy(), ddf_parsed["genes"].compute().to_numpy()) assert set(df["genes"].cat.categories.tolist()) == set(ddf_parsed["genes"].compute().cat.categories.tolist()) - # two behavior to investigate later/report to dask (they originate in dask) - # TODO: df['genes'].cat.categories has dtype 'object', while ddf_parsed['genes'].compute().cat.categories has dtype - # 'string' - # this problem should disappear after pandas 3.0 is released - assert df["genes"].cat.categories.dtype == "object" + if Version(pd.__version__) >= Version("3"): + assert df["genes"].cat.categories.dtype == "string" + else: + assert df["genes"].cat.categories.dtype == "object" assert ddf_parsed["genes"].compute().cat.categories.dtype == "string" + # behavior to investigate later/report to dask # TODO: the list of categories are not preserving the order assert df["genes"].cat.categories.tolist() != ddf_parsed["genes"].compute().cat.categories.tolist()