mirror of
https://github.com/Rapptz/discord.py.git
synced 2025-05-11 16:29:49 +00:00
[commands] Initial implementation of help command.
This commit is contained in:
parent
4ea015067f
commit
958d278771
discord/ext/commands
@ -14,3 +14,4 @@ from .bot import Bot, when_mentioned
|
||||
from .context import Context
|
||||
from .core import *
|
||||
from .errors import *
|
||||
from .formatter import HelpFormatter
|
||||
|
@ -29,11 +29,13 @@ import discord
|
||||
import inspect
|
||||
import importlib
|
||||
import sys
|
||||
import functools
|
||||
|
||||
from .core import GroupMixin, Command
|
||||
from .core import GroupMixin, Command, command
|
||||
from .view import StringView
|
||||
from .context import Context
|
||||
from .errors import CommandNotFound
|
||||
from .formatter import HelpFormatter
|
||||
|
||||
def _get_variable(name):
|
||||
stack = inspect.stack()
|
||||
@ -50,6 +52,28 @@ def when_mentioned(bot, msg):
|
||||
to being mentioned, e.g. ``@bot ``."""
|
||||
return '{0.user.mention} '.format(bot)
|
||||
|
||||
@command(pass_context=True, name='help')
|
||||
@asyncio.coroutine
|
||||
def _default_help_command(ctx, *commands : str):
|
||||
"""Shows this message."""
|
||||
bot = ctx.bot
|
||||
destination = ctx.message.channel if not bot.pm_help else ctx.message.author
|
||||
# help by itself just lists our own commands.
|
||||
if len(commands) == 0:
|
||||
pages = bot.formatter.format_help_for(ctx, bot)
|
||||
else:
|
||||
try:
|
||||
command = functools.reduce(dict.__getitem__, commands, bot.commands)
|
||||
except KeyError as e:
|
||||
yield from bot.send_message(destination, 'No command called {} found.'.format(e))
|
||||
return
|
||||
|
||||
pages = bot.formatter.format_help_for(ctx, command)
|
||||
|
||||
for page in pages:
|
||||
yield from bot.send_message(destination, page)
|
||||
|
||||
|
||||
class Bot(GroupMixin, discord.Client):
|
||||
"""Represents a discord bot.
|
||||
|
||||
@ -74,13 +98,34 @@ class Bot(GroupMixin, discord.Client):
|
||||
multiple checks for the prefix should be used and the first one to
|
||||
match will be the invocation prefix. You can get this prefix via
|
||||
:attr:`Context.prefix`.
|
||||
description : str
|
||||
The content prefixed into the default help message.
|
||||
formatter : :class:`HelpFormatter`
|
||||
The formatter used to format the help message. By default, it uses a
|
||||
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:`remove_command` with 'help' as the argument would do the
|
||||
trick.
|
||||
pm_help : bool
|
||||
A boolean that indicates if the help command should PM the user instead of
|
||||
sending it to the channel it received it from. Defaults to ``False``.
|
||||
"""
|
||||
def __init__(self, command_prefix, **options):
|
||||
def __init__(self, command_prefix, formatter=None, description=None, pm_help=False, **options):
|
||||
super().__init__(**options)
|
||||
self.command_prefix = command_prefix
|
||||
self.extra_events = {}
|
||||
self.cogs = {}
|
||||
self.extensions = {}
|
||||
self.description = description
|
||||
self.pm_help = pm_help
|
||||
if formatter is not None:
|
||||
if not isinstance(formatter, HelpFormatter):
|
||||
raise discord.ClientException('Formatter must be a subclass of HelpFormatter')
|
||||
self.formatter = formatter
|
||||
else:
|
||||
self.formatter = HelpFormatter()
|
||||
|
||||
self.add_command(_default_help_command)
|
||||
|
||||
# internal helpers
|
||||
|
||||
|
@ -93,6 +93,11 @@ class Command:
|
||||
:exc:`CommandError` should be used. Note that if the checks fail then
|
||||
:exc:`CheckFailure` exception is raised to the :func:`on_command_error`
|
||||
event.
|
||||
description : str
|
||||
The message prefixed into the default help command.
|
||||
hidden : bool
|
||||
If ``True``, the default help command does not show this in the
|
||||
help output.
|
||||
"""
|
||||
def __init__(self, name, callback, **kwargs):
|
||||
self.name = name
|
||||
@ -102,6 +107,8 @@ class Command:
|
||||
self.brief = kwargs.get('brief')
|
||||
self.aliases = kwargs.get('aliases', [])
|
||||
self.pass_context = kwargs.get('pass_context', False)
|
||||
self.description = kwargs.get('description')
|
||||
self.hidden = kwargs.get('hidden', False)
|
||||
signature = inspect.signature(callback)
|
||||
self.params = signature.parameters.copy()
|
||||
self.checks = kwargs.get('checks', [])
|
||||
@ -276,12 +283,8 @@ class Command:
|
||||
try:
|
||||
if not self.enabled:
|
||||
raise DisabledCommand('{0.name} command is disabled'.format(self))
|
||||
|
||||
predicates = self.checks
|
||||
if predicates:
|
||||
check = all(predicate(ctx) for predicate in predicates)
|
||||
if not check:
|
||||
raise CheckFailure('The check functions for command {0.name} failed.'.format(self))
|
||||
if not self.can_run(ctx):
|
||||
raise CheckFailure('The check functions for command {0.name} failed.'.format(self))
|
||||
except CommandError as exc:
|
||||
self.handle_local_error(exc, ctx)
|
||||
ctx.bot.dispatch('command_error', exc, ctx)
|
||||
@ -327,6 +330,41 @@ class Command:
|
||||
"""The name of the cog this command belongs to. None otherwise."""
|
||||
return type(self.instance).__name__ if self.instance is not None else None
|
||||
|
||||
@property
|
||||
def short_doc(self):
|
||||
"""Gets the "short" documentation of a command.
|
||||
|
||||
By default, this is the :attr:`brief` attribute.
|
||||
If that lookup leads to an empty string then the first line of the
|
||||
:attr:`help` attribute is used instead.
|
||||
"""
|
||||
if self.brief:
|
||||
return self.brief
|
||||
if self.help:
|
||||
return self.help.split('\n', 1)[0]
|
||||
return ''
|
||||
|
||||
def can_run(self, context):
|
||||
"""Checks if the command can be executed by checking all the predicates
|
||||
inside the :attr:`checks` attribute.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
context : :class:`Context`
|
||||
The context of the command currently being invoked.
|
||||
|
||||
Returns
|
||||
--------
|
||||
bool
|
||||
A boolean indicating if the command can be invoked.
|
||||
"""
|
||||
|
||||
predicates = self.checks
|
||||
if not predicates:
|
||||
# since we have no checks, then we just return True.
|
||||
return True
|
||||
return all(predicate(context) for predicate in predicates)
|
||||
|
||||
class GroupMixin:
|
||||
"""A mixin that implements common functionality for classes that behave
|
||||
similar to :class:`Group` and are allowed to register commands.
|
||||
|
285
discord/ext/commands/formatter.py
Normal file
285
discord/ext/commands/formatter.py
Normal file
@ -0,0 +1,285 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2016 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 textwrap
|
||||
import itertools
|
||||
|
||||
from .core import GroupMixin, Command
|
||||
|
||||
# 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 HelpFormatter:
|
||||
"""The default base implementation that handles formatting of the help
|
||||
command.
|
||||
|
||||
To override the behaviour of the formatter, :meth:`format`
|
||||
should be overridden. A number of utility functions are provided for use
|
||||
inside that method.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
show_hidden : bool
|
||||
Dictates if hidden commands should be shown in the output.
|
||||
Defaults to ``False``.
|
||||
show_check_faiure : bool
|
||||
Dictates if commands that have their :attr:`Command.checks` failed
|
||||
shown. Defaults to ``False``.
|
||||
width : int
|
||||
The maximum number of characters that fit in a line.
|
||||
Defaults to 80.
|
||||
"""
|
||||
def __init__(self, show_hidden=False, show_check_faiure=False, width=80):
|
||||
self.wrapper = textwrap.TextWrapper(width=width)
|
||||
self.show_hidden = show_hidden
|
||||
self.show_check_faiure = show_check_faiure
|
||||
|
||||
def has_subcommands(self):
|
||||
"""bool : Specifies if the command has subcommands."""
|
||||
return isinstance(self.command, GroupMixin)
|
||||
|
||||
def is_bot(self):
|
||||
"""bool : Specifies if the command being formatted is the bot itself."""
|
||||
return self.command is self.context.bot
|
||||
|
||||
def shorten(self, text):
|
||||
"""Shortens text to fit into the :attr:`width`."""
|
||||
tmp = self.wrapper.max_lines
|
||||
self.wrapper.max_lines = 1
|
||||
res = self.wrapper.fill(text)
|
||||
self.wrapper.max_lines = tmp
|
||||
del tmp
|
||||
return res
|
||||
|
||||
@property
|
||||
def max_name_size(self):
|
||||
"""int : Returns the largest name length of a command or if it has subcommands
|
||||
the largest subcommand name."""
|
||||
try:
|
||||
return max(map(lambda c: len(c.name), self.command.commands.values()))
|
||||
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.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.name)
|
||||
|
||||
def get_command_signature(self):
|
||||
"""Retrieves the signature portion of the help page."""
|
||||
result = []
|
||||
prefix = self.clean_prefix
|
||||
cmd = self.command
|
||||
if len(cmd.aliases) > 0:
|
||||
aliases = '|'.join(cmd.aliases)
|
||||
name = '{0}[{1.name}|{2}]'.format(prefix, cmd, aliases)
|
||||
result.append(name)
|
||||
else:
|
||||
result.append(prefix + cmd.name)
|
||||
|
||||
params = cmd.clean_params
|
||||
if len(params) > 0:
|
||||
for name, param in params.items():
|
||||
cleaned_name = name.replace('_', '-')
|
||||
if param.default is not param.empty:
|
||||
result.append('{0}={1}'.format(cleaned_name, param.default))
|
||||
elif param.kind == param.VAR_POSITIONAL:
|
||||
result.append(cleaned_name + '...')
|
||||
else:
|
||||
result.append(cleaned_name)
|
||||
|
||||
return ' '.join(result)
|
||||
|
||||
def get_ending_note(self):
|
||||
return "Type {0}help command for more info on a command.\n" \
|
||||
"You can also type {0}help category for more info on a category.".format(self.clean_prefix)
|
||||
|
||||
def filter_command_list(self):
|
||||
"""Returns a filtered list of commands based on the two attributes
|
||||
provided, :attr:`show_check_faiure` and :attr:`show_hidden`.
|
||||
|
||||
Returns
|
||||
--------
|
||||
iterable
|
||||
An iterable with the filter being applied. The resulting value is
|
||||
a (key, value) tuple of the command name and the command itself.
|
||||
"""
|
||||
def predicate(tuple):
|
||||
cmd = tuple[1]
|
||||
if cmd.hidden and not self.show_hidden:
|
||||
return False
|
||||
|
||||
if self.show_check_faiure:
|
||||
# we don't wanna bother doing the checks if the user does not
|
||||
# care about them, so just return true.
|
||||
return True
|
||||
return cmd.can_run(self.context)
|
||||
|
||||
return filter(predicate, self.command.commands.items())
|
||||
|
||||
def _check_new_page(self):
|
||||
# be a little on the safe side
|
||||
if self._count > 1920:
|
||||
# add the page
|
||||
self._current_page.append('```')
|
||||
self._pages.append('\n'.join(self._current_page))
|
||||
self._current_page = ['```']
|
||||
self._count = 4
|
||||
|
||||
def _add_subcommands_to_page(self, max_width, commands):
|
||||
for name, command in commands:
|
||||
if name in command.aliases:
|
||||
# skip aliases
|
||||
continue
|
||||
|
||||
entry = ' {0:<{width}} {1}'.format(name, command.short_doc, width=max_width)
|
||||
shortened = self.shorten(entry)
|
||||
self._count += len(shortened)
|
||||
self._check_new_page()
|
||||
self._current_page.append(shortened)
|
||||
|
||||
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:`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 self.format()
|
||||
|
||||
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._pages = []
|
||||
self._count = 4 # ``` + '\n'
|
||||
self._current_page = ['```']
|
||||
|
||||
# we need a padding of ~80 or so
|
||||
|
||||
if self.command.description:
|
||||
# <description> portion
|
||||
self._current_page.append(self.command.description)
|
||||
self._current_page.append('')
|
||||
self._count += len(self.command.description)
|
||||
|
||||
if not self.is_bot():
|
||||
# <signature portion>
|
||||
signature = self.get_command_signature()
|
||||
self._count += 2 + len(signature) # '\n' sig '\n'
|
||||
self._current_page.append(signature)
|
||||
self._current_page.append('')
|
||||
|
||||
# <long doc> section
|
||||
if self.command.help:
|
||||
self._count += 2 + len(self.command.help)
|
||||
self._current_page.append(self.command.help)
|
||||
self._current_page.append('')
|
||||
self._check_new_page()
|
||||
|
||||
if not self.has_subcommands():
|
||||
self._current_page.append('```')
|
||||
self._pages.append('\n'.join(self._current_page))
|
||||
return self._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 '\u200bNo Category:'
|
||||
|
||||
if self.is_bot():
|
||||
data = sorted(self.filter_command_list(), key=category)
|
||||
for category, commands in itertools.groupby(data, key=category):
|
||||
# there simply is no prettier way of doing this.
|
||||
commands = list(commands)
|
||||
if len(commands) > 0:
|
||||
self._current_page.append(category)
|
||||
self._count += len(category)
|
||||
self._check_new_page()
|
||||
|
||||
self._add_subcommands_to_page(max_width, commands)
|
||||
else:
|
||||
self._current_page.append('Commands:')
|
||||
self._count += 1 + len(self._current_page[-1])
|
||||
self._add_subcommands_to_page(max_width, self.filter_command_list())
|
||||
|
||||
# add the ending note
|
||||
self._current_page.append('')
|
||||
ending_note = self.get_ending_note()
|
||||
self._count += len(ending_note)
|
||||
self._check_new_page()
|
||||
self._current_page.append(ending_note)
|
||||
|
||||
if len(self._current_page) > 1:
|
||||
self._current_page.append('```')
|
||||
self._pages.append('\n'.join(self._current_page))
|
||||
|
||||
return self._pages
|
Loading…
x
Reference in New Issue
Block a user