Skip to content
Open
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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ and this project adheres to [Semantic Versioning][].

- `update()`, `push_obs()`, `push_var()`, `pull_obs()`, and `pull_var()` now try harder to preserve the dtype of the dataframe columns

### Deprecated

- `MuData.mod_names`, `MuData.obs_keys`, `MuData.var_keys`, `MuData.obsm_keys`, `MuData.varm_keys`, `MuData.uns_keys`, `MuData.obs_vector`, `MuData.var_vector`.
Their analogs in AnnData will be deprecated in AnnData 0.13.

## [0.3.3]

### Fixed
Expand Down
9 changes: 9 additions & 0 deletions src/mudata/_core/mudata.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
_make_index_unique,
_restore_index,
_update_and_concat,
deprecated,
try_convert_dataframe_to_numpy_dtypes,
)
from .views import DictView
Expand Down Expand Up @@ -1248,6 +1249,7 @@ def _attr_vector(self, key: str, attr: str) -> np.ndarray:
raise KeyError(f"There is no key {key} in MuData .{attr} or in .{attr} of any modalities.")
return df[key].to_numpy()

@deprecated(version="0.3.4")
def obs_vector(self, key: str, layer: str | None = None) -> np.ndarray:
"""Return an array of values for the requested key of length n_obs.

Expand Down Expand Up @@ -1391,6 +1393,7 @@ def n_var(self) -> int:
# )
return self._var.shape[0]

@deprecated(version="0.3.4")
def var_vector(self, key: str, layer: str | None = None) -> np.ndarray:
"""Return an array of values for the requested key of length n_var.

Expand Down Expand Up @@ -1558,22 +1561,27 @@ def uns(self):
# _keys methods to increase compatibility
# with calls requiring those AnnData methods

@deprecated(version="0.3.4", msg="Use `obs.columns` instead.")
def obs_keys(self) -> list[str]:
"""List keys of observation annotation :attr:`obs`."""
return self._obs.keys().tolist()

@deprecated(version="0.3.4", msg="Use `var.columns` instead.")
def var_keys(self) -> list[str]:
"""List keys of variable annotation :attr:`var`."""
return self._var.keys().tolist()

@deprecated(version="0.3.4", msg="Use `obsm.keys()` instead.")
def obsm_keys(self) -> list[str]:
"""List keys of observation annotation :attr:`obsm`."""
return list(self._obsm.keys())

@deprecated(version="0.3.4", msg="Use `varm.keys()` instead.")
def varm_keys(self) -> list[str]:
"""List keys of variable annotation :attr:`varm`."""
return list(self._varm.keys())

@deprecated(version="0.3.4", msg="Use `uns.keys()` instead.")
def uns_keys(self) -> list[str]:
"""List keys of unstructured annotation."""
return list(self._uns.keys())
Expand All @@ -1599,6 +1607,7 @@ def axis(self) -> Literal[-1, 0, 1]:
return self._axis

@property
@deprecated(version="0.3.4", msg="Use `mod.keys()` instead.")
def mod_names(self) -> list[str]:
"""Names of modalities (alias for `list(mdata.mod.keys())`)"""
return list(self._mod.keys())
Expand Down
52 changes: 49 additions & 3 deletions src/mudata/_core/utils.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,58 @@
from __future__ import annotations

from collections import Counter
from collections.abc import Mapping, Sequence
from contextlib import suppress
from typing import Literal, TypeVar
from functools import wraps
from typing import TYPE_CHECKING, Literal
from warnings import warn

import numpy as np
import pandas as pd

T = TypeVar("T", pd.Series, pd.DataFrame)
if TYPE_CHECKING:
from collections.abc import Callable, Mapping, Sequence


def deprecated(version: str, msg: str | None = None):
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems like a good candidate for scverse-misc right?

We don't seem to have this in AnnData: https://anndata.readthedocs.io/en/stable/generated/anndata.AnnData.var_vector.html#anndata.AnnData.var_vector i.e., the deprecation does not appear in the docs

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you guys have this mixin class that explicitly hides deprecated methods from the docs. But I'm happy to move this to scverse-misc.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah true, I guess we will deprecate in 0.13 but maybe this is a nice alternative? I'd like to hear from @flying-sheep what his take is. My feeling is that this is a cleaner solution than deleting from the docs.

def decorate(func: Callable):
if func.__name__ == func.__qualname__:
warnmsg = f"The function {func.__name__} is deprecated and will be removed in the future."
else:
warnmsg = f"The method {func.__qualname__} is deprecated and will be removed in the future."

doc = func.__doc__
indentation = 0
if doc is not None:
lines = doc.expandtabs().splitlines()
with suppress(StopIteration):
for line in lines[1:]:
if not len(line):
continue
for indentation, char in enumerate(line): # noqa: B007
if not char.isspace():
raise StopIteration # break out of both loops
indentation = " " * indentation

docmsg = f"{indentation}.. version-deprecated:: {version}"
if msg is not None:
docmsg += f"\n{indentation} {msg}"
warnmsg += f" {msg}"

if doc is None:
doc = docmsg
else:
body = "\n".join(lines[1:])
doc = f"{lines[0]}\n\n{docmsg}\n{body}"
func.__doc__ = doc

@wraps(func)
def decorated(*args, **kwargs):
warn(warnmsg, FutureWarning, stacklevel=2)
return func(*args, **kwargs)

return decorated

return decorate


def _make_index_unique(df: pd.DataFrame, force: bool = False) -> pd.DataFrame:
Expand Down
59 changes: 59 additions & 0 deletions tests/test_deprecation_decorator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import pytest

from mudata._core.utils import deprecated


@pytest.fixture(params=[None, "Test message."])
def msg(request: pytest.FixtureRequest):
return request.param


@pytest.fixture(
params=[
None,
"Test function",
"""Test function

This is a test.

Parameters
----------
foo
bar
bar
baz
""",
]
)
def docstring(request):
return request.param


@pytest.fixture
def deprecated_func(msg, docstring):
def func(foo, bar):
return 42

func.__doc__ = docstring
return deprecated(version="foo", msg=msg)(func)


def test_deprecation_decorator(deprecated_func, docstring, msg):
with pytest.warns(FutureWarning, match="deprecated"):
assert deprecated_func(1, 2) == 42

lines = deprecated_func.__doc__.expandtabs().splitlines()
if docstring is None:
assert lines[0].startswith(".. version-deprecated::")
else:
lines_orig = docstring.expandtabs().splitlines()
assert lines[0] == lines_orig[0]
assert len(lines[1].strip()) == 0
if len(lines_orig) == 1:
assert lines[2].startswith(".. version-deprecated")
if msg is not None:
assert lines[3] == f" {msg}"
else:
assert lines[2].startswith(" .. version-deprecated")
if msg is not None:
assert lines[3] == f" {msg}"
Loading