Skip to content
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
Empty file modified .devcontainer/py/Dockerfile
100644 → 100755
Empty file.
Empty file modified .devcontainer/py/devcontainer.json
100644 → 100755
Empty file.
Empty file modified .github/workflows/ci.yml
100644 → 100755
Empty file.
Empty file modified .gitignore
100644 → 100755
Empty file.
Empty file modified Makefile
100644 → 100755
Empty file.
Empty file modified README.md
100644 → 100755
Empty file.
Empty file modified demo.ipynb
100644 → 100755
Empty file.
Empty file modified make_readme.sh
100644 → 100755
Empty file.
Empty file modified poetry.lock
100644 → 100755
Empty file.
2 changes: 1 addition & 1 deletion pyproject.toml
100644 → 100755
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "omicslog"
version = "0.1.0"
version = "0.2.0"
description = "AnnData subclass that automatically logs mutations to .uns['_omicslog']"
authors = ["Stefano Mangiola <stefano.mangiola@adelaide.edu.au>", "Juan Henao <juanhenao.sanchez@gmail.com>"]
manteiners = ["Juan Henao <juanhenao.sanchez@gmail.com>"]
Expand Down
Empty file modified src/omicslog/__init__.py
100644 → 100755
Empty file.
53 changes: 49 additions & 4 deletions src/omicslog/core.py
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,46 @@ def _parent_set(obj, attr: str, value) -> None:
return
object.__setattr__(obj, attr, value)

class _LoggingDataFrame(pd.DataFrame):
"""DataFrame subclass that logs column mutations to the owning AnnData.

Passes anndata's `isinstance(_, pd.DataFrame)` accessor checks (unlike a
generic proxy) while still intercepting __setitem__/__delitem__.
"""
_metadata = ["_owner", "_label"]

@property
def _constructor(self):
# derived frames (slices, ops) are plain DataFrames, not logged
return pd.DataFrame

def __setitem__(self, key, value):
owner = object.__getattribute__(self, "_owner")
label = object.__getattribute__(self, "_label")
existing = self.columns
verb = "updated" if (isinstance(key, str) and key in existing) else "added"
super().__setitem__(key, value)
# write through to the real container on the AnnData
ad.AnnData.obs.fset(owner, pd.DataFrame(self)) if label == "obs" \
else ad.AnnData.var.fset(owner, pd.DataFrame(self))
if isinstance(key, str):
_append_log_messages(owner, [_format_log_message(label, f"'{key}' {verb}")])

def __delitem__(self, key):
owner = object.__getattribute__(self, "_owner")
label = object.__getattribute__(self, "_label")
super().__delitem__(key)
ad.AnnData.obs.fset(owner, pd.DataFrame(self)) if label == "obs" \
else ad.AnnData.var.fset(owner, pd.DataFrame(self))
_append_log_messages(owner, [_format_log_message(label, f"'{key}' removed")])


def _logged_frame(df: pd.DataFrame, owner, label: str) -> "_LoggingDataFrame":
out = _LoggingDataFrame(df.copy())
object.__setattr__(out, "_owner", owner)
object.__setattr__(out, "_label", label)
return out

class _LoggingProxy:
"""
Transparent proxy for dict-like AnnData components (layers, obsm, varm, ...).
Expand Down Expand Up @@ -270,19 +310,19 @@ def varp(self, value):

@property
def obs(self):
return _LoggingProxy(super().obs, self, "obs")
return _logged_frame(ad.AnnData.obs.fget(self), self, "obs")

@obs.setter
def obs(self, value):
_parent_set(self, "obs", value)
ad.AnnData.obs.fset(self, pd.DataFrame(value) if isinstance(value, _LoggingDataFrame) else value)

@property
def var(self):
return _LoggingProxy(super().var, self, "var")
return _logged_frame(ad.AnnData.var.fget(self), self, "var")

@var.setter
def var(self, value):
ad.AnnData.var.fset(self, value)
ad.AnnData.var.fset(self, pd.DataFrame(value) if isinstance(value, _LoggingDataFrame) else value)

def _unwrap(self) -> ad.AnnData:
"""Plain AnnData view of the real underlying data, without the logging proxies.
Expand Down Expand Up @@ -323,8 +363,13 @@ def _snapshot(self) -> AnnDataSnapshot:
return AnnDataSnapshot.from_anndata(self)

def __getitem__(self, index):
from anndata.acc import AdRef, RefAcc
if isinstance(index, (AdRef, RefAcc)):
return super().__getitem__(index) # returns a column array, not an AnnData
pre = self._snapshot()
result = super().__getitem__(index)
if not isinstance(result, ad.AnnData):
return result
logged_result = self.from_anndata(result)
msgs = _subset_messages(pre, logged_result._snapshot(), operation="subset")
_inherit_and_append(self, logged_result, msgs)
Expand Down
Empty file modified test_omicslog.h5ad
100644 → 100755
Empty file.
Empty file modified tests/__init__.py
100644 → 100755
Empty file.
Loading