Source code for datasalad.settings.setting

from __future__ import annotations

from copy import copy
from typing import (
    TYPE_CHECKING,
    Any,
)

if TYPE_CHECKING:
    from collections.abc import Callable


[docs] class UnsetValue: """Placeholder type to indicate a value that has not been set"""
class Setting: """Representation of an individual setting""" def __init__( self, value: Any | UnsetValue = UnsetValue, *, coercer: Callable | None = None, lazy: bool = False, ): """ ``value`` can be of any type. A setting instance created with default :class:`UnsetValue` represents a setting with no known value. The ``coercer`` is a callable that processes a setting value on access via :attr:`value`. This callable can perform arbitrary processing, including type conversion and validation. If ``lazy`` is ``True``, ``value`` must be a callable that requires no parameters. This callable will be executed each time :attr:`value` is accessed, and its return value is passed to the ``coercer``. """ if lazy and not callable(value): msg = 'callable required for lazy evaluation' raise ValueError(msg) self._value = value self._coercer = coercer self._lazy = lazy @property def pristine_value(self) -> Any: """Original, uncoerced value""" return self._value @property def value(self) -> Any: """Value of a setting after coercion For a lazy setting, accessing this property also triggers the evaluation. """ # we ignore the type error here # "error: "UnsetValue" not callable" # because we rule this out in the constructor val = self._value() if self._lazy else self._value # type: ignore [operator] if self._coercer: return self._coercer(val) return val @property def coercer(self) -> Callable | None: """``coercer`` of a setting, or ``None`` if there is none""" return self._coercer @property def is_lazy(self) -> bool: """Flag whether the setting evaluates on access""" return self._lazy def update(self, other: Setting) -> None: """Update the item from another This replaces any ``value`` or ``coercer`` set in the other setting. If case the other's ``value`` is :class:`UnsetValue` no update of the ``value`` is made. Likewise, if ``coercer`` is ``None``, no update is made. Update to or from a ``lazy`` value will also update the ``lazy`` property accordingly. """ if other._value is not UnsetValue: self._value = other._value # we also need to synchronize the lazy eval flag # so we can do the right thing (TM) with the # new value self._lazy = other._lazy if other._coercer: self._coercer = other._coercer def __str__(self) -> str: # wrap the value in the classname to make clear that # the actual object type is different from the value return f'{self.__class__.__name__}({self._value})' def __repr__(self) -> str: # wrap the value in the classname to make clear that # the actual object type is different from the value return ( f'{self.__class__.__name__}(' f'{self._value!r}' f', coercer={self._coercer!r}' f', lazy={self._lazy}' ')' ) def __eq__(self, item: object) -> bool: """ This default implementation of comparing for equality only compare the types, value, and coercer of the two items. If additional criteria are relevant for derived classes :meth:`__eq__` has to be reimplemented. """ if not isinstance(item, type(self)): return False return ( self._lazy == item._lazy and self._value == item._value and self._coercer == item._coercer ) def __hash__(self): return hash((self.value, self.coercer, self.lazy)) def copy(self): """Return a shallow copy of the instance""" return copy(self)