Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save sarena01/37f8c4f5548abb79d2d313fed71c8ca2 to your computer and use it in GitHub Desktop.
Save sarena01/37f8c4f5548abb79d2d313fed71c8ca2 to your computer and use it in GitHub Desktop.
Walkthrough guide on subclassing HelpCommand

A basic walkthrough guide on subclassing HelpCommand

This guide will walkthrough the ways to create a custom help command by subclassing HelpCommand.

Table Content

Brief explanation of subclassing

In simple terms, a subclass is a way to inherit a class behaviour/attributes from another class. Here's how you would subclass a class in Python.

class A:
    def __init__(self, attribute1):
        self.attribute1 = attribute1

    def method1(self):
        print("method 1")

    def method2(self):
        print("method 2")


class B(A):
    def __init__(self, attribute1, attribute2):
        super().__init__(attribute1) # This calls A().__init__ magic method.
        self.attribute2 = attribute2

    def method1(self): # Overrides A().method1
        print("Hi")

Given the example, the variable instance_a contains the instance of class A. As expected, the output will be this.

>>> instance_a = A(1)
>>> instance_a.attribute1
1
>>> instance_a.method1()
"method 1"

How about B class? instance_b contains the instance of class B, it inherits attributes/methods from class A. Meaning it will have everything that class A have, but with an additional attribute/methods.

>>> instance_b = B(1, 2)
>>> instance_b.attribute1
1
>>> instance_b.attribute2
2
>>> instance_b.method1()
"Hi"
>>> instance_b.method2()
"method 2"

Make sure to look and practice more into subclassing classes to understand fully on what it is before diving into subclassing HelpCommand.

Why subclassing HelpCommand is better

Firstly, let me show you the wrong way of creating a help command.

One of the most incorrect ways of to create a help command.

bot = commands.Bot(command_prefix="uwu ", help_command=None)

# OR
bot.help_command = None

# OR
bot.remove_command("help")

@bot.command()
async def help(ctx):
    ...

This is directly from YouTube, which are known to have bad tutorials for discord.py.

Why are these bad?

1. Command handling specifically for HelpCommand

Missing out on command handling that are specifically for HelpCommand.

For instance, say my prefix is !. Command handling such as

!help

!help <command>

!help <group>

!help <cog>

For a HelpCommand, all of these are handled in the background, including showing appropriate error when command/group/cog are an invalid argument that were given. You can also show custom error when an invalid argument are given.

For people who remove the HelpCommand? There is no handling, you have to do it yourself.

For example

Bad way
import discord
from discord.ext import commands

intents = discord.Intents.all()
bot = commands.Bot(command_prefix="!", help_command=None, intents=intents)

@bot.command()
async def help(ctx, argument=None):
    # !help
    if argument is None:
        await ctx.send("This is help")
    
    elif argument in bot.all_commands:
        command = bot.get_command(argument)
        if isinstance(command, commands.Group):
            # !help <group>
           await ctx.send("This is help group")
        else:
            # !help <command>
           await ctx.send("This is help command")
    elif argument in bot.cogs:
        # !help <cog>
        cog = bot.get_cog(argument)
        await ctx.send("This is help cog")
    else:
       await ctx.send("Invalid command or cog")

This is an ok implementation and all, but you have to handle more than this. I'm only simplifying the code.

Now for the subclassed HelpCommand code.

Good way

import discord
from discord.ext import commands
intents = discord.Intents.all()
bot = commands.Bot(command_prefix="!", intents=intents)

class MyHelp(commands.HelpCommand):
   # !help
    async def send_bot_help(self, mapping):
        await self.context.send("This is help")
       
   # !help <command>
    async def send_command_help(self, command):
        await self.context.send("This is help command")
      
   # !help <group>
    async def send_group_help(self, group):
        await self.context.send("This is help group")
    
   # !help <cog>
    async def send_cog_help(self, cog):
        await self.context.send("This is help cog")

bot.help_command = MyHelp()

Not only does HelpCommand looks better, it is also much more readable compared to the bad way. Oh, did I mention that HelpCommand also handles invalid arguments for you? Yeah. It does.

2. Utilities

HelpCommand contains a bunch of useful methods you can use in order to assist you in creating your help command and formatting.

Methods / Attributes Usage
HelpCommand.filter_commands() Filter commands to only show commands that the user can run. This help hide any secret commands from the general user.
Context.clean_prefix HelpCommand.clean_prefix was removed in version 2.0 of discord.py and replaced with Context.clean_prefix. This cleans your prefix from @everyone and @here as well as @mentions
HelpCommand.get_command_signature() Get the command signature and format them such as command [argument] for optional and command <argument> for required.
HelpCommand.prepare_help_command() Triggers before every send_x_help method are triggered, this work exactly like command.before_invoke
HelpCommand.get_bot_mapping() Get all command that are available in the bot, sort them by Cogs and None for No Category as key in a dictionary. This method is triggered before HelpCommand.send_bot_help is triggered, and will get passed as the parameter.
HelpCommand.get_destination() Returns a Messageable on where the help command was invoked.
HelpCommand.command_callback The method that handles all help/help cog/ help command/ help group and call which method appropriately. This is useful if you want to modify the behaviour of this. Though more knowledge is needed for you to do that. Most don't use this.
Context.send_help() Calling send_command_help based on what the Context command object were. This is useful to be used when the user incorrectly invoke the command. Which you can call this method to show help quickly and efficiently. (Only works if you have HelpCommand configured)

All of this does not exist when you set bot.help_command to None. You miss out on this.

3. Modular/Dynamic

Since it's a class, most people would make it modular. They put it in a cog for example. There is a common code given in discord.py created by Vex.

from discord.ext import commands
class MyHelpCommand(commands.MinimalHelpCommand):
    pass

class MyCog(commands.Cog):
    def __init__(self, bot):
        self.bot = bot
        self._original_help_command = bot.help_command
        bot.help_command = MyHelpCommand()
        bot.help_command.cog = self
        
    def cog_unload(self):
        self.bot.help_command = self._original_help_command

What does this mean?

Well, first we have a HelpCommand made there called MyHelpCommand. When MyCog class is loaded, bot.help_command is stored into self._original_help_command. This preserve the old help command that was attached to the bot, and then it is assigned to a new HelpCommand that you've made.

cog_unload is triggered when the cog is unloaded, which assign bot.help_command to the original help command.

What good does this give?

For example, you have a custom help command that is currently attached to bot.help_command. But you want to develop a new help command or modify the existing without killing the bot. So you can just unload the cog, which will assign the old help command to the bot so that you will always have a backup HelpCommand ready while you're modifying and testing your custom help.

Getting started

With that out of the way, let's get started. For subclassing HelpCommand, first, you would need to know the types of HelpCommand. Where each class has their own usage.

Types of HelpCommand class

There are a few types of HelpCommand classes that you can choose;

  1. DefaultHelpCommand a help command that is given by default.
  2. MinimalHelpCommand a slightly better help command.
  3. HelpCommand an empty class that is the base class for every HelpCommand you see. On its own, it will not do anything.

By default, help command is using the class DefaultHelpCommand. This is stored in bot.help_command. This attribute will ONLY accept instances that subclasses HelpCommand. Here is how you were to use the DefaultHelpCommand instance.

import discord
from discord.ext import commands
intents = discord.Intents.all()
bot = commands.Bot(command_prefix="uwu ", intents=intents)
bot.help_command = commands.DefaultHelpCommand()

# OR

help_command = commands.DefaultHelpCommand()
intents = discord.Intents.all()
bot = commands.Bot(command_prefix="uwu ", help_command=help_command, intents=intents)
# Both are equivalent

Here's an example of what that looks like.

img.png

Now, of course, this is done by default. I'm only showing you this as a demonstration. Don't scream at me

Let's do the same thing with MinimalHelpCommand next.

import discord
from discord.ext import commands
intents = discord.Intents.all()
bot = commands.Bot(command_prefix="uwu ", intents=intents)
bot.help_command = commands.MinimalHelpCommand()

This is how that would look like:

minhelpcommand.png

Embed MinimalHelpCommand

Now say, you want the content to be inside an embed. But you don't want to change the content of DefaultHelpCommand/MinimalHelpCommand since you want a simple HelpCommand with minimal work. There is a short code from ?tag embed help example by gogert in discord.py server, a sample code you can follow shows this;

import discord
from discord.ext import commands
intents = discord.Intents.all()
bot = commands.Bot(command_prefix="uwu ", intents=intents)

class MyNewHelp(commands.MinimalHelpCommand):
    async def send_pages(self):
        destination = self.get_destination()
        for page in self.paginator.pages:
            emby = discord.Embed(description=page)
            await destination.send(embed=emby)

bot.help_command = MyNewHelp()

The resulting code will show that it have the content of MinimalHelpCommand but in an embed.

embedminimalhelp.png

How does this work?

Looking over the MinimalHelpCommand source code, every method that is responsible for <prefix>help <argument> will call MinimalHelpCommand.send_pages when it is about to send the content. This makes it easy to just override send_pages without having to override any other method there are in MinimalHelpCommand.

HelpCommand

Basic methods to override

If you want to use HelpCommand class, we need to understand the basic of subclassing HelpCommand. Here are a list of HelpCommand relevant methods, and it's responsibility.

  1. HelpCommand.send_bot_help(mapping) Gets called with <prefix>help
  2. HelpCommand.send_command_help(command) Gets called with <prefix>help <command>
  3. HelpCommand.send_group_help(group) Gets called with <prefix>help <group>
  4. HelpCommand.send_cog_help(cog) Gets called with <prefix>help <cog>

Useful attributes

  1. HelpCommand.context the Context object in the help command.

For more, Click here

HelpCommand Flowchart

This is a bare minimum on what you should know on how a HelpCommand operate. As of discord version 1.* and 2.0. It remained the same flow. helpcommandflowchart.png

Seems simple enough? Now let's see what happens if you override one of the methods. Here's an example code of how you would do that. This override will say "hello!" when you type <prefix>help to demonstrate on what's going on.

We'll use HelpCommand.get_destination() to get the abc.Messageable instance for sending a message to the correct channel.

Code Example

import discord
from discord.ext import commands
intents = discord.Intents.all()
bot = commands.Bot(command_prefix="uwu ", intents=intents)

class MyHelp(commands.HelpCommand):
    async def send_bot_help(self, mapping):
        channel = self.get_destination()
        await channel.send("hello!")

bot.help_command = MyHelp()

Output

hellohelpcommand.png

Keep in mind, using HelpCommand class will require overriding every send_x_help methods. For example, <prefix>help jsk is a command that should call send_command_help method. However, since HelpCommand is an empty class, it will not say anything.

help command

Let's work our way to create a <prefix> help. Given the documentation, await send_bot_help(mapping) method receives mapping(Mapping[Optional[Cog], List[Command]]) as its parameter. await indicates that it should be an async function.

What does this mean?

  • Mapping[] is a collections.abc.Mapping, for simplicity’s sake, this usually refers to a dictionary since it's under collections.abc.Mapping.
  • Optional[Cog] is a Cog object that has a chance to be None.
  • List[Command] is a list of Command objects.
  • Mapping[Optional[Cog], List[Command]] means it's a map object with Optional[Cog] as it's key and List[Command] as its value.

All of these are typehints in the typing module. You can learn more about it here.

Now, for an example, we will use this mapping given in the parameter of send_bot_help. For each of the command, we'll use HelpCommand.get_command_signature(command) to get the command signature of a command in an str form.

Example Code

import discord
from discord.ext import commands
intents = discord.Intents.all()
bot = commands.Bot(command_prefix="uwu ", intents=intents)

class MyHelp(commands.HelpCommand):
    async def send_bot_help(self, mapping):
        embed = discord.Embed(title="Help")
        for cog, commands in mapping.items():
           command_signatures = [self.get_command_signature(c) for c in commands]
           if command_signatures:
                cog_name = getattr(cog, "qualified_name", "No Category")
                embed.add_field(name=cog_name, value="\n".join(command_signatures), inline=False)

        channel = self.get_destination()
        await channel.send(embed=embed)

bot.help_command = MyHelp()

How does it work?

  1. Create an embed.
  2. Use dict.items() to get an iterable of (Cog, list[Command]).
  3. Each element in list[Command], we will call self.get_command_signature(command) to get the proper signature of the command.
  4. If the list is empty, meaning, no commands is available in the cog, we don't need to show it, hence if command_signatures:.
  5. cog has a chance to be None, this refers to No Category. We'll use getattr to avoid getting an error to get cog's name through Cog.qualified_name.
  6. Using str.join each command will be displayed on a separate line.
  7. Once all of this is finished, display it.

The result

samplehelp.png

This looks pretty... terrible. Those '|' are aliases of the command hence it appeared with a second command name. Let's make the signature prettier, and what if you wanna hide commands that you don't want to be shown on the help command? Such as "sync" command there, that's only for the developer not other people.

We'll subclass commands.MinimalHelpCommand to use their MinimalHelpCommand.get_command_signature. It's actually more prettier than the default HelpCommand signature.

We'll use HelpCommand.filter_commands, this method will filter commands by removing any commands that the user cannot use. It is a handy method to use. This works by checking if the Command.hidden is set to True and, it will run Command.can_run to see if it raise any errors. If there is any, it will be filtered.

The Example

import discord
from discord.ext import commands
intents = discord.Intents.all()
bot = commands.Bot(command_prefix="uwu ", intents=intents)

class MyHelp(commands.MinimalHelpCommand):
    async def send_bot_help(self, mapping):
        embed = discord.Embed(title="Help")
        for cog, commands in mapping.items():
           filtered = await self.filter_commands(commands, sort=True)
           command_signatures = [self.get_command_signature(c) for c in filtered]
           if command_signatures:
                cog_name = getattr(cog, "qualified_name", "No Category")
                embed.add_field(name=cog_name, value="\n".join(command_signatures), inline=False)

        channel = self.get_destination()
        await channel.send(embed=embed)

bot.help_command = MyHelp()

The resulting output

betterhelpcommand.png

This looks more readable than the other one with a small modification to the code. While this should cover most of your needs, you may want to know more helpful attribute that is available on HelpCommand in the official documentation.

help [argument] command

Now that the hard part is done, let's take a look at <prefix>help [argument]. The method responsible for this is as follows;

  1. send_command_help
  2. send_cog_help
  3. send_group_help

As a demonstration, let's go for send_command_help this method receive a Command object. For this, it's simple, all you have show is the attribute of the command.

For example, this is your command code, your goal is you want to show the help,aliases and the signature.

Command Code

@bot.command(help="Generic help command for command hello.",
             aliases=["h", "hellos", "hell", "hehe"])
async def hello(ctx, bot: discord.Member):
   pass

Then it's simple, you can display each of the attribute by Command.help and Command.aliases.

For the signature, instead of using the previous get_command_signature, we're going to subclass MinimalHelpCommand.

Help Code

import discord
from discord.ext import commands
intents = discord.Intents.all()
bot = commands.Bot(command_prefix="uwu ", intents=intents)

class MyHelp(commands.MinimalHelpCommand):
    async def send_command_help(self, command):
        embed = discord.Embed(title=self.get_command_signature(command))
        embed.add_field(name="Help", value=command.help)
        alias = command.aliases
        if alias:
            embed.add_field(name="Aliases", value=", ".join(alias), inline=False)

        channel = self.get_destination()
        await channel.send(embed=embed)

bot.help_command = MyHelp()

What you get

commandhelpcommand.png

As you can see, it is very easy to create <prefix>help [argument]. The class already handles the pain of checking whether the given argument is a command, a cog, or a group command. It's up to you on how you want to display it, whether it's through a plain message, an embed or even using discord.ext.menus.

Command Attributes

Let's say, someone is spamming your help command. For a normal command, all you have to do to combat this is using a cooldown decorator and slap that thing above the command declaration. Or, what about if you want an alias? Usually, you would put an aliases kwargs in the command decorator. However, HelpCommand is a bit special, It's a god damn class. You can't just put a decorator on it and expect it to work.

That is when HelpCommand.command_attrs come to the rescue. This attribute can be set during the HelpCommand declaration, or a direct attribute assignment. According to the documentation, it accepts exactly the same thing as a command decorator in a form of a dictionary.

For example, we want to rename the help command as "hell" instead of "help" for whatever reason. We also want to make an alias for "help" so users can call the command with "hell" and "help". Finally, we want to put a cooldown, because help command messages are big, and we don't want people to spam those. So, what would the code look like?

Example Code

import discord
from discord.ext import commands

intents = discord.Intents.all()
attributes = {
   'name': "hell",
   'aliases': ["help", "helps"],
   'cooldown': commands.CooldownMapping.from_cooldown(2, 5.0, commands.BucketType.user)
} 

# During declaration
help_object = commands.MinimalHelpCommand(command_attrs=attributes)

# OR through attribute assignment
help_object = commands.MinimalHelpCommand()
help_object.command_attrs = attributes

bot = commands.Bot(command_prefix="uwu ", help_command=help_object, intents=intents)

How does it work?

  1. sets the name into "hell" is refers to here 'name': "hell".
  2. sets the aliases by passing the list of str to the aliases key, which refers to here 'aliases': ["help", "helps"].
  3. sets the cooldown through the "cooldown" key by passing in a CooldownMapping object. This object will make a cooldown with a rate of 2, per 5 with a bucket type BucketType.user, which in simple terms, for every discord.User, they can call the command twice, every 5 seconds.
  4. We're going to use MinimalHelpCommand as the HelpCommand object.

Note: on Number 3, Cooldown has been updated on 2.0. Please check the code on your own instead.

The result

cooldownhelp.png

As you can see, the name of the help command is now "hell", and you can also trigger the help command by "help". It will also raise an OnCommandCooldown error if it was triggered 3 times in 5 seconds due to our Cooldown object. Of course, I didn't show that in the result, but you can try it yourself. You should handle the error in an error handler when an OnCommandCooldown is raised.

Error handling for HelpCommand

Basic error message override

What happens when <prefix>help command fails to get a command/cog/group? Simple, HelpCommand.send_error_message will be called. HelpCommand will not call on_command_error when it can't find an existing command. It will also give you an str instead of an error instance.

import discord
from discord.ext import commands
intents = discord.Intents.all()
bot = commands.Bot(command_prefix="uwu ", intents=intents)

class MyHelp(commands.HelpCommand):
    async def send_error_message(self, error):
        embed = discord.Embed(title="Error", description=error)
        channel = self.get_destination()
        await channel.send(embed=embed)

bot.help_command = MyHelp()

error is a string that will only contain the message, all you have to do is display the message.

The output:

errorhelp.png

How about a local error handler?

Indeed, we have it. HelpCommand.on_help_command_error, this method is responsible for handling any error just like any other local error handler.

Code

import discord
from discord.ext import commands
intents = discord.Intents.all()
bot = commands.Bot(command_prefix="uwu ", intents=intents)

class MyHelp(commands.HelpCommand):
    async def send_bot_help(self, mapping):
        raise commands.BadArgument("Something broke")

    async def on_help_command_error(self, ctx, error):
        if isinstance(error, commands.BadArgument):
            embed = discord.Embed(title="Error", description=str(error))
            await ctx.send(embed=embed)
        else:
            raise error

bot.help_command = MyHelp()

Rather basic, just raise an error that subclasses commands.CommandError such as commands.BadArgument. The error raised will cause on_help_command_error to be invoked. The code shown will catch this commands.BadArgument instance that is stored in error variable, and show the message.

Output:

errorhandler.png

To be fair, you should create a proper error handler through this official documentation. Here.

There is also a lovely example by Mysty on error handling in general. Here. This example shows how to properly create a global error handler and local error handler.

Setting Cog for HelpCommand

Super easy lol

It works just like setting a cog on a Command object, you basically have to assign a commands.Cog instance into HelpCommand.cog. It is pretty common for discord.py users to put a HelpCommand into a cog/separate file since people want organization.

Code Example

This code example is if you're in a Cog file, which you have access to a Cog instance

from discord.ext import commands

# Unimportant part
class MyHelp(commands.HelpCommand):
    async def send_bot_help(self, mapping):
        channel = self.get_destination()
        await channel.send("hey")


class YourCog(commands.Cog):
    def __init__(self, bot):
       self.bot = bot
        
       # Focus here
       # Setting the cog for the help
       help_command = MyHelp()
       help_command.cog = self # Instance of YourCog class
       bot.help_command = help_command


async def setup(bot):
    await bot.add_cog(YourCog(bot))

How does it work?

  1. It instantiates the HelpCommand class. help_command = MyHelp()
  2. It assigns the instance of YourCog(self) into cog attribute. When you assign a Cog on a HelpCommand, discord.py will automatically know that the HelpCommand belongs to that Cog. It was stated here.

The end

I hope that reading this walkthrough will assist you and give a better understanding on how to subclass HelpCommand. All the example code given are to demonstrate the feature of HelpCommand and feel free to try it. There are lots of creative things you can do to create a HelpCommand.

If you want a generic help command, here's an example of a help command written by pikaninja Here's the code Here's how it looks like.

help_simple

For my implementation, its a bit complex. I wrote a library for myself that I use in several bots. You can see the codes through this repository. Which you're free to use if you want a quick setup for your help command.

Looks like this

menuhelpdefault.png

Now, of course, any question regarding HelpCommand should be asked in the discord.py server because I don't really check this gist as much, and because there is a lot of helpful discord.py helpers if you're nice enough to them.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment