7 changed files with 1259 additions and 480 deletions
@ -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 |
File diff suppressed because it is too large
Loading…
Reference in new issue