Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

alignedmapping repr & delete methods for anndata attrs #242

Merged
merged 4 commits into from
Nov 13, 2019
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
2 changes: 1 addition & 1 deletion anndata/core/alignedmapping.py
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down
34 changes: 34 additions & 0 deletions anndata/core/anndata.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
"""\
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:
"""\
Expand All @@ -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)."""
Expand All @@ -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]:
"""\
Expand All @@ -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]:
"""\
Expand All @@ -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]:
"""\
Expand All @@ -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]:
"""\
Expand All @@ -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``)."""
Expand Down
28 changes: 25 additions & 3 deletions anndata/tests/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
)


Expand All @@ -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,
Expand Down
11 changes: 11 additions & 0 deletions anndata/tests/test_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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]]),
Expand Down
78 changes: 70 additions & 8 deletions anndata/tests/test_helpers.py
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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():
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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))
62 changes: 62 additions & 0 deletions anndata/tests/test_repr.py
Original file line number Diff line number Diff line change
@@ -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
)
14 changes: 14 additions & 0 deletions anndata/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]])
Expand Down