Files
lilacbot/src/settings/groups.py

205 lines
6.8 KiB
Python

from typing import Generic, Type, TypeVar, Optional, overload
from data import RowModel
from .data import ModelData
from .ui import InteractiveSetting
from .base import BaseSetting
from utils.lib import tabulate
T = TypeVar('T', bound=InteractiveSetting)
class SettingDotDict(Generic[T], dict[str, Type[T]]):
"""
Dictionary structure allowing simple dot access to items.
"""
__getattr__ = dict.__getitem__ # type: ignore
__setattr__ = dict.__setitem__ # type: ignore
__delattr__ = dict.__delitem__ # type: ignore
class SettingGroup:
"""
A SettingGroup is a collection of settings under one name.
"""
__initial_settings__: list[Type[InteractiveSetting]] = []
_title: Optional[str] = None
_description: Optional[str] = None
def __init_subclass__(cls, title: Optional[str] = None):
cls._title = title or cls._title
cls._description = cls._description or cls.__doc__
settings: list[Type[InteractiveSetting]] = []
for item in cls.__dict__.values():
if isinstance(item, type) and issubclass(item, InteractiveSetting):
settings.append(item)
cls.__initial_settings__ = settings
def __init_settings__(self):
settings = SettingDotDict()
for setting in self.__initial_settings__:
settings[setting.__name__] = setting
return settings
def __init__(self, title=None, description=None) -> None:
self.title: str = title or self._title or self.__class__.__name__
self.description: str = description or self._description or ""
self.settings: SettingDotDict[InteractiveSetting] = self.__init_settings__()
def attach(self, cls: Type[T], name: Optional[str] = None):
name = name or cls.setting_id
self.settings[name] = cls
return cls
def detach(self, cls):
return self.settings.pop(cls.__name__, None)
def update(self, smap):
self.settings.update(smap.settings)
def reduce(self, *keys):
for key in keys:
self.settings.pop(key, None)
return
async def make_setting_table(self, parent_id, **kwargs):
"""
Convenience method for generating a rendered setting table.
"""
rows = []
for setting in self.settings.values():
if not setting._virtual:
set = await setting.get(parent_id, **kwargs)
name = set.display_name
value = str(set.formatted)
rows.append((name, value, set.hover_desc))
table_rows = tabulate(
*rows,
row_format="[`{invis}{key:<{pad}}{colon}`](https://lionbot.org \"{field[2]}\")\t{value}"
)
return '\n'.join(table_rows)
class ModelSetting(ModelData, BaseSetting):
...
class ModelConfig:
"""
A ModelConfig provides a central point of configuration for any object described by a single Model.
An instance of a ModelConfig represents configuration for a single object
(given by a single row of the corresponding Model).
The ModelConfig also supports registration of non-model configuration,
to support associated settings (e.g. list-settings) for the object.
This is an ABC, and must be subclassed for each object-type.
"""
settings: SettingDotDict
_model_settings: set
model: Type[RowModel]
def __init__(self, parent_id, row, **kwargs):
self.parent_id = parent_id
self.row = row
self.kwargs = kwargs
@classmethod
def register_setting(cls, setting_cls):
"""
Decorator to register a non-model setting as part of the object configuration.
The setting class may be re-accessed through the `settings` class attr.
Subclasses may provide alternative access pathways to key non-model settings.
"""
cls.settings[setting_cls.setting_id] = setting_cls
return setting_cls
@classmethod
def register_model_setting(cls, model_setting_cls):
"""
Decorator to register a model setting as part of the object configuration.
The setting class may be accessed through the `settings` class attr.
A fresh setting instance may also be retrieved (using cached data)
through the `get` instance method.
Subclasses are recommended to provide model settings as properties
for simplified access and type checking.
"""
cls._model_settings.add(model_setting_cls.setting_id)
return cls.register_setting(model_setting_cls)
def get(self, setting_id):
"""
Retrieve a freshly initialised copy of the given model-setting.
The given `setting_id` must have been previously registered through `register_model_setting`.
This uses cached data, and so is not guaranteed to be up-to-date.
"""
if setting_id not in self._model_settings:
# TODO: Log
raise ValueError
setting_cls = self.settings[setting_id]
data = setting_cls._read_from_row(self.parent_id, self.row, **self.kwargs)
return setting_cls(self.parent_id, data, **self.kwargs)
class ModelSettings:
"""
A ModelSettings instance aggregates multiple `ModelSetting` instances
bound to the same parent id on a single Model.
This enables a single point of access
for settings of a given Model,
with support for caching or deriving as needed.
This is an abstract base class,
and should be subclassed to define the contained settings.
"""
_settings: SettingDotDict = SettingDotDict()
model: Type[RowModel]
def __init__(self, parent_id, row, **kwargs):
self.parent_id = parent_id
self.row = row
self.kwargs = kwargs
@classmethod
async def fetch(cls, *parent_id, **kwargs):
"""
Load an instance of this ModelSetting with the given parent_id
and setting keyword arguments.
"""
row = await cls.model.fetch_or_create(*parent_id)
return cls(parent_id, row, **kwargs)
@classmethod
def attach(self, setting_cls):
"""
Decorator to attach the given setting class to this modelsetting.
"""
# This violates the interface principle, use structured typing instead?
if not (issubclass(setting_cls, BaseSetting) and issubclass(setting_cls, ModelData)):
raise ValueError(
f"The provided setting class must be `ModelSetting`, not {setting_cls.__class__.__name__}."
)
self._settings[setting_cls.setting_id] = setting_cls
return setting_cls
def get(self, setting_id):
setting_cls = self._settings.get(setting_id)
data = setting_cls._read_from_row(self.parent_id, self.row, **self.kwargs)
return setting_cls(self.parent_id, data, **self.kwargs)
def __getitem__(self, setting_id):
return self.get(setting_id)