From c429089d8c9fc59ad11132d449c796598bba3c04 Mon Sep 17 00:00:00 2001 From: Luca Marconato Date: Fri, 8 May 2026 13:33:48 +0200 Subject: [PATCH 1/2] Fix adata layers None after anndata X unification --- src/spatialdata/_core/validation.py | 4 ++++ tests/io/test_multi_table.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/spatialdata/_core/validation.py b/src/spatialdata/_core/validation.py index df354fc3..ad1e7a91 100644 --- a/src/spatialdata/_core/validation.py +++ b/src/spatialdata/_core/validation.py @@ -150,6 +150,8 @@ def check_all_keys_case_insensitively_unique(keys: Collection[str], location: tu exc_type=ValueError, ) as collect_error: for key in keys: + if key is None: + continue normalized_key = key.lower() with collect_error(location=location + (key,)): check_key_is_case_insensitively_unique(key, seen) @@ -247,6 +249,8 @@ def validate_table_attr_keys(data: AnnData, location: tuple[str, ...] = ()) -> N with collect_error(location=attr_path): check_all_keys_case_insensitively_unique(getattr(data, attr).keys(), location=attr_path) for key in getattr(data, attr): + if key is None: + continue key_path = attr_path + (key,) with collect_error(location=key_path): if attr in ("obs", "var"): diff --git a/tests/io/test_multi_table.py b/tests/io/test_multi_table.py index abaaea8d..19c70897 100644 --- a/tests/io/test_multi_table.py +++ b/tests/io/test_multi_table.py @@ -47,7 +47,7 @@ def test_null_values_in_instance_key_column(self, full_sdata: SpatialData): n_obs = full_sdata["table"].n_obs full_sdata["table"].obs["instance_id"] = range(n_obs) # introduce null values - full_sdata["table"].obs.loc[0, "instance_id"] = None + full_sdata["table"].obs.at[full_sdata["table"].obs_names[0], "instance_id"] = None with pytest.raises(ValueError, match="must not contain null values, but it does."): full_sdata.validate_table_in_spatialdata(table=full_sdata["table"]) From c88389eaae492074d52de7f6d23ddbc2d995a116 Mon Sep 17 00:00:00 2001 From: Luca Marconato Date: Mon, 11 May 2026 10:46:00 +0200 Subject: [PATCH 2/2] Handle layers[None] (X) correctly in sanitize_table and add test coverage Skip None when building new_keys so it is never passed to sanitize_name, and preserve it verbatim in new_dict so the layers setter round-trips X correctly on anndata >= 0.13 where X lives at layers[None]. Add test_sanitize_table_layers_preserves_x to verify X is intact after sanitization; the None-in-layers assertion is guarded by a pre-check so the test is valid on both anndata 0.12.x and >= 0.13. Co-Authored-By: Claude Sonnet 4.6 --- src/spatialdata/_core/_utils.py | 6 +++--- tests/utils/test_sanitize.py | 16 ++++++++++++++++ 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/src/spatialdata/_core/_utils.py b/src/spatialdata/_core/_utils.py index a5581565..9dfd613b 100644 --- a/src/spatialdata/_core/_utils.py +++ b/src/spatialdata/_core/_utils.py @@ -158,9 +158,9 @@ def get_unique_name(name: str, attr: str, is_dataframe_column: bool = False) -> # Handle other attributes for attr in ("obsm", "obsp", "varm", "varp", "uns", "layers"): d = getattr(sanitized, attr) - new_keys = {old: get_unique_name(old, attr) for old in d} - # Create new dictionary with sanitized keys - new_dict = {new_keys[old]: value for old, value in d.items()} + # None is a valid key in layers (anndata >= 0.13: represents X); skip sanitizing it + new_keys = {old: get_unique_name(old, attr) for old in d if old is not None} + new_dict = {(new_keys[old] if old is not None else old): value for old, value in d.items()} setattr(sanitized, attr, new_dict) return None if inplace else sanitized diff --git a/tests/utils/test_sanitize.py b/tests/utils/test_sanitize.py index 6b2fd315..dac06943 100644 --- a/tests/utils/test_sanitize.py +++ b/tests/utils/test_sanitize.py @@ -164,6 +164,22 @@ def test_sanitize_table_uns_and_layers(): assert list(sanitized.layers.keys()) == ["bad_layer"] +def test_sanitize_table_layers_preserves_x(): + # anndata >= 0.13 stores X as layers[None]; sanitize_table must not corrupt it + X = np.array([[0, 1], [1, 0]]) + ad = AnnData(X=X, obs=pd.DataFrame({"x": [1, 2]}, index=["0", "1"]), var=pd.DataFrame(index=["v1", "v2"])) + ad.layers["bad#layer"] = np.array([[1, 0], [0, 1]]) + none_in_layers_before = None in ad.layers + sanitized = sanitize_table(ad, inplace=False) + assert sanitized.X is not None + np.testing.assert_array_equal(sanitized.X, X) + string_keys = [k for k in sanitized.layers if k is not None] + assert string_keys == ["bad_layer"] + # If anndata stores X as layers[None], the None key must survive sanitization + if none_in_layers_before: + assert None in sanitized.layers + + def test_sanitize_table_empty_returns_empty(): ad = AnnData() sanitized = sanitize_table(ad, inplace=False)