aboutsummaryrefslogtreecommitdiff
path: root/discord
diff options
context:
space:
mode:
Diffstat (limited to 'discord')
-rw-r--r--discord/ext/commands/__init__.py2
-rw-r--r--discord/ext/commands/bot.py147
-rw-r--r--discord/ext/commands/core.py20
-rw-r--r--discord/ext/commands/formatter.py352
-rw-r--r--discord/ext/commands/help.py1144
5 files changed, 1189 insertions, 476 deletions
diff --git a/discord/ext/commands/__init__.py b/discord/ext/commands/__init__.py
index b8195c62..b14fd655 100644
--- a/discord/ext/commands/__init__.py
+++ b/discord/ext/commands/__init__.py
@@ -14,7 +14,7 @@ from .bot import Bot, AutoShardedBot, when_mentioned, when_mentioned_or
from .context import Context
from .core import *
from .errors import *
-from .formatter import HelpFormatter, Paginator
+from .help import *
from .converter import *
from .cooldowns import *
from .cog import *
diff --git a/discord/ext/commands/bot.py b/discord/ext/commands/bot.py
index 694b466d..efa375d5 100644
--- a/discord/ext/commands/bot.py
+++ b/discord/ext/commands/bot.py
@@ -38,7 +38,7 @@ from .core import GroupMixin, Command
from .view import StringView
from .context import Context
from .errors import CommandNotFound, CommandError
-from .formatter import HelpFormatter
+from .help import HelpCommand, DefaultHelpCommand
from .cog import Cog
def when_mentioned(bot, msg):
@@ -84,71 +84,17 @@ def when_mentioned_or(*prefixes):
return inner
-_mentions_transforms = {
- '@everyone': '@\u200beveryone',
- '@here': '@\u200bhere'
-}
-
-_mention_pattern = re.compile('|'.join(_mentions_transforms.keys()))
-
def _is_submodule(parent, child):
return parent == child or child.startswith(parent + ".")
-async def _default_help_command(ctx, *commands : str):
- """Shows this message."""
- bot = ctx.bot
- destination = ctx.message.author if bot.pm_help else ctx.message.channel
-
- def repl(obj):
- return _mentions_transforms.get(obj.group(0), '')
-
- # help by itself just lists our own commands.
- if len(commands) == 0:
- pages = await bot.formatter.format_help_for(ctx, bot)
- elif len(commands) == 1:
- # try to see if it is a cog name
- name = _mention_pattern.sub(repl, commands[0])
- command = None
- if name in bot.cogs:
- command = bot.cogs[name]
- else:
- command = bot.all_commands.get(name)
- if command is None:
- await destination.send(bot.command_not_found.format(name))
- return
-
- pages = await bot.formatter.format_help_for(ctx, command)
- else:
- name = _mention_pattern.sub(repl, commands[0])
- command = bot.all_commands.get(name)
- if command is None:
- await destination.send(bot.command_not_found.format(name))
- return
-
- for key in commands[1:]:
- try:
- key = _mention_pattern.sub(repl, key)
- command = command.all_commands.get(key)
- if command is None:
- await destination.send(bot.command_not_found.format(key))
- return
- except AttributeError:
- await destination.send(bot.command_has_no_subcommands.format(command, key))
- return
-
- pages = await bot.formatter.format_help_for(ctx, command)
+class _DefaultRepr:
+ def __repr__(self):
+ return '<default-help-command>'
- if bot.pm_help is None:
- characters = sum(map(len, pages))
- # modify destination based on length of pages.
- if characters > 1000:
- destination = ctx.message.author
-
- for page in pages:
- await destination.send(page)
+_default = _DefaultRepr()
class BotBase(GroupMixin):
- def __init__(self, command_prefix, formatter=None, description=None, pm_help=False, **options):
+ def __init__(self, command_prefix, help_command=_default, description=None, **options):
super().__init__(**options)
self.command_prefix = command_prefix
self.extra_events = {}
@@ -158,32 +104,19 @@ class BotBase(GroupMixin):
self._check_once = []
self._before_invoke = None
self._after_invoke = None
+ self._help_command = None
self.description = inspect.cleandoc(description) if description else ''
- self.pm_help = pm_help
self.owner_id = options.get('owner_id')
- self.command_not_found = options.pop('command_not_found', 'No command called "{}" found.')
- self.command_has_no_subcommands = options.pop('command_has_no_subcommands', 'Command {0.name} has no subcommands.')
if options.pop('self_bot', False):
self._skip_check = lambda x, y: x != y
else:
self._skip_check = lambda x, y: x == y
- self.help_attrs = options.pop('help_attrs', {})
-
- if 'name' not in self.help_attrs:
- self.help_attrs['name'] = 'help'
-
- if formatter is not None:
- if not isinstance(formatter, HelpFormatter):
- raise discord.ClientException('Formatter must be a subclass of HelpFormatter')
- self.formatter = formatter
+ if help_command is _default:
+ self.help_command = DefaultHelpCommand()
else:
- self.formatter = HelpFormatter()
-
- # pay no mind to this ugliness.
- help_cmd = Command(_default_help_command, **self.help_attrs)
- self.add_command(help_cmd)
+ self.help_command = help_command
# internal helpers
@@ -577,6 +510,9 @@ class BotBase(GroupMixin):
if cog is None:
return
+ help_command = self._help_command
+ if help_command and help_command.cog is cog:
+ help_command.cog = None
cog._eject(self)
# extensions
@@ -685,6 +621,27 @@ class BotBase(GroupMixin):
if _is_submodule(lib_name, module):
del sys.modules[module]
+ # help command stuff
+
+ @property
+ def help_command(self):
+ return self._help_command
+
+ @help_command.setter
+ def help_command(self, value):
+ if value is not None:
+ if not isinstance(value, HelpCommand):
+ raise discord.ClientException('help_command must be a subclass of HelpCommand')
+ if self._help_command is not None:
+ self._help_command._remove_from_bot(self)
+ self._help_command = value
+ value._add_to_bot(self)
+ elif self._help_command is not None:
+ self._help_command._remove_from_bot(self)
+ self._help_command = None
+ else:
+ self._help_command = None
+
# command processing
async def get_prefix(self, message):
@@ -899,40 +856,16 @@ class Bot(BotBase, discord.Client):
Whether the commands should be case insensitive. Defaults to ``False``. This
attribute does not carry over to groups. You must set it to every group if
you require group commands to be case insensitive as well.
- description : :class:`str`
+ description: :class:`str`
The content prefixed into the default help message.
- self_bot : :class:`bool`
+ self_bot: :class:`bool`
If ``True``, the bot will only listen to commands invoked by itself rather
than ignoring itself. If ``False`` (the default) then the bot will ignore
itself. This cannot be changed once initialised.
- formatter : :class:`.HelpFormatter`
- The formatter used to format the help message. By default, it uses
- the :class:`.HelpFormatter`. Check it for more info on how to override it.
- If you want to change the help command completely (add aliases, etc) then
- a call to :meth:`~.Bot.remove_command` with 'help' as the argument would do the
- trick.
- pm_help : Optional[:class:`bool`]
- A tribool that indicates if the help command should PM the user instead of
- sending it to the channel it received it from. If the boolean is set to
- ``True``, then all help output is PM'd. If ``False``, none of the help
- output is PM'd. If ``None``, then the bot will only PM when the help
- message becomes too long (dictated by more than 1000 characters).
- Defaults to ``False``.
- help_attrs : :class:`dict`
- A dictionary of options to pass in for the construction of the help command.
- This allows you to change the command behaviour without actually changing
- the implementation of the command. The attributes will be the same as the
- ones passed in the :class:`.Command` constructor. Note that ``pass_context``
- will always be set to ``True`` regardless of what you pass in.
- command_not_found : :class:`str`
- The format string used when the help command is invoked with a command that
- is not found. Useful for i18n. Defaults to ``"No command called {} found."``.
- The only format argument is the name of the command passed.
- command_has_no_subcommands : :class:`str`
- The format string used when the help command is invoked with requests for a
- subcommand but the command does not have any subcommands. Defaults to
- ``"Command {0.name} has no subcommands."``. The first format argument is the
- :class:`.Command` attempted to get a subcommand and the second is the name.
+ help_command: Optional[:class:`.HelpCommand`]
+ The help command implementation to use. This can be dynamically
+ set at runtime. To remove the help command pass ``None``. For more
+ information on implementing a help command, see :ref:`ext_commands_help_command`.
owner_id: Optional[:class:`int`]
The ID that owns the bot. If this is not set and is then queried via
:meth:`.is_owner` then it is fetched automatically using
diff --git a/discord/ext/commands/core.py b/discord/ext/commands/core.py
index 72f25a69..f928e7f3 100644
--- a/discord/ext/commands/core.py
+++ b/discord/ext/commands/core.py
@@ -810,27 +810,15 @@ class Command(_BaseCommand):
@property
def signature(self):
"""Returns a POSIX-like signature useful for help command output."""
- result = []
- parent = self.full_parent_name
-
- if len(self.aliases) > 0:
- aliases = '|'.join(self.aliases)
- fmt = '[%s|%s]' % (self.name, aliases)
- if parent:
- fmt = parent + ' ' + fmt
- result.append(fmt)
- else:
- name = self.name if not parent else parent + ' ' + self.name
- result.append(name)
-
if self.usage is not None:
- result.append(self.usage)
- return ' '.join(result)
+ return self.usage
+
params = self.clean_params
if not params:
- return ' '.join(result)
+ return ''
+ result = []
for name, param in params.items():
greedy = isinstance(param.annotation, converters._Greedy)
diff --git a/discord/ext/commands/formatter.py b/discord/ext/commands/formatter.py
deleted file mode 100644
index d9d61576..00000000
--- a/discord/ext/commands/formatter.py
+++ /dev/null
@@ -1,352 +0,0 @@
-# -*- coding: utf-8 -*-
-
-"""
-The MIT License (MIT)
-
-Copyright (c) 2015-2019 Rapptz
-
-Permission is hereby granted, free of charge, to any person obtaining a
-copy of this software and associated documentation files (the "Software"),
-to deal in the Software without restriction, including without limitation
-the rights to use, copy, modify, merge, publish, distribute, sublicense,
-and/or sell copies of the Software, and to permit persons to whom the
-Software is furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in
-all copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
-OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
-FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
-DEALINGS IN THE SOFTWARE.
-"""
-
-import itertools
-import inspect
-import discord.utils
-
-from .core import GroupMixin, Command
-from .errors import CommandError
-# from discord.iterators import _FilteredAsyncIterator
-
-# help -> shows info of bot on top/bottom and lists subcommands
-# help command -> shows detailed info of command
-# help command <subcommand chain> -> same as above
-
-# <description>
-
-# <command signature with aliases>
-
-# <long doc>
-
-# Cog:
-# <command> <shortdoc>
-# <command> <shortdoc>
-# Other Cog:
-# <command> <shortdoc>
-# No Category:
-# <command> <shortdoc>
-
-# Type <prefix>help command for more info on a command.
-# You can also type <prefix>help category for more info on a category.
-
-class Paginator:
- """A class that aids in paginating code blocks for Discord messages.
-
- Attributes
- -----------
- prefix: :class:`str`
- The prefix inserted to every page. e.g. three backticks.
- suffix: :class:`str`
- The suffix appended at the end of every page. e.g. three backticks.
- max_size: :class:`int`
- The maximum amount of codepoints allowed in a page.
- """
- def __init__(self, prefix='```', suffix='```', max_size=2000):
- self.prefix = prefix
- self.suffix = suffix
- self.max_size = max_size - len(suffix)
- self._current_page = [prefix]
- self._count = len(prefix) + 1 # prefix + newline
- self._pages = []
-
- def add_line(self, line='', *, empty=False):
- """Adds a line to the current page.
-
- If the line exceeds the :attr:`max_size` then an exception
- is raised.
-
- Parameters
- -----------
- line: str
- The line to add.
- empty: bool
- Indicates if another empty line should be added.
-
- Raises
- ------
- RuntimeError
- The line was too big for the current :attr:`max_size`.
- """
- if len(line) > self.max_size - len(self.prefix) - 2:
- raise RuntimeError('Line exceeds maximum page size %s' % (self.max_size - len(self.prefix) - 2))
-
- if self._count + len(line) + 1 > self.max_size:
- self.close_page()
-
- self._count += len(line) + 1
- self._current_page.append(line)
-
- if empty:
- self._current_page.append('')
- self._count += 1
-
- def close_page(self):
- """Prematurely terminate a page."""
- self._current_page.append(self.suffix)
- self._pages.append('\n'.join(self._current_page))
- self._current_page = [self.prefix]
- self._count = len(self.prefix) + 1 # prefix + newline
-
- @property
- def pages(self):
- """Returns the rendered list of pages."""
- # we have more than just the prefix in our current page
- if len(self._current_page) > 1:
- self.close_page()
- return self._pages
-
- def __repr__(self):
- fmt = '<Paginator prefix: {0.prefix} suffix: {0.suffix} max_size: {0.max_size} count: {0._count}>'
- return fmt.format(self)
-
-class HelpFormatter:
- """The default base implementation that handles formatting of the help
- command.
-
- To override the behaviour of the formatter, :meth:`~.HelpFormatter.format`
- should be overridden. A number of utility functions are provided for use
- inside that method.
-
- Attributes
- -----------
- show_hidden: :class:`bool`
- Dictates if hidden commands should be shown in the output.
- Defaults to ``False``.
- show_check_failure: :class:`bool`
- Dictates if commands that have their :attr:`.Command.checks` failed
- shown. Defaults to ``False``.
- width: :class:`int`
- The maximum number of characters that fit in a line.
- Defaults to 80.
- commands_heading: :class:`str`
- The command list's heading string used when the help command is invoked with a category name.
- Useful for i18n. Defaults to ``"Commands:"``
- no_category: :class:`str`
- The string used when there is a command which does not belong to any category(cog).
- Useful for i18n. Defaults to ``"No Category"``
- """
- def __init__(self, show_hidden=False, show_check_failure=False, width=80,
- commands_heading="Commands:", no_category="No Category"):
- self.width = width
- self.show_hidden = show_hidden
- self.show_check_failure = show_check_failure
- self.commands_heading = commands_heading
- self.no_category = no_category
-
- def has_subcommands(self):
- """:class:`bool`: Specifies if the command has subcommands."""
- return isinstance(self.command, GroupMixin)
-
- def is_bot(self):
- """:class:`bool`: Specifies if the command being formatted is the bot itself."""
- return self.command is self.context.bot
-
- def is_cog(self):
- """:class:`bool`: Specifies if the command being formatted is actually a cog."""
- return not self.is_bot() and not isinstance(self.command, Command)
-
- def shorten(self, text):
- """Shortens text to fit into the :attr:`width`."""
- if len(text) > self.width:
- return text[:self.width - 3] + '...'
- return text
-
- @property
- def max_name_size(self):
- """:class:`int`: Returns the largest name length of a command or if it has subcommands
- the largest subcommand name."""
- try:
- commands = self.command.all_commands if not self.is_cog() else self.context.bot.all_commands
- if commands:
- return max(map(lambda c: discord.utils._string_width(c.name) if self.show_hidden or not c.hidden else 0, commands.values()))
- return 0
- except AttributeError:
- return len(self.command.name)
-
- @property
- def clean_prefix(self):
- """The cleaned up invoke prefix. i.e. mentions are ``@name`` instead of ``<@id>``."""
- user = self.context.guild.me if self.context.guild else self.context.bot.user
- # this breaks if the prefix mention is not the bot itself but I
- # consider this to be an *incredibly* strange use case. I'd rather go
- # for this common use case rather than waste performance for the
- # odd one.
- return self.context.prefix.replace(user.mention, '@' + user.display_name)
-
- def get_command_signature(self):
- """Retrieves the signature portion of the help page."""
- prefix = self.clean_prefix
- cmd = self.command
- return prefix + cmd.signature
-
- def get_ending_note(self):
- """Returns help command's ending note. This is mainly useful to override for i18n purposes."""
- command_name = self.context.invoked_with
- return "Type {0}{1} command for more info on a command.\n" \
- "You can also type {0}{1} category for more info on a category.".format(self.clean_prefix, command_name)
-
- async def filter_command_list(self):
- """Returns a filtered list of commands based on the two attributes
- provided, :attr:`show_check_failure` and :attr:`show_hidden`.
- Also filters based on if :meth:`~.HelpFormatter.is_cog` is valid.
-
- Returns
- --------
- iterable
- An iterable with the filter being applied. The resulting value is
- a (key, value) :class:`tuple` of the command name and the command itself.
- """
-
- def sane_no_suspension_point_predicate(tup):
- cmd = tup[1]
- if self.is_cog():
- # filter commands that don't exist to this cog.
- if cmd.cog is not self.command:
- return False
-
- if cmd.hidden and not self.show_hidden:
- return False
-
- return True
-
- async def predicate(tup):
- if sane_no_suspension_point_predicate(tup) is False:
- return False
-
- cmd = tup[1]
- try:
- return await cmd.can_run(self.context)
- except CommandError:
- return False
-
- iterator = self.command.all_commands.items() if not self.is_cog() else self.context.bot.all_commands.items()
- if self.show_check_failure:
- return filter(sane_no_suspension_point_predicate, iterator)
-
- # Gotta run every check and verify it
- ret = []
- for elem in iterator:
- valid = await predicate(elem)
- if valid:
- ret.append(elem)
-
- return ret
-
- def _add_subcommands_to_page(self, max_width, commands):
- for name, command in commands:
- if name in command.aliases:
- # skip aliases
- continue
- width_gap = discord.utils._string_width(name) - len(name)
- entry = ' {0:<{width}} {1}'.format(name, command.short_doc, width=max_width-width_gap)
- shortened = self.shorten(entry)
- self._paginator.add_line(shortened)
-
- async def format_help_for(self, context, command_or_bot):
- """Formats the help page and handles the actual heavy lifting of how
- the help command looks like. To change the behaviour, override the
- :meth:`~.HelpFormatter.format` method.
-
- Parameters
- -----------
- context: :class:`.Context`
- The context of the invoked help command.
- command_or_bot: :class:`.Command` or :class:`.Bot`
- The bot or command that we are getting the help of.
-
- Returns
- --------
- list
- A paginated output of the help command.
- """
- self.context = context
- self.command = command_or_bot
- return await self.format()
-
- async def format(self):
- """Handles the actual behaviour involved with formatting.
-
- To change the behaviour, this method should be overridden.
-
- Returns
- --------
- list
- A paginated output of the help command.
- """
- self._paginator = Paginator()
-
- # we need a padding of ~80 or so
-
- description = self.command.description if not self.is_cog() else inspect.getdoc(self.command)
-
- if description:
- # <description> portion
- self._paginator.add_line(description, empty=True)
-
- if isinstance(self.command, Command):
- # <signature portion>
- signature = self.get_command_signature()
- self._paginator.add_line(signature, empty=True)
-
- # <long doc> section
- if self.command.help:
- self._paginator.add_line(self.command.help, empty=True)
-
- # end it here if it's just a regular command
- if not self.has_subcommands():
- self._paginator.close_page()
- return self._paginator.pages
-
- max_width = self.max_name_size
-
- def category(tup):
- cog = tup[1].cog_name
- # we insert the zero width space there to give it approximate
- # last place sorting position.
- return cog + ':' if cog is not None else '\u200b' + self.no_category + ':'
-
- filtered = await self.filter_command_list()
- if self.is_bot():
- data = sorted(filtered, key=category)
- for category, commands in itertools.groupby(data, key=category):
- # there simply is no prettier way of doing this.
- commands = sorted(commands)
- if len(commands) > 0:
- self._paginator.add_line(category)
-
- self._add_subcommands_to_page(max_width, commands)
- else:
- filtered = sorted(filtered)
- if filtered:
- self._paginator.add_line(self.commands_heading)
- self._add_subcommands_to_page(max_width, filtered)
-
- # add the ending note
- self._paginator.add_line()
- ending_note = self.get_ending_note()
- self._paginator.add_line(ending_note)
- return self._paginator.pages
diff --git a/discord/ext/commands/help.py b/discord/ext/commands/help.py
new file mode 100644
index 00000000..3f956e55
--- /dev/null
+++ b/discord/ext/commands/help.py
@@ -0,0 +1,1144 @@
+# -*- coding: utf-8 -*-
+
+"""
+The MIT License (MIT)
+
+Copyright (c) 2015-2019 Rapptz
+
+Permission is hereby granted, free of charge, to any person obtaining a
+copy of this software and associated documentation files (the "Software"),
+to deal in the Software without restriction, including without limitation
+the rights to use, copy, modify, merge, publish, distribute, sublicense,
+and/or sell copies of the Software, and to permit persons to whom the
+Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+DEALINGS IN THE SOFTWARE.
+"""
+
+import itertools
+import functools
+import inspect
+import re
+import discord.utils
+
+from .core import Group, Command
+from .errors import CommandError
+
+__all__ = [
+ 'Paginator',
+ 'HelpCommand',
+ 'DefaultHelpCommand',
+ 'MinimalHelpCommand',
+]
+
+# help -> shows info of bot on top/bottom and lists subcommands
+# help command -> shows detailed info of command
+# help command <subcommand chain> -> same as above
+
+# <description>
+
+# <command signature with aliases>
+
+# <long doc>
+
+# Cog:
+# <command> <shortdoc>
+# <command> <shortdoc>
+# Other Cog:
+# <command> <shortdoc>
+# No Category:
+# <command> <shortdoc>
+
+# Type <prefix>help command for more info on a command.
+# You can also type <prefix>help category for more info on a category.
+
+class Paginator:
+ """A class that aids in paginating code blocks for Discord messages.
+
+ Attributes
+ -----------
+ prefix: Optional[:class:`str`]
+ The prefix inserted to every page. e.g. three backticks.
+ suffix: Optional[:class:`str`]
+ The suffix appended at the end of every page. e.g. three backticks.
+ max_size: :class:`int`
+ The maximum amount of codepoints allowed in a page.
+ """
+ def __init__(self, prefix='```', suffix='```', max_size=2000):
+ self.prefix = prefix
+ self.suffix = suffix
+ self.max_size = max_size - (0 if suffix is None else len(suffix))
+ self.clear()
+
+ def clear(self):
+ """Clears the paginator to have no pages."""
+ if self.prefix is not None:
+ self._current_page = [self.prefix]
+ self._count = len(self.prefix) + 1 # prefix + newline
+ else:
+ self._current_page = []
+ self._count = 0
+ self._pages = []
+
+ @property
+ def _prefix_len(self):
+ return len(self.prefix) if self.prefix else 0
+
+ def add_line(self, line='', *, empty=False):
+ """Adds a line to the current page.
+
+ If the line exceeds the :attr:`max_size` then an exception
+ is raised.
+
+ Parameters
+ -----------
+ line: str
+ The line to add.
+ empty: bool
+ Indicates if another empty line should be added.
+
+ Raises
+ ------
+ RuntimeError
+ The line was too big for the current :attr:`max_size`.
+ """
+ max_page_size = self.max_size - self._prefix_len - 2
+ if len(line) > max_page_size:
+ raise RuntimeError('Line exceeds maximum page size %s' % (max_page_size))
+
+ if self._count + len(line) + 1 > self.max_size:
+ self.close_page()
+
+ self._count += len(line) + 1
+ self._current_page.append(line)
+
+ if empty:
+ self._current_page.append('')
+ self._count += 1
+
+ def close_page(self):
+ """Prematurely terminate a page."""
+ if self.suffix is not None:
+ self._current_page.append(self.suffix)
+ self._pages.append('\n'.join(self._current_page))
+
+ if self.prefix is not None:
+ self._current_page = [self.prefix]
+ self._count = len(self.prefix) + 1 # prefix + newline
+ else:
+ self._current_page = []
+ self._count = 0
+
+ @property
+ def pages(self):
+ """Returns the rendered list of pages."""
+ # we have more than just the prefix in our current page
+ if len(self._current_page) > 1:
+ self.close_page()
+ return self._pages
+
+ def __repr__(self):
+ fmt = '<Paginator prefix: {0.prefix} suffix: {0.suffix} max_size: {0.max_size} count: {0._count}>'
+ return fmt.format(self)
+
+def _not_overriden(f):
+ f.__help_command_not_overriden__ = True
+ return f
+
+class _HelpCommandImpl(Command):
+ def __init__(self, inject, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self._injected = inject
+
+ on_error = inject.on_help_command_error
+ try:
+ on_error.__help_command_not_overriden__
+ except AttributeError:
+ # overridden
+ self.on_error = on_error
+
+ async def _parse_arguments(self, ctx):
+ # Make the parser think we don't have a cog so it doesn't
+ # inject the parameter into `ctx.args`.
+ original_cog = self.cog
+ self.cog = None
+ try:
+ await super()._parse_arguments(ctx)
+ finally:
+ self.cog = original_cog
+
+ async def _on_error_cog_implementation(self, dummy, ctx, error):
+ await self._injected.on_help_command_error(ctx, error)
+
+ @property
+ def clean_params(self):
+ result = self.params.copy()
+ try:
+ result.popitem(last=False)
+ except Exception:
+ raise ValueError('Missing context parameter') from None
+ else:
+ return result
+
+ def _inject_into_cog(self, cog):
+ # Warning: hacky
+
+ # Make the cog think that get_commands returns this command
+ # as well if we inject it without modifying __cog_commands__
+ # since that's used for the injection and ejection of cogs.
+ def wrapped_get_commands(*, _original=cog.get_commands):
+ ret = _original()
+ ret.append(self)
+ return ret
+
+ # Ditto here
+ def wrapped_walk_commands(*, _original=cog.walk_commands):
+ yield from _original()
+ yield self
+
+ functools.update_wrapper(wrapped_get_commands, cog.get_commands)
+ functools.update_wrapper(wrapped_walk_commands, cog.walk_commands)
+ cog.get_commands = wrapped_get_commands
+ cog.walk_commands = wrapped_walk_commands
+ self.cog = cog
+
+ on_error = self._injected.on_help_command_error
+ try:
+ on_error.__help_command_not_overriden__
+ except AttributeError:
+ # overridden so let's swap it with our cog specific implementation
+ self.on_error = self._on_error_cog_implementation
+
+ def _eject_cog(self):
+ if self.cog is None:
+ return
+
+ # revert back into their original methods
+ cog = self.cog
+ cog.get_commands = cog.get_commands.__wrapped__
+ cog.walk_commands = cog.walk_commands.__wrapped__
+ self.cog = None
+
+ on_error = self._injected.on_help_command_error
+ try:
+ on_error.__help_command_not_overriden__
+ except AttributeError:
+ # overridden so let's swap it with our cog specific implementation
+ self.on_error = self._on_error_cog_implementation
+
+class HelpCommand:
+ r"""The base implementation for help command formatting.
+
+ Attributes
+ ------------
+ context: Optional[:class:`Context`]
+ The context that invoked this help formatter. This is generally set after
+ the help command assigned, :func:`command_callback`\, has been called.
+ show_hidden: :class:`bool`
+ Specifies if hidden commands should be shown in the output.
+ Defaults to ``False``.
+ verify_checks: :class:`bool`
+ Specifies if commands should have their :attr:`.Command.checks` called
+ and verified. Defaults to ``True``.
+ command_attrs: :class:`dict`
+ A dictionary of options to pass in for the construction of the help command.
+ This allows you to change the command behaviour without actually changing
+ the implementation of the command. The attributes will be the same as the
+ ones passed in the :class:`.Command` constructor.
+ """
+
+ MENTION_TRANSFORMS = {
+ '@everyone': '@\u200beveryone',
+ '@here': '@\u200bhere',
+ r'<@!?[0-9]{17,22}>': '@deleted-user',
+ r'<@&[0-9]{17,22}>': '@deleted-role'
+ }
+
+ MENTION_PATTERN = re.compile('|'.join(MENTION_TRANSFORMS.keys()))
+
+ def __init__(self, **options):
+ self.show_hidden = options.pop('show_hidden', False)
+ self.verify_checks = options.pop('verify_checks', True)
+ self.command_attrs = attrs = options.pop('command_attrs', {})
+ attrs.setdefault('name', 'help')
+ attrs.setdefault('help', 'Shows this message')
+ self.context = None
+ self._command_impl = None
+
+ def _add_to_bot(self, bot):
+ command = _HelpCommandImpl(self, self.command_callback, **self.command_attrs)
+ bot.add_command(command)
+ self._command_impl = command
+
+ def _remove_from_bot(self, bot):
+ bot.remove_command(self._command_impl.name)
+ self._command_impl._eject_cog()
+ self._command_impl = None
+
+ @property
+ def clean_prefix(self):
+ """The cleaned up invoke prefix. i.e. mentions are ``@name`` instead of ``<@id>``."""
+ user = self.context.guild.me if self.context.guild else self.context.bot.user
+ # this breaks if the prefix mention is not the bot itself but I
+ # consider this to be an *incredibly* strange use case. I'd rather go
+ # for this common use case rather than waste performance for the
+ # odd one.
+ return self.context.prefix.replace(user.mention, '@' + user.display_name)
+
+ def get_command_signature(self, command):
+ """Retrieves the signature portion of the help page.
+
+ Parameters
+ ------------
+ command: :class:`Command`
+ The command to get the signature of.
+
+ Returns
+ --------
+ :class:`str`
+ The signature for the command.
+ """
+
+ parent = command.full_parent_name
+ if len(command.aliases) > 0:
+ aliases = '|'.join(command.aliases)
+ fmt = '[%s|%s]' % (command.name, aliases)
+ if parent:
+ fmt = parent + ' ' + fmt
+ alias = fmt
+ else:
+ alias = command.name if not parent else parent + ' ' + command.name
+
+ return '%s%s %s' % (self.clean_prefix, alias, command.signature)
+
+ def remove_mentions(self, string):
+ """Removes mentions from the string to prevent abuse.
+
+ This includes ``@everyone``, ``@here``, member mentions and role mentions.
+ """
+
+ def replace(obj, *, transforms=self.MENTION_TRANSFORMS):
+ return transforms.get(obj.group(0), '@invalid')
+
+ return self.MENTION_PATTERN.sub(replace, string)
+
+ @property
+ def cog(self):
+ """A property for retrieving or setting the cog for the help command.
+
+ When a cog is set for the help command, it is as-if the help command
+ belongs to that cog. All cog special methods will apply to the help
+ command and it will be automatically unset on unload.
+
+ To unbind the cog from the help command, you can set it to ``None``.
+
+ Returns
+ --------
+ Optional[:class:`Cog`]
+ The cog that is currently set for the help command.
+ """
+ return self._command_impl.cog
+
+ @cog.setter
+ def cog(self, cog):
+ # Remove whatever cog is currently valid, if any
+ self._command_impl._eject_cog()
+
+ # If a new cog is set then inject it.
+ if cog is not None:
+ self._command_impl._inject_into_cog(cog)
+
+ def command_not_found(self, string):
+ """|maybecoro|
+
+ A method called when a command is not found in the help command.
+ This is useful to override for i18n.
+
+ Defaults to ``No command called {0} found.``
+
+ Parameters
+ ------------
+ string: :class:`str`
+ The string that contains the invalid command. Note that this has
+ had mentions removed to prevent abuse.
+
+ Returns
+ ---------
+ :class:`str`
+ The string to use when a command has not been found.
+ """
+ return 'No command called "{}" found.'.format(string)
+
+ def subcommand_not_found(self, command, string):
+ """|maybecoro|
+
+ A method called when a command did not have a subcommand requested in the help command.
+ This is useful to override for i18n.
+
+ Defaults to either:
+
+ - ``'Command "{command.qualified_name}" has no subcommands.'``
+ - If there is no subcommand in the ``command`` parameter.
+ - ``'Command "{command.qualified_name}" has no subcommand named {string}'``
+ - If the ``command`` parameter has subcommands but not one named ``string``.
+
+ Parameters
+ ------------
+ command: :class:`Command`
+ The command that did not have the subcommand requested.
+ string: :class:`str`
+ The string that contains the invalid subcommand. Note that this has
+ had mentions removed to prevent abuse.
+
+ Returns
+ ---------
+ :class:`str`
+ The string to use when the command did not have the subcommand requested.
+ """
+ if isinstance(command, Group) and len(command.all_commands) > 0:
+ return 'Command "{0.qualified_name}" has no subcommand named {1}'.format(command, string)
+ return 'Command "{0.qualified_name}" has no subcommands.'.format(command)
+
+ async def filter_commands(self, commands, *, sort=False, key=None):
+ """|coro|
+
+ Returns a filtered list of commands and optionally sorts them.
+
+ This takes into account the :attr:`verify_checks` and :attr:`show_hidden`
+ attributes.
+
+ Parameters
+ ------------
+ commands: Iterable[:class:`Command`]
+ An iterable of commands that are getting filtered.
+ sort: :class:`bool`
+ Whether to sort the result.
+ key: Optional[Callable[:class:`Command`, Any]]
+ An optional key function to pass to :func:`py:sorted` that
+ takes a :class:`Command` as its sole parameter. If ``sort`` is
+ passed as ``True`` then this will default as the command name.
+
+ Returns
+ ---------
+ List[:class:`Command`]
+ A list of commands that passed the filter.
+ """
+
+ if sort and key is None:
+ key = lambda c: c.name
+
+ iterator = commands if self.show_hidden else filter(lambda c: not c.hidden, commands)
+
+ if not self.verify_checks:
+ # if we do not need to verify the checks then we can just
+ # run it straight through normally without using await.
+ return sorted(iterator, key=key) if sort else list(iterator)
+
+ # if we're here then we need to check every command if it can run
+ async def predicate(cmd):
+ try:
+ return await cmd.can_run(self.context)
+ except CommandError:
+ return False
+
+ ret = []
+ for cmd in iterator:
+ valid = await predicate(cmd)
+ if valid:
+ ret.append(cmd)
+
+ if sort:
+ ret.sort(key=key)
+ return ret
+
+ def get_max_size(self, commands):
+ """Returns the largest name length of the specified command list.
+
+ Parameters
+ ------------
+ commands: Sequence[:class:`Command`]
+ A sequence of commands to check for the largest size.
+
+ Returns
+ --------
+ :class:`int`
+ The maximum width of the commands.
+ """
+
+ as_lengths = (
+ discord.utils._string_width(c.name)
+ for c in commands
+ )
+ return max(as_lengths, default=0)
+
+ def get_destination(self):
+ """Returns the :class:`abc.Messageable` where the help command will be output.
+
+ You can override this method to customise the behaviour.
+
+ By default this returns the context's channel.
+ """
+ return self.context.channel
+
+ async def send_error_message(self, error):
+ """|coro|
+
+ Handles the implementation when an error happens in the help command.
+ For example, the result of :meth:`command_not_found` or
+ :meth:`command_has_no_subcommand_found` will be passed here.
+
+ You can override this method to customise the behaviour.
+
+ By default, this sends the error message to the destination
+ specified by :meth:`get_destination`.
+
+ .. note::
+
+ You can access the invocation context with :attr:`HelpCommand.context`.
+
+ Parameters
+ ------------
+ error: :class:`str`
+ The error message to display to the user. Note that this has
+ had mentions removed to prevent abuse.
+ """
+ destination = self.get_destination()
+ await destination.send(error)
+
+ @_not_overriden
+ async def on_help_command_error(self, ctx, error):
+ """|coro|
+
+ The help command's error handler, as specified by :ref:`ext_commands_error_handler`.
+
+ Useful to override if you need some specific behaviour when the error handler
+ is called.
+
+ By default this method does nothing and just propagates to the default
+ error handlers.
+
+ Parameters
+ ------------
+ ctx: :class:`Context`
+ The invocation context.
+ error: :class:`CommandError`
+ The error that was raised.
+ """
+ pass
+
+ async def send_bot_help(self, mapping):
+ """|coro|
+
+ Handles the implementation of the bot command page in the help command.
+ This function is called when the help command is called with no arguments.
+
+ It should be noted that this method does not return anything -- rather the
+ actual message sending should be done inside this method. Well behaved subclasses
+ should use :meth:`get_destination` to know where to send, as this is a customisation
+ point for other users.
+
+ You can override this method to customise the behaviour.
+
+ .. note::
+
+ You can access the invocation context with :attr:`HelpCommand.context`.
+
+ Also, the commands in the mapping are not filtered. To do the filtering
+ you will have to call :meth:`filter_commands` yourself.
+
+ Parameters
+ ------------
+ mapping: Mapping[Optional[:class:`Cog`], List[:class:`Command`]
+ A mapping of cogs to commands that have been requested by the user for help.
+ The key of the mapping is the :class:`~.commands.Cog` that the command belongs to, or
+ ``None`` if there isn't one, and the value is a list of commands that belongs to that cog.
+ """
+ return None
+
+ async def send_cog_help(self, cog):
+ """|coro|
+
+ Handles the implementation of the cog page in the help command.
+ This function is called when the help command is called with a cog as the argument.
+
+ It should be noted that this method does not return anything -- rather the
+ actual message sending should be done inside this method. Well behaved subclasses
+ should use :meth:`get_destination` to know where to send, as this is a customisation
+ point for other users.
+
+ You can override this method to customise the behaviour.
+
+ .. note::
+
+ You can access the invocation context with :attr:`HelpCommand.context`.
+
+ To get the commands that belong to this cog see :meth:`Cog.get_commands`.
+ The commands returned not filtered. To do the filtering you will have to call
+ :meth:`filter_commands` yourself.
+
+ Parameters
+ -----------
+ cog: :class:`Cog`
+ The cog that was requested for help.
+ """
+ return None
+
+ async def send_group_help(self, group):
+ """|coro|
+
+ Handles the implementation of the group page in the help command.
+ This function is called when the help command is called with a group as the argument.
+
+ It should be noted that this method does not return anything -- rather the
+ actual message sending should be done inside this method. Well behaved subclasses
+ should use :meth:`get_destination` to know where to send, as this is a customisation
+ point for other users.
+
+ You can override this method to customise the behaviour.
+
+ .. note::
+
+ You can access the invocation context with :attr:`HelpCommand.context`.
+
+ To get the commands that belong to this group without aliases see
+ :attr:`Group.commands`. The commands returned not filtered. To do the
+ filtering you will have to call :meth:`filter_commands` yourself.
+
+ Parameters
+ -----------
+ group: :class:`Group`
+ The group that was requested for help.
+ """
+ return None
+
+ async def send_command_help(self, command):
+ """|coro|
+
+ Handles the implementation of the single command page in the help command.
+
+ It should be noted that this method does not return anything -- rather the
+ actual message sending should be done inside this method. Well behaved subclasses
+ should use :meth:`get_destination` to know where to send, as this is a customisation
+ point for other users.
+
+ You can override this method to customise the behaviour.
+
+ .. note::
+
+ You can access the invocation context with :attr:`HelpCommand.context`.
+
+ .. admonition:: Showing Help
+ :class: helpful
+
+ There are certain attributes and methods that are helpful for a help command
+ to show such as the following:
+
+ - :attr:`Command.help`
+ - :attr:`Command.brief`
+ - :attr:`Command.short_doc`
+ - :attr:`Command.description`
+ - :meth:`get_command_signature`
+
+ There are more than just these attributes but feel free to play around with
+ these to help you get started to get the output that you want.
+
+ Parameters
+ -----------
+ command: :class:`Command`
+ The command that was requested for help.
+ """
+ return None
+
+ async def prepare_help_command(self, ctx, command=None):
+ """|coro|
+
+ A low level method that can be used to prepare the help command
+ before it does anything. For example, if you need to prepare
+ some state in your subclass before the command does its processing
+ then this would be the place to do it.
+
+ The default implementation is empty.
+
+ .. note::
+
+ This is called *inside* the help command callback body. So all
+ the usual rules that happen inside apply here as well.
+
+ Parameters
+ -----------
+ ctx: :class:`Context`
+ The invocation context.
+ command: Optional[:class:`str`]
+ The argument passed to the help command.
+ """
+ return None
+
+ async def command_callback(self, ctx, *, command=None):
+ """|coro|
+
+ The actual implementation of the help command.
+
+ It is not recommended to override this method and instead change
+ the behaviour through the methods that actually get dispatched.
+
+ - :meth:`send_bot_help`
+ - :meth:`send_cog_help`
+ - :meth:`send_group_help`
+ - :meth:`send_command_help`
+ - :meth:`get_destination`
+ - :meth:`command_not_found`
+ - :meth:`subcommand_not_found`
+ - :meth:`send_error_message`
+ - :meth:`on_help_command_error`
+ - :meth:`prepare_help_command`
+
+ """
+ self.context = ctx
+ await self.prepare_help_command(ctx, command)
+ bot = ctx.bot
+
+ if command is None:
+ mapping = {
+ cog: cog.get_commands()
+ for cog in bot.cogs.values()
+ }
+ mapping[None] = [c for c in bot.all_commands.values() if c.cog is None]
+ return await self.send_bot_help(mapping)
+
+ # Check if it's a cog
+ cog = bot.get_cog(command)
+ if cog is not None:
+ return await self.send_cog_help(cog)
+
+ maybe_coro = discord.utils.maybe_coroutine
+
+ # If it's not a cog then it's a command.
+ # Since we want to have detailed errors when someone
+ # passes an invalid subcommand, we need to walk through
+ # the command group chain ourselves.
+ keys = command.split(' ')
+ cmd = bot.all_commands.get(keys[0])
+ if cmd is None:
+ string = await maybe_coro(self.command_not_found, self.remove_mentions(keys[0]))
+ return await self.send_error_message(string)
+
+ for key in keys[1:]:
+ try:
+ found = cmd.all_commands.get(key)
+ except AttributeError:
+ string = await maybe_coro(self.subcommand_not_found, cmd, self.remove_mentions(key))
+ return await self.send_error_message(string)
+ else:
+ if found is None:
+ string = await maybe_coro(self.subcommand_not_found, cmd, self.remove_mentions(key))
+ return await self.send_error_message(string)
+ cmd = found
+
+ if isinstance(cmd, Group):
+ await self.send_group_help(cmd)
+ else:
+ await self.send_command_help(cmd)
+
+class DefaultHelpCommand(HelpCommand):
+ """The implementation of the default help command.
+
+ This derives from :class:`HelpCommand`.
+
+ It extends it with the following attributes.
+
+ Attributes
+ ------------
+ width: :class:`int`
+ The maximum number of characters that fit in a line.
+ Defaults to 80.
+ sort_commands: :class:`bool`
+ Whether to sort the commands in the output alphabetically. Defaults to ``True``.
+ indent: :class:`int`
+ How much to intend the commands from a heading. Defaults to ``2``.
+ commands_heading: :class:`str`
+ The command list's heading string used when the help command is invoked with a category name.
+ Useful for i18n. Defaults to ``"Commands:"``
+ no_category: :class:`str`
+ The string used when there is a command which does not belong to any category(cog).
+ Useful for i18n. Defaults to ``"No Category"``
+ paginator: :class:`Paginator`
+ The paginator used to paginate the help command output.
+ """
+
+ def __init__(self, **options):
+ self.width = options.pop('width', 80)
+ self.indent = options.pop('indent', 2)
+ self.sort_commands = options.pop('sort_commands', True)
+ self.commands_heading = options.pop('commands_heading', "Commands:")
+ self.no_category = options.pop('no_category', 'No Category')
+ self.paginator = options.pop('paginator', None)
+
+ if self.paginator is None:
+ self.paginator = Paginator()
+
+ super().__init__(**options)
+
+ def shorten_text(self, text):
+ """Shortens text to fit into the :attr:`width`."""
+ if len(text) > self.width:
+ return text[:self.width - 3] + '...'
+ return text
+
+ def get_ending_note(self):
+ """Returns help command's ending note. This is mainly useful to override for i18n purposes."""
+ command_name = self.context.invoked_with
+ return "Type {0}{1} command for more info on a command.\n" \
+ "You can also type {0}{1} category for more info on a category.".format(self.clean_prefix, command_name)
+
+ def add_indented_commands(self, commands, *, heading, max_size=None):
+ """Indents a list of commands after the specified heading.
+
+ The formatting is added to the :attr:`paginator`.
+
+ The default implementation is the command name indented by
+ :attr:`indent` spaces, padded to ``max_size`` followed by
+ the command's :attr:`Command.short_doc` and then shortened
+ to fit into the :attr:`width`.
+
+ Parameters
+ -----------
+ commands: Sequence[:class:`Command`]
+ A list of commands to indent for output.
+ heading: :class:`str`
+ The heading to add to the output. This is only added
+ if the list of commands is greater than 0.
+ max_size: Optional[:class:`int`]
+ The max size to use for the gap between indents.
+ If unspecified, calls :meth:`get_max_size` on the
+ commands parameter.
+ """
+
+ if not commands:
+ return
+
+ self.paginator.add_line(heading)
+ max_size = max_size or self.get_max_size(commands)
+
+ get_width = discord.utils._string_width
+ for command in commands:
+ name = command.name
+ width = max_size - (get_width(name) - len(name))
+ entry = '{0}{1:<{width}} {2}'.format(self.indent * ' ', name, command.short_doc, width=width)
+ self.paginator.add_line(self.shorten_text(entry))
+
+ async def send_pages(self):
+ """A helper utility to send the page output from :attr:`paginator` to the destination."""
+ destination = self.get_destination()
+ for page in self.paginator.pages:
+ await destination.send(page)
+
+ def add_command_formatting(self, command):
+ """A utility function to format the non-indented block of commands and groups.
+
+ Parameters
+ ------------
+ command: :class:`Command`
+ The command to format.
+ """
+
+ if command.description:
+ self.paginator.add_line(command.description, empty=True)
+
+ signature = self.get_command_signature(command)
+ self.paginator.add_line(signature, empty=True)
+
+ if command.help:
+ self.paginator.add_line(command.help, empty=True)
+
+ async def prepare_help_command(self, ctx, command):
+ self.paginator.clear()
+
+ async def send_bot_help(self, mapping):
+ ctx = self.context
+ bot = ctx.bot
+
+ if bot.description:
+ # <description> portion
+ self.paginator.add_line(bot.description, empty=True)
+
+ no_category = '\u200b{0.no_category}:'.format(self)
+ def get_category(command, *, no_category=no_category):
+ cog = command.cog
+ return cog.qualified_name + ':' if cog is not None else no_category
+
+ filtered = await self.filter_commands(bot.commands, sort=True, key=get_category)
+ max_size = self.get_max_size(filtered)
+ to_iterate = itertools.groupby(filtered, key=get_category)
+
+ # Now we can add the commands to the page.
+ for category, commands in to_iterate:
+ commands = sorted(commands, key=lambda c: c.name) if self.sort_commands else list(commands)
+ self.add_indented_commands(commands, heading=category, max_size=max_size)
+
+ note = self.get_ending_note()
+ if note:
+ self.paginator.add_line()
+ self.paginator.add_line(note)
+
+ await self.send_pages()
+
+ async def send_command_help(self, command):
+ self.add_command_formatting(command)
+ self.paginator.close_page()
+ await self.send_pages()
+
+ async def send_group_help(self, group):
+ self.add_command_formatting(group)
+
+ filtered = await self.filter_commands(group.commands, sort=self.sort_commands)
+ self.add_indented_commands(filtered, heading=self.commands_heading)
+
+ if filtered:
+ note = self.get_ending_note()
+ if note:
+ self.paginator.add_line()
+ self.paginator.add_line(note)
+
+ await self.send_pages()
+
+ async def send_cog_help(self, cog):
+ if cog.description:
+ self.paginator.add_line(cog.description, empty=True)
+
+ filtered = await self.filter_commands(cog.get_commands(), sort=self.sort_commands)
+ self.add_indented_commands(filtered, heading=self.commands_heading)
+
+ note = self.get_ending_note()
+ if note:
+ self.paginator.add_line()
+ self.paginator.add_line(note)
+
+ await self.send_pages()
+
+class MinimalHelpCommand(HelpCommand):
+ """An implementation of a help command with minimal output.
+
+ This derives from :class:`HelpCommand`.
+
+ Attributes
+ ------------
+ sort_commands: :class:`bool`
+ Whether to sort the commands in the output alphabetically. Defaults to ``True``.
+ commands_heading: :class:`str`
+ The command list's heading string used when the help command is invoked with a category name.
+ Useful for i18n. Defaults to ``"Commands"``
+ aliases_heading: :class:`str`
+ The alias list's heading string used to list the aliases of the command. Useful for i18n.
+ Defaults to ``"Aliases:"``.
+ no_category: :class:`str`
+ The string used when there is a command which does not belong to any category(cog).
+ Useful for i18n. Defaults to ``"No Category"``
+ paginator: :class:`Paginator`
+ The paginator used to paginate the help command output.
+ """
+
+ def __init__(self, **options):
+ self.sort_commands = options.pop('sort_commands', True)
+ self.commands_heading = options.pop('commands_heading', "Commands")
+ self.aliases_heading = options.pop('aliases_heading', "Aliases:")
+ self.no_category = options.pop('no_category', 'No Category')
+ self.paginator = options.pop('paginator', None)
+
+ if self.paginator is None:
+ self.paginator = Paginator(suffix=None, prefix=None)
+
+ super().__init__(**options)
+
+ async def send_pages(self):
+ """A helper utility to send the page output from :attr:`paginator` to the destination."""
+ destination = self.get_destination()
+ for page in self.paginator.pages:
+ await destination.send(page)
+
+ def get_opening_note(self):
+ """Returns help command's opening note. This is mainly useful to override for i18n purposes.
+
+ The default implementation returns ::
+
+ Use `{prefix}{command_name} <command>` for more info on a command.
+ You can also use `{prefix}{command_name} <category>` for more info on a category.
+
+ """
+ command_name = self.context.invoked_with
+ return "Use `{0}{1} <command>` for more info on a command.\n" \
+ "You can also use `{0}{1} <category>` for more info on a category.".format(self.clean_prefix, command_name)
+
+ def get_command_signature(self, command):
+ return '%s%s %s' % (self.clean_prefix, command.qualified_name, command.signature)
+
+ def get_ending_note(self):
+ """Return the help command's ending note. This is mainly useful to override for i18n purposes.
+
+ The default implementation does nothing.
+ """
+ return None
+
+ def add_bot_commands_formatting(self, commands, heading):
+ """Adds the minified bot heading with commands to the output.
+
+ The formatting should be added to the :attr:`paginator`.
+
+ The default implementation is a bold underline heading followed
+ by commands separated by an EN SPACE (U+2002) in the next line.
+
+ Parameters
+ -----------
+ commands: Sequence[:class:`Command`]
+ A list of commands that belong to the heading.
+ heading: :class:`str`
+ The heading to add to the line.
+ """
+ if commands:
+ # U+2002 Middle Dot
+ joined = '\u2002'.join(c.name for c in commands)
+ self.paginator.add_line('__**%s**__' % heading)
+ self.paginator.add_line(joined)
+
+ def add_subcommand_formatting(self, command):
+ """Adds formatting information on a subcommand.
+
+ The formatting should be added to the :attr:`paginator`.
+
+ The default implementation is the prefix and the :attr:`Command.qualified_name`
+ optionally followed by an En dash and the command's :attr:`Command.short_doc`.
+
+ Parameters
+ -----------
+ command: :class:`Command`
+ The command to show information of.
+ """
+ fmt = '{0}{1} \N{EN DASH} {2}' if command.short_doc else '{0}{1}'
+ self.paginator.add_line(fmt.format(self.clean_prefix, command.qualified_name, command.short_doc))
+
+ def add_aliases_formatting(self, aliases):
+ """Adds the formatting information on a command's aliases.
+
+ The formatting should be added to the :attr:`paginator`.
+
+ The default implementation is the :attr:`aliases_heading` bolded
+ followed by a comma separated list of aliases.
+
+ This is not called if there are no aliases to format.
+
+ Parameters
+ -----------
+ aliases: Sequence[:class:`str`]
+ A list of aliases to format.
+ """
+ self.paginator.add_line('**%s** %s' % (self.aliases_heading, ', '.join(aliases)), empty=True)
+
+ def add_command_formatting(self, command):
+ """A utility function to format commands and groups.
+
+ Parameters
+ ------------
+ command: :class:`Command`
+ The command to format.
+ """
+
+ if command.description:
+ self.paginator.add_line(command.description, empty=True)
+
+ signature = self.get_command_signature(command)
+ if command.aliases:
+ self.paginator.add_line(signature)
+ self.add_aliases_formatting(command.aliases)
+ else:
+ self.paginator.add_line(signature, empty=True)
+
+ if command.help:
+ self.paginator.add_line(command.help, empty=True)
+
+ async def prepare_help_command(self, ctx, command):
+ self.paginator.clear()
+
+ async def send_bot_help(self, mapping):
+ ctx = self.context
+ bot = ctx.bot
+
+ if bot.description:
+ self.paginator.add_line(bot.description, empty=True)
+
+ note = self.get_opening_note()
+ if note:
+ self.paginator.add_line(note, empty=True)
+
+ no_category = '\u200b{0.no_category}'.format(self)
+ def get_category(command, *, no_category=no_category):
+ cog = command.cog
+ return cog.qualified_name if cog is not None else no_category
+
+ filtered = await self.filter_commands(bot.commands, sort=True, key=get_category)
+ to_iterate = itertools.groupby(filtered, key=get_category)
+
+ for category, commands in to_iterate:
+ commands = sorted(commands, key=lambda c: c.name) if self.sort_commands else list(commands)
+ self.add_bot_commands_formatting(commands, category)
+
+ note = self.get_ending_note()
+ if note:
+ self.paginator.add_line()
+ self.paginator.add_line(note)
+
+ await self.send_pages()
+
+ async def send_cog_help(self, cog):
+ bot = self.context.bot
+ if bot.description:
+ self.paginator.add_line(bot.description, empty=True)
+
+ note = self.get_opening_note()
+ if note:
+ self.paginator.add_line(note, empty=True)
+
+ filtered = await self.filter_commands(cog.get_commands(), sort=self.sort_commands)
+ if filtered:
+ self.paginator.add_line('**%s %s**' % (cog.qualified_name, self.commands_heading))
+ for command in filtered:
+ self.add_subcommand_formatting(command)
+
+ note = self.get_ending_note()
+ if note:
+ self.paginator.add_line()
+ self.paginator.add_line(note)
+
+ await self.send_pages()
+
+ async def send_group_help(self, group):
+ self.add_command_formatting(group)
+
+ filtered = await self.filter_commands(group.commands, sort=self.sort_commands)
+ if filtered:
+ note = self.get_opening_note()
+ if note:
+ self.paginator.add_line(note, empty=True)
+
+ self.paginator.add_line('**%s**' % self.commands_heading)
+ for command in filtered:
+ self.add_subcommand_formatting(command)
+
+ note = self.get_ending_note()
+ if note:
+ self.paginator.add_line()
+ self.paginator.add_line(note)
+
+ await self.send_pages()
+
+ async def send_command_help(self, command):
+ self.add_command_formatting(command)
+ self.paginator.close_page()
+ await self.send_pages()