Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support specifying top-level aliases in the command and group decorator #91

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions bot/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,12 @@

import os
import typing
from functools import partial, partialmethod

import loguru
from bot.command import Command
from bot.group import Group
from discord.ext import commands
from loguru import logger

from .constants import LOG_FILE
Expand All @@ -24,3 +28,12 @@ def should_rotate(message: loguru.Message, file: typing.TextIO) -> bool:

logger.add(LOG_FILE, rotation=should_rotate)
logger.info("Logging Process Started")


# Monkey-patch discord.py decorators to use the Command subclass which supports root aliases.
# Must be patched before any cogs are added.
commands.command = partial(commands.command, cls=Command)
commands.GroupMixin.command = partialmethod(commands.GroupMixin.command, cls=Command)

commands.group = partial(commands.group, cls=Group)
commands.GroupMixin.group = partialmethod(commands.GroupMixin.group, cls=Group)
43 changes: 43 additions & 0 deletions bot/bot.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import os
from typing import Optional

from aiohttp import ClientSession
from discord import Embed
Expand Down Expand Up @@ -55,3 +56,45 @@ async def close(self) -> None:

if self.http_session:
await self.http_session.close()

def add_command(self, command: commands.Command) -> None:
"""Add `command` as normal and then add its root aliases to the bot."""
super().add_command(command)
self._add_root_aliases(command)

def remove_command(self, name: str) -> Optional[commands.Command]:
"""
Remove a command/alias as normal and then remove its root aliases from the bot.

Individual root aliases cannot be removed by this function.
To remove them, either remove the entire command or manually edit `bot.all_commands`.
"""
command = super().remove_command(name)
if command is None:
# Even if it's a root alias, there's no way to get the Bot instance to remove the alias.
return

self._remove_root_aliases(command)
return command

def _add_root_aliases(self, command: commands.Command) -> None:
"""Recursively add root aliases for `command` and any of its subcommands."""
if isinstance(command, commands.Group):
for subcommand in command.commands:
self._add_root_aliases(subcommand)

for alias in getattr(command, "root_aliases", ()):
if alias in self.all_commands:
# If alias matches any of the commands in the bot, then raise CommandRegistrationError
raise commands.CommandRegistrationError(alias, alias_conflict=True)

self.all_commands[alias] = command

def _remove_root_aliases(self, command: commands.Command) -> None:
"""Recursively remove root aliases for `command` and any of its subcommands."""
if isinstance(command, commands.Group):
for subcommand in command.commands:
self._remove_root_aliases(subcommand)

for alias in getattr(command, "root_aliases", ()):
self.all_commands.pop(alias, None)
23 changes: 23 additions & 0 deletions bot/command.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from discord.ext import commands


class Command(commands.Command):
"""
A `discord.ext.commands.Command` subclass which supports root aliases.

A `root_aliases` keyword argument is added, which is a sequence of alias names that will act as
top-level commands rather than being aliases of the command's group.

Example:
`!gh issue 72` can also be called as `!issue 72`.
`!gh src eval` can also be called as `!src eval`.
"""

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.root_aliases = kwargs.get("root_aliases", [])

if not isinstance(self.root_aliases, (list, tuple)):
raise TypeError(
"Root aliases must be of type list or tuple, containing strings."
)
15 changes: 10 additions & 5 deletions bot/exts/github/github.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import typing

from bot.bot import Bot
from bot.constants import BOT_REPO_URL
from discord import Embed
from discord.ext import commands
Expand All @@ -18,15 +19,15 @@ class Github(commands.Cog):
└ source Displays information about the bot's source code.
"""

def __init__(self, bot: commands.Bot) -> None:
def __init__(self, bot: Bot) -> None:
self.bot = bot

@commands.group(name="github", aliases=("gh",), invoke_without_command=True)
async def github_group(self, ctx: commands.Context) -> None:
"""Commands for Github."""
await ctx.send_help(ctx.command)

@github_group.command(name="profile")
@github_group.command(name="profile", root_aliases=("profile",))
@commands.cooldown(1, 10, BucketType.user)
async def profile(self, ctx: commands.Context, username: str) -> None:
"""
Expand All @@ -39,7 +40,7 @@ async def profile(self, ctx: commands.Context, username: str) -> None:

await ctx.send(embed=embed)

@github_group.command(name="issue", aliases=("pr",))
@github_group.command(name="issue", aliases=("pr",), root_aliases=("pr", "issue"))
async def issue(
self,
ctx: commands.Context,
Expand All @@ -63,7 +64,11 @@ async def issue(

await ctx.send(embed=embed)

@github_group.command(name="source", aliases=("src", "inspect"))
@github_group.command(
name="source",
aliases=("src", "inspect"),
root_aliases=("src", "inspect", "source"),
)
async def source_command(
self, ctx: commands.Context, *, source_item: typing.Optional[str] = None
) -> None:
Expand All @@ -86,6 +91,6 @@ async def source_command(
await ctx.send(embed=embed)


def setup(bot: commands.Bot) -> None:
def setup(bot: Bot) -> None:
"""Load the Github cog."""
bot.add_cog(Github(bot))
23 changes: 23 additions & 0 deletions bot/group.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from discord.ext import commands


class Group(commands.Group):
"""
A `discord.ext.commands.Group` subclass which supports root aliases.

A `root_aliases` keyword argument is added, which is a sequence of alias names that will act as
top-level groups rather than being aliases of the group's group

Example:
`!cmd gh issue 72` can also be called as `!gh issue 72`.
`!cmd src eval` can also be called as `!gh src eval`.
"""

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.root_aliases = kwargs.get("root_aliases", [])

if not isinstance(self.root_aliases, (list, tuple)):
raise TypeError(
"Root aliases must be of type list or tuple, containing strings."
)