Source code for sunset.key

import logging
import sys
import weakref
from collections.abc import Callable, Iterable
from types import GenericAlias
from typing import (
    Any,
    Generic,
    TypeVar,
    cast,
)

if sys.version_info < (3, 11):  # pragma: no cover
    from typing_extensions import Self
else:
    from typing import Self

from sunset.exporter import maybe_escape
from sunset.lock import SettingsLock
from sunset.notifier import Notifier
from sunset.protocols import BaseField, Serializer, UpdateNotifier
from sunset.serializers import lookup

_T = TypeVar("_T")


[docs] class Key(BaseField, Generic[_T]): """ A single setting key containing a typed value. Keys support inheritance. If a Key does not have a value explicitly set, and it has a parent layer, then the value it will return is that of its parent layer. Else its value if unset is the default value it was instantiated with. Keys can call a callback when their reported value changes, whether it's their own value that changed, or that inherited from a parent layer. Set this callback with the :meth:`onValueChangeCall()` method. You can control the values that can be set on this Key by passing a `validator` argument when instantiating it. Args: default: The value that this Key will return when not otherwise set; the type of this default determines the type of the values that can be set on this Key. If the type of the default is not one of bool, int, float, str, or an `enum.Enum` subclass, and is also not a class that implements the :class:`~sunset.Serializable` protocol, then a serializer argument must also be passed. serializer: An implementation of the :class:`~sunset.Serializer` protocol for the type of this Key's values. This argument must be passed if the type in question is not supported by a native SunsetSettings serializer. If a serializer is passed, it will be used even if SunsetSettings has its own serializer for that type. validator: A function that returns True if the given value can be set on this Key, else False. This allows you to control what values are allowable for this Key. Default: None. value_type: If given, the type that will be used by runtime safety checks instead of the type of the default value. This is rarely needed, but can be useful e.g. if the Key is meant to hold values from multiple possible subclasses of a base class. Example: >>> from sunset import Key >>> key: Key[int] = Key(default=0) >>> key.get() 0 >>> key.set(42) True >>> key.get() 42 >>> child_key: Key[int] = Key(default=0) >>> child_key.setParent(key) >>> child_key.get() 42 >>> child_key.set(101) True >>> child_key.get() 101 >>> key.set(36) True >>> key.get() 36 >>> child_key.get() 101 >>> child_key.clear() >>> child_key.get() 36 """ _default: _T _value: _T | None _serializer: Serializer[_T] _validator: Callable[[_T], bool] _bad_value_string: str | None _value_change_notifier: Notifier[_T] _update_notifier: Notifier[[UpdateNotifier]] _parent_ref: weakref.ref["Key[_T]"] | None _children_ref: weakref.WeakSet["Key[_T]"] _type: type[_T] def __init__( self, default: _T, serializer: Serializer[_T] | None = None, validator: Callable[[_T], bool] | None = None, value_type: type[_T] | None = None, ) -> None: super().__init__() # Keep a runtime reference to the practical type contained in this # key. if value_type is not None: if not isinstance(default, value_type): msg = ( f"Default Key value '{default}' has type" f" '{default.__class__.__name__}', which is not compatible" " with explicitly given value type" f" '{value_type.__name__}'." ) raise TypeError(msg) self._type = value_type else: self._type = default.__class__ self._default = default self._value = None self._bad_value_string = None if serializer is None: serializer = lookup(self._type) if serializer is None: msg = ( f"Default Key value '{default}' has type" f" '{self._type.__name__}', which is not supported by a native" " serializer. Please construct the Key with an explicit serializer" " argument." ) raise TypeError(msg) if validator is not None: self._validator = validator else: self._validator = lambda _: True self._serializer = serializer self._value_change_notifier = Notifier() self._update_notifier = Notifier() self._parent_ref = None self._children_ref = weakref.WeakSet()
[docs] @SettingsLock.with_read_lock def get(self) -> _T: """ Returns the current value of this Key. If this Key does not currently have a value set on it, return its fallback value, that being, the value of its parent if it has one, else the default. Returns: This Key's apparent value. """ return self.fallback() if (value := self._value) is None else value
[docs] @SettingsLock.with_read_lock def fallback(self) -> _T: """ Returns the value that this Key will fall back to when it does not have a value currently set. That value is the value of its parent if it has one, or the default value for the Key if not. """ return self._default if (parent := self.parent()) is None else parent.get()
[docs] @SettingsLock.with_write_lock def set(self, value: _T) -> bool: """ Sets the given value on this Key. Args: value: The value that this Key will now hold. Must be of the type bound to this Key, i.e. the same type as this Key's default value. Returns: True if the value was successfully set, else False, for instance if the validator refused the value. """ # Safety check in case the user is holding it wrong. if not isinstance(value, self._type): return False if not self._validator(value): logging.debug("Validator rejected value for Key %r: %r", self, value) # noqa: LOG015 return False # Setting a Key's value programmatically always resets bad values. self._bad_value_string = None previously_set = self.isSet() prev_value = self.get() self._value = value if prev_value != self.get(): self._value_change_notifier.trigger(self.get()) for child in self.children(): child._notifyParentValueChanged() # noqa: SLF001 if not previously_set or prev_value != self.get(): self._update_notifier.trigger(self) return True
[docs] @SettingsLock.with_write_lock def clear(self) -> None: """ Clears the value currently set on this Key, if any. """ # Clearing the Key always resets bad values. self._bad_value_string = None if not self.isSet(): return prev_value = self.get() self._value = None if prev_value != self.get(): self._value_change_notifier.trigger(self.get()) for child in self.children(): child._notifyParentValueChanged() # noqa: SLF001 self._update_notifier.trigger(self)
[docs] @SettingsLock.with_write_lock def updateValue(self, updater: Callable[[_T], _T]) -> None: """ Atomically updates this Key's value using the given update function. The function will be called with the Key's current value, and the value it returns will be set as the Key's new value. Args: updater: A function that takes an argument of the same type held in this key, and returns an argument of the same type. """ self.set(updater(self.get()))
[docs] @SettingsLock.with_read_lock def isSet(self) -> bool: """ Returns whether there is a value currently set on this Key. Returns: True if a value is set on this Key, else False. """ return self._value is not None
[docs] def onValueChangeCall(self, callback: Callable[[_T], Any]) -> None: """ Adds a callback to be called whenever the value returned by calling :meth:`get()` on this Key would change, even if this Key itself was not updated; for instance, this will happen if there is no value currently set on it and its parent's value changed. The callback will be called with the new value as its argument. If you want a callback to be called whenever this Key is updated, even if its apparent value does not change, use :meth:`onUpdateCall()` instead. For example, if you call :meth:`set()` with a value of `0` on a Key newly created with a default value of `0`, callbacks added with :meth:`onUpdateCall()` are called and callbacks added with :meth:`onValueChangeCall()` are not. Args: callback: A callable that takes one argument of the same type as the values held by this Key. Note: This method does not increase the reference count of the given callback. """ self._value_change_notifier.add(callback)
[docs] def onUpdateCall(self, callback: Callable[["Key[_T]"], Any]) -> None: """ Adds a callback to be called whenever this Key is updated, even if the value returned by :meth:`get()` does not end up changing. The callback will be called with this Key instance as its argument. If you want a callback to be called only when the apparent value of this Key changes, use :meth:`onValueChangeCall()` instead. For example, if a Key has no value set on it and has a parent whose value is updated, then callbacks added on this Key with :meth:`onValueChangeCall()` are called, and callbacks added with :meth:`onUpdateCall()` are not, because it's not *this* Key that was updated. Args: callback: A callable that will be called with one argument of type :class:`~sunset.Key`. Note: This method does not increase the reference count of the given callback. """ self._update_notifier.add(callback) # type: ignore[arg-type]
[docs] def setValidator(self, validator: Callable[[_T], bool]) -> None: """ Replaces this Key's validator. Args: validator: A function that returns True if the given value can be set on this Key, else False. This allows you to control what values are allowable for this Key. """ self._validator = validator
@SettingsLock.with_write_lock def setParent(self, parent: Self | None) -> None: """ Makes the given Key the parent of this one. If None, remove this Key's parent, if any. A Key with a parent will inherit its parent's value when this Key's own value is not currently set. This method is for internal purposes and you will typically not need to call it directly. Args: parent: Either a Key that will become this Key's parent, or None. The parent Key must have the same type as this Key. Note: A Key and its parent, if any, do not increase each other's reference count. """ # Runtime check to affirm the type check of the method. if parent is not None and type(self) is not type(parent): # pragma: no cover return old_parent = self.parent() if old_parent is not None: old_parent._children_ref.discard(self) # noqa: SLF001 if parent is None: self._parent_ref = None return if parent._type is not self._type: # noqa: SLF001 # This should not happen... unless the user is holding it wrong. # So, better safe than sorry. return parent._children_ref.add(self) # noqa: SLF001 self._parent_ref = weakref.ref(parent) @SettingsLock.with_read_lock def parent(self) -> Self | None: """ Returns the parent of this Key, if any. Returns: A Key instance of the same type as this one, or None. """ # Make the type of self._parent_ref more specific for the purpose of # type checking. parent = cast(weakref.ref[Self] | None, self._parent_ref) return parent() if parent is not None else None @SettingsLock.with_read_lock def children(self) -> Iterable[Self]: """ Returns an iterable with the Keys that have this Key as their parent. Returns: An iterable of Keys of the same type as this one. """ return [cast(Self, child) for child in self._children_ref] @SettingsLock.with_read_lock def dumpFields(self) -> Iterable[tuple[str, str | None]]: if not self.skipOnSave(): if self.isSet(): return [("", self._serializer.toStr(self.get()))] if self._bad_value_string is not None: # If a bad value was set in the settings file for this Key, and # the Key was not modified since, then save the bad value again. # This way, typos in the settings file don't outright destroy # the entry. return [("", self._bad_value_string)] return [] @SettingsLock.with_write_lock def restoreFields(self, fields: Iterable[tuple[str, str | None]]) -> bool: any_change = False with self._update_notifier.inhibit(): if not fields: any_change = self.isSet() self.clear() return any_change # There should normally be only one value to be restored for a Key, but # mistakes happen. There's no perfect way to deal with that. So we loop over # fields until we find one with a path that strictly corresponds to this # Key. If there are more fields in the list with values for this Key, they # are ignored. In other words, if a settings file contains multiple entries # for a Key, only the first one is taken into account. for label, value in fields: if label != "": # Keys don't have sub-fields. If a label was given, then the path is # incorrect and does not correspond to this Key. continue if value is None: any_change = self.isSet() self.clear() elif (val := self._serializer.fromStr(value)) is not None and self.set( val ): any_change = True else: # Keep track of the value that failed to restore, so that we can # dump it again when saving. That way, if a user makes a typo while # editing the settings file, the faulty entry is not entirely lost # when we save. logging.error("Invalid value for Key %r: %s", self, value) # noqa: LOG015 self._bad_value_string = value break return any_change def _notifyParentValueChanged(self) -> None: if self.isSet(): return self._value_change_notifier.trigger(self.get()) for child in self.children(): child._notifyParentValueChanged() # noqa: SLF001 def _typeHint(self) -> GenericAlias: return GenericAlias(type(self), self._type) def _newInstance(self) -> Self: """ Internal. Returns a new instance of this Key with the same default value. Returns: A new Key. """ return self.__class__( default=self._default, serializer=self._serializer, validator=self._validator, value_type=self._type, ) def __repr__(self) -> str: type_name = self._type.__name__ value = maybe_escape(self._serializer.toStr(self.get())) if not self.isSet(): value = f"({value})" return f"<Key[{type_name}]:{value}>"