Source code for sctoolbox.utils.decorator

"""Decorators and related functions."""

import scanpy as sc
import functools
import pandas as pd
import matplotlib

from beartype.typing import Callable
from beartype import beartype

import sctoolbox.utils.general as utils


[docs] @beartype def log_anndata(func: Callable) -> Callable: """ Decorate function to log adata inside function call. Parameters ---------- func : Callable Function to decorate. Returns ------- Callable Decorated function """ # TODO store datatypes not supported by scanpy.write as string representation (repr()) @functools.wraps(func) # preserve information of the decorated func def wrapper(*args, **kwargs): # find anndata object within parameters (if there are more use the first one) adata = None for param in list(args) + list(kwargs.values()): if isinstance(param, sc.AnnData): adata = param break if adata is None: raise ValueError("Can only log functions that receive an AnnData object as parameter.") # init adata if necessary if "sctoolbox" not in adata.uns.keys(): adata.uns["sctoolbox"] = dict() if "log" not in adata.uns["sctoolbox"].keys(): adata.uns["sctoolbox"]["log"] = dict() funcname = func.__name__ if funcname not in adata.uns["sctoolbox"]["log"].keys(): adata.uns["sctoolbox"]["log"][funcname] = {} # Convert objects to safe representations, e.g. anndata objects to string representation and tuple to list args_repr = {f"arg{i+1}": element for i, element in enumerate(args)} # create dict with arg1, arg2, ... as keys instead of list to prevent errors with wrongly shaped arrays kwargs_repr = kwargs convert = {sc.AnnData: repr, tuple: list, matplotlib.axes._axes.Axes: str} for typ, convfunc in convert.items(): args_repr = {param: convfunc(element) if isinstance(element, typ) else element for param, element in args_repr.items()} kwargs_repr = {param: convfunc(element) if isinstance(element, typ) else element for param, element in kwargs_repr.items()} # log information on run d = {} d["timestamp"] = utils.get_datetime() d["user"] = utils.get_user() d["func"] = funcname d["args"] = args_repr d["kwargs"] = kwargs_repr run_n = len(adata.uns["sctoolbox"]["log"][funcname]) + 1 adata.uns["sctoolbox"]["log"][funcname][f"run_{run_n}"] = d return func(*args, **kwargs) return wrapper
[docs] @beartype def get_parameter_table(adata: sc.AnnData) -> pd.DataFrame: """ Get a table of all function calls with their parameters from the adata.uns["sctoolbox"] dictionary. Parameters ---------- adata : sc.AnnData Annotated data matrix with logged function calls. Returns ------- pd.DataFrame Table with all function calls and their parameters. Raises ------ ValueError If no logs are found. """ if "sctoolbox" not in adata.uns.keys() or "log" not in adata.uns["sctoolbox"].keys(): raise ValueError("No sctoolbox function calls logged in adata.") # Create an overview table for each function function_tables = [] for function in adata.uns["sctoolbox"]["log"].keys(): table = pd.DataFrame.from_dict(adata.uns["sctoolbox"]["log"][function], orient='index') table.sort_values("timestamp", inplace=True) table.insert(len(table.columns), "func_count", table.index) function_tables.append(table) # Concatenate all tables and sort by timestamp complete_table = pd.concat(function_tables) complete_table.sort_values("timestamp", inplace=True) complete_table.reset_index(drop=True, inplace=True) # reorder columns first_cols = ["func", "args", "kwargs"] complete_table = complete_table.reindex(columns=first_cols + list(set(complete_table.columns) - set(first_cols))) return complete_table
[docs] @beartype def debug_func_log(func: Callable) -> None: """ Decorate function to print function call with arguments and keyword arguments. In progress. Parameters ---------- func : Callable Function to decorate. """ @functools.wraps(func) def wrapper(*args, **kwargs): print(f"DEBUG: {func.__name__} called with args: {args} and kwargs: {kwargs}") return func(*args, **kwargs)