From 27bed5010f071b09dfc67b0d059d44954562e131 Mon Sep 17 00:00:00 2001 From: Jakub Kuczys <6032823+jack1142@users.noreply.github.com> Date: Sat, 16 Apr 2022 21:29:12 +0200 Subject: [PATCH] Make `controls` in `menu()` optional (#5678) * Make `controls` in `menu()` optional You might wonder, shouldn't we pass `None` to functions from controls? No, we shouldn't because when `None` is passed, only DEFAULT_CONTROLS can be used and that means that the length of pages list won't change. * Update usage in core and core cogs * Add missing docstrings to `redbot.core.utils.menus` module --- redbot/cogs/alias/alias.py | 4 +-- redbot/cogs/audio/core/commands/audioset.py | 10 +++---- redbot/cogs/audio/core/commands/equalizer.py | 4 +-- .../cogs/audio/core/commands/localtracks.py | 4 +-- .../cogs/audio/core/commands/miscellaneous.py | 4 +-- redbot/cogs/audio/core/commands/player.py | 4 +-- redbot/cogs/audio/core/commands/playlists.py | 8 ++--- redbot/cogs/audio/core/commands/queue.py | 3 +- redbot/cogs/customcom/customcom.py | 6 ++-- redbot/cogs/economy/economy.py | 8 ++--- redbot/cogs/general/general.py | 4 +-- redbot/cogs/modlog/modlog.py | 6 ++-- redbot/cogs/warnings/warnings.py | 6 ++-- redbot/core/core_commands.py | 4 +-- redbot/core/utils/menus.py | 29 ++++++++++++++++--- 15 files changed, 59 insertions(+), 45 deletions(-) diff --git a/redbot/cogs/alias/alias.py b/redbot/cogs/alias/alias.py index 421a5ff5f..5c6318741 100644 --- a/redbot/cogs/alias/alias.py +++ b/redbot/cogs/alias/alias.py @@ -9,7 +9,7 @@ import discord from redbot.core import Config, commands, checks from redbot.core.i18n import Translator, cog_i18n from redbot.core.utils.chat_formatting import box, pagify -from redbot.core.utils.menus import menu, DEFAULT_CONTROLS +from redbot.core.utils.menus import menu from redbot.core.bot import Red from .alias_entry import AliasEntry, AliasCache, ArgParseError @@ -185,7 +185,7 @@ class Alias(commands.Cog): if len(alias_list) == 1: await ctx.send(alias_list[0]) return - await menu(ctx, alias_list, DEFAULT_CONTROLS) + await menu(ctx, alias_list) @commands.group() async def alias(self, ctx: commands.Context): diff --git a/redbot/cogs/audio/core/commands/audioset.py b/redbot/cogs/audio/core/commands/audioset.py index 2d4453af7..62c3aea8f 100644 --- a/redbot/cogs/audio/core/commands/audioset.py +++ b/redbot/cogs/audio/core/commands/audioset.py @@ -14,7 +14,7 @@ from redbot.core import bank, commands from redbot.core.data_manager import cog_data_path from redbot.core.i18n import Translator from redbot.core.utils.chat_formatting import box, humanize_number -from redbot.core.utils.menus import DEFAULT_CONTROLS, menu, start_adding_reactions +from redbot.core.utils.menus import menu, start_adding_reactions from redbot.core.utils.predicates import MessagePredicate, ReactionPredicate from ...audio_dataclasses import LocalPath @@ -102,7 +102,7 @@ class AudioSetCommands(MixinMeta, metaclass=CompositeMetaClass): discord.Embed(title=_("Global Whitelist"), description=page, colour=embed_colour) for page in pages ) - await menu(ctx, pages, DEFAULT_CONTROLS) + await menu(ctx, pages) @command_audioset_perms_global_whitelist.command(name="clear") async def command_audioset_perms_global_whitelist_clear(self, ctx: commands.Context): @@ -196,7 +196,7 @@ class AudioSetCommands(MixinMeta, metaclass=CompositeMetaClass): discord.Embed(title=_("Global Blacklist"), description=page, colour=embed_colour) for page in pages ) - await menu(ctx, pages, DEFAULT_CONTROLS) + await menu(ctx, pages) @command_audioset_perms_global_blacklist.command(name="clear") async def command_audioset_perms_global_blacklist_clear(self, ctx: commands.Context): @@ -292,7 +292,7 @@ class AudioSetCommands(MixinMeta, metaclass=CompositeMetaClass): discord.Embed(title=_("Whitelist"), description=page, colour=embed_colour) for page in pages ) - await menu(ctx, pages, DEFAULT_CONTROLS) + await menu(ctx, pages) @command_audioset_perms_whitelist.command(name="clear") async def command_audioset_perms_whitelist_clear(self, ctx: commands.Context): @@ -385,7 +385,7 @@ class AudioSetCommands(MixinMeta, metaclass=CompositeMetaClass): discord.Embed(title=_("Blacklist"), description=page, colour=embed_colour) for page in pages ) - await menu(ctx, pages, DEFAULT_CONTROLS) + await menu(ctx, pages) @command_audioset_perms_blacklist.command(name="clear") async def command_audioset_perms_blacklist_clear(self, ctx: commands.Context): diff --git a/redbot/cogs/audio/core/commands/equalizer.py b/redbot/cogs/audio/core/commands/equalizer.py index 808188e8e..0b07ef341 100644 --- a/redbot/cogs/audio/core/commands/equalizer.py +++ b/redbot/cogs/audio/core/commands/equalizer.py @@ -10,7 +10,7 @@ from red_commons.logging import getLogger from redbot.core import commands from redbot.core.i18n import Translator from redbot.core.utils.chat_formatting import box, humanize_number, pagify -from redbot.core.utils.menus import DEFAULT_CONTROLS, menu, start_adding_reactions +from redbot.core.utils.menus import menu, start_adding_reactions from redbot.core.utils.predicates import MessagePredicate, ReactionPredicate from ...equalizer import Equalizer @@ -141,7 +141,7 @@ class EqualizerCommands(MixinMeta, metaclass=CompositeMetaClass): text=_("{num} preset(s)").format(num=humanize_number(len(list(eq_presets.keys())))) ) page_list.append(embed) - await menu(ctx, page_list, DEFAULT_CONTROLS) + await menu(ctx, page_list) @command_equalizer.command(name="load") async def command_equalizer_load(self, ctx: commands.Context, eq_preset: str): diff --git a/redbot/cogs/audio/core/commands/localtracks.py b/redbot/cogs/audio/core/commands/localtracks.py index c364520f4..f1dd483e0 100644 --- a/redbot/cogs/audio/core/commands/localtracks.py +++ b/redbot/cogs/audio/core/commands/localtracks.py @@ -8,7 +8,7 @@ from red_commons.logging import getLogger from redbot.core import commands from redbot.core.i18n import Translator -from redbot.core.utils.menus import DEFAULT_CONTROLS, close_menu, menu, next_page, prev_page +from redbot.core.utils.menus import close_menu, menu, next_page, prev_page from ...audio_dataclasses import LocalPath, Query from ..abc import MixinMeta @@ -113,7 +113,7 @@ class LocalTrackCommands(MixinMeta, metaclass=CompositeMetaClass): dj_enabled = await self.config.guild(ctx.guild).dj_enabled() if dj_enabled and not await self._can_instaskip(ctx, ctx.author): - return await menu(ctx, folder_page_list, DEFAULT_CONTROLS) + return await menu(ctx, folder_page_list) else: await menu(ctx, folder_page_list, local_folder_controls) diff --git a/redbot/cogs/audio/core/commands/miscellaneous.py b/redbot/cogs/audio/core/commands/miscellaneous.py index 1422d55ad..b6c123c64 100644 --- a/redbot/cogs/audio/core/commands/miscellaneous.py +++ b/redbot/cogs/audio/core/commands/miscellaneous.py @@ -12,7 +12,7 @@ from redbot.core import commands from redbot.core.i18n import Translator from redbot.core.utils import AsyncIter from redbot.core.utils.chat_formatting import humanize_number, pagify -from redbot.core.utils.menus import DEFAULT_CONTROLS, menu +from redbot.core.utils.menus import menu from ..abc import MixinMeta from ..cog_utils import CompositeMetaClass @@ -92,7 +92,7 @@ class MiscellaneousCommands(MixinMeta, metaclass=CompositeMetaClass): pages += 1 servers_embed.append(em) - await menu(ctx, servers_embed, DEFAULT_CONTROLS) + await menu(ctx, servers_embed) @commands.command(name="percent") @commands.guild_only() diff --git a/redbot/cogs/audio/core/commands/player.py b/redbot/cogs/audio/core/commands/player.py index b425b80f9..d2f5ef134 100644 --- a/redbot/cogs/audio/core/commands/player.py +++ b/redbot/cogs/audio/core/commands/player.py @@ -15,7 +15,7 @@ from redbot.core import commands from redbot.core.commands import UserInputOptional from redbot.core.i18n import Translator from redbot.core.utils import AsyncIter -from redbot.core.utils.menus import DEFAULT_CONTROLS, close_menu, menu, next_page, prev_page +from redbot.core.utils.menus import close_menu, menu, next_page, prev_page from ...audio_dataclasses import _PARTIALLY_SUPPORTED_MUSIC_EXT, Query from ...errors import ( @@ -930,6 +930,6 @@ class PlayerCommands(MixinMeta, metaclass=CompositeMetaClass): search_page_list.append(embed) if dj_enabled and not can_skip: - return await menu(ctx, search_page_list, DEFAULT_CONTROLS) + return await menu(ctx, search_page_list) await menu(ctx, search_page_list, search_controls) diff --git a/redbot/cogs/audio/core/commands/playlists.py b/redbot/cogs/audio/core/commands/playlists.py index 5871368bd..d6e20e351 100644 --- a/redbot/cogs/audio/core/commands/playlists.py +++ b/redbot/cogs/audio/core/commands/playlists.py @@ -19,7 +19,7 @@ from redbot.core.data_manager import cog_data_path from redbot.core.i18n import Translator from redbot.core.utils import AsyncIter from redbot.core.utils.chat_formatting import bold, pagify -from redbot.core.utils.menus import DEFAULT_CONTROLS, menu +from redbot.core.utils.menus import menu from redbot.core.utils.predicates import MessagePredicate from ...apis.api_utils import FakePlaylist @@ -899,7 +899,7 @@ class PlaylistCommands(MixinMeta, metaclass=CompositeMetaClass): ) ) page_list.append(embed) - await menu(ctx, page_list, DEFAULT_CONTROLS) + await menu(ctx, page_list) @commands.cooldown(1, 15, commands.BucketType.guild) @command_playlist.command(name="list", usage="[args]", cooldown_after_parsing=True) @@ -1052,7 +1052,7 @@ class PlaylistCommands(MixinMeta, metaclass=CompositeMetaClass): async for page_num in AsyncIter(range(1, len_playlist_list_pages + 1)): embed = await self._build_playlist_list_page(ctx, page_num, abc_names, name) playlist_embeds.append(embed) - await menu(ctx, playlist_embeds, DEFAULT_CONTROLS) + await menu(ctx, playlist_embeds) @command_playlist.command(name="queue", usage=" [args]", cooldown_after_parsing=True) @commands.cooldown(1, 300, commands.BucketType.member) @@ -1742,7 +1742,7 @@ class PlaylistCommands(MixinMeta, metaclass=CompositeMetaClass): ), ) if embeds: - await menu(ctx, embeds, DEFAULT_CONTROLS) + await menu(ctx, embeds) @command_playlist.command(name="upload", usage="[args]") @commands.is_owner() diff --git a/redbot/cogs/audio/core/commands/queue.py b/redbot/cogs/audio/core/commands/queue.py index f8c21e888..80aacc318 100644 --- a/redbot/cogs/audio/core/commands/queue.py +++ b/redbot/cogs/audio/core/commands/queue.py @@ -14,7 +14,6 @@ from redbot.core import commands from redbot.core.i18n import Translator from redbot.core.utils import AsyncIter from redbot.core.utils.menus import ( - DEFAULT_CONTROLS, close_menu, menu, next_page, @@ -304,7 +303,7 @@ class QueueCommands(MixinMeta, metaclass=CompositeMetaClass): async for page_num in AsyncIter(range(1, len_search_pages + 1)): embed = await self._build_queue_search_page(ctx, page_num, search_list) search_page_list.append(embed) - await menu(ctx, search_page_list, DEFAULT_CONTROLS) + await menu(ctx, search_page_list) @command_queue.command(name="shuffle") @commands.cooldown(1, 30, commands.BucketType.guild) diff --git a/redbot/cogs/customcom/customcom.py b/redbot/cogs/customcom/customcom.py index 6d6c30ac8..4f1462776 100644 --- a/redbot/cogs/customcom/customcom.py +++ b/redbot/cogs/customcom/customcom.py @@ -309,7 +309,7 @@ class CustomCommands(commands.Cog): if len(msg) > 2000: msg = f"{msg[:1997]}..." msglist.append(msg) - await menus.menu(ctx, msglist, menus.DEFAULT_CONTROLS) + await menus.menu(ctx, msglist) @customcom.command(name="search") @commands.guild_only() @@ -572,11 +572,11 @@ class CustomCommands(commands.Cog): ) embed.set_footer(text=_("Page {num}/{total}").format(num=idx, total=len(pages))) embed_pages.append(embed) - await menus.menu(ctx, embed_pages, menus.DEFAULT_CONTROLS) + await menus.menu(ctx, embed_pages) else: content = "\n".join(map("{0[0]:<12} : {0[1]}".format, results)) pages = list(map(box, pagify(content, page_length=2000, shorten_by=10))) - await menus.menu(ctx, pages, menus.DEFAULT_CONTROLS) + await menus.menu(ctx, pages) @customcom.command(name="show") async def cc_show(self, ctx, command_name: str): diff --git a/redbot/cogs/economy/economy.py b/redbot/cogs/economy/economy.py index 4b0d84742..d13a85865 100644 --- a/redbot/cogs/economy/economy.py +++ b/redbot/cogs/economy/economy.py @@ -14,7 +14,7 @@ from redbot.core.bot import Red from redbot.core.i18n import Translator, cog_i18n from redbot.core.utils import AsyncIter from redbot.core.utils.chat_formatting import box, humanize_number -from redbot.core.utils.menus import close_menu, menu, DEFAULT_CONTROLS +from redbot.core.utils.menus import close_menu, menu from .converters import positive_int T_ = Translator("Economy", __file__) @@ -516,11 +516,7 @@ class Economy(commands.Cog): highscores.append(box(temp_msg, lang="md")) if highscores: - await menu( - ctx, - highscores, - DEFAULT_CONTROLS if len(highscores) > 1 else {"\N{CROSS MARK}": close_menu}, - ) + await menu(ctx, highscores) else: await ctx.send(_("No balances found.")) diff --git a/redbot/cogs/general/general.py b/redbot/cogs/general/general.py index c1f2188e7..f618e950a 100644 --- a/redbot/cogs/general/general.py +++ b/redbot/cogs/general/general.py @@ -9,7 +9,7 @@ import discord from redbot.core import commands from redbot.core.bot import Red from redbot.core.i18n import Translator, cog_i18n -from redbot.core.utils.menus import menu, DEFAULT_CONTROLS +from redbot.core.utils.menus import menu from redbot.core.utils.chat_formatting import ( bold, escape, @@ -511,7 +511,6 @@ class General(commands.Cog): await menu( ctx, pages=embeds, - controls=DEFAULT_CONTROLS, message=None, page=0, timeout=30, @@ -537,7 +536,6 @@ class General(commands.Cog): await menu( ctx, pages=messages, - controls=DEFAULT_CONTROLS, message=None, page=0, timeout=30, diff --git a/redbot/cogs/modlog/modlog.py b/redbot/cogs/modlog/modlog.py index 711fe738c..925b11c40 100644 --- a/redbot/cogs/modlog/modlog.py +++ b/redbot/cogs/modlog/modlog.py @@ -9,7 +9,7 @@ from redbot.core import checks, commands, modlog from redbot.core.bot import Red from redbot.core.i18n import Translator, cog_i18n from redbot.core.utils.chat_formatting import bold, box, pagify -from redbot.core.utils.menus import DEFAULT_CONTROLS, menu +from redbot.core.utils.menus import menu from redbot.core.utils.predicates import MessagePredicate _ = Translator("ModLog", __file__) @@ -84,7 +84,7 @@ class ModLog(commands.Cog): ) rendered_cases.append(message) - await menu(ctx, rendered_cases, DEFAULT_CONTROLS) + await menu(ctx, rendered_cases) @commands.command() @commands.guild_only() @@ -119,7 +119,7 @@ class ModLog(commands.Cog): ) for page in pagify(message, ["\n\n", "\n"], priority=True): rendered_cases.append(page) - await menu(ctx, rendered_cases, DEFAULT_CONTROLS) + await menu(ctx, rendered_cases) @commands.command() @commands.guild_only() diff --git a/redbot/cogs/warnings/warnings.py b/redbot/cogs/warnings/warnings.py index a3caa4e8e..947619afa 100644 --- a/redbot/cogs/warnings/warnings.py +++ b/redbot/cogs/warnings/warnings.py @@ -19,7 +19,7 @@ from redbot.core.commands import UserInputOptional from redbot.core.i18n import Translator, cog_i18n from redbot.core.utils import AsyncIter from redbot.core.utils.chat_formatting import warning, pagify -from redbot.core.utils.menus import menu, DEFAULT_CONTROLS +from redbot.core.utils.menus import menu _ = Translator("Warnings", __file__) @@ -324,7 +324,7 @@ class Warnings(commands.Cog): ).format(reason_name=r, **v) ) if msg_list: - await menu(ctx, msg_list, DEFAULT_CONTROLS) + await menu(ctx, msg_list) else: await ctx.send(_("There are no reasons configured!")) @@ -359,7 +359,7 @@ class Warnings(commands.Cog): ).format(**r) ) if msg_list: - await menu(ctx, msg_list, DEFAULT_CONTROLS) + await menu(ctx, msg_list) else: await ctx.send(_("There are no actions configured!")) diff --git a/redbot/core/core_commands.py b/redbot/core/core_commands.py index bee8a97a6..0402c2f35 100644 --- a/redbot/core/core_commands.py +++ b/redbot/core/core_commands.py @@ -18,7 +18,7 @@ import pip import traceback from pathlib import Path from redbot.core import data_manager -from redbot.core.utils.menus import menu, DEFAULT_CONTROLS +from redbot.core.utils.menus import menu from redbot.core.commands import GuildConverter, RawUserIdConverter from string import ascii_letters, digits from typing import TYPE_CHECKING, Union, Tuple, List, Optional, Iterable, Sequence, Dict, Set @@ -1659,7 +1659,7 @@ class Core(commands.commands._RuleDropper, commands.Cog, CoreLogic): if len(pages) == 1: await ctx.send(pages[0]) else: - await menu(ctx, pages, DEFAULT_CONTROLS) + await menu(ctx, pages) @commands.command(require_var_positional=True) @checks.is_owner() diff --git a/redbot/core/utils/menus.py b/redbot/core/utils/menus.py index a7c123bf6..871f3bab2 100644 --- a/redbot/core/utils/menus.py +++ b/redbot/core/utils/menus.py @@ -6,7 +6,7 @@ import asyncio import contextlib import functools from types import MappingProxyType -from typing import Callable, Iterable, List, Mapping, TypeVar, Union +from typing import Callable, Iterable, List, Mapping, Optional, TypeVar, Union import discord @@ -22,7 +22,7 @@ _ControlCallable = Callable[[commands.Context, _PageList, discord.Message, int, async def menu( ctx: commands.Context, pages: _PageList, - controls: Mapping[str, _ControlCallable], + controls: Optional[Mapping[str, _ControlCallable]] = None, message: discord.Message = None, page: int = 0, timeout: float = 30.0, @@ -45,10 +45,12 @@ async def menu( The command context pages: `list` of `str` or `discord.Embed` The pages of the menu. - controls: Mapping[str, Callable], + controls: Optional[Mapping[str, Callable]] A mapping of emoji to the function which handles the action for the emoji. The signature of the function should be the same as of this function and should additionally accept an ``emoji`` parameter of type `str`. + If not passed, `DEFAULT_CONTROLS` is used *or* + only a close menu control is shown when ``pages`` is of length 1. message: discord.Message The message representing the menu. Usually :code:`None` when first opening the menu @@ -68,6 +70,11 @@ async def menu( isinstance(x, str) for x in pages ): raise RuntimeError("All pages must be of the same type") + if controls is None: + if len(pages) == 1: + controls = {"\N{CROSS MARK}": close_menu} + else: + controls = DEFAULT_CONTROLS for key, value in controls.items(): maybe_coro = value if isinstance(value, functools.partial): @@ -144,6 +151,10 @@ async def next_page( timeout: float, emoji: str, ) -> _T: + """ + Function for showing next page which is suitable + for use in ``controls`` mapping that is passed to `menu()`. + """ if page == len(pages) - 1: page = 0 # Loop around to the first item else: @@ -160,6 +171,10 @@ async def prev_page( timeout: float, emoji: str, ) -> _T: + """ + Function for showing previous page which is suitable + for use in ``controls`` mapping that is passed to `menu()`. + """ if page == 0: page = len(pages) - 1 # Loop around to the last item else: @@ -176,6 +191,10 @@ async def close_menu( timeout: float, emoji: str, ) -> None: + """ + Function for closing (deleting) menu which is suitable + for use in ``controls`` mapping that is passed to `menu()`. + """ with contextlib.suppress(discord.NotFound): await message.delete() @@ -191,7 +210,7 @@ def start_adding_reactions( This is particularly useful if you wish to start waiting for a reaction whilst the reactions are still being added - in fact, - this is exactly what `menu` uses to do that. + this is exactly what `menu()` uses to do that. Parameters ---------- @@ -216,6 +235,8 @@ def start_adding_reactions( return asyncio.create_task(task()) +#: Default controls for `menu()` that contain controls for +#: previous page, closing menu, and next page. DEFAULT_CONTROLS: Mapping[str, _ControlCallable] = MappingProxyType( { "\N{LEFTWARDS BLACK ARROW}\N{VARIATION SELECTOR-16}": prev_page,