From 8c8ee5999bbf8bc165b5d97eecaff51deed9d3f1 Mon Sep 17 00:00:00 2001 From: ilan-gold Date: Wed, 11 Feb 2026 17:46:36 +0100 Subject: [PATCH 1/7] fix: integer/reshape for `CoW` of `X` --- src/anndata/_core/anndata.py | 4 ++++ tests/test_views.py | 34 +++++++++++++++++++++++++++------- 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/src/anndata/_core/anndata.py b/src/anndata/_core/anndata.py index 4cfe4eddd..1c86fda8c 100644 --- a/src/anndata/_core/anndata.py +++ b/src/anndata/_core/anndata.py @@ -600,6 +600,10 @@ def X(self) -> XDataType | None: def _handle_view_X_cow(self, value: XDataType | None): if self._is_view: if settings.copy_on_write_X: + if np.isscalar(value) and value is not None: + value = np.full(self.shape, fill_value=value) + if hasattr(value, "shape") and value.shape != self.shape: + value = value.reshape(self.shape) msg = "Setting element `.X` of view, initializing view as actual." warnings.warn(msg, ImplicitModificationWarning, stacklevel=2) new = self._mutated_copy(X=value) diff --git a/tests/test_views.py b/tests/test_views.py index 83bc58748..3095e3cdc 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -24,6 +24,7 @@ SparseCSRArrayView, SparseCSRMatrixView, ) +from anndata._warnings import ImplicitModificationWarning from anndata.compat import NUMPY_2, CSArray, CupyCSCMatrix, DaskArray from anndata.tests.helpers import ( BASE_MATRIX_PARAMS, @@ -119,21 +120,35 @@ def mapping_name(request): return request.param +@pytest.fixture(params=[True, False], ids=["CoW", "update"]) +def copy_on_write_X(request): + return request.param + + # ------------------------------------------------------------------------------ # The test functions # ------------------------------------------------------------------------------ -def test_views(): +def test_views(*, copy_on_write_X: bool): + ad.settings.copy_on_write_X = copy_on_write_X X = np.array(X_list, dtype="int32") adata = ad.AnnData(X, obs=obs_dict, var=var_dict, uns=uns_dict) assert adata[:, 0].is_view assert adata[:, 0].X.tolist() == np.reshape([1, 4, 7], (3, 1)).tolist() - adata[:2, 0].X = [0, 0] + with ( + pytest.warns(ImplicitModificationWarning, match=r"initializing view as actual") + if copy_on_write_X + else nullcontext() + ): + adata[:2, 0].X = [0, 0] - assert adata[:, 0].X.tolist() == np.reshape([0, 0, 7], (3, 1)).tolist() + assert ( + adata[:, 0].X.tolist() + == np.reshape([1, 4, 7] if copy_on_write_X else [0, 0, 7], (3, 1)).tolist() + ) adata_subset = adata[:2, [0, 1]] @@ -385,7 +400,8 @@ def test_not_set_subset_X_dask(matrix_type_no_gpu, subset_func): @IGNORE_SPARSE_EFFICIENCY_WARNING -def test_set_scalar_subset_X(matrix_type, subset_func): +def test_set_scalar_subset_X(matrix_type, subset_func, *, copy_on_write_X: bool): + ad.settings.copy_on_write_X = copy_on_write_X adata = ad.AnnData(matrix_type(np.zeros((10, 10)))) orig_X_val = adata.X.copy() subset_idx = subset_func(adata.obs_names) @@ -394,9 +410,13 @@ def test_set_scalar_subset_X(matrix_type, subset_func): adata_subset.X = 1 - assert adata_subset.is_view - assert np.all(asarray(adata[subset_idx, :].X) == 1) - if isinstance(adata.X, CupyCSCMatrix): + assert adata_subset.is_view != copy_on_write_X + assert np.all( + asarray((adata_subset if copy_on_write_X else adata[subset_idx, :]).X) == 1 + ) + if copy_on_write_X: + assert asarray(orig_X_val == adata.X).all() + elif isinstance(adata.X, CupyCSCMatrix): # Comparison broken for CSC matrices # https://github.com/cupy/cupy/issues/7757 assert asarray(orig_X_val.tocsr() != adata.X.tocsr()).sum() == mul( From 4fa9234964023b1a25b24a878a6dde201cc249d5 Mon Sep 17 00:00:00 2001 From: ilan-gold Date: Thu, 12 Feb 2026 13:11:05 +0100 Subject: [PATCH 2/7] fix: add warning to setting with integer --- src/anndata/_core/anndata.py | 2 ++ tests/test_views.py | 20 +++++++++++++++++--- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/src/anndata/_core/anndata.py b/src/anndata/_core/anndata.py index 1c86fda8c..c95a053d3 100644 --- a/src/anndata/_core/anndata.py +++ b/src/anndata/_core/anndata.py @@ -601,6 +601,8 @@ def _handle_view_X_cow(self, value: XDataType | None): if self._is_view: if settings.copy_on_write_X: if np.isscalar(value) and value is not None: + msg = "The ability to set with a scalar value will be removed in the future. Initializing as an `np.array` with the shape of the current view." + warnings.warn(msg, FutureWarning, stacklevel=2) value = np.full(self.shape, fill_value=value) if hasattr(value, "shape") and value.shape != self.shape: value = value.reshape(self.shape) diff --git a/tests/test_views.py b/tests/test_views.py index 3095e3cdc..9cf855ccf 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -1,6 +1,6 @@ from __future__ import annotations -from contextlib import nullcontext +from contextlib import ExitStack, nullcontext from copy import deepcopy from functools import partial from importlib.metadata import version @@ -407,8 +407,22 @@ def test_set_scalar_subset_X(matrix_type, subset_func, *, copy_on_write_X: bool) subset_idx = subset_func(adata.obs_names) adata_subset = adata[subset_idx, :] - - adata_subset.X = 1 + ctx_managers = ( + ( + pytest.warns( + ImplicitModificationWarning, match=r"initializing view as actual" + ), + pytest.warns( + FutureWarning, match=r"The ability to set with a scalar value" + ), + ) + if copy_on_write_X + else (nullcontext(),) + ) + with ExitStack() as stack: + for mgr in ctx_managers: + stack.enter_context(mgr) + adata_subset.X = 1 assert adata_subset.is_view != copy_on_write_X assert np.all( From e928752e3287bbae8c966b8e2cbc6645de7c441a Mon Sep 17 00:00:00 2001 From: ilan-gold Date: Thu, 12 Feb 2026 13:21:10 +0100 Subject: [PATCH 3/7] chore: add another warning --- src/anndata/_core/anndata.py | 4 +++- tests/test_views.py | 18 +++++++++++++----- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/src/anndata/_core/anndata.py b/src/anndata/_core/anndata.py index c95a053d3..8e4c676c3 100644 --- a/src/anndata/_core/anndata.py +++ b/src/anndata/_core/anndata.py @@ -601,10 +601,12 @@ def _handle_view_X_cow(self, value: XDataType | None): if self._is_view: if settings.copy_on_write_X: if np.isscalar(value) and value is not None: - msg = "The ability to set with a scalar value will be removed in the future. Initializing as an `np.array` with the shape of the current view." + msg = "The ability to set X with a scalar value will be removed in the future. Initializing as an `np.array` with the shape of the current view." warnings.warn(msg, FutureWarning, stacklevel=2) value = np.full(self.shape, fill_value=value) if hasattr(value, "shape") and value.shape != self.shape: + msg = "Automatic reshaping when setting X will be removed in the future." + warnings.warn(msg, FutureWarning, stacklevel=2) value = value.reshape(self.shape) msg = "Setting element `.X` of view, initializing view as actual." warnings.warn(msg, ImplicitModificationWarning, stacklevel=2) diff --git a/tests/test_views.py b/tests/test_views.py index 9cf855ccf..7992080a7 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -138,11 +138,19 @@ def test_views(*, copy_on_write_X: bool): assert adata[:, 0].is_view assert adata[:, 0].X.tolist() == np.reshape([1, 4, 7], (3, 1)).tolist() - with ( - pytest.warns(ImplicitModificationWarning, match=r"initializing view as actual") + ctx_managers = ( + ( + pytest.warns( + ImplicitModificationWarning, match=r"initializing view as actual" + ), + pytest.warns(FutureWarning, match=r"Automatic reshaping"), + ) if copy_on_write_X - else nullcontext() - ): + else (nullcontext(),) + ) + with ExitStack() as stack: + for mgr in ctx_managers: + stack.enter_context(mgr) adata[:2, 0].X = [0, 0] assert ( @@ -413,7 +421,7 @@ def test_set_scalar_subset_X(matrix_type, subset_func, *, copy_on_write_X: bool) ImplicitModificationWarning, match=r"initializing view as actual" ), pytest.warns( - FutureWarning, match=r"The ability to set with a scalar value" + FutureWarning, match=r"The ability to set X with a scalar value" ), ) if copy_on_write_X From 9deccd9cdc1f047abe5cb7112314251644e73560 Mon Sep 17 00:00:00 2001 From: "Philipp A." Date: Thu, 12 Feb 2026 13:28:21 +0100 Subject: [PATCH 4/7] Apply suggestions from code review --- tests/test_views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_views.py b/tests/test_views.py index 7992080a7..cf32fe47f 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -146,7 +146,7 @@ def test_views(*, copy_on_write_X: bool): pytest.warns(FutureWarning, match=r"Automatic reshaping"), ) if copy_on_write_X - else (nullcontext(),) + else () ) with ExitStack() as stack: for mgr in ctx_managers: From 0f59925f0b1b1967280412af0003f67d2f473369 Mon Sep 17 00:00:00 2001 From: "Philipp A." Date: Thu, 12 Feb 2026 13:28:40 +0100 Subject: [PATCH 5/7] Apply suggestion from @flying-sheep --- tests/test_views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_views.py b/tests/test_views.py index cf32fe47f..f70e0418e 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -425,7 +425,7 @@ def test_set_scalar_subset_X(matrix_type, subset_func, *, copy_on_write_X: bool) ), ) if copy_on_write_X - else (nullcontext(),) + else () ) with ExitStack() as stack: for mgr in ctx_managers: From 8fd9e6876817067bdf6f76e55e673dce81ebd790 Mon Sep 17 00:00:00 2001 From: ilan-gold Date: Thu, 12 Feb 2026 13:38:32 +0100 Subject: [PATCH 6/7] fix: `yq` pin --- .github/workflows/test-gpu.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test-gpu.yml b/.github/workflows/test-gpu.yml index 11c3b99fc..de2c2466d 100644 --- a/.github/workflows/test-gpu.yml +++ b/.github/workflows/test-gpu.yml @@ -53,8 +53,9 @@ jobs: run: nvidia-smi - name: Install yq # https://cirun.slack.com/archives/C09SNDRB3A8/p1766512487317849?thread_ts=1766512112.938459&cid=C09SNDRB3A8 + # https://github.com/mikefarah/yq/issues/2592 forces version pin run: | - sudo wget -qO /usr/local/bin/yq https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 + sudo wget -qO /usr/local/bin/yq http://github.com/mikefarah/yq/releases/download/v4.49.2/yq_linux_amd64 sudo chmod +x /usr/local/bin/yq - name: Extract max Python version from classifiers From f3ada7d1c110955c0683498f5b0475c17f29c7c8 Mon Sep 17 00:00:00 2001 From: ilan-gold Date: Thu, 12 Feb 2026 13:51:07 +0100 Subject: [PATCH 7/7] fix: scalar gpu --- tests/test_views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_views.py b/tests/test_views.py index f70e0418e..143053ca2 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -437,7 +437,7 @@ def test_set_scalar_subset_X(matrix_type, subset_func, *, copy_on_write_X: bool) asarray((adata_subset if copy_on_write_X else adata[subset_idx, :]).X) == 1 ) if copy_on_write_X: - assert asarray(orig_X_val == adata.X).all() + assert (asarray(orig_X_val) == asarray(adata.X)).all() elif isinstance(adata.X, CupyCSCMatrix): # Comparison broken for CSC matrices # https://github.com/cupy/cupy/issues/7757