Skip to content

Commit

Permalink
Switch to composition for client class, implement localizations for c…
Browse files Browse the repository at this point in the history
…ommands groups and options
  • Loading branch information
tandemdude committed Mar 4, 2024
1 parent be6a2d2 commit 605fc0c
Show file tree
Hide file tree
Showing 15 changed files with 632 additions and 142 deletions.
3 changes: 1 addition & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,7 @@ docs/source/api-reference.rst

# Test files
bot_test.py
extension_test.py
testext/
_test_translations/
.token

# Stupid OSX files
Expand Down
3 changes: 3 additions & 0 deletions lightbulb/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
from lightbulb.commands import *
from lightbulb.context import *
from lightbulb.internal import *
from lightbulb.localization import *

__all__ = [
"exceptions",
Expand Down Expand Up @@ -60,6 +61,8 @@
"Context",
"ensure_di_context",
"with_di",
"Localization",
"LocalizationManager",
]

# Do not change the below field manually. It is updated by CI upon release.
Expand Down
54 changes: 37 additions & 17 deletions lightbulb/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
#
# You should have received a copy of the GNU Lesser General Public License
# along with Lightbulb. If not, see <https://www.gnu.org/licenses/>.
import abc
import collections
import functools
import logging
Expand All @@ -27,8 +26,9 @@
from lightbulb.commands import commands
from lightbulb.commands import execution
from lightbulb.commands import groups
from lightbulb.internal import di
from lightbulb.internal import di as di_
from lightbulb.internal import utils
from lightbulb.localization import localization as localization_

__all__ = ["Client", "GatewayEnabledClient", "RestEnabledClient", "client_from_app"]

Expand Down Expand Up @@ -56,7 +56,7 @@ class RestClientAppT(hikari.InteractionServerAware, hikari.RESTAware, t.Protocol
"""Protocol indicating an application supports an interaction server."""


class Client(di.DependencySupplier, abc.ABC):
class Client:
"""
Base client implementation supporting generic application command handling.
Expand All @@ -66,13 +66,17 @@ class Client(di.DependencySupplier, abc.ABC):
commands should be created in by default. Can be overridden on a per-command basis.
execution_step_order (:obj:`~typing.Sequence` [ :obj:`~lightbulb.commands.execution.ExecutionStep` ]): The
order that execution steps will be run in upon command processing.
default_locale: (:obj:`~hikari.locales.Locale`): The default locale to use for command names and descriptions,
as well as option names and descriptions. If you are not using localizations then this will do nothing.
"""

__slots__ = (
"_commands",
"rest",
"default_enabled_guilds",
"execution_step_order",
"_di",
"_localization",
"_commands",
"_application",
)

Expand All @@ -81,27 +85,37 @@ def __init__(
rest: hikari.api.RESTClient,
default_enabled_guilds: t.Sequence[hikari.Snowflakeish],
execution_step_order: t.Sequence[execution.ExecutionStep],
default_locale: hikari.Locale,
) -> None:
super().__init__()

self.rest = rest
self.default_enabled_guilds = default_enabled_guilds
self.execution_step_order = execution_step_order

self._di = di_.DependencyInjectionManager()
self._localization = localization_.LocalizationManager(default_locale)

self._commands: CommandMapT = collections.defaultdict(lambda: collections.defaultdict(utils.CommandCollection))
self._application: t.Optional[hikari.PartialApplication] = None

@property
def di(self) -> di_.DependencyInjectionManager:
return self._di

@property
def localization(self) -> localization_.LocalizationManager:
return self._localization

@t.overload
def register(
self, *, guilds: t.Optional[t.Sequence[hikari.Snowflakeish]] = None
) -> t.Callable[[CommandOrGroupT], CommandOrGroupT]:
...
) -> t.Callable[[CommandOrGroupT], CommandOrGroupT]: ...

@t.overload
def register(
self, command: CommandOrGroupT, *, guilds: t.Optional[t.Sequence[hikari.Snowflakeish]] = None
) -> CommandOrGroupT:
...
) -> CommandOrGroupT: ...

def register(
self,
Expand Down Expand Up @@ -201,7 +215,11 @@ async def sync_application_commands(self) -> None:

builders: t.List[hikari.api.CommandBuilder] = []
for cmds in guild_commands.values():
builders.extend(c.as_command_builder() for c in [cmds.slash, cmds.user, cmds.message] if c is not None)
builders.extend(
c.as_command_builder(self.localization)
for c in [cmds.slash, cmds.user, cmds.message]
if c is not None
)

await self.rest.set_application_commands(application, builders, guild_id)

Expand All @@ -219,14 +237,12 @@ def _get_subcommand(
@t.overload
def _resolve_options_and_command(
self, interaction: hikari.AutocompleteInteraction
) -> t.Optional[t.Tuple[t.Sequence[hikari.AutocompleteInteractionOption], t.Type[commands.CommandBase]]]:
...
) -> t.Optional[t.Tuple[t.Sequence[hikari.AutocompleteInteractionOption], t.Type[commands.CommandBase]]]: ...

@t.overload
def _resolve_options_and_command(
self, interaction: hikari.CommandInteraction
) -> t.Optional[t.Tuple[t.Sequence[hikari.CommandInteractionOption], t.Type[commands.CommandBase]]]:
...
) -> t.Optional[t.Tuple[t.Sequence[hikari.CommandInteractionOption], t.Type[commands.CommandBase]]]: ...

def _resolve_options_and_command(
self, interaction: t.Union[hikari.AutocompleteInteraction, hikari.CommandInteraction]
Expand Down Expand Up @@ -298,7 +314,7 @@ async def handle_autocomplete_interaction(self, interaction: hikari.Autocomplete

LOGGER.debug("%r - invoking autocomplete", command._command_data.qualified_name)

with di.ensure_di_context(self):
with di_.ensure_di_context(self.di):
try:
assert option.autocomplete_provider is not hikari.UNDEFINED
await option.autocomplete_provider(context)
Expand Down Expand Up @@ -355,7 +371,7 @@ async def handle_application_command_interaction(self, interaction: hikari.Comma

LOGGER.debug("invoking command - %r", command._command_data.qualified_name)

with di.ensure_di_context(self):
with di_.ensure_di_context(self.di):
try:
await execution.ExecutionPipeline(context, self.execution_step_order)._run()
except Exception as e:
Expand Down Expand Up @@ -443,6 +459,7 @@ def client_from_app(
app: t.Union[GatewayClientAppT, RestClientAppT],
default_enabled_guilds: t.Sequence[hikari.Snowflakeish] = (GLOBAL_COMMAND_KEY,),
execution_step_order: t.Sequence[execution.ExecutionStep] = DEFAULT_EXECUTION_STEP_ORDER,
default_locale: hikari.Locale = hikari.Locale.EN_US,
) -> Client:
"""
Create and return the appropriate client implementation from the given application.
Expand All @@ -453,13 +470,16 @@ def client_from_app(
commands should be created in by default.
execution_step_order (:obj:`~typing.Sequence` [ :obj:`~lightbulb.commands.execution.ExecutionStep` ]): The
order that execution steps will be run in upon command processing.
default_locale: (:obj:`~hikari.locales.Locale`): The default locale to use for command names and descriptions,
as well as option names and descriptions. If you are not using localizations then this will do nothing.
Defaults to :obj:`hikari.locales.Locale.EN_US`.
Returns:
:obj:`~Client`: The created client instance.
"""
if isinstance(app, GatewayClientAppT):
LOGGER.debug("building gateway client from app")
return GatewayEnabledClient(app, default_enabled_guilds, execution_step_order)
return GatewayEnabledClient(app, default_enabled_guilds, execution_step_order, default_locale)

LOGGER.debug("building REST client from app")
return RestEnabledClient(app, default_enabled_guilds, execution_step_order)
return RestEnabledClient(app, default_enabled_guilds, execution_step_order, default_locale)
79 changes: 56 additions & 23 deletions lightbulb/commands/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,13 @@

from lightbulb.commands import execution
from lightbulb.commands import options as options_
from lightbulb.commands import utils
from lightbulb.internal import constants

if t.TYPE_CHECKING:
from lightbulb import context as context_
from lightbulb.commands import groups
from lightbulb.localization import localization

__all__ = ["CommandData", "CommandMeta", "CommandBase", "UserCommand", "MessageCommand", "SlashCommand"]

Expand Down Expand Up @@ -64,10 +66,10 @@ class CommandData:
"""The name of the command."""
description: str
"""The description of the command."""
localize: bool
"""Whether the command name and description should be localized."""
nsfw: bool
"""Whether the command is marked as nsfw."""
localizations: t.Any # TODO
"""Not yet implemented"""
dm_enabled: bool
"""Whether the command is enabled in direct messages. This field is ignored for subcommands."""
default_member_permissions: hikari.UndefinedOr[hikari.Permissions]
Expand Down Expand Up @@ -104,42 +106,65 @@ def qualified_name(self) -> str:

return " ".join(names[::-1])

def as_command_builder(self) -> hikari.api.CommandBuilder:
def as_command_builder(self, localization_manager: localization.LocalizationManager) -> hikari.api.CommandBuilder:
"""
Convert the command data into a hikari command builder object.
Returns:
:obj:`hikari.api.CommandBuilder`: The builder object for this command data.
"""
name, description = self.name, self.description
name_localizations: utils.LocalizationMappingT = {}
description_localizations: utils.LocalizationMappingT = {}

if self.localize:
name, description, name_localizations, description_localizations = utils.localize_name_and_description(
name, description or None, localization_manager
)

if self.type is hikari.CommandType.SLASH:
bld = hikari.impl.SlashCommandBuilder(name=self.name, description=self.description)
bld = (
hikari.impl.SlashCommandBuilder(name=name, description=description)
.set_name_localizations(name_localizations)
.set_description_localizations(description_localizations)
.set_is_dm_enabled(self.dm_enabled)
.set_default_member_permissions(self.default_member_permissions)
)
for option in self.options.values():
bld.add_option(option.to_command_option())

bld.set_is_dm_enabled(self.dm_enabled)
bld.set_default_member_permissions(self.default_member_permissions)
bld.add_option(option.to_command_option(localization_manager))

return bld

return (
hikari.impl.ContextMenuCommandBuilder(type=self.type, name=self.name)
.set_name_localizations(name_localizations)
.set_is_dm_enabled(self.dm_enabled)
.set_default_member_permissions(self.default_member_permissions)
)

def to_command_option(self) -> hikari.CommandOption:
def to_command_option(self, localization_manager: localization.LocalizationManager) -> hikari.CommandOption:
"""
Convert the command data into a sub-command command option.
Returns:
:obj:`hikari.CommandOption`: The sub-command option for this command data.
"""
name, description = self.name, self.description
name_localizations: utils.LocalizationMappingT = {}
description_localizations: utils.LocalizationMappingT = {}

if self.localize:
name, description, name_localizations, description_localizations = utils.localize_name_and_description(
name, description, localization_manager
)

return hikari.CommandOption(
type=hikari.OptionType.SUB_COMMAND,
name=self.name,
description=self.description,
# TODO - localisations
options=[option.to_command_option() for option in self.options.values()],
name=name,
name_localizations=name_localizations,
description=description,
description_localizations=description_localizations,
options=[option.to_command_option(localization_manager) for option in self.options.values()],
)


Expand All @@ -158,8 +183,10 @@ class CommandMeta(type):
is subclassed. I.e. subclassing :obj:`SlashCommand` sets this parameter to :obj:`hikari.CommandType.SLASH`.
name (:obj:`str`, required): The name of the command.
description (:obj:`str`, optional): The description of the command. Only required for slash commands.
localize (:obj:`bool`, optional): Whether to localize the command's name and description. If :obj:`true`,
then the ``name`` and ``description`` arguments will instead be interpreted as localization keys from
which the actual name and description will be retrieved from. Defaults to :obj:`False`.
nsfw (:obj:`bool`, optional): Whether the command should be marked as nsfw. Defaults to :obj:`False`.
localizations (TODO, optional): Not yet implemented
dm_enabled (:obj:`bool`, optional): Whether the command can be used in direct messages. Defaults to :obj:`True`.
default_member_permissions (:obj:`hikari.Permissions`, optional): The default permissions required for a
guild member to use the command. If unspecified, all users can use the command by default. Set to
Expand Down Expand Up @@ -196,8 +223,8 @@ def __new__(cls, cls_name: str, bases: t.Tuple[type, ...], attrs: t.Dict[str, t.
cmd_name: str = kwargs.pop("name")
description: str = kwargs.pop("description", "")

localize: bool = kwargs.pop("localize", False)
nsfw: bool = kwargs.pop("nsfw", False)
localizations: t.Any = kwargs.pop("localizations", None)
dm_enabled: bool = kwargs.pop("dm_enabled", True)
default_member_permissions: hikari.UndefinedOr[hikari.Permissions] = kwargs.pop(
"default_member_permissions", hikari.UNDEFINED
Expand Down Expand Up @@ -228,8 +255,8 @@ def __new__(cls, cls_name: str, bases: t.Tuple[type, ...], attrs: t.Dict[str, t.
type=cmd_type,
name=cmd_name,
description=description,
localize=localize,
nsfw=nsfw,
localizations=localizations,
dm_enabled=dm_enabled,
default_member_permissions=default_member_permissions,
hooks=hooks,
Expand Down Expand Up @@ -326,24 +353,24 @@ def _resolve_option(self, option: options_.Option[T, D]) -> t.Union[T, D]:
return t.cast(T, resolved_option)

@classmethod
def as_command_builder(cls) -> hikari.api.CommandBuilder:
def as_command_builder(cls, localization_manager: localization.LocalizationManager) -> hikari.api.CommandBuilder:
"""
Convert the command into a hikari command builder object.
Returns:
:obj:`hikari.api.CommandBuilder`: The builder object for this command.
"""
return cls._command_data.as_command_builder()
return cls._command_data.as_command_builder(localization_manager)

@classmethod
def to_command_option(cls) -> hikari.CommandOption:
def to_command_option(cls, localization_manager: localization.LocalizationManager) -> hikari.CommandOption:
"""
Convert the command into a sub-command command option.
Returns:
:obj:`hikari.CommandOption`: The sub-command option for this command.
"""
return cls._command_data.to_command_option()
return cls._command_data.to_command_option(localization_manager)


class SlashCommand(CommandBase, metaclass=CommandMeta, type=hikari.CommandType.SLASH):
Expand All @@ -356,8 +383,10 @@ class SlashCommand(CommandBase, metaclass=CommandMeta, type=hikari.CommandType.S
Parameters:
name (:obj:`str`, required): The name of the command.
description (:obj:`str`, required): The description of the command.
localize (:obj:`bool`, optional): Whether to localize the command's name and description. If :obj:`true`,
then the ``name`` and ``description`` arguments will instead be interpreted as localization keys from
which the actual name and description will be retrieved from. Defaults to :obj:`False`.
nsfw (:obj:`bool`, optional): Whether the command should be marked as nsfw. Defaults to :obj:`False`.
localizations (TODO, optional): Not yet implemented
dm_enabled (:obj:`bool`, optional): Whether the command can be used in direct messages. Defaults to :obj:`True`.
default_member_permissions (:obj:`hikari.Permissions`, optional): The default permissions required for a
guild member to use the command. If unspecified, all users can use the command by default. Set to
Expand Down Expand Up @@ -392,8 +421,10 @@ class UserCommand(CommandBase, metaclass=CommandMeta, type=hikari.CommandType.US
Parameters:
name (:obj:`str`, required): The name of the command.
localize (:obj:`bool`, optional): Whether to localize the command's name and description. If :obj:`true`,
then the ``name`` and ``description`` arguments will instead be interpreted as localization keys from
which the actual name and description will be retrieved from. Defaults to :obj:`False`.
nsfw (:obj:`bool`, optional): Whether the command should be marked as nsfw. Defaults to :obj:`False`.
localizations (TODO, optional): Not yet implemented
dm_enabled (:obj:`bool`, optional): Whether the command can be used in direct messages. Defaults to :obj:`True`.
default_member_permissions (:obj:`hikari.Permissions`, optional): The default permissions required for a
guild member to use the command. If unspecified, all users can use the command by default. Set to
Expand Down Expand Up @@ -431,8 +462,10 @@ class MessageCommand(CommandBase, metaclass=CommandMeta, type=hikari.CommandType
Parameters:
name (:obj:`str`, required): The name of the command.
localize (:obj:`bool`, optional): Whether to localize the command's name and description. If :obj:`true`,
then the ``name`` and ``description`` arguments will instead be interpreted as localization keys from
which the actual name and description will be retrieved from. Defaults to :obj:`False`.
nsfw (:obj:`bool`, optional): Whether the command should be marked as nsfw. Defaults to :obj:`False`.
localizations (TODO, optional): Not yet implemented
dm_enabled (:obj:`bool`, optional): Whether the command can be used in direct messages. Defaults to :obj:`True`.
default_member_permissions (:obj:`hikari.Permissions`, optional): The default permissions required for a
guild member to use the command. If unspecified, all users can use the command by default. Set to
Expand Down
Loading

0 comments on commit 605fc0c

Please sign in to comment.