Source code for optmanage.option

"""
    Descriptor to define options in an option manager.
"""

# OptManage: A library to create flexible option managers.
# Copyright (C) 2023 Hashberg Ltd

from __future__ import annotations

import sys
from typing import (
    TYPE_CHECKING,
    Any,
    Generic,
    Protocol,
    Type,
    TypeVar,
    overload,
)
from typing_validation import can_validate, validate

if sys.version_info[1] >= 12:
    from typing import Self
else:
    from typing_extensions import Self

if TYPE_CHECKING:
    from .manager import OptionManager

ValueT = TypeVar("ValueT")
"""
    Invariant type variable for option values (see :class:`Option`).
"""

ValueT_contra = TypeVar("ValueT_contra", contravariant=True)
""" Contravariant type variable for option values (see :class:`Option`). """


[docs] class Validator(Protocol[ValueT_contra]): """ Structural type for an option validator. There are two ways to signal that a value is invalid: - returning :obj:`False` - raising any :class:`Exception` There are two ways to signal that a value is valid: - returning :obj:`True` (as opposed to :obj:`False`) - returning :obj:`None` (as opposed to raising an exception) In the context of option validation, a :obj:`ValueError` will be raised if validation fails: if the validator raised an exception, the :obj:`ValueError` is re-raised from it. """ def __call__(self, value: ValueT_contra, /) -> bool|None: ...
[docs] class Option(Generic[ValueT]): """ Descriptor for an option. For usage examples, see :class:`OptionManager`. See https://docs.python.org/3/reference/datamodel.html#descriptors for a discussion of descriptors in Python. """ __values: dict[OptionManager, ValueT] __value_stacks: dict[OptionManager, list[ValueT]] __default: ValueT __type: Type[ValueT] __validator: Validator[ValueT] | None __owner: Type[OptionManager] __name: str __slots__ = ( "__owner", "__name", "__values", "__value_stacks", "__default", "__type", "__validator", )
[docs] def __new__( cls, ty: Any, default: ValueT, validator: Validator[ValueT] | None = None, ) -> Self: """ Creates a new option descriptor. For usage examples, see :class:`OptionManager`. :param ty: The type of the option. :param default: The default value for the option. :param validator: An optional validator function, see :class:`Validator` :meta public: """ if not can_validate(ty): raise TypeError(f"Cannot validate type {ty!r}.") if validator is not None and not callable(validator): raise TypeError(f"Expected callable validator, got {validator!r}.") instance = super().__new__(cls) instance.__values = {} instance.__value_stacks = {} instance.__default = default instance.__type = ty instance.__validator = validator instance.validate(instance.__default) return instance
@property def default(self) -> ValueT: """ The default value for this option. """ return self.__default @property def type(self) -> Type[ValueT]: """ The type of this option. """ return self.__type @property def validator(self) -> Validator[ValueT] | None: """ The validator for this option, or :obj:`None` if no additional validation logic is specified (other than runtime type-checking). """ return self.__validator @property def name(self) -> str: """ The name of this option. :raises AttributeError: if accessed before the option has been bound to an option manager. """ return self.__name @property def owner(self) -> Type[OptionManager]: """ The owner of this option. :raises AttributeError: if accessed before the option has been bound to an option manager. """ return self.__owner
[docs] def validate(self, value: ValueT) -> None: """ Validates a given value for this option: - Checks that the value has the correct type. - If a validator is specified, checks that the value is valid. If the value is not valid, :obj:`ValueError` is raised. """ validate(value, self.__type) if (validator := self.__validator) is not None: validator_res: bool|None validator_err: Exception|None = None try: validator_res = validator(value) except Exception as e: validator_res = False validator_err = e if validator_res is False: name_str = ( f" {self.__name!r}" if hasattr(self, "__name") else "" ) raise ValueError( f"Invalid value for option{name_str}: " f"{value!r}." ) from validator_err
[docs] def reset(self, instance: OptionManager) -> ValueT: """ Resets the option to its default value. """ # pylint: disable = import-outside-toplevel from .manager import OptionManager validate(instance, OptionManager) if self._has_temporary_value(instance): raise ValueError( f"Option {self.__name!r} has a temporary value, " "cannot be permanently reset." ) if instance not in (values := self.__values): return self.__default old_value = values[instance] del values[instance] return old_value
[docs] def __set_name__(self, owner: Type[OptionManager], name: str) -> None: """ Hook to automatically bind the option to the option manager that owns it. :meta public: """ self.__owner = owner self.__name = name owner._bind_option(name, self)
[docs] def __set__(self, instance: OptionManager, value: ValueT) -> None: """ Sets the option to the given value. The value is validated before being set. :raises TypeError: If the value has the wrong type. :raises ValueError: If the value is invalid for this option. :raises ValueError: If the option has a temporarily set value. :meta public: """ if self._has_temporary_value(instance): raise ValueError( f"Option {self.__name!r} has a temporary value, " "cannot be permanently set." ) self.validate(value) self._set(instance, value)
@overload def __get__( self, instance: None, _: Type[OptionManager] | None = None ) -> Self: ... @overload def __get__( self, instance: OptionManager, _: Type[OptionManager] | None = None ) -> ValueT: ...
[docs] def __get__( self, instance: OptionManager | None, _: Type[OptionManager] | None = None, ) -> ValueT | Self: """ Gets the current value for the given option. If no value has been set, the default value is returned. :meta public: """ if instance is None: return self return self._get(instance)
def _get(self, instance: OptionManager) -> ValueT: if instance not in (values := self.__values): return self.__default return values[instance] def _set(self, instance: OptionManager, value: ValueT) -> ValueT: old_value = self.__values.get(instance, self.__default) self.__values[instance] = value return old_value def _push(self, instance: OptionManager, value: ValueT) -> ValueT: old_value = self._get(instance) self.__value_stacks.setdefault(instance, []).append(old_value) return self._set(instance, value) def _pop(self, instance: OptionManager) -> ValueT: stack = self.__value_stacks.get(instance, None) if not stack: raise ValueError(f"Option {self.__name!r} has no temporary value.") prev_value = self._set(instance, stack.pop()) if not stack: del self.__value_stacks[instance] return prev_value def _has_temporary_value(self, instance: OptionManager) -> bool: return instance in self.__value_stacks