Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
22 changes: 10 additions & 12 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ jobs:
runs-on: ${{ matrix.os }}
defaults:
run:
shell: bash -e {0}
shell: bash # bash also on windows

strategy:
fail-fast: false
Expand All @@ -32,29 +32,27 @@ 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"
python-version: ${{ matrix.python }}
- 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
else
uv sync --extra test
sed -i '' 's/requires-python.*//' pyproject.toml # otherwise uv complains that anndata requires python>=3.12 and we only do >=3.11 😱
Copy link
Member Author

@flying-sheep flying-sheep Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changing the whole test setup to hatch would remove the need for this hack btw.

Not saying you should do it if the current setup works for you, only that having something manage multiple test conditions for you (locally the same way as in CI) is something I enjoy in developing my projects (even though learning hatch’s config semantics takes time)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could also use pixi no? E.g. would solve the same thing (bit biased here as I use pixi more haha))

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
Expand All @@ -63,7 +61,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
Expand Down
27 changes: 15 additions & 12 deletions .readthedocs.yaml
Original file line number Diff line number Diff line change
@@ -1,19 +1,22 @@
# https://docs.readthedocs.io/en/stable/config-file/v2.html
version: 2
build:
os: ubuntu-20.04
os: ubuntu-24.04
tools:
python: "3.11"
sphinx:
configuration: docs/conf.py
fail_on_warning: true
python:
install:
- method: pip
path: .
extra_requirements:
- docs
- torch
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 --extra=torch
- uv run make --directory=docs html
- mv docs/_build $READTHEDOCS_OUTPUT
submodules:
include:
- "docs/tutorials/notebooks"
Expand Down
28 changes: 15 additions & 13 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -80,38 +89,31 @@ benchmark = [
"asv",
"memray",
]
torch = [
"torch"
]
extra = [
"napari-spatialdata[all]",
"spatialdata-plot",
"spatialdata-io",
]

[tool.coverage.run]
source = ["spatialdata"]
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
"-s", # print output from tests
]
# These are all markers coming from xarray, dask or anndata. Added here to silence warnings.
markers = [
"slow: marks tests as slow (deselect with '-m \"not slow\"')",
"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]
Expand Down
2 changes: 1 addition & 1 deletion tests/core/test_centroids.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 <class 'anndata._core.anndata.AnnData'> is not supported."):
with pytest.raises(ValueError, match=r"The object type <class 'anndata.*AnnData'> is not supported"):
get_centroids(adata)


Expand Down
6 changes: 5 additions & 1 deletion tests/io/test_pyramids_performance.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,4 +88,8 @@ def test_write_image_multiscale_performance(sdata_with_image: SpatialData, tmp_p
num_chunks_all_scales.item(),
num_chunks_all_scales.item() + 1,
}
assert actual_num_chunk_reads == num_chunks_scale0.item()
# We set a range here as with certain dask versions more reads occur. This checks whether the range is still
# acceptable, if not then we can check whether it is due to SpatialData or Dask and act accordingly.
# In addition, we could do use a mock side effect to check that the entry points from within spatialdata are within
# the expected range.
assert actual_num_chunk_reads in range(0, num_chunks_scale0.item() * 2 + 1)
29 changes: 17 additions & 12 deletions tests/io/test_readwrite.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -1067,7 +1068,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
Expand Down Expand Up @@ -1096,14 +1097,18 @@ 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
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])
13 changes: 7 additions & 6 deletions tests/models/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -311,7 +312,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"}])
Expand Down Expand Up @@ -937,12 +938,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()