diff --git a/bot/settings/__init__.py b/bot/settings/__init__.py index 71fb05aa..760b5576 100644 --- a/bot/settings/__init__.py +++ b/bot/settings/__init__.py @@ -1,4 +1,3 @@ from .data import ModelData from .base import BaseSetting from .ui import SettingWidget, InteractiveSetting -from .groups import SettingGroup diff --git a/bot/settings/base.py b/bot/settings/base.py index 86d8095a..e17cf86e 100644 --- a/bot/settings/base.py +++ b/bot/settings/base.py @@ -66,11 +66,11 @@ class BaseSetting(Generic[ParentID, SettingData, SettingValue]): return self._default @property - def value(self) -> Optional[SettingValue]: + def value(self) -> SettingValue: # Actually optional *if* _default is None """ Context-aware object or objects associated with the setting. """ - return self._data_to_value(self.parent_id, self.data) + return self._data_to_value(self.parent_id, self.data) # type: ignore @value.setter def value(self, new_value: Optional[SettingValue]): diff --git a/bot/settings/groups.py b/bot/settings/groups.py index 2fa75b41..ca469937 100644 --- a/bot/settings/groups.py +++ b/bot/settings/groups.py @@ -1,5 +1,79 @@ -from discord.ui import View +from typing import Generic, Type, TypeVar, Optional +from .ui import InteractiveSetting + +from utils.lib import tabulate -class SettingGroup(View): - ... +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.__name__ + 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): + """ + Convenience method for generating a rendered setting table. + """ + rows = [] + for setting in self.settings.values(): + name = f"{setting.display_name}" + set = await setting.get(parent_id) + value = 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) diff --git a/bot/settings/setting_types.py b/bot/settings/setting_types.py index b538f90e..06d815df 100644 --- a/bot/settings/setting_types.py +++ b/bot/settings/setting_types.py @@ -749,7 +749,7 @@ class EnumSetting(InteractiveSetting[ParentID, ET, ET]): This should almost always include the strings from `_outputs`. """ - _enum: ET + _enum: Type[ET] _outputs: dict[ET, str] _inputs: dict[str, ET] diff --git a/bot/settings/ui.py b/bot/settings/ui.py index f64625a6..7d659c5f 100644 --- a/bot/settings/ui.py +++ b/bot/settings/ui.py @@ -8,7 +8,7 @@ from discord.ui.button import ButtonStyle, Button, button from discord.ui.modal import Modal from discord.ui.text_input import TextInput -from utils.lib import prop_tabulate, recover_context +from utils.lib import tabulate, recover_context from utils.ui import FastModal from meta.config import conf @@ -214,6 +214,15 @@ class InteractiveSetting(BaseSetting[ParentID, SettingData, SettingValue]): else: return f"Setting Updated! New value: {self.formatted}" + @property + def hover_desc(self): + return '\n'.join(( + self.display_name, + '=' * len(self.display_name), + self.long_desc, + f"\nAccepts: {self.accepts}" + )) + async def update_response(self, interaction: discord.Interaction, **kwargs): """ Respond to an interaction which triggered a setting update. @@ -256,10 +265,10 @@ class InteractiveSetting(BaseSetting[ParentID, SettingData, SettingValue]): @property def desc_table(self): - fields = ("Current value", "Default value") - values = (self.formatted or "Not Set", - self._format_data(self.parent_id, self.default) or "None") - return prop_tabulate(fields, values) + return tabulate( + ("Current Value", self.formatted or "Not Set"), + ("Default Value", self._format_data(self.parent_id, self.default) or "None"), + ) @property def input_field(self) -> TextInput: