This guide will walkthrough the ways to create a custom help command by subclassing HelpCommand.
- Brief explanation of subclassing
- Why subclassing is better
- Getting started
- HelpCommand
- Paginated Help Command
- The end
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.
Firstly, let me show you the wrong way of creating 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.
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
bot = commands.Bot(command_prefix="!", help_command=None)
@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.
bot = commands.Bot(command_prefix="!")
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.
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. |
| HelpCommand.clean_prefix | Get a clean prefix that escape mentions and format them in a readable way such as @Name instead of <@id> format. Works up to version 1.7.1. |
| Context.clean_prefix | HelpCommand.clean_prefix was removed in version 2.0 of discord.py and replaced with Context.clean_prefix |
| 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 behavour 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.
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.
class MyHelpCommand(commands.MinimalHelpCommand):
def get_command_signature(self, command):
return '{0.clean_prefix}{1.qualified_name} {1.signature}'.format(self, command)
class MyCog(commands.Cog):
def __init__(self, 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_commandWell, 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.
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.
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.
There are a few types of HelpCommand classes that you can choose;
DefaultHelpCommanda help command that is given by default.MinimalHelpCommanda slightly better help command.HelpCommandan 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.
from discord.ext import commands
bot = commands.Bot(command_prefix="uwu ")
bot.help_command = commands.DefaultHelpCommand()
# OR
bot = commands.Bot(command_prefix="uwu ", help_command=commands.DefaultHelpCommand())
# Both are equivalent
Let's do the same thing with MinimalHelpCommand next.
from discord.ext import commands
bot = commands.Bot(command_prefix="uwu ")
bot.help_command = commands.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
bot = commands.Bot(command_prefix="uwu ")
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.
Looking over the MinimalHelpCommand source code, here.
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.
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.
HelpCommand.send_bot_help(mapping)Gets called with<prefix>helpHelpCommand.send_command_help(command)Gets called with<prefix>help <command>HelpCommand.send_group_help(group)Gets called with<prefix>help <group>HelpCommand.send_cog_help(cog)Gets called with<prefix>help <cog>
HelpCommand.contextthe Context object in the help command.HelpCommand.clean_prefixa cleanup prefix version that remove any mentions.
For more, Click here
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.
from discord.ext import commands
bot = commands.Bot(command_prefix="uwu ")
class MyHelp(commands.HelpCommand):
async def send_bot_help(self, mapping):
channel = self.get_destination()
await channel.send("hello!")
bot.help_command = MyHelp()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.
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.
Mapping[]is acollections.abc.Mapping, for simplicity’s sake, this usually refers to a dictionary since it's undercollections.abc.Mapping.Optional[Cog]is aCogobject that has a chance to beNone.List[Command]is a list ofCommandobjects.Mapping[Optional[Cog], List[Command]]means it's a map object withOptional[Cog]as it's key andList[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.
import discord
from discord.ext import commands
bot = commands.Bot(command_prefix="uwu ")
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()- Create an embed.
- Use dict.items() to get an iterable of
(Cog, list[Command]). - Each element in
list[Command], we will callself.get_command_signature(command)to get the proper signature of the command. - If the list is empty, meaning, no commands is available in the cog, we don't need to show it, hence
if command_signatures:. coghas a chance to beNone, this refers to No Category. We'll usegetattrto avoid getting an error to get cog's name throughCog.qualified_name.- Using
str.joineach command will be displayed on a separate line. - Once all of this is finished, display it.
I agree, this does not look pretty. But this demonstrate how to create a help command with minimal effort. The reason why
it looks ugly, is that we're using HelpCommand.get_command_signature(command).
Let's override that method to make it a little more readable.
We'll borrow codes from MinimalHelpCommand.get_command_signature.
Optionally, we can subclass MinimalHelpCommand instead of copying codes. I'm doing this as a demonstration of
overriding other methods.
We'll also 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.
import discord
from discord.ext import commands
bot = commands.Bot(command_prefix="uwu ")
class MyHelp(commands.HelpCommand):
def get_command_signature(self, command):
return '%s%s %s' % (self.clean_prefix, command.qualified_name, command.signature)
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()This looks more readable than the other one. 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.
Now that the hard part is done, let's take a look at <prefix>help [argument]. The method responsible for this is as
follows;
send_command_helpsend_cog_helpsend_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.
@bot.command(help="Shows all bot's command usage in the server on a sorted list.",
aliases=["br", "brrrr", "botranks", "botpos", "botposition", "botpositions"])
async def botrank(ctx, bot: discord.Member):
passThen 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.
import discord
from discord.ext import commands
bot = commands.Bot(command_prefix="uwu ")
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()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.
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?
from discord.ext import commands
attributes = {
'name': "hell",
'aliases': ["help", "helps"],
'cooldown': commands.Cooldown(2, 5.0, commands.BucketType.user)
}
# For 2.0, you would use CooldownMapping.from_cooldown(rate, per, type)
# because Cooldown no longer have type as it's arguments.
# 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)- sets the name into
"hell"is refers to here'name': "hell". - sets the aliases by passing the list of
strto thealiaseskey, which refers to here'aliases': ["help", "helps"]. - sets the cooldown through the
"cooldown"key by passing in aCooldownobject. This object will make a cooldown with a rate of 2, per 5 with a bucket typeBucketType.user, which in simple terms, for everydiscord.User, they can call the command twice, every 5 seconds. - We're going to use
MinimalHelpCommandas theHelpCommandobject.
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.
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
bot = commands.Bot(command_prefix="uwu ")
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.
Indeed, we have it. HelpCommand.on_help_command_error,
this method is responsible for handling any error just like any other local error handler.
import discord
from discord.ext import commands
bot = commands.Bot(command_prefix="uwu ")
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.
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.
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.
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
def setup(bot):
bot.add_cog(YourCog(bot))- It instantiates the HelpCommand class.
help_command = MyHelp() - It assigns the instance of
YourCog(self) intocogattribute
Seems like there are a lot of request for this to be made, and my original plan was to wait for menus to be integrated with buttons. However, looking at the current event of where it seems like that won't happen. To clarify, I will not be walking through all of the specific feature that the classes I'm using in this tutorial.
In this part, we will be using discord-ext-menus to handle our pagination. This is an external library that Danny wrote that seems to never reached 2.0. You might be asking, "why even bother using it if it won't ever reach 2.0". I will answer that by saying it is still useful for pagination use. You can even use this for other pagination, not just for help command. Even with 2.0, you will be able to use this efficiently with buttons. I will show that later in the walkthrough.
Another walkthrough I made regarding paginator, here I talk about it more in depth. click here
Now for the people who have never use ext-menus before, you may be asking me, "Stella, what the hell is a MenuPages class". To explain, this class inherits Menu class which handles everything such as waiting for user reactions, and adding reactions to your message. You can do fun things with Menu class such as a confirmation message with reactions which are shown here. But for our use, we will use them as the button for the pagination. For MenuPages, this class is specifically for handling pages, when you click on a reaction, it will show you the next page.
Now, here are the basic implementation for our use.
Code Example For MenuPages
import discord
from discord.ext import menus
class MyMenuPages(menus.MenuPages, inherit_buttons=False):
@menus.button("<:before_check:754948796487565332>", position=menus.First(1))
async def go_before(self, payload):
"""Goes to the previous page."""
await self.show_checked_page(self.current_page - 1)
@menus.button("<:next_check:754948796361736213>", position=menus.Last(0))
async def go_after(self, payload):
"""Goes to the next page."""
await self.show_checked_page(self.current_page + 1)
@menus.button("<:before_fast_check:754948796139569224>", position=menus.First(0))
async def go_first(self, payload):
"""Goes to the first page."""
await self.show_page(0)
@menus.button("<:next_fast_check:754948796391227442>", position=menus.Last(1))
async def go_last(self, payload):
"""Goes to the last page."""
await self.show_page(self._source.get_max_pages() - 1)
@menus.button("<:stop_check:754948796365930517>", position=menus.First(2))
async def go_stop(self, payload):
"""Remove this message."""
self.stop()Explanation
inherit_buttonskwargs is set to False, this is to remove all defaultmenus.buttonto set it your own.MenuPages.show_pageis to show the page at a position that you gave, in this case,0.MenuPages.show_checked_pageis to show a page, similar toshow_page, but this will check if the position exist.IndexErrorerror are ignored when it is raised.First/Lastclass is an anchor during adding reaction. For example, when given asFirst(0),Last(0),First(1)First(2),Last(1). Regardless of order,First(0)reaction will be added first, followed byFirst(1),First(2). After that,Last(0)andLast(1)are added. There are alsoPosition, but I don't use them here.MenuPages.stop()will end the menu session, you can set them to delete the message bydelete_message_afterorclear_reactions_afterto clear the reactions after the menu session ended.
You will need to create ListPageSource class to use with this class. Jump Here
With the introduction of Buttons in 2.0. We are able to use them as a replacement of MenuPages. In fact, we
can even re-use the code in MenuPages and recreate them for pagination.
Similar to MenuPages, we have View that handles all the interaction with the user. This can be use in Button,
Select. It is documented here. There are
example usage for View written by Danny himself. This will help you understand how to use them, as I won't be explaining
every niche things about them. The example is found here
where he wrote a tictactoe game with View and
Button.
In order to create a Pagination view, we will also subclass the MenuPages class to borrow the features that are
available from them. They handle the pages for us, which is insanely useful thanks to Danny's way of coding.
Example Code for View Button
import discord
from discord import ui
from discord.ext import menus
class MyMenuPages(ui.View, menus.MenuPages):
def __init__(self, source, *, delete_message_after=False):
super().__init__(timeout=60)
self._source = source
self.current_page = 0
self.ctx = None
self.message = None
self.delete_message_after = delete_message_after
async def start(self, ctx, *, channel=None, wait=False):
# We wont be using wait/channel, you can implement them yourself. This is to match the MenuPages signature.
await self._source._prepare_once()
self.ctx = ctx
self.message = await self.send_initial_message(ctx, ctx.channel)
async def _get_kwargs_from_page(self, page):
"""This method calls ListPageSource.format_page class"""
value = await super()._get_kwargs_from_page(page)
if 'view' not in value:
value.update({'view': self})
return value
async def interaction_check(self, interaction):
"""Only allow the author that invoke the command to be able to use the interaction"""
return interaction.user == self.ctx.author
@ui.button(emoji='<:before_fast_check:754948796139569224>', style=discord.ButtonStyle.blurple)
async def first_page(self, button, interaction):
await self.show_page(0)
@ui.button(emoji='<:before_check:754948796487565332>', style=discord.ButtonStyle.blurple)
async def before_page(self, button, interaction):
await self.show_checked_page(self.current_page - 1)
@ui.button(emoji='<:stop_check:754948796365930517>', style=discord.ButtonStyle.blurple)
async def stop_page(self, button, interaction):
self.stop()
if self.delete_message_after:
await self.message.delete(delay=0)
@ui.button(emoji='<:next_check:754948796361736213>', style=discord.ButtonStyle.blurple)
async def next_page(self, button, interaction):
await self.show_checked_page(self.current_page + 1)
@ui.button(emoji='<:next_fast_check:754948796391227442>', style=discord.ButtonStyle.blurple)
async def last_page(self, button, interaction):
await self.show_page(self._source.get_max_pages() - 1)Explanation
ui.Viewcontains all the handling of Button interaction. While, we also need partial use ofmenus.MenuPageswhere it handles the core of pagination. This include callingListPageSource.format_pagewhich will be use.- We won't call
super().start(...)because those handles reactions, we're only borrowing the methods from them. timeout=60kwargs is passed toui.View, Notmenus.MenuPages. To understand how this works, learn Method Resolution Order (MRO). As I won't be covering them here.View.interaction_checkis for the check. You can check if the interaction is your author. Returns True will result in calling the Button callback. Raising an error or returning False will prevent them from being called.ui.buttonbasically is the button you're adding toView.- for
show_check_page/show_pageare explained in the1.7.xsection. Jump Here message.delete(delay=0)is a way to delete message without checking if they exist. When error, it silences them.
You will need to create ListPageSource class to use with this class. Jump Here
As I stated before, ListPageSource are only from the external library discord-ext-menus. Make sure to install them.
Now MyMenuPages are created for our use, lets create our ListPageSource now. To put it simply, ListPageSource is
to format each page where MyMenuPages will call this class. There are also other types of class format. However, I
will just use ListPageSource for simplicity sake.
If you want to read more about ListPageSource, you can read them here. Click Here.
Now this will be our basic implementation of ListPageSource.
Example Code For ListPageSource
import discord
from itertools import starmap
from discord.ext import menus
class HelpPageSource(menus.ListPageSource):
def __init__(self, data, helpcommand):
super().__init__(data, per_page=6)
self.helpcommand = helpcommand
def format_command_help(self, no, command):
signature = self.helpcommand.get_command_signature(command)
docs = self.helpcommand.get_command_brief(command)
return f"{no}. {signature}\n{docs}"
async def format_page(self, menu, entries):
page = menu.current_page
max_page = self.get_max_pages()
starting_number = page * self.per_page + 1
iterator = starmap(self.format_command_help, enumerate(entries, start=starting_number))
page_content = "\n".join(iterator)
embed = discord.Embed(
title=f"Help Command[{page + 1}/{max_page}]",
description=page_content,
color=0xffcccb
)
author = menu.ctx.author
embed.set_footer(text=f"Requested by {author}", icon_url=author.avatar_url) # author.avatar in 2.0
return embedExplanation
per_pagekwargs is the amount of elements fromdatathat will be passed toListPageSource.format_pageasentries.starting_numberis just the starting number for the list of 6 that will be shown. So for example, at page 1, it will be 1, at page 2, it will be 13 and page 3 would be 19.menu.current_pagewill start at 0, hence we add 1.`itertools.starmapworks exactly likemapbut it unpacks the argument when calling a function, in this case,HelpPageSource.format_command_help.- We can get Context from
menu.ctxattribute. - For
ListPageSource.format_page, whatever you return from this method, will be shown as a single page. You can return adiscord.Embed, astror adictthat will be as kwargs formessage.edit.
The output of them are in the Integrating class of pagination section.
Now we will be creating the HelpCommand that uses classes that we've made. This HelpCommand class will also be able to use the 2.0 button. Hence, why it's in its own section.
Example Code
import discord
from itertools import chain
from discord.ext import commands
class MyHelp(commands.MinimalHelpCommand):
def get_command_brief(self, command):
return command.short_doc or "Command is not documented."
async def send_bot_help(self, mapping):
all_commands = list(chain.from_iterable(mapping.values()))
formatter = HelpPageSource(all_commands, self)
menu = MyMenuPages(formatter, delete_message_after=True)
await menu.start(self.context)
bot = commands.Bot("uwu ", help_command=MyHelp())Explanation
itertools.chain.from_iterableflatten the result ofmapping.values()into a single list. This is up to you on how you want to handle the command. I'm just making it flat to be as basic as possible.HelpPageSourceacts as the formatter, this class will be the one that gets the data and the one that shows each page to the user.MyMenuPagesdepending on which class you're using, this will handle theHelpPageSourceclass. It will handle the buttons from the user, and determine whether to show the previous/next/forward/backward or stop.Command.short_docwill show the value fromCommand.briefor if None, show the first sentence ofCommand.help.
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 need an inspiration, this is my help command. I mean, nobody asked but yeah here it is.
Here's my code if you want to take a look. It's a bit complicated, but you only have to focus on the HelpCommand class my help command.
If you want a more easier help command, here's an example of a help command written by pikaninja Here's the code Here's how it looks like.
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. :D
If you're still confused about paginator, I've made a full walkthrough regarding menus and View pagination. Click Here














you do it the same way as outside, when defining your discord.Embed set the colour kwarg