Add a provisional API for replacing the help formatter (#4011)

* Adds an API for replacing the help formatter

* Apply suggestions from code review

Co-authored-by: jack1142 <6032823+jack1142@users.noreply.github.com>

* add note about provisionality

Co-authored-by: jack1142 <6032823+jack1142@users.noreply.github.com>
This commit is contained in:
Michael H 2020-08-05 20:12:14 -04:00 committed by GitHub
parent 6cef336417
commit 4f808306ba
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 177 additions and 23 deletions

View File

@ -44,3 +44,17 @@ extend functionalities used throughout the bot, as outlined below.
:no-undoc-members: :no-undoc-members:
.. autoclass:: APIToken .. autoclass:: APIToken
******************
Help Functionality
******************
.. warning::
The content in this section is provisional and may change
without prior notice or warning. Updates to this will be communicated
on `this issue <https://github.com/Cog-Creators/Red-DiscordBot/issues/4084>`_
.. automodule:: redbot.core.commands.help
:members:

View File

@ -208,6 +208,61 @@ class RedBase(
self._deletion_requests: MutableMapping[int, asyncio.Lock] = weakref.WeakValueDictionary() self._deletion_requests: MutableMapping[int, asyncio.Lock] = weakref.WeakValueDictionary()
def set_help_formatter(self, formatter: commands.help.HelpFormatterABC):
"""
Set's Red's help formatter.
.. warning::
This method is provisional.
The formatter must implement all methods in
``commands.help.HelpFormatterABC``
Cogs which set a help formatter should inform users of this.
Users should not use multiple cogs which set a help formatter.
This should specifically be an instance of a formatter.
This allows cogs to pass a config object or similar to the
formatter prior to the bot using it.
See ``commands.help.HelpFormatterABC`` for more details.
Raises
------
RuntimeError
If the default formatter has already been replaced
TypeError
If given an invalid formatter
"""
if not isinstance(formatter, commands.help.HelpFormatterABC):
raise TypeError(
"Help formatters must inherit from `commands.help.HelpFormatterABC` "
"and implement the required interfaces."
)
# do not switch to isinstance, we want to know that this has not been overriden,
# even with a subclass.
if type(self._help_formatter) is commands.help.RedHelpFormatter:
self._help_formatter = formatter
else:
raise RuntimeError("The formatter has already been overriden.")
def reset_help_formatter(self):
"""
Resets Red's help formatter.
.. warning::
This method is provisional.
This exists for use in ``cog_unload`` for cogs which replace the formatter
as well as for a rescue command in core_commands.
"""
self._help_formatter = commands.help.RedHelpFormatter()
def get_command(self, name: str) -> Optional[commands.Command]: def get_command(self, name: str) -> Optional[commands.Command]:
com = super().get_command(name) com = super().get_command(name)
assert com is None or isinstance(com, commands.Command) assert com is None or isinstance(com, commands.Command)
@ -786,12 +841,18 @@ class RedBase(
return await super().start(*args, **kwargs) return await super().start(*args, **kwargs)
async def send_help_for( async def send_help_for(
self, ctx: commands.Context, help_for: Union[commands.Command, commands.GroupMixin, str] self,
ctx: commands.Context,
help_for: Union[commands.Command, commands.GroupMixin, str],
*,
from_help_command: bool = False,
): ):
""" """
Invokes Red's helpformatter for a given context and object. Invokes Red's helpformatter for a given context and object.
""" """
return await self._help_formatter.send_help(ctx, help_for) return await self._help_formatter.send_help(
ctx, help_for, from_help_command=from_help_command
)
async def embed_requested(self, channel, user, command=None) -> bool: async def embed_requested(self, channel, user, command=None) -> bool:
""" """

View File

@ -1,13 +1,10 @@
# Warning: The implementation below touches several private attributes. # Warning: The implementation below touches several private attributes.
# While this implementation will be updated, and public interfaces maintained, derived classes # While this implementation will be updated, and public interfaces maintained,
# should not assume these private attributes are version safe, and use the provided HelpSettings # derived classes should not assume these private attributes are version safe,
# class for these settings. # and use the provided HelpSettings class for these settings.
# This is a full replacement of discord.py's help command # This is a full replacement of discord.py's help command
# #
# At a later date, there should be things added to support extra formatter
# registration from 3rd party cogs.
#
# This exists due to deficiencies in discord.py which conflict # This exists due to deficiencies in discord.py which conflict
# with our needs for per-context help settings # with our needs for per-context help settings
# see https://github.com/Rapptz/discord.py/issues/2123 # see https://github.com/Rapptz/discord.py/issues/2123
@ -30,8 +27,7 @@
# Additionally, this gives our users a bit more customization options including by # Additionally, this gives our users a bit more customization options including by
# 3rd party cogs down the road. # 3rd party cogs down the road.
# Note: 3rd party help must not remove the copyright notice import abc
import asyncio import asyncio
from collections import namedtuple from collections import namedtuple
from dataclasses import dataclass from dataclasses import dataclass
@ -48,7 +44,7 @@ from ..utils.mod import mass_purge
from ..utils._internal_utils import fuzzy_command_search, format_fuzzy_results from ..utils._internal_utils import fuzzy_command_search, format_fuzzy_results
from ..utils.chat_formatting import box, pagify from ..utils.chat_formatting import box, pagify
__all__ = ["red_help", "RedHelpFormatter", "HelpSettings"] __all__ = ["red_help", "RedHelpFormatter", "HelpSettings", "HelpFormatterABC"]
_ = Translator("Help", __file__) _ = Translator("Help", __file__)
@ -65,6 +61,11 @@ EMPTY_STRING = "\N{ZERO WIDTH SPACE}"
class HelpSettings: class HelpSettings:
""" """
A representation of help settings. A representation of help settings.
.. warning::
This class is provisional.
""" """
page_char_limit: int = 1000 page_char_limit: int = 1000
@ -78,8 +79,10 @@ class HelpSettings:
# Contrib Note: This is intentional to not accept the bot object # Contrib Note: This is intentional to not accept the bot object
# There are plans to allow guild and user specific help settings # There are plans to allow guild and user specific help settings
# Adding a non-context based method now would involve a breaking change later. # Adding a non-context based method now would involve a breaking
# At a later date, more methods should be exposed for non-context based creation. # change later.
# At a later date, more methods should be exposed for
# non-context based creation.
# #
# This is also why we aren't just caching the # This is also why we aren't just caching the
# current state of these settings on the bot object. # current state of these settings on the bot object.
@ -102,23 +105,72 @@ class NoSubCommand(Exception):
self.not_found = not_found self.not_found = not_found
class RedHelpFormatter: class HelpFormatterABC(abc.ABC):
"""
Describes the required interface of a help formatter.
Additional notes for 3rd party developers are included in this class.
.. note::
You may define __init__ however you want
(such as to include config),
Red will not initialize a formatter for you,
and must be passed an initialized formatter.
If you want to use Red's existing settings, use ``HelpSettings.from_context``
.. warning::
This class is documented but provisional with expected changes.
In the future, this class will receive changes to support
invoking the help command without context.
"""
@abc.abstractmethod
async def send_help(
self, ctx: Context, help_for: HelpTarget = None, *, from_help_command: bool = False
):
"""
This is (currently) the only method you must implement.
This method should handle any and all errors which may arise.
The types subclasses must handle are defined as ``HelpTarget``
"""
...
class RedHelpFormatter(HelpFormatterABC):
""" """
Red's help implementation Red's help implementation
This is intended to be overridable in parts to only change some behavior. This is intended to be overridable in parts to only change some behavior.
While currently, there is a global formatter, later plans include a context specific While this exists as a class for easy partial overriding,
formatter selector as well as an API for cogs to register/un-register a formatter with the bot. most implementations should not need or want a shared state.
When implementing your own formatter, at minimum you must provide an implementation of .. warning::
`send_help` with identical signature.
While this exists as a class for easy partial overriding, most implementations This class is documented but may receive changes between
should not need or want a shared state. versions without warning as needed.
The supported way to modify help is to write a separate formatter.
The primary reason for this class being documented is to allow
the opaque use of the class as a fallback, as any method in base
class which is intended for use will be present and implemented here.
.. note::
This class may use various internal methods which are not safe to
use in third party code.
The internal methods used here may change,
with this class being updated at the same time.
""" """
async def send_help(self, ctx: Context, help_for: HelpTarget = None): async def send_help(
self, ctx: Context, help_for: HelpTarget = None, *, from_help_command: bool = False
):
""" """
This delegates to other functions. This delegates to other functions.
@ -724,4 +776,4 @@ async def red_help(ctx: Context, *, thing_to_get_help_for: str = None):
(Help) you know I need someone (Help) you know I need someone
(Help!) (Help!)
""" """
await ctx.bot.send_help_for(ctx, thing_to_get_help_for) await ctx.bot.send_help_for(ctx, thing_to_get_help_for, from_help_command=True)

View File

@ -27,7 +27,6 @@ from redbot.core.data_manager import storage_type
from . import ( from . import (
__version__, __version__,
version_info as red_version_info, version_info as red_version_info,
VersionInfo,
checks, checks,
commands, commands,
errors, errors,
@ -2080,6 +2079,34 @@ class Core(commands.commands._RuleDropper, commands.Cog, CoreLogic):
"""Manage settings for the help command.""" """Manage settings for the help command."""
pass pass
@helpset.command(name="resetformatter")
async def helpset_resetformatter(self, ctx: commands.Context):
""" This resets [botname]'s help formatter to the default formatter """
ctx.bot.reset_help_formatter()
await ctx.send(
_(
"The help formatter has been reset. "
"This will not prevent cogs from modifying help, "
"you may need to remove a cog if this has been an issue."
)
)
@helpset.command(name="resetsettings")
async def helpset_resetsettings(self, ctx: commands.Context):
"""
This resets [botname]'s help settings to their defaults.
This may not have an impact when using custom formatters from 3rd party cogs
"""
await ctx.bot._config.help.clear()
await ctx.send(
_(
"The help settings have been reset to their defaults. "
"This may not have an impact when using 3rd party help formatters."
)
)
@helpset.command(name="usemenus") @helpset.command(name="usemenus")
async def helpset_usemenus(self, ctx: commands.Context, use_menus: bool = None): async def helpset_usemenus(self, ctx: commands.Context, use_menus: bool = None):
""" """