From 09d1ef1a785944b4a8176abfe0e5ade91ccd278b Mon Sep 17 00:00:00 2001 From: Isaac Virshup Date: Tue, 5 Nov 2019 18:18:51 +1100 Subject: [PATCH 1/4] Fix aligned mapping repr --- anndata/core/alignedmapping.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/anndata/core/alignedmapping.py b/anndata/core/alignedmapping.py index ee7fc0b57..6e1007cd7 100644 --- a/anndata/core/alignedmapping.py +++ b/anndata/core/alignedmapping.py @@ -55,7 +55,7 @@ class AlignedMapping(cabc.MutableMapping, ABC): """The actual class (which has it's own data) for this aligned mapping.""" def __repr__(self): - return f"{type(self).__name__} with keys: {self.keys()}" + return f"{type(self).__name__} with keys: {', '.join(self.keys())}" def _ipython_key_completions_(self) -> List[Hashable]: return list(self.keys()) From 258c6f3a12c07cb55075a3355d8e9a728a982db8 Mon Sep 17 00:00:00 2001 From: Isaac Virshup Date: Tue, 5 Nov 2019 18:56:53 +1100 Subject: [PATCH 2/4] Add deleter methods for alignedmapping attributes --- anndata/core/anndata.py | 34 ++++++++++++++++++++++++++++++++++ anndata/tests/test_base.py | 11 +++++++++++ anndata/tests/test_views.py | 14 ++++++++++++++ 3 files changed, 59 insertions(+) diff --git a/anndata/core/anndata.py b/anndata/core/anndata.py index 8f85758f8..3bba5d523 100644 --- a/anndata/core/anndata.py +++ b/anndata/core/anndata.py @@ -1036,6 +1036,10 @@ def layers(self, value): self._init_as_actual(self.copy()) self._layers = layers + @layers.deleter + def layers(self): + self.layers = dict() + @property def raw(self) -> Raw: """\ @@ -1075,6 +1079,8 @@ def raw(self, value: 'AnnData'): @raw.deleter def raw(self): + if self.isview: + self._init_as_actual(self.copy()) self._raw = None @property @@ -1103,6 +1109,10 @@ def obs(self, value: pd.DataFrame): self._init_as_actual(self.copy()) self._obs = value + @obs.deleter + def obs(self): + self.obs = pd.DataFrame(index=self.obs_names) + @property def var(self) -> pd.DataFrame: """\ @@ -1121,6 +1131,10 @@ def var(self, value: pd.DataFrame): self._init_as_actual(self.copy()) self._var = value + @var.deleter + def var(self): + self.var = pd.DataFrame(index=self.var_names) + @property def uns(self) -> MutableMapping: """Unstructured annotation (ordered dictionary).""" @@ -1136,6 +1150,10 @@ def uns(self, value: MutableMapping): self._init_as_actual(self.copy()) self._uns = value + @uns.deleter + def uns(self): + self.uns = OrderedDict() + @property def obsm(self) -> Union[AxisArrays, AxisArraysView]: """\ @@ -1156,6 +1174,10 @@ def obsm(self, value): self._init_as_actual(self.copy()) self._obsm = obsm + @obsm.deleter + def obsm(self): + self.obsm = dict() + @property def varm(self) -> Union[AxisArrays, AxisArraysView]: """\ @@ -1176,6 +1198,10 @@ def varm(self, value): self._init_as_actual(self.copy()) self._varm = varm + @varm.deleter + def varm(self): + self.varm = dict() + @property def obsp(self) -> Union[PairwiseArrays, PairwiseArraysView]: """\ @@ -1196,6 +1222,10 @@ def obsp(self, value): self._init_as_actual(self.copy()) self._obsp = obsp + @obsp.deleter + def obsp(self): + self.obsp = dict() + @property def varp(self) -> Union[PairwiseArrays, PairwiseArraysView]: """\ @@ -1216,6 +1246,10 @@ def varp(self, value): self._init_as_actual(self.copy()) self._varp = varp + @varp.deleter + def varp(self): + self.varp = dict() + @property def obs_names(self) -> pd.Index: """Names of observations (alias for ``.obs.index``).""" diff --git a/anndata/tests/test_base.py b/anndata/tests/test_base.py index 49db025e2..09eef037d 100644 --- a/anndata/tests/test_base.py +++ b/anndata/tests/test_base.py @@ -8,6 +8,7 @@ from scipy.sparse import csr_matrix, isspmatrix_csr from anndata import AnnData +from helpers import assert_equal, gen_adata # some test objects that we use below @@ -101,6 +102,16 @@ def test_df_warnings(): adata.X = df +def test_attr_deletion(): + full = gen_adata((30, 30)) + # Empty has just X, obs_names, var_names + empty = AnnData(full.X, obs=full.obs[[]], var=full.var[[]]) + for attr in ["obs", "var", "obsm", "varm", "obsp", "varp", "layers"]: + delattr(full, attr) + assert_equal(getattr(full, attr), getattr(empty, attr)) + assert_equal(full, empty, exact=True) + + def test_names(): adata = AnnData( np.array([[1, 2, 3], [4, 5, 6]]), diff --git a/anndata/tests/test_views.py b/anndata/tests/test_views.py index fd131a845..09176958f 100644 --- a/anndata/tests/test_views.py +++ b/anndata/tests/test_views.py @@ -338,6 +338,20 @@ def test_view_delitem(attr): assert view_hash != joblib.hash(view) +@pytest.mark.parametrize( + 'attr', ["obs", "var", "obsm", "varm", "obsp", "varp", "layers"] +) +def test_view_delattr(attr): + base = gen_adata((10, 10)) + # Indexing into obs and var just to get indexes + subset = base[5:7, :5] + empty = ad.AnnData(subset.X, obs=subset.obs[[]], var=subset.var[[]]) + delattr(subset, attr) + assert not subset.isview + # Should now have same value as default + assert_equal(getattr(subset, attr), getattr(empty, attr)) + + def test_layers_view(): X = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]]) L = np.array([[10, 11, 12], [13, 14, 15], [16, 17, 18]]) From 818f46501abd4ddbd9df2989a0f1148932d4f06b Mon Sep 17 00:00:00 2001 From: Isaac Virshup Date: Tue, 5 Nov 2019 19:05:03 +1100 Subject: [PATCH 3/4] Improve comparison of axisarrays in tests --- anndata/tests/helpers.py | 28 +++++++++++-- anndata/tests/test_helpers.py | 78 +++++++++++++++++++++++++++++++---- 2 files changed, 95 insertions(+), 11 deletions(-) diff --git a/anndata/tests/helpers.py b/anndata/tests/helpers.py index 227f97028..ba8977697 100644 --- a/anndata/tests/helpers.py +++ b/anndata/tests/helpers.py @@ -13,6 +13,7 @@ from anndata.core.sparsedataset import SparseDataset from anndata import AnnData from anndata.core.views import ArrayView +from anndata.core.alignedmapping import AlignedMapping @singledispatch @@ -271,6 +272,7 @@ def assert_equal(a, b, exact=False, elem_name=None): def assert_equal_ndarray(a, b, exact=False, elem_name=None): b = asarray(b) if not exact and is_numeric_dtype(a) and is_numeric_dtype(b): + assert a.shape == b.shape, format_msg(elem_name) assert np.allclose(a, b, equal_nan=True), format_msg(elem_name) elif ( # Structured dtype not exact @@ -308,7 +310,12 @@ def are_equal_dataframe(a, b, exact=False, elem_name=None): assert_equal(b, a, exact, elem_name) # , a.values maybe? report_name(pd.testing.assert_frame_equal)( - a, b, check_index_type=exact, check_exact=exact, _elem_name=elem_name + a, + b, + check_index_type=exact, + check_exact=exact, + _elem_name=elem_name, + check_frame_type=False, ) @@ -321,12 +328,27 @@ def assert_equal_mapping(a, b, exact=False, elem_name=None): assert_equal(a[k], b[k], exact, f"{elem_name}/{k}") +@assert_equal.register(AlignedMapping) +def assert_equal_alignedmapping(a, b, exact=False, elem_name=None): + a_indices = (a.parent.obs_names, a.parent.var_names) + b_indices = (b.parent.obs_names, b.parent.var_names) + for axis_idx in a.axes: + assert_equal( + a_indices[axis_idx], + b_indices[axis_idx], + exact=exact, + elem_name=axis_idx, + ) + assert a.attrname == b.attrname, format_msg(elem_name) + assert_equal_mapping(a, b, exact=exact, elem_name=elem_name) + + @assert_equal.register(pd.Index) def assert_equal_index(a, b, exact=False, elem_name=None): if not exact: report_name(pd.testing.assert_index_equal)( - a[np.argsort(a)], - b[np.argsort(b)], + a, + b, check_names=False, check_categorical=False, _elem_name=elem_name, diff --git a/anndata/tests/test_helpers.py b/anndata/tests/test_helpers.py index e85efdd97..2d06d0252 100644 --- a/anndata/tests/test_helpers.py +++ b/anndata/tests/test_helpers.py @@ -1,9 +1,11 @@ from string import ascii_letters +import pandas as pd import pytest import numpy as np from scipy import sparse +import anndata as ad from anndata.tests.helpers import assert_equal, report_name, gen_adata # Testing to see if all error types can have the key name appended. @@ -32,6 +34,12 @@ # assert tag in str(err.value) +@pytest.fixture(scope="function") +def reusable_adata(): + """Reusable anndata for when tests shouldn't mutate it""" + return gen_adata((10, 10)) + + # Does this work for every warning? def test_report_name(): def raise_error(): @@ -64,14 +72,15 @@ def test_assert_equal(): adata = gen_adata((10, 10)) adata.raw = adata.copy() assert_equal(adata, adata.copy(), exact=True) - assert_equal( - adata, - adata[ - np.random.permutation(adata.obs_names), - np.random.permutation(adata.var_names), - ].copy(), - exact=False, - ) + # TODO: I'm not sure this is good behaviour, I've disabled in for now. + # assert_equal( + # adata, + # adata[ + # np.random.permutation(adata.obs_names), + # np.random.permutation(adata.var_names), + # ].copy(), + # exact=False, + # ) adata2 = adata.copy() to_modify = list(adata2.layers.keys())[0] del adata2.layers[to_modify] @@ -113,3 +122,56 @@ def test_assert_equal_raw(): to_compare.raw = mod.copy() with pytest.raises(AssertionError): assert_equal(orig, to_compare) + + +# TODO: Should views be equal to actual? +# Should they not be if an exact comparison is made? +def test_assert_equal_alignedmapping(): + adata1 = gen_adata((10, 10)) + adata2 = adata1.copy() + + for attr in ["obsm", "varm", "layers", "obsp", "varp"]: + assert_equal(getattr(adata1, attr), getattr(adata2, attr)) + + # Checking that subsetting other axis only changes some attrs + obs_subset = adata2[:5, :] + for attr in ["obsm", "layers", "obsp"]: + with pytest.raises(AssertionError): + assert_equal(getattr(adata1, attr), getattr(obs_subset, attr)) + for attr in ["varm", "varp"]: + assert_equal(getattr(adata1, attr), getattr(obs_subset, attr)) + + var_subset = adata2[:, 5:] + for attr in ["varm", "layers", "varp"]: + with pytest.raises(AssertionError): + assert_equal(getattr(adata1, attr), getattr(var_subset, attr)) + for attr in ["obsm", "obsp"]: + assert_equal(getattr(adata1, attr), getattr(var_subset, attr)) + + +def test_assert_equal_alignedmapping_empty(): + chars = np.array(list(ascii_letters)) + adata = ad.AnnData( + X=np.zeros((10, 10)), + obs=pd.DataFrame( + [], index=np.random.choice(chars[:20], 10, replace=False) + ), + var=pd.DataFrame( + [], index=np.random.choice(chars[:20], 10, replace=False) + ), + ) + diff_idx = ad.AnnData( + X=np.zeros((10, 10)), + obs=pd.DataFrame( + [], index=np.random.choice(chars[20:], 10, replace=False) + ), + var=pd.DataFrame( + [], index=np.random.choice(chars[20:], 10, replace=False) + ), + ) + same_idx = ad.AnnData(adata.X, obs=adata.obs.copy(), var=adata.var.copy()) + + for attr in ["obsm", "varm", "layers", "obsp", "varp"]: + with pytest.raises(AssertionError): + assert_equal(getattr(adata, attr), getattr(diff_idx, attr)) + assert_equal(getattr(adata, attr), getattr(same_idx, attr)) From ba696498266c7bec2afb1778c312c1d1313fa1fa Mon Sep 17 00:00:00 2001 From: Isaac Virshup Date: Tue, 5 Nov 2019 19:05:51 +1100 Subject: [PATCH 4/4] Add repr tests --- anndata/tests/test_repr.py | 62 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 anndata/tests/test_repr.py diff --git a/anndata/tests/test_repr.py b/anndata/tests/test_repr.py new file mode 100644 index 000000000..015cca27c --- /dev/null +++ b/anndata/tests/test_repr.py @@ -0,0 +1,62 @@ +import re +from string import ascii_letters + +import numpy as np +import pandas as pd +import pytest + +import anndata as ad + +ADATA_ATTRS = ("obs", "var", "varm", "obsm", "layers", "obsp", "varp", "uns") + + +@pytest.fixture +def adata(): + return ad.AnnData( + np.zeros((20, 10)), + obs=pd.DataFrame( + {"obs_key": list(ascii_letters[:20])}, + index=[f"cell{i}" for i in range(20)], + ), + var=pd.DataFrame( + {"var_key": np.arange(10)}, index=[f"gene{i}" for i in range(10)] + ), + varm={"varm_key": np.zeros((10, 20))}, + obsm={"obsm_key": np.zeros((20, 20))}, + layers={"layers_key": np.zeros((20, 10))}, + obsp={"obsp_key": np.zeros((20, 20))}, + varp={"varp_key": np.zeros((10, 10))}, + uns={"uns_key": dict(zip("abc", range(3)))}, + ) + + +@pytest.fixture(params=ADATA_ATTRS) +def adata_attr(request): + return request.param + + +def test_anndata_repr(adata): + assert f"{adata.n_obs} × {adata.n_vars}" in repr(adata) + + for idxr in [ + (slice(10, 20), 12), + (12, 10), + (["cell1", "cell2"], slice(10, 15)), + ]: + v = adata[idxr] + v_repr = repr(v) + assert f"{v.n_obs} × {v.n_vars}" in v_repr + assert "View of" in v_repr + for attr in ADATA_ATTRS: + assert re.search( + rf"^\s+{attr}:[^$]*{attr}_key.*$", v_repr, flags=re.MULTILINE + ) + + +def test_removal(adata, adata_attr): + attr = adata_attr + assert re.search(rf"^\s+{attr}:.*$", repr(adata), flags=re.MULTILINE) + delattr(adata, attr) + assert ( + re.search(rf"^\s+{attr}:.*$", repr(adata), flags=re.MULTILINE) is None + )