diff --git a/docs/framework_commands.rst b/docs/framework_commands.rst index d5e61b240..8a7ae7716 100644 --- a/docs/framework_commands.rst +++ b/docs/framework_commands.rst @@ -44,3 +44,17 @@ extend functionalities used throughout the bot, as outlined below. :no-undoc-members: .. 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 `_ + + +.. automodule:: redbot.core.commands.help + :members: diff --git a/redbot/core/bot.py b/redbot/core/bot.py index 1a8e9fc28..fe71aa20f 100644 --- a/redbot/core/bot.py +++ b/redbot/core/bot.py @@ -208,6 +208,61 @@ class RedBase( 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]: com = super().get_command(name) assert com is None or isinstance(com, commands.Command) @@ -786,12 +841,18 @@ class RedBase( return await super().start(*args, **kwargs) 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. """ - 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: """ diff --git a/redbot/core/commands/help.py b/redbot/core/commands/help.py index 2f17fed11..0a42dadd5 100644 --- a/redbot/core/commands/help.py +++ b/redbot/core/commands/help.py @@ -1,13 +1,10 @@ # Warning: The implementation below touches several private attributes. -# While this implementation will be updated, and public interfaces maintained, derived classes -# should not assume these private attributes are version safe, and use the provided HelpSettings -# class for these settings. +# While this implementation will be updated, and public interfaces maintained, +# derived classes should not assume these private attributes are version safe, +# and use the provided HelpSettings class for these settings. # 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 # with our needs for per-context help settings # 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 # 3rd party cogs down the road. -# Note: 3rd party help must not remove the copyright notice - +import abc import asyncio from collections import namedtuple 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.chat_formatting import box, pagify -__all__ = ["red_help", "RedHelpFormatter", "HelpSettings"] +__all__ = ["red_help", "RedHelpFormatter", "HelpSettings", "HelpFormatterABC"] _ = Translator("Help", __file__) @@ -65,6 +61,11 @@ EMPTY_STRING = "\N{ZERO WIDTH SPACE}" class HelpSettings: """ A representation of help settings. + + .. warning:: + + This class is provisional. + """ page_char_limit: int = 1000 @@ -78,8 +79,10 @@ class HelpSettings: # Contrib Note: This is intentional to not accept the bot object # There are plans to allow guild and user specific help settings - # Adding a non-context based method now would involve a breaking change later. - # At a later date, more methods should be exposed for non-context based creation. + # Adding a non-context based method now would involve a breaking + # 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 # current state of these settings on the bot object. @@ -102,23 +105,72 @@ class NoSubCommand(Exception): 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 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 - formatter selector as well as an API for cogs to register/un-register a formatter with the bot. + While this exists as a class for easy partial overriding, + most implementations should not need or want a shared state. - When implementing your own formatter, at minimum you must provide an implementation of - `send_help` with identical signature. + .. warning:: - While this exists as a class for easy partial overriding, most implementations - should not need or want a shared state. + This class is documented but may receive changes between + 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. @@ -724,4 +776,4 @@ async def red_help(ctx: Context, *, thing_to_get_help_for: str = None): (Help) you know I need someone (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) diff --git a/redbot/core/core_commands.py b/redbot/core/core_commands.py index 470eddd2b..de3a4db02 100644 --- a/redbot/core/core_commands.py +++ b/redbot/core/core_commands.py @@ -27,7 +27,6 @@ from redbot.core.data_manager import storage_type from . import ( __version__, version_info as red_version_info, - VersionInfo, checks, commands, errors, @@ -2080,6 +2079,34 @@ class Core(commands.commands._RuleDropper, commands.Cog, CoreLogic): """Manage settings for the help command.""" 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") async def helpset_usemenus(self, ctx: commands.Context, use_menus: bool = None): """