Skip to content

Commit e9112a8

Browse files
committed
accessors: allow resolving AdAcc's
1 parent 7cca1dc commit e9112a8

3 files changed

Lines changed: 49 additions & 13 deletions

File tree

docs/accessors.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
See the corresponding [AnnData documentation](inv:anndata:*:doc#accessors).
99

1010
:::{important}
11-
This functionality requires AnnData 0.13 or newer.
11+
This functionality requires AnnData 0.13.2 or newer.
1212
:::
1313

1414
The central [accessor](inv:anndata:*:term#accessor) is [](#A).

src/mudata/acc/__init__.py

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
MultiMapAcc,
2222
RefAcc,
2323
)
24+
from anndata.acc._parse_str import _check_vec
2425
from anndata.compat import XVariable
2526
from anndata.typing import InMemoryArray
2627

@@ -250,33 +251,56 @@ def __getitem__(self, k: str, /) -> ModAcc[R]:
250251
def __repr__(self) -> str:
251252
return "A"
252253

253-
def resolve(self, spec: str, *, strict: bool = True) -> R | None:
254+
def resolve(
255+
self, spec: str, *, strict: bool = True, vec: bool | None = None
256+
) -> (
257+
R
258+
| ModLayerAcc[R]
259+
| ModMultiAcc[R]
260+
| ModGraphAcc[R]
261+
| MultiAcc[R]
262+
| ModMapAcc[R]
263+
| GraphAcc[R]
264+
| ModAcc[R]
265+
| None
266+
):
254267
"""Create :class:`~anndata.acc.AdRef` from a simplified string."""
255268
if not strict:
256269
try:
257-
self.resolve(spec)
270+
self.resolve(spec, vec=vec)
258271
except ValueError:
259272
return None
260273

261274
firstdot = spec.find(".")
262275
if firstdot < 0:
263-
raise ValueError(f"Cannot parse accessor {spec!r} that is not period-separated.")
276+
firstdot = None
264277
firstattr = spec[:firstdot]
265278
match firstattr:
266279
case "mod":
280+
do_vec = firstdot is not None
281+
if not do_vec:
282+
_check_vec(spec, vec=vec, actual=do_vec)
283+
return self.mod
284+
267285
modend = spec.find(".", firstdot + 1)
286+
do_vec = modend >= 0
287+
if not do_vec:
288+
modend = None
289+
_check_vec(spec, vec=vec, actual=do_vec)
268290
mod = spec[firstdot + 1 : modend]
269291
if not mod:
270292
raise ValueError(f"Cannot parse accessor{spec!r} that has an empty modality.")
271293
acc = self.mod[mod]
272-
return super().resolve.__func__(acc, spec[modend + 1 :], strict=strict)
294+
return super().resolve.__func__(acc, spec[modend + 1 :], strict=strict, vec=vec) if do_vec else acc
273295
case "obsmap" | "varmap":
274-
if firstdot == len(spec):
275-
raise ValueError(f"Cannot parse accessor{spec!r} that has an empty modality.")
296+
do_vec = firstdot is not None and firstdot < len(spec)
297+
_check_vec(spec, vec=vec, actual=do_vec)
298+
if not do_vec:
299+
return getattr(self, firstattr)
276300
mod = spec[firstdot + 1 :]
277301
return getattr(self, firstattr)[mod]
278302
case _:
279-
return super().resolve(spec, strict=strict)
303+
return super().resolve(spec, strict=strict, vec=vec)
280304

281305
def to_json(self, ref: R) -> list[str | int | None]:
282306
"""Serialize :class:`~anndata.acc.AdRef` to a JSON-compatible list.

tests/test_accessors.py

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import json
22
from collections.abc import Mapping
3+
from contextlib import nullcontext
34
from dataclasses import fields
45
from importlib import metadata, resources
56
from urllib.request import urlopen
@@ -139,16 +140,27 @@ def test_no_data():
139140
assert field.name not in ("X", "layers")
140141

141142

142-
def test_resolve():
143-
assert A.resolve("mod.rna.X[:, ACT1]") == A.mod["rna"].X[:, "ACT1"]
144-
assert A.resolve("obsmap.rna") == A.obsmap["rna"]
143+
@pytest.mark.parametrize("vec", [None, True, False])
144+
def test_resolve_vec_true(vec: bool):
145+
with pytest.raises(ValueError, match="refers to a vector/") if vec is False else nullcontext():
146+
assert A.resolve("mod.rna.X[:, ACT1]", vec=vec) == A.mod["rna"].X[:, "ACT1"]
147+
assert A.resolve("obsmap.rna", vec=vec) == A.obsmap["rna"]
145148

149+
150+
@pytest.mark.parametrize("vec", [None, True, False])
151+
def test_resolve_vec_false(vec: bool):
152+
with pytest.raises(ValueError, match="refers to a whole container") if vec is True else nullcontext():
153+
assert A.resolve("mod.rna.X", vec=vec) == A.mod["rna"].X
154+
assert A.resolve("mod.rna", vec=vec) == A.mod["rna"]
155+
assert A.resolve("mod", vec=vec) == A.mod
156+
assert A.resolve("obsmap", vec=vec) == A.obsmap
157+
158+
159+
def test_resolve_raises():
146160
with pytest.raises(ValueError, match="Unknown accessor"):
147161
A.resolve("rna.X[:, :]")
148-
149162
with pytest.raises(ValueError, match="empty modality"):
150163
A.resolve("mod..X[:, :]")
151-
152164
with pytest.raises(ValueError, match="period-separated"):
153165
A.resolve("abcd")
154166

0 commit comments

Comments
 (0)