Module aethersprite.settings

Settings module; interfaced with via aethersprite.extensions.base.settings

This provides methods by which extension authors can register and use settings in their code, and end users can manipulate those settings via bot commands (in the aforementioned settings extension).

Examples for registering a setting and getting/changing/resetting its value:

There are commands for doing each of these things already in the base extension mentioned above that provide authorization checks, but these are just simple examples.

from discord.ext.commands import command
from aethersprite.settings import register, settings

register("my.setting", "default value", False, lambda x: True,
         "There are many settings like it, but this one is mine.")

@command()
async def check(ctx):
    await ctx.send(settings["my.setting"].get(ctx))

@command()
async def change(ctx):
    settings["my.setting"].set(ctx, "new value")

@command()
async def reset(ctx):
    settings["my.setting"].clear(ctx)
Expand source code
"""
Settings module; interfaced with via `aethersprite.extensions.base.settings`

This provides methods by which extension authors can register and use settings
in their code, and end users can manipulate those settings via bot commands
(in the aforementioned `settings` extension).

Examples for registering a setting and getting/changing/resetting its value:

> There are commands for doing each of these things already in the base
> extension mentioned above that provide authorization checks, but these are
> just simple examples.

```python
from discord.ext.commands import command
from aethersprite.settings import register, settings

register("my.setting", "default value", False, lambda x: True,
         "There are many settings like it, but this one is mine.")

@command()
async def check(ctx):
    await ctx.send(settings["my.setting"].get(ctx))

@command()
async def change(ctx):
    settings["my.setting"].set(ctx, "new value")

@command()
async def reset(ctx):
    settings["my.setting"].clear(ctx)
```
"""

# stdlib
from __future__ import annotations
import typing

if typing.TYPE_CHECKING:
    from .filters import SettingFilter

# 3rd party
from discord.ext.commands import Context
from sqlitedict import SqliteDict

# local
from . import data_folder

# TODO cleanup settings for missing servers/channels on startup


class Setting(object):
    """Setting class; represents an individual setting definition"""

    # Setting values
    _values = SqliteDict(
        f"{data_folder}settings.sqlite3", tablename="values", autocommit=True
    )

    def __init__(
        self,
        name: str,
        default: typing.Any | None,
        validate: typing.Callable,
        channel: bool = False,
        description: str | None = None,
        filter: "SettingFilter | None" = None,
    ):
        if name is None:
            raise ValueError("Name must not be None")

        self.name = name
        """The setting's name"""

        self.default = default
        """Default value"""

        self.validate = validate
        """Validator function"""

        self.channel = channel
        """If this is a channel (and not a guild) setting"""

        self.description = description
        """This setting's description"""

        self.filter = filter
        """The filter used to manipulate setting input/output"""

    def _ctxkey(self, ctx: Context, channel: int | None = None) -> str:
        """
        Get the key to use when storing/accessing the setting.

        Args:
            ctx: The Discord connection context
            channel: The channel (if not the same as the context)

        Returns:
            The composite key
        """

        assert ctx.guild
        key = str(
            ctx.guild["id"] if isinstance(ctx.guild, dict) else ctx.guild.id
        )

        if self.channel:
            key += f"#{ctx.channel.id if channel is None else channel}"

        return key

    def set(
        self,
        ctx: Context,
        value: str | None,
        raw: bool = False,
        channel: int | None = None,
    ) -> bool:
        """
        Change the setting's value.

        Args:
            ctx: The Discord connection context
            value: The value to assign (or ``None`` for the default)
            raw: Set to True to bypass filtering
            channel: The channel (if not the same as the context)

        Returns:
            Success
        """

        key = self._ctxkey(ctx, channel)
        vals = self._values[key] if key in self._values else {}

        try:
            if not raw and self.filter is not None:
                filtered = self.filter.in_(ctx, value)

                if filtered is None:
                    return False

                value = filtered
        except ValueError:
            return False

        if value is None:
            vals[self.name] = self.default
        else:
            if not self.validate(value):
                return False

            vals[self.name] = value

        self._values[key] = vals

        return True

    def get(
        self, ctx: Context, raw: bool = False, channel: int | None = None
    ) -> typing.Any | None:
        """
        Get the setting's value.

        Args:
            ctx: The Discord connection context
            raw: Set to True to bypass filtering
            channel: The channel (if not the same as the context)

        Returns:
            The setting's value
        """

        key = self._ctxkey(ctx, channel)
        val = (
            self._values[key][self.name]
            if key in self._values and self.name in self._values[key]
            else None
        )

        if not raw and self.filter is not None:
            val = self.filter.out(ctx, val)

        return self.default if val is None else val


def register(
    name: str,
    default: typing.Any | None,
    validator: typing.Callable,
    channel: bool = False,
    description: str | None = None,
    filter: "SettingFilter | None" = None,
):
    """
    Register a setting.

    Args:
        name: The name of the setting
        default: The default value if none is provided
        validator: The validation function for the setting's value
        channel: If this is a channel (and not a guild) setting
        filter: The filter to use for setting/getting values
    """

    global settings

    if name in settings:
        raise Exception(f"Setting already exists: {name}")

    settings[name] = Setting(
        name, default, validator, channel, description, filter=filter
    )


settings: dict[str, Setting] = {}
"""Setting definitions"""

Global variables

var settings : dict[str, Setting]

Setting definitions

Functions

def register(name: str, default: typing.Any | None, validator: typing.Callable, channel: bool = False, description: str | None = None, filter: "'SettingFilter | None'" = None)

Register a setting.

Args

name
The name of the setting
default
The default value if none is provided
validator
The validation function for the setting's value
channel
If this is a channel (and not a guild) setting
filter
The filter to use for setting/getting values
Expand source code
def register(
    name: str,
    default: typing.Any | None,
    validator: typing.Callable,
    channel: bool = False,
    description: str | None = None,
    filter: "SettingFilter | None" = None,
):
    """
    Register a setting.

    Args:
        name: The name of the setting
        default: The default value if none is provided
        validator: The validation function for the setting's value
        channel: If this is a channel (and not a guild) setting
        filter: The filter to use for setting/getting values
    """

    global settings

    if name in settings:
        raise Exception(f"Setting already exists: {name}")

    settings[name] = Setting(
        name, default, validator, channel, description, filter=filter
    )

Classes

class Setting (name: str, default: typing.Any | None, validate: typing.Callable, channel: bool = False, description: str | None = None, filter: "'SettingFilter | None'" = None)

Setting class; represents an individual setting definition

Expand source code
class Setting(object):
    """Setting class; represents an individual setting definition"""

    # Setting values
    _values = SqliteDict(
        f"{data_folder}settings.sqlite3", tablename="values", autocommit=True
    )

    def __init__(
        self,
        name: str,
        default: typing.Any | None,
        validate: typing.Callable,
        channel: bool = False,
        description: str | None = None,
        filter: "SettingFilter | None" = None,
    ):
        if name is None:
            raise ValueError("Name must not be None")

        self.name = name
        """The setting's name"""

        self.default = default
        """Default value"""

        self.validate = validate
        """Validator function"""

        self.channel = channel
        """If this is a channel (and not a guild) setting"""

        self.description = description
        """This setting's description"""

        self.filter = filter
        """The filter used to manipulate setting input/output"""

    def _ctxkey(self, ctx: Context, channel: int | None = None) -> str:
        """
        Get the key to use when storing/accessing the setting.

        Args:
            ctx: The Discord connection context
            channel: The channel (if not the same as the context)

        Returns:
            The composite key
        """

        assert ctx.guild
        key = str(
            ctx.guild["id"] if isinstance(ctx.guild, dict) else ctx.guild.id
        )

        if self.channel:
            key += f"#{ctx.channel.id if channel is None else channel}"

        return key

    def set(
        self,
        ctx: Context,
        value: str | None,
        raw: bool = False,
        channel: int | None = None,
    ) -> bool:
        """
        Change the setting's value.

        Args:
            ctx: The Discord connection context
            value: The value to assign (or ``None`` for the default)
            raw: Set to True to bypass filtering
            channel: The channel (if not the same as the context)

        Returns:
            Success
        """

        key = self._ctxkey(ctx, channel)
        vals = self._values[key] if key in self._values else {}

        try:
            if not raw and self.filter is not None:
                filtered = self.filter.in_(ctx, value)

                if filtered is None:
                    return False

                value = filtered
        except ValueError:
            return False

        if value is None:
            vals[self.name] = self.default
        else:
            if not self.validate(value):
                return False

            vals[self.name] = value

        self._values[key] = vals

        return True

    def get(
        self, ctx: Context, raw: bool = False, channel: int | None = None
    ) -> typing.Any | None:
        """
        Get the setting's value.

        Args:
            ctx: The Discord connection context
            raw: Set to True to bypass filtering
            channel: The channel (if not the same as the context)

        Returns:
            The setting's value
        """

        key = self._ctxkey(ctx, channel)
        val = (
            self._values[key][self.name]
            if key in self._values and self.name in self._values[key]
            else None
        )

        if not raw and self.filter is not None:
            val = self.filter.out(ctx, val)

        return self.default if val is None else val

Instance variables

var channel

If this is a channel (and not a guild) setting

var default

Default value

var description

This setting's description

var filter

The filter used to manipulate setting input/output

var name

The setting's name

var validate

Validator function

Methods

def get(self, ctx: Context, raw: bool = False, channel: int | None = None) ‑> typing.Any | None

Get the setting's value.

Args

ctx
The Discord connection context
raw
Set to True to bypass filtering
channel
The channel (if not the same as the context)

Returns

The setting's value

Expand source code
def get(
    self, ctx: Context, raw: bool = False, channel: int | None = None
) -> typing.Any | None:
    """
    Get the setting's value.

    Args:
        ctx: The Discord connection context
        raw: Set to True to bypass filtering
        channel: The channel (if not the same as the context)

    Returns:
        The setting's value
    """

    key = self._ctxkey(ctx, channel)
    val = (
        self._values[key][self.name]
        if key in self._values and self.name in self._values[key]
        else None
    )

    if not raw and self.filter is not None:
        val = self.filter.out(ctx, val)

    return self.default if val is None else val
def set(self, ctx: Context, value: str | None, raw: bool = False, channel: int | None = None) ‑> bool

Change the setting's value.

Args

ctx
The Discord connection context
value
The value to assign (or None for the default)
raw
Set to True to bypass filtering
channel
The channel (if not the same as the context)

Returns

Success

Expand source code
def set(
    self,
    ctx: Context,
    value: str | None,
    raw: bool = False,
    channel: int | None = None,
) -> bool:
    """
    Change the setting's value.

    Args:
        ctx: The Discord connection context
        value: The value to assign (or ``None`` for the default)
        raw: Set to True to bypass filtering
        channel: The channel (if not the same as the context)

    Returns:
        Success
    """

    key = self._ctxkey(ctx, channel)
    vals = self._values[key] if key in self._values else {}

    try:
        if not raw and self.filter is not None:
            filtered = self.filter.in_(ctx, value)

            if filtered is None:
                return False

            value = filtered
    except ValueError:
        return False

    if value is None:
        vals[self.name] = self.default
    else:
        if not self.validate(value):
            return False

        vals[self.name] = value

    self._values[key] = vals

    return True