Source code for optmanage.manager

"""
    Base class for option managers.
"""

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

from __future__ import annotations

from collections.abc import Iterator, Mapping
from contextlib import contextmanager
from typing import Any, ClassVar, Type
from .option import Option


[docs] class OptionManager: """ A base class that can be used to implement flexible option managers, supporting options with default values, static type hints, runtime type checking, and custom runtime validation logic. Subclasses should define options as class attributes, using the :class:`Option` descriptor. A simple example of an option manager: .. code-block:: python class MyOptions(OptionManager): ''' Options of some library. ''' validate = Option(bool, True) ''' Whether to validate arguments to functions and methods. ''' eq_atol = Option(float, 1e-08, lambda x: x >= 0) ''' Absolute tolerance used for equality comparisons.''' print_prec = Option(int, 3, lambda x: x >= 0) ''' Number of decimal digits to be displayed when printing. ''' Each option is defined as a class attribute of type :class:`Option`, passing a default value, a type, and optionally a validator function: .. code-block:: python validate = Option(bool, True) # option type ^^^^ ^^^^ default value print_prec = Option(int, 3, lambda x: x >= 0) # optional validator ^^^^^^^^^^^^^^^^ The option manager can then be instantiated as usual: .. code-block:: python options = MyOptions() """ __options: ClassVar[dict[Type[OptionManager], dict[str, Option[Any]]]] = {} def __init_subclass__(cls) -> None: """ Hook to create the initial options mapping for a subclass of :class:`OptionManager`, by collecting all options from all superclasses of ``cls`` which are themselves subclasses of :class:`OptionManager`. Options declared in ``cls`` are bound subsequently. :raises KeyError: If a duplicate option name is found. """ __options = OptionManager.__options mro_options_list = [ __options.get(cls, {}), *(__options[base] for base in cls.__mro__[1:] if base in __options), ] merged_options: dict[str, Option[Any]] = {} for options in reversed(mro_options_list): for k, opt in options.items(): prev_opt = merged_options.get(k, None) if prev_opt is None: merged_options[k] = opt elif prev_opt is not opt: error = ( f"Duplicate option name {k!r} for option managers " f"{prev_opt.owner.__name__} and " f"{opt.owner.__name__} in MRO of {cls.__name__}." ) raise KeyError(error) OptionManager.__options[cls] = merged_options @classmethod def _bind_option(cls, name: str, option: Option[Any]) -> None: """ Binds an option to this option manager. :raises KeyError: If an option with the same name is already bound. """ assert cls is not OptionManager, "Cannot bind option to OptionManager." assert option.owner is cls, "Attempting to incorrectly bind option." options = OptionManager.__options.setdefault(cls, {}) if name in options: raise KeyError(f"Duplicate option name {name!r}.") options[name] = option @classmethod def _validate_values(cls, **option_values: Any) -> dict[str, Option[Any]]: """ Validates the given option values. See :meth:`Option.validate`. """ cls_options = cls.__options[cls] options: dict[str, Option[Any]] = {} for k, v in option_values.items(): (opt := cls_options[k]).validate(v) options[k] = opt return options __slots__ = ("__weakref__",)
[docs] def set(self, **option_values: Any) -> Mapping[str, Any]: """ Sets values for the given options. Returns the old values of the options that were set. Example usage: .. code-block:: python options.set(validate=False, print_prec=5) # Permanently sets 'validate' and 'print_prec' options. For errors, see :meth:`Option.__set__`. """ options = type(self)._validate_values(**option_values) return {k: options[k]._set(self, v) for k, v in option_values.items()}
[docs] def keys(self) -> Iterator[str]: """Iterator over option names.""" options = OptionManager.__options[type(self)] return iter(options.keys())
[docs] def values(self) -> Iterator[Any]: """Iterator over option values.""" options = OptionManager.__options[type(self)] return (opt._get(self) for opt in options.values())
[docs] def items(self) -> Iterator[tuple[str, Any]]: """Iterator over option name-value pairs.""" options = OptionManager.__options[type(self)] return ((k, opt._get(self)) for k, opt in options.items())
[docs] def reset(self) -> Mapping[str, Any]: """ Resets all options to their default values. Returns the old values for all options. Example usage: .. code-block:: python options.reset() # Permanently resets all options to default values. For errors, see :meth:`Option.reset`. """ return { k: opt.reset(self) for k, opt in OptionManager.__options[type(self)].items() }
[docs] @contextmanager def __call__(self, **option_values: Any) -> Iterator[None]: """ With-context manager to temporarily set option values. Example usage: .. code-block:: python with options(print_prec=5): # Sets 'print_prec' to 5 temporarily in this context. ... # Value of 'print_prec' is restored to its previous value here. :meta public: """ options = type(self)._validate_values(**option_values) for k, v in option_values.items(): options[k]._push(self, v) try: yield finally: for k in option_values: options[k]._pop(self)
[docs] def __getitem__(self, name: str) -> Any: """ Gets the value of the given option. :meta public: """ try: return getattr(self, name) except AttributeError: # pylint: disable = raise-missing-from raise KeyError(f"Option {name!r} not found.")
[docs] def __setitem__(self, name: str, value: Any) -> None: """ Sets the value of the given option. For errors, see :meth:`Option.__set__`. :meta public: """ try: setattr(self, name, value) except AttributeError: # pylint: disable = raise-missing-from raise KeyError(f"Option {name!r} not found.")
def __repr__(self) -> str: id_str = f"{id(self):#x}" options_lines = [f" {k!r}: {v!r}," for k, v in self.items()] lines = ["OptionManager({", *options_lines, "}) at " + id_str] return "\n".join(lines)