Source code for stats_misc.utils.helpers

"""
Helper functions to support function and class actions.
"""
import numbers
import warnings
import numpy as np
import pandas as pd
from typing import Any
from stats_misc.constants import (
    CLASS_NAME,
)

# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[docs] class ManagedProperty(object): """ A generic property factory defining setters and getters, with optional type validation. Parameters ---------- name : `str` The name of the setters and getters types: `Type`, default `NoneType` Either a single type, or a tuple of types to test against. Methods ------- enable_setter() Enables the setter for the property, allowing attribute assignment. disable_setter() Disables the setter for the property, making the property read-only. set_with_setter(instance, value) Enables the setter, sets the property value, and then disables the setter, ensuring controlled updates. Returns ------- property A property object with getter and setter. """ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ def __init__(self, name: str, types: tuple[type] | type | None = None) -> None: """ Initialize the ManagedProperty. """ self.name = name self.types = types self._setter_enabled = True # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ def __get__(self, instance: object, owner: type) -> Any: """Getter for the property.""" if instance is None: return self return instance.__dict__.get(self.name) # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ def __set__(self, instance: object, value: Any) -> None: """Setter for the property.""" if not self._setter_enabled: raise AttributeError(f"The property '{self.name}' is read-only.") if self.types and not isinstance(value, self.types): raise ValueError( f"Expected any of {self.types}, got {type(value)} " f"for property '{self.name}'." ) instance.__dict__[self.name] = value # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[docs] def enable_setter(self) -> None: """Enable the setter for the property.""" self._setter_enabled = True
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[docs] def disable_setter(self) -> None: """Disable the setter for the property.""" self._setter_enabled = False
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[docs] def set_with_setter(self, instance: object, value: Any) -> None: """ Enable the setter, set the property value, and then disable the setter. Parameters ---------- instance : `object` The instance on which the property is being set. value : `Any` The value to assign to the property. """ try: self.enable_setter() setattr(instance, self.name, value) finally: self.disable_setter()
# @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
[docs] class Results(object): """ A general results class. Parameters ---------- set_args : `list` of `str` Names of the attributes to set on the instance. **kwargs : `Any` Values for each name in ``set_args``. Raises ------ AttributeError If an unrecognised keyword is provided. Warns ----- UserWarning For each name in ``set_args`` not supplied in ``kwargs``. """ # ///////////////////////////////////////////////////////////////////////// # Initiation the class # NOTE include * to force all named arguments to be named (no positional) # args when calling innit. def __init__(self, *, set_args: list[str], **kwargs: Any) -> None: """ Initialise a `Results` instance. """ SET_ARGS = '_setargs' setattr(self,SET_ARGS, set_args) # now set values for k in kwargs.keys(): if k not in getattr(self, SET_ARGS): raise AttributeError("unrecognised argument '{0}'".format(k)) # Loops over `SET_ARGS`, assigns the kwargs content to name `s`. # if argument is missing in kwargs, print a warning. for s in getattr(self, SET_ARGS): try: setattr(self, s, kwargs[s]) except KeyError: warnings.warn("argument '{0}' is set to 'None'".format(s)) setattr(self, s, None) # ///////////////////////////////////////////////////////////////////////// def __str__(self) -> str: """Return a human-readable string representation.""" # assigns a back up name if clas_name is not provided CLASS_NAME_ = getattr(self, CLASS_NAME, type(self).__name__) return f"A `{CLASS_NAME_}` results class." # ///////////////////////////////////////////////////////////////////////// def __repr__(self) -> str: """Return an unambiguous string representation.""" CLASS_NAME_ = getattr(self, CLASS_NAME, type(self).__name__) args = getattr(self, '_setargs') parts = [] # join the keys and values for arg in args: # skip if arg == CLASS_NAME: continue # format value value = getattr(self, arg, None) if isinstance(value, float): formatted = f"{value:.3f}" # check for confidnece intervals elif ( isinstance(value, (list, tuple)) and\ all(isinstance(v, numbers.Real) for v in value) and\ len(value) == 2 ): formatted = f"[{value[0]:.3f}, {value[1]:.3f}]" if isinstance(value, tuple): formatted = f"({value[0]:.3f}, {value[1]:.3f})" # check for array like objects elif isinstance(value, (list, tuple, np.ndarray, pd.Series)): formatted = self._repr_summary(value) else: formatted = repr(value) parts.append(f" {arg}={formatted}") # return a pretty string body = "\n".join(parts) return f"{CLASS_NAME_}\n{body}\n" # ///////////////////////////////////////////////////////////////////////// def _repr_summary( self, value: np.ndarray | list | tuple | pd.Series, max_items: int = 6, precision: int = 3, ) -> str: """ Format an array-like value as a short summary string. Parameters ---------- value : `np.ndarray`, `list`, `tuple`, or `pd.Series` The array-like value to format. max_items : `int`, default 6 Maximum number of elements to display before truncating. precision : `int`, default 3 Decimal precision for floating-point values. Returns ------- str A compact string representation of ``value``. """ if isinstance(value, np.ndarray): array_str = np.array2string( value, precision=precision, threshold=max_items, edgeitems=3, suppress_small=True ) # Indent continuation lines indent_str = ' ' * 2 lines = array_str.splitlines() if len(lines) > 1: indented_array = (f"\n{indent_str} ").join(lines) else: indented_array = lines[0] return ( f"array({indented_array}, shape={value.shape}, " f"dtype={value.dtype})" ) elif isinstance(value, (list, tuple)): sample = value[:max_items] suffix = ", ..." if len(value) > max_items else "" items = ', '.join(repr(v) for v in sample) return f"{type(value).__name__}([{items}{suffix}])" elif isinstance(value, pd.Series): sample = value.iloc[:max_items].tolist() suffix = ", ..." if len(value) > max_items else "" return (f"Series([{', '.join(repr(v) for v in sample)}{suffix}], " f"name={value.name})") return repr(value)