From 2a70e736dbb84739b938ce91b700a4d82588b073 Mon Sep 17 00:00:00 2001 From: jdhenaos Date: Thu, 17 Sep 2026 16:16:12 +0200 Subject: [PATCH] new anndata version sync --- .devcontainer/py/Dockerfile | 0 .devcontainer/py/devcontainer.json | 0 .github/workflows/ci.yml | 0 .gitignore | 0 Makefile | 0 README.md | 0 demo.ipynb | 0 make_readme.sh | 0 poetry.lock | 0 pyproject.toml | 2 +- src/omicslog/__init__.py | 0 src/omicslog/core.py | 53 ++++++++++++++++++++++++++--- test_omicslog.h5ad | Bin tests/__init__.py | 0 14 files changed, 50 insertions(+), 5 deletions(-) mode change 100644 => 100755 .devcontainer/py/Dockerfile mode change 100644 => 100755 .devcontainer/py/devcontainer.json mode change 100644 => 100755 .github/workflows/ci.yml mode change 100644 => 100755 .gitignore mode change 100644 => 100755 Makefile mode change 100644 => 100755 README.md mode change 100644 => 100755 demo.ipynb mode change 100644 => 100755 make_readme.sh mode change 100644 => 100755 poetry.lock mode change 100644 => 100755 pyproject.toml mode change 100644 => 100755 src/omicslog/__init__.py mode change 100644 => 100755 src/omicslog/core.py mode change 100644 => 100755 test_omicslog.h5ad mode change 100644 => 100755 tests/__init__.py diff --git a/.devcontainer/py/Dockerfile b/.devcontainer/py/Dockerfile old mode 100644 new mode 100755 diff --git a/.devcontainer/py/devcontainer.json b/.devcontainer/py/devcontainer.json old mode 100644 new mode 100755 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml old mode 100644 new mode 100755 diff --git a/.gitignore b/.gitignore old mode 100644 new mode 100755 diff --git a/Makefile b/Makefile old mode 100644 new mode 100755 diff --git a/README.md b/README.md old mode 100644 new mode 100755 diff --git a/demo.ipynb b/demo.ipynb old mode 100644 new mode 100755 diff --git a/make_readme.sh b/make_readme.sh old mode 100644 new mode 100755 diff --git a/poetry.lock b/poetry.lock old mode 100644 new mode 100755 diff --git a/pyproject.toml b/pyproject.toml old mode 100644 new mode 100755 index 502cbff..c6fbbba --- a/pyproject.toml +++ b/pyproject.toml @@ -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 ", "Juan Henao "] manteiners = ["Juan Henao "] diff --git a/src/omicslog/__init__.py b/src/omicslog/__init__.py old mode 100644 new mode 100755 diff --git a/src/omicslog/core.py b/src/omicslog/core.py old mode 100644 new mode 100755 index 0ad120d..52fd1e5 --- a/src/omicslog/core.py +++ b/src/omicslog/core.py @@ -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, ...). @@ -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. @@ -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) diff --git a/test_omicslog.h5ad b/test_omicslog.h5ad old mode 100644 new mode 100755 diff --git a/tests/__init__.py b/tests/__init__.py old mode 100644 new mode 100755