rewrite: Setting abstract framework.
This commit is contained in:
163
bot/settings/base.py
Normal file
163
bot/settings/base.py
Normal file
@@ -0,0 +1,163 @@
|
||||
from typing import Generic, TypeVar, Type, Optional, overload
|
||||
|
||||
|
||||
"""
|
||||
Setting metclass?
|
||||
Parse setting docstring to generate default info?
|
||||
Or just put it in the decorator we are already using
|
||||
"""
|
||||
|
||||
|
||||
# Typing using Generic[parent_id_type, data_type, value_type]
|
||||
# value generic, could be Union[?, UNSET]
|
||||
ParentID = TypeVar('ParentID')
|
||||
SettingData = TypeVar('SettingData')
|
||||
SettingValue = TypeVar('SettingValue')
|
||||
|
||||
T = TypeVar('T', bound='BaseSetting')
|
||||
|
||||
|
||||
class BaseSetting(Generic[ParentID, SettingData, SettingValue]):
|
||||
"""
|
||||
Abstract base class describing a stored configuration setting.
|
||||
A setting consists of logic to load the setting from storage,
|
||||
present it in a readable form, understand user entered values,
|
||||
and write it again in storage.
|
||||
Additionally, the setting has attributes attached describing
|
||||
the setting in a user-friendly manner for display purposes.
|
||||
"""
|
||||
_default: Optional[SettingData] = None # Default data value for the setting
|
||||
|
||||
def __init__(self, parent_id: ParentID, data: Optional[SettingData], **kwargs):
|
||||
self.parent_id = parent_id
|
||||
self._data = data
|
||||
|
||||
# Instance generation
|
||||
@classmethod
|
||||
async def get(cls: Type[T], parent_id: ParentID, **kwargs) -> T:
|
||||
"""
|
||||
Return a setting instance initialised from the stored value, associated with the given parent id.
|
||||
"""
|
||||
data = await cls._reader(parent_id, **kwargs)
|
||||
return cls(parent_id, data, **kwargs)
|
||||
|
||||
# Main interface
|
||||
@property
|
||||
def data(self) -> Optional[SettingData]:
|
||||
"""
|
||||
Retrieves the current internal setting data if it is set, otherwise the default data
|
||||
"""
|
||||
return self._data if self._data is not None else self.default
|
||||
|
||||
@data.setter
|
||||
def data(self, new_data: Optional[SettingData]):
|
||||
"""
|
||||
Sets the internal raw data.
|
||||
Does not write the changes.
|
||||
"""
|
||||
self._data = new_data
|
||||
|
||||
@property
|
||||
def default(self) -> Optional[SettingData]:
|
||||
"""
|
||||
Retrieves the default value for this setting.
|
||||
Settings should override this if the default depends on the object id.
|
||||
"""
|
||||
return self._default
|
||||
|
||||
@property
|
||||
def value(self) -> Optional[SettingValue]:
|
||||
"""
|
||||
Context-aware object or objects associated with the setting.
|
||||
"""
|
||||
return self._data_to_value(self.parent_id, self.data)
|
||||
|
||||
@value.setter
|
||||
def value(self, new_value: Optional[SettingValue]):
|
||||
"""
|
||||
Setter which reads the discord-aware object and converts it to data.
|
||||
Does not write the new value.
|
||||
"""
|
||||
self._data = self._data_from_value(self.parent_id, new_value)
|
||||
|
||||
async def write(self, **kwargs) -> None:
|
||||
"""
|
||||
Write current data to the database.
|
||||
For settings which override this,
|
||||
ensure you handle deletion of values when internal data is None.
|
||||
"""
|
||||
await self._writer(self.parent_id, self._data, **kwargs)
|
||||
|
||||
# Raw converters
|
||||
@overload
|
||||
@classmethod
|
||||
def _data_from_value(cls: Type[T], parent_id: ParentID, value: SettingValue, **kwargs) -> SettingData:
|
||||
...
|
||||
|
||||
@overload
|
||||
@classmethod
|
||||
def _data_from_value(cls: Type[T], parent_id: ParentID, value: None, **kwargs) -> None:
|
||||
...
|
||||
|
||||
@classmethod
|
||||
def _data_from_value(
|
||||
cls: Type[T], parent_id: ParentID, value: Optional[SettingValue], **kwargs
|
||||
) -> Optional[SettingData]:
|
||||
"""
|
||||
Convert a high-level setting value to internal data.
|
||||
Must be overridden by the setting.
|
||||
Be aware of UNSET values, these should always pass through as None
|
||||
to provide an unsetting interface.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@overload
|
||||
@classmethod
|
||||
def _data_to_value(cls: Type[T], parent_id: ParentID, data: SettingData, **kwargs) -> SettingValue:
|
||||
...
|
||||
|
||||
@overload
|
||||
@classmethod
|
||||
def _data_to_value(cls: Type[T], parent_id: ParentID, data: None, **kwargs) -> None:
|
||||
...
|
||||
|
||||
@classmethod
|
||||
def _data_to_value(
|
||||
cls: Type[T], parent_id: ParentID, data: Optional[SettingData], **kwargs
|
||||
) -> Optional[SettingValue]:
|
||||
"""
|
||||
Convert internal data to high-level setting value.
|
||||
Must be overriden by the setting.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
# Database access
|
||||
@classmethod
|
||||
async def _reader(cls: Type[T], parent_id: ParentID, **kwargs) -> Optional[SettingData]:
|
||||
"""
|
||||
Retrieve the setting data associated with the given parent_id.
|
||||
May be None if the setting is not set.
|
||||
Must be overridden by the setting.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@classmethod
|
||||
async def _writer(cls: Type[T], parent_id: ParentID, data: Optional[SettingData], **kwargs) -> None:
|
||||
"""
|
||||
Write provided setting data to storage.
|
||||
Must be overridden by the setting unless the `write` method is overridden.
|
||||
If the data is None, the setting is UNSET and should be deleted.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@classmethod
|
||||
async def setup(cls, bot):
|
||||
"""
|
||||
Initialisation task to be executed during client initialisation.
|
||||
May be used for e.g. populating a cache or required client setup.
|
||||
|
||||
Main application must execute the initialisation task before the setting is used.
|
||||
Further, the task must always be executable, if the setting is loaded.
|
||||
Conditional initialisation should go in the relevant module's init tasks.
|
||||
"""
|
||||
return None
|
||||
Reference in New Issue
Block a user