rewrite: Setting abstract framework.
This commit is contained in:
4
bot/settings/__init__.py
Normal file
4
bot/settings/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
||||
from .data import ModelData
|
||||
from .base import BaseSetting
|
||||
from .ui import SettingWidget, InteractiveSetting
|
||||
from .groups import SettingGroup
|
||||
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
|
||||
216
bot/settings/data.py
Normal file
216
bot/settings/data.py
Normal file
@@ -0,0 +1,216 @@
|
||||
from typing import Type
|
||||
import json
|
||||
|
||||
from data import RowModel, Table, ORDER
|
||||
|
||||
|
||||
class ModelData:
|
||||
"""
|
||||
Mixin for settings stored in a single row and column of a Model.
|
||||
Assumes that the parent_id is the identity key of the Model.
|
||||
|
||||
This does not create a reference to the Row.
|
||||
"""
|
||||
# Table storing the desired data
|
||||
_model: Type[RowModel]
|
||||
|
||||
# Column with the desired data
|
||||
_column: str
|
||||
|
||||
# Whether to create a row if not found
|
||||
_create_row = False
|
||||
|
||||
# High level data cache to use, leave as None to disable cache.
|
||||
_cache = None # Map[id -> value]
|
||||
|
||||
@classmethod
|
||||
async def _reader(cls, parent_id, use_cache=True, **kwargs):
|
||||
"""
|
||||
Read in the requested column associated to the parent id.
|
||||
"""
|
||||
if cls._cache is not None and parent_id in cls._cache and use_cache:
|
||||
return cls._cache[parent_id]
|
||||
|
||||
model = cls._model
|
||||
if cls._create_row:
|
||||
row = await model.fetch_or_create(parent_id)
|
||||
else:
|
||||
row = await model.fetch(parent_id)
|
||||
data = row[cls._column] if row else None
|
||||
|
||||
if cls._cache is not None:
|
||||
cls._cache[parent_id] = data
|
||||
|
||||
return data
|
||||
|
||||
@classmethod
|
||||
async def _writer(cls, parent_id, data, **kwargs):
|
||||
"""
|
||||
Write the provided entry to the table.
|
||||
This does *not* create the row if it does not exist.
|
||||
It only updates.
|
||||
"""
|
||||
# TODO: Better way of getting the key?
|
||||
if not isinstance(parent_id, tuple):
|
||||
parent_id = (parent_id, )
|
||||
model = cls._model
|
||||
rows = await model.table.update_where(
|
||||
**model._dict_from_id(parent_id)
|
||||
).set(
|
||||
**{cls._column: data}
|
||||
)
|
||||
# If we didn't update any rows, create a new row
|
||||
if not rows:
|
||||
await model.table.fetch_or_create(**model._dict_from_id, **{cls._column: data})
|
||||
...
|
||||
|
||||
if cls._cache is not None:
|
||||
cls._cache[parent_id] = data
|
||||
|
||||
|
||||
class ListData:
|
||||
"""
|
||||
Mixin for list types implemented on a Table.
|
||||
Implements a reader and writer.
|
||||
This assumes the list is the only data stored in the table,
|
||||
and removes list entries by deleting rows.
|
||||
"""
|
||||
# Table storing the setting data
|
||||
_table_interface: Table
|
||||
|
||||
# Name of the column storing the id
|
||||
_id_column: str
|
||||
|
||||
# Name of the column storing the data to read
|
||||
_data_column: str
|
||||
|
||||
# Name of column storing the order index to use, if any. Assumed to be Serial on writing.
|
||||
_order_column: str
|
||||
_order_type: ORDER = ORDER.ASC
|
||||
|
||||
# High level data cache to use, set to None to disable cache.
|
||||
_cache = None # Map[id -> value]
|
||||
|
||||
@classmethod
|
||||
async def _reader(cls, parent_id, use_cache=True, **kwargs):
|
||||
"""
|
||||
Read in all entries associated to the given id.
|
||||
"""
|
||||
if cls._cache is not None and parent_id in cls._cache and use_cache:
|
||||
return cls._cache[parent_id]
|
||||
|
||||
table = cls._table_interface # type: Table
|
||||
query = table.select_where(**{cls._id_column: parent_id}).select(cls._data_column)
|
||||
if cls._order_column:
|
||||
query.order_by(cls._order_column, order=cls._order_type)
|
||||
|
||||
rows = await query
|
||||
data = [row[cls._data_column] for row in rows]
|
||||
|
||||
if cls._cache is not None:
|
||||
cls._cache[parent_id] = data
|
||||
|
||||
return data
|
||||
|
||||
@classmethod
|
||||
async def _writer(cls, id, data, add_only=False, remove_only=False, **kwargs):
|
||||
"""
|
||||
Write the provided list to storage.
|
||||
"""
|
||||
table = cls._table_interface
|
||||
conn = await table.connector.get_connection()
|
||||
with conn.transaction():
|
||||
# Handle None input as an empty list
|
||||
if data is None:
|
||||
data = []
|
||||
|
||||
current = await cls._reader(id, use_cache=False, **kwargs)
|
||||
if not cls._order_column and (add_only or remove_only):
|
||||
to_insert = [item for item in data if item not in current] if not remove_only else []
|
||||
to_remove = data if remove_only else (
|
||||
[item for item in current if item not in data] if not add_only else []
|
||||
)
|
||||
|
||||
# Handle required deletions
|
||||
if to_remove:
|
||||
params = {
|
||||
cls._id_column: id,
|
||||
cls._data_column: to_remove
|
||||
}
|
||||
await table.delete_where(**params)
|
||||
|
||||
# Handle required insertions
|
||||
if to_insert:
|
||||
columns = (cls._id_column, cls._data_column)
|
||||
values = [(id, value) for value in to_insert]
|
||||
await table.insert_many(columns, *values)
|
||||
|
||||
if cls._cache is not None:
|
||||
new_current = [item for item in current + to_insert if item not in to_remove]
|
||||
cls._cache[id] = new_current
|
||||
else:
|
||||
# Remove all and add all to preserve order
|
||||
delete_params = {cls._id_column: id}
|
||||
await table.delete_where(**delete_params)
|
||||
|
||||
if data:
|
||||
columns = (cls._id_column, cls._data_column)
|
||||
values = [(id, value) for value in data]
|
||||
await table.insert_many(columns, *values)
|
||||
|
||||
if cls._cache is not None:
|
||||
cls._cache[id] = data
|
||||
|
||||
|
||||
class KeyValueData:
|
||||
"""
|
||||
Mixin for settings implemented in a Key-Value table.
|
||||
The underlying table should have a Unique constraint on the `(_id_column, _key_column)` pair.
|
||||
"""
|
||||
_table_interface: Table
|
||||
|
||||
_id_column: str
|
||||
|
||||
_key_column: str
|
||||
|
||||
_value_column: str
|
||||
|
||||
_key: str
|
||||
|
||||
@classmethod
|
||||
async def _reader(cls, id, **kwargs):
|
||||
params = {
|
||||
cls._id_column: id,
|
||||
cls._key_column: cls._key
|
||||
}
|
||||
|
||||
row = await cls._table_interface.select_one_where(**params).select(cls._value_column)
|
||||
data = row[cls._value_column] if row else None
|
||||
|
||||
if data is not None:
|
||||
data = json.loads(data)
|
||||
|
||||
return data
|
||||
|
||||
@classmethod
|
||||
async def _writer(cls, id, data, **kwargs):
|
||||
params = {
|
||||
cls._id_column: id,
|
||||
cls._key_column: cls._key
|
||||
}
|
||||
if data is not None:
|
||||
values = {
|
||||
cls._value_column: json.dumps(data)
|
||||
}
|
||||
rows = await cls._table_interface.update_where(**params).set(**values)
|
||||
if not rows:
|
||||
await cls._table_interface.insert_many(
|
||||
(cls._id_column, cls._key_column, cls._value_column),
|
||||
(id, cls._key, json.dumps(data))
|
||||
)
|
||||
else:
|
||||
await cls._table_interface.delete_where(**params)
|
||||
|
||||
|
||||
# class UserInputError(SafeCancellation):
|
||||
# pass
|
||||
5
bot/settings/groups.py
Normal file
5
bot/settings/groups.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from discord.ui import View
|
||||
|
||||
|
||||
class SettingGroup(View):
|
||||
...
|
||||
13
bot/settings/mock.py
Normal file
13
bot/settings/mock.py
Normal file
@@ -0,0 +1,13 @@
|
||||
import discord
|
||||
from discord import app_commands
|
||||
|
||||
|
||||
class LocalString:
|
||||
def __init__(self, string):
|
||||
self.string = string
|
||||
|
||||
def as_string(self):
|
||||
return self.string
|
||||
|
||||
|
||||
_ = LocalString
|
||||
1065
bot/settings/setting_types.py
Normal file
1065
bot/settings/setting_types.py
Normal file
File diff suppressed because it is too large
Load Diff
355
bot/settings/ui.py
Normal file
355
bot/settings/ui.py
Normal file
@@ -0,0 +1,355 @@
|
||||
from typing import Optional, Callable, Any, Dict, Coroutine, Generic, TypeVar, List
|
||||
import asyncio
|
||||
from contextvars import copy_context
|
||||
|
||||
import discord
|
||||
from discord import ui
|
||||
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.ui import FastModal
|
||||
from meta.config import conf
|
||||
|
||||
from .base import BaseSetting, ParentID, SettingData, SettingValue
|
||||
|
||||
|
||||
ST = TypeVar('ST', bound='InteractiveSetting')
|
||||
|
||||
|
||||
class SettingModal(FastModal):
|
||||
input_field: TextInput = TextInput(label="Edit Setting")
|
||||
|
||||
def update_field(self, new_field):
|
||||
self.remove_item(self.input_field)
|
||||
self.add_item(new_field)
|
||||
self.input_field = new_field
|
||||
|
||||
|
||||
class SettingWidget(Generic[ST], ui.View):
|
||||
# TODO: Permission restrictions and callback!
|
||||
# Context variables for permitted user(s)? Subclass ui.View with PermittedView?
|
||||
# Don't need to descend permissions to Modal
|
||||
# Maybe combine with timeout manager
|
||||
|
||||
def __init__(self, setting: ST, auto_write=True, **kwargs):
|
||||
self.setting = setting
|
||||
self.update_children()
|
||||
super().__init__(**kwargs)
|
||||
self.auto_write = auto_write
|
||||
|
||||
self._interaction: Optional[discord.Interaction] = None
|
||||
self._modal: Optional[SettingModal] = None
|
||||
self._exports: List[ui.Item] = self.make_exports()
|
||||
|
||||
self._context = copy_context()
|
||||
|
||||
def update_children(self):
|
||||
"""
|
||||
Method called before base View initialisation.
|
||||
Allows updating the children components (usually explicitly defined callbacks),
|
||||
before Item instantiation.
|
||||
"""
|
||||
pass
|
||||
|
||||
def order_children(self, *children):
|
||||
"""
|
||||
Helper method to set and order the children using bound methods.
|
||||
"""
|
||||
child_map = {child.__name__: child for child in self.__view_children_items__}
|
||||
self.__view_children_items__ = [child_map[child.__name__] for child in children]
|
||||
|
||||
def update_child(self, child, new_args):
|
||||
args = getattr(child, '__discord_ui_model_kwargs__')
|
||||
args |= new_args
|
||||
|
||||
def make_exports(self):
|
||||
"""
|
||||
Called post-instantiation to populate self._exports.
|
||||
"""
|
||||
return self.children
|
||||
|
||||
def refresh(self):
|
||||
"""
|
||||
Update widget components from current setting data, if applicable.
|
||||
E.g. to update the default entry in a select list after a choice has been made,
|
||||
or update button colours.
|
||||
This does not trigger a discord ui update,
|
||||
that is the responsibility of the interaction handler.
|
||||
"""
|
||||
pass
|
||||
|
||||
async def show(self, interaction: discord.Interaction, key: Any = None, override=False, **kwargs):
|
||||
"""
|
||||
Complete standard setting widget UI flow for this setting.
|
||||
The SettingWidget components may be attached to other messages as needed,
|
||||
and they may be triggered individually,
|
||||
but this coroutine defines the standard interface.
|
||||
Intended for use by any interaction which wants to "open the setting".
|
||||
|
||||
Extra keyword arguments are passed directly to the interaction reply (for e.g. ephemeral).
|
||||
"""
|
||||
if key is None:
|
||||
# By default, only have one widget listener per interaction.
|
||||
key = ('widget', interaction.id)
|
||||
|
||||
# If there is already a widget listening on this key, respect override
|
||||
if self.setting.get_listener(key) and not override:
|
||||
# Refuse to spawn another widget
|
||||
return
|
||||
|
||||
async def update_callback(new_data):
|
||||
self.setting.data = new_data
|
||||
await interaction.edit_original_response(embed=self.setting.embed, view=self, **kwargs)
|
||||
|
||||
self.setting.register_callback(key)(update_callback)
|
||||
await interaction.response.send_message(embed=self.setting.embed, view=self, **kwargs)
|
||||
await self.wait()
|
||||
try:
|
||||
# Try and detach the view, since we aren't handling events anymore.
|
||||
await interaction.edit_original_response(view=None)
|
||||
except discord.HTTPException:
|
||||
pass
|
||||
self.setting.deregister_callback(key)
|
||||
|
||||
def attach(self, group_view: ui.View):
|
||||
"""
|
||||
Attach this setting widget to a view representing several settings.
|
||||
"""
|
||||
for item in self._exports:
|
||||
group_view.add_item(item)
|
||||
|
||||
@button(style=ButtonStyle.secondary, label="Edit", row=4)
|
||||
async def edit_button(self, interaction: discord.Interaction, button: ui.Button):
|
||||
"""
|
||||
Spawn a simple edit modal,
|
||||
populated with `setting.input_field`.
|
||||
"""
|
||||
recover_context(self._context)
|
||||
# Spawn the setting modal
|
||||
await interaction.response.send_modal(self.modal)
|
||||
|
||||
@button(style=ButtonStyle.danger, label="Reset", row=4)
|
||||
async def reset_button(self, interaction: discord.Interaction, button: Button):
|
||||
recover_context(self._context)
|
||||
await interaction.response.defer(thinking=True, ephemeral=True)
|
||||
await self.setting.interactive_set(None, interaction)
|
||||
|
||||
@property
|
||||
def modal(self) -> Modal:
|
||||
"""
|
||||
Build a Modal dialogue for updating the setting.
|
||||
Refreshes (and re-attaches) the input field each time this is called.
|
||||
"""
|
||||
if self._modal is not None:
|
||||
self._modal.update_field(self.setting.input_field)
|
||||
return self._modal
|
||||
|
||||
# TODO: Attach shared timeouts to the modal
|
||||
self._modal = modal = SettingModal(
|
||||
title=f"Edit {self.setting.display_name}",
|
||||
)
|
||||
modal.update_field(self.setting.input_field)
|
||||
|
||||
@modal.submit_callback()
|
||||
async def edit_submit(interaction: discord.Interaction):
|
||||
# TODO: Catch and handle UserInputError
|
||||
await interaction.response.defer(thinking=True, ephemeral=True)
|
||||
data = await self.setting._parse_string(self.setting.parent_id, modal.input_field.value)
|
||||
await self.setting.interactive_set(data, interaction)
|
||||
|
||||
return modal
|
||||
|
||||
|
||||
class InteractiveSetting(BaseSetting[ParentID, SettingData, SettingValue]):
|
||||
__slots__ = ('_widget',)
|
||||
|
||||
# Configuration interface descriptions
|
||||
display_name: str # User readable name of the setting
|
||||
desc: str # User readable brief description of the setting
|
||||
long_desc: str # User readable long description of the setting
|
||||
accepts: str # User readable description of the acceptable values
|
||||
|
||||
Widget = SettingWidget
|
||||
|
||||
# A list of callback coroutines to call when the setting updates
|
||||
# This can be used globally to refresh state when the setting updates,
|
||||
# Or locallly to e.g. refresh an active widget.
|
||||
# The callbacks are called on write, so they may be bypassed by direct use of _writer!
|
||||
_listeners_: Dict[Any, Callable[[Optional[SettingData]], Coroutine[Any, Any, None]]] = {}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self._widget: Optional[SettingWidget] = None
|
||||
|
||||
async def write(self, **kwargs) -> None:
|
||||
await super().write(**kwargs)
|
||||
for listener in self._listeners_.values():
|
||||
asyncio.create_task(listener(self.data))
|
||||
|
||||
def get_listener(self, key):
|
||||
return self._listeners_.get(key, None)
|
||||
|
||||
@classmethod
|
||||
def register_callback(cls, name=None):
|
||||
def wrapped(coro):
|
||||
cls._listeners_[name or coro.__name__] = coro
|
||||
return coro
|
||||
return wrapped
|
||||
|
||||
@classmethod
|
||||
def deregister_callback(cls, name):
|
||||
cls._listeners_.pop(name, None)
|
||||
|
||||
@property
|
||||
def update_message(self):
|
||||
"""
|
||||
Response message sent when the setting has successfully been updated.
|
||||
Should generally be one line.
|
||||
"""
|
||||
if self.data is None:
|
||||
return "Setting reset!"
|
||||
else:
|
||||
return f"Setting Updated! New value: {self.formatted}"
|
||||
|
||||
async def update_response(self, interaction: discord.Interaction, **kwargs):
|
||||
"""
|
||||
Respond to an interaction which triggered a setting update.
|
||||
Usually just wraps `update_message` in an embed and sends it back.
|
||||
Passes any extra `kwargs` to the message creation method.
|
||||
"""
|
||||
embed = discord.Embed(
|
||||
description=f"{str(conf.emojis.tick)} {self.update_message}",
|
||||
colour=discord.Color.green()
|
||||
)
|
||||
if interaction.response.is_done():
|
||||
await interaction.edit_original_response(embed=embed, **kwargs)
|
||||
else:
|
||||
await interaction.response.send_message(embed=embed, **kwargs)
|
||||
|
||||
async def interactive_set(self, new_data: Optional[SettingData], interaction: discord.Interaction, **kwargs):
|
||||
self.data = new_data
|
||||
await self.write()
|
||||
await self.update_response(interaction, **kwargs)
|
||||
|
||||
@property
|
||||
def embed_field(self):
|
||||
"""
|
||||
Returns a {name, value} pair for use in an Embed field.
|
||||
"""
|
||||
name = self.display_name
|
||||
value = f"{self.long_dec}\n{self.desc_table}"
|
||||
return {'name': name, 'value': value}
|
||||
|
||||
@property
|
||||
def embed(self):
|
||||
"""
|
||||
Returns a full embed describing this setting.
|
||||
"""
|
||||
embed = discord.Embed(
|
||||
title="Configuration options for `{}`".format(self.display_name),
|
||||
)
|
||||
embed.description = "{}\n{}".format(self.long_desc.format(self=self), self.desc_table)
|
||||
return embed
|
||||
|
||||
@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)
|
||||
|
||||
@property
|
||||
def input_field(self) -> TextInput:
|
||||
"""
|
||||
TextInput field used for string-based setting modification.
|
||||
May be added to external modal for grouped setting editing.
|
||||
This property is not persistent, and creates a new field each time.
|
||||
"""
|
||||
return TextInput(
|
||||
label=self.display_name,
|
||||
placeholder=self.accepts,
|
||||
default=self.input_formatted,
|
||||
required=False
|
||||
)
|
||||
|
||||
@property
|
||||
def widget(self):
|
||||
"""
|
||||
Returns the Discord UI View associated with the current setting.
|
||||
"""
|
||||
if self._widget is None:
|
||||
self._widget = self.Widget(self)
|
||||
return self._widget
|
||||
|
||||
@classmethod
|
||||
def set_widget(cls, WidgetCls):
|
||||
"""
|
||||
Convenience decorator to create the widget class for this setting.
|
||||
"""
|
||||
cls.Widget = WidgetCls
|
||||
return WidgetCls
|
||||
|
||||
@property
|
||||
def formatted(self):
|
||||
"""
|
||||
Default user-readable form of the setting.
|
||||
Should be a short single line.
|
||||
"""
|
||||
return self._format_data(self.parent_id, self.data)
|
||||
|
||||
@property
|
||||
def input_formatted(self) -> str:
|
||||
"""
|
||||
Format the current value as a default value for an input field.
|
||||
Returned string must be acceptable through parse_string.
|
||||
Does not take into account defaults.
|
||||
"""
|
||||
if self._data is not None:
|
||||
return str(self._data)
|
||||
else:
|
||||
return ""
|
||||
|
||||
@property
|
||||
def summary(self):
|
||||
"""
|
||||
Formatted summary of the data.
|
||||
May be implemented in `_format_data(..., summary=True, ...)` or overidden.
|
||||
"""
|
||||
return self._format_data(self.parent_id, self.data, summary=True)
|
||||
|
||||
@classmethod
|
||||
async def from_string(cls, parent_id, userstr: str, **kwargs):
|
||||
"""
|
||||
Return a setting instance initialised from a parsed user string.
|
||||
"""
|
||||
data = await cls._parse_string(parent_id, userstr, **kwargs)
|
||||
return cls(parent_id, data, **kwargs)
|
||||
|
||||
@classmethod
|
||||
async def _parse_string(cls, parent_id, string: str, **kwargs) -> Optional[SettingData]:
|
||||
"""
|
||||
Parse user provided string (usually from a TextInput) into raw setting data.
|
||||
Must be overriden by the setting if the setting is user-configurable.
|
||||
Returns None if the setting was unset.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@classmethod
|
||||
def _format_data(cls, parent_id, data, **kwargs):
|
||||
"""
|
||||
Convert raw setting data into a formatted user-readable string,
|
||||
representing the current value.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
"""
|
||||
command callback for set command?
|
||||
autocomplete for set command?
|
||||
|
||||
Might be better in a ConfigSetting subclass.
|
||||
But also mix into the base setting types.
|
||||
"""
|
||||
Reference in New Issue
Block a user