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
This commit is contained in:
Jakub Kuczys 2022-04-16 21:29:12 +02:00 committed by GitHub
parent 955b40ac6d
commit 27bed5010f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 59 additions and 45 deletions

View File

@ -9,7 +9,7 @@ import discord
from redbot.core import Config, commands, checks from redbot.core import Config, commands, checks
from redbot.core.i18n import Translator, cog_i18n from redbot.core.i18n import Translator, cog_i18n
from redbot.core.utils.chat_formatting import box, pagify 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 redbot.core.bot import Red
from .alias_entry import AliasEntry, AliasCache, ArgParseError from .alias_entry import AliasEntry, AliasCache, ArgParseError
@ -185,7 +185,7 @@ class Alias(commands.Cog):
if len(alias_list) == 1: if len(alias_list) == 1:
await ctx.send(alias_list[0]) await ctx.send(alias_list[0])
return return
await menu(ctx, alias_list, DEFAULT_CONTROLS) await menu(ctx, alias_list)
@commands.group() @commands.group()
async def alias(self, ctx: commands.Context): async def alias(self, ctx: commands.Context):

View File

@ -14,7 +14,7 @@ from redbot.core import bank, commands
from redbot.core.data_manager import cog_data_path from redbot.core.data_manager import cog_data_path
from redbot.core.i18n import Translator from redbot.core.i18n import Translator
from redbot.core.utils.chat_formatting import box, humanize_number 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 redbot.core.utils.predicates import MessagePredicate, ReactionPredicate
from ...audio_dataclasses import LocalPath from ...audio_dataclasses import LocalPath
@ -102,7 +102,7 @@ class AudioSetCommands(MixinMeta, metaclass=CompositeMetaClass):
discord.Embed(title=_("Global Whitelist"), description=page, colour=embed_colour) discord.Embed(title=_("Global Whitelist"), description=page, colour=embed_colour)
for page in pages for page in pages
) )
await menu(ctx, pages, DEFAULT_CONTROLS) await menu(ctx, pages)
@command_audioset_perms_global_whitelist.command(name="clear") @command_audioset_perms_global_whitelist.command(name="clear")
async def command_audioset_perms_global_whitelist_clear(self, ctx: commands.Context): 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) discord.Embed(title=_("Global Blacklist"), description=page, colour=embed_colour)
for page in pages for page in pages
) )
await menu(ctx, pages, DEFAULT_CONTROLS) await menu(ctx, pages)
@command_audioset_perms_global_blacklist.command(name="clear") @command_audioset_perms_global_blacklist.command(name="clear")
async def command_audioset_perms_global_blacklist_clear(self, ctx: commands.Context): 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) discord.Embed(title=_("Whitelist"), description=page, colour=embed_colour)
for page in pages for page in pages
) )
await menu(ctx, pages, DEFAULT_CONTROLS) await menu(ctx, pages)
@command_audioset_perms_whitelist.command(name="clear") @command_audioset_perms_whitelist.command(name="clear")
async def command_audioset_perms_whitelist_clear(self, ctx: commands.Context): 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) discord.Embed(title=_("Blacklist"), description=page, colour=embed_colour)
for page in pages for page in pages
) )
await menu(ctx, pages, DEFAULT_CONTROLS) await menu(ctx, pages)
@command_audioset_perms_blacklist.command(name="clear") @command_audioset_perms_blacklist.command(name="clear")
async def command_audioset_perms_blacklist_clear(self, ctx: commands.Context): async def command_audioset_perms_blacklist_clear(self, ctx: commands.Context):

View File

@ -10,7 +10,7 @@ from red_commons.logging import getLogger
from redbot.core import commands from redbot.core import commands
from redbot.core.i18n import Translator from redbot.core.i18n import Translator
from redbot.core.utils.chat_formatting import box, humanize_number, pagify 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 redbot.core.utils.predicates import MessagePredicate, ReactionPredicate
from ...equalizer import Equalizer 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())))) text=_("{num} preset(s)").format(num=humanize_number(len(list(eq_presets.keys()))))
) )
page_list.append(embed) page_list.append(embed)
await menu(ctx, page_list, DEFAULT_CONTROLS) await menu(ctx, page_list)
@command_equalizer.command(name="load") @command_equalizer.command(name="load")
async def command_equalizer_load(self, ctx: commands.Context, eq_preset: str): async def command_equalizer_load(self, ctx: commands.Context, eq_preset: str):

View File

@ -8,7 +8,7 @@ from red_commons.logging import getLogger
from redbot.core import commands from redbot.core import commands
from redbot.core.i18n import Translator 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 ...audio_dataclasses import LocalPath, Query
from ..abc import MixinMeta from ..abc import MixinMeta
@ -113,7 +113,7 @@ class LocalTrackCommands(MixinMeta, metaclass=CompositeMetaClass):
dj_enabled = await self.config.guild(ctx.guild).dj_enabled() dj_enabled = await self.config.guild(ctx.guild).dj_enabled()
if dj_enabled and not await self._can_instaskip(ctx, ctx.author): 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: else:
await menu(ctx, folder_page_list, local_folder_controls) await menu(ctx, folder_page_list, local_folder_controls)

View File

@ -12,7 +12,7 @@ from redbot.core import commands
from redbot.core.i18n import Translator from redbot.core.i18n import Translator
from redbot.core.utils import AsyncIter from redbot.core.utils import AsyncIter
from redbot.core.utils.chat_formatting import humanize_number, pagify 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 ..abc import MixinMeta
from ..cog_utils import CompositeMetaClass from ..cog_utils import CompositeMetaClass
@ -92,7 +92,7 @@ class MiscellaneousCommands(MixinMeta, metaclass=CompositeMetaClass):
pages += 1 pages += 1
servers_embed.append(em) servers_embed.append(em)
await menu(ctx, servers_embed, DEFAULT_CONTROLS) await menu(ctx, servers_embed)
@commands.command(name="percent") @commands.command(name="percent")
@commands.guild_only() @commands.guild_only()

View File

@ -15,7 +15,7 @@ from redbot.core import commands
from redbot.core.commands import UserInputOptional from redbot.core.commands import UserInputOptional
from redbot.core.i18n import Translator from redbot.core.i18n import Translator
from redbot.core.utils import AsyncIter 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 ...audio_dataclasses import _PARTIALLY_SUPPORTED_MUSIC_EXT, Query
from ...errors import ( from ...errors import (
@ -930,6 +930,6 @@ class PlayerCommands(MixinMeta, metaclass=CompositeMetaClass):
search_page_list.append(embed) search_page_list.append(embed)
if dj_enabled and not can_skip: 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) await menu(ctx, search_page_list, search_controls)

View File

@ -19,7 +19,7 @@ from redbot.core.data_manager import cog_data_path
from redbot.core.i18n import Translator from redbot.core.i18n import Translator
from redbot.core.utils import AsyncIter from redbot.core.utils import AsyncIter
from redbot.core.utils.chat_formatting import bold, pagify 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 redbot.core.utils.predicates import MessagePredicate
from ...apis.api_utils import FakePlaylist from ...apis.api_utils import FakePlaylist
@ -899,7 +899,7 @@ class PlaylistCommands(MixinMeta, metaclass=CompositeMetaClass):
) )
) )
page_list.append(embed) page_list.append(embed)
await menu(ctx, page_list, DEFAULT_CONTROLS) await menu(ctx, page_list)
@commands.cooldown(1, 15, commands.BucketType.guild) @commands.cooldown(1, 15, commands.BucketType.guild)
@command_playlist.command(name="list", usage="[args]", cooldown_after_parsing=True) @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)): 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) embed = await self._build_playlist_list_page(ctx, page_num, abc_names, name)
playlist_embeds.append(embed) playlist_embeds.append(embed)
await menu(ctx, playlist_embeds, DEFAULT_CONTROLS) await menu(ctx, playlist_embeds)
@command_playlist.command(name="queue", usage="<name> [args]", cooldown_after_parsing=True) @command_playlist.command(name="queue", usage="<name> [args]", cooldown_after_parsing=True)
@commands.cooldown(1, 300, commands.BucketType.member) @commands.cooldown(1, 300, commands.BucketType.member)
@ -1742,7 +1742,7 @@ class PlaylistCommands(MixinMeta, metaclass=CompositeMetaClass):
), ),
) )
if embeds: if embeds:
await menu(ctx, embeds, DEFAULT_CONTROLS) await menu(ctx, embeds)
@command_playlist.command(name="upload", usage="[args]") @command_playlist.command(name="upload", usage="[args]")
@commands.is_owner() @commands.is_owner()

View File

@ -14,7 +14,6 @@ from redbot.core import commands
from redbot.core.i18n import Translator from redbot.core.i18n import Translator
from redbot.core.utils import AsyncIter from redbot.core.utils import AsyncIter
from redbot.core.utils.menus import ( from redbot.core.utils.menus import (
DEFAULT_CONTROLS,
close_menu, close_menu,
menu, menu,
next_page, next_page,
@ -304,7 +303,7 @@ class QueueCommands(MixinMeta, metaclass=CompositeMetaClass):
async for page_num in AsyncIter(range(1, len_search_pages + 1)): async for page_num in AsyncIter(range(1, len_search_pages + 1)):
embed = await self._build_queue_search_page(ctx, page_num, search_list) embed = await self._build_queue_search_page(ctx, page_num, search_list)
search_page_list.append(embed) search_page_list.append(embed)
await menu(ctx, search_page_list, DEFAULT_CONTROLS) await menu(ctx, search_page_list)
@command_queue.command(name="shuffle") @command_queue.command(name="shuffle")
@commands.cooldown(1, 30, commands.BucketType.guild) @commands.cooldown(1, 30, commands.BucketType.guild)

View File

@ -309,7 +309,7 @@ class CustomCommands(commands.Cog):
if len(msg) > 2000: if len(msg) > 2000:
msg = f"{msg[:1997]}..." msg = f"{msg[:1997]}..."
msglist.append(msg) msglist.append(msg)
await menus.menu(ctx, msglist, menus.DEFAULT_CONTROLS) await menus.menu(ctx, msglist)
@customcom.command(name="search") @customcom.command(name="search")
@commands.guild_only() @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.set_footer(text=_("Page {num}/{total}").format(num=idx, total=len(pages)))
embed_pages.append(embed) embed_pages.append(embed)
await menus.menu(ctx, embed_pages, menus.DEFAULT_CONTROLS) await menus.menu(ctx, embed_pages)
else: else:
content = "\n".join(map("{0[0]:<12} : {0[1]}".format, results)) content = "\n".join(map("{0[0]:<12} : {0[1]}".format, results))
pages = list(map(box, pagify(content, page_length=2000, shorten_by=10))) 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") @customcom.command(name="show")
async def cc_show(self, ctx, command_name: str): async def cc_show(self, ctx, command_name: str):

View File

@ -14,7 +14,7 @@ from redbot.core.bot import Red
from redbot.core.i18n import Translator, cog_i18n from redbot.core.i18n import Translator, cog_i18n
from redbot.core.utils import AsyncIter from redbot.core.utils import AsyncIter
from redbot.core.utils.chat_formatting import box, humanize_number 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 from .converters import positive_int
T_ = Translator("Economy", __file__) T_ = Translator("Economy", __file__)
@ -516,11 +516,7 @@ class Economy(commands.Cog):
highscores.append(box(temp_msg, lang="md")) highscores.append(box(temp_msg, lang="md"))
if highscores: if highscores:
await menu( await menu(ctx, highscores)
ctx,
highscores,
DEFAULT_CONTROLS if len(highscores) > 1 else {"\N{CROSS MARK}": close_menu},
)
else: else:
await ctx.send(_("No balances found.")) await ctx.send(_("No balances found."))

View File

@ -9,7 +9,7 @@ import discord
from redbot.core import commands from redbot.core import commands
from redbot.core.bot import Red from redbot.core.bot import Red
from redbot.core.i18n import Translator, cog_i18n 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 ( from redbot.core.utils.chat_formatting import (
bold, bold,
escape, escape,
@ -511,7 +511,6 @@ class General(commands.Cog):
await menu( await menu(
ctx, ctx,
pages=embeds, pages=embeds,
controls=DEFAULT_CONTROLS,
message=None, message=None,
page=0, page=0,
timeout=30, timeout=30,
@ -537,7 +536,6 @@ class General(commands.Cog):
await menu( await menu(
ctx, ctx,
pages=messages, pages=messages,
controls=DEFAULT_CONTROLS,
message=None, message=None,
page=0, page=0,
timeout=30, timeout=30,

View File

@ -9,7 +9,7 @@ from redbot.core import checks, commands, modlog
from redbot.core.bot import Red from redbot.core.bot import Red
from redbot.core.i18n import Translator, cog_i18n from redbot.core.i18n import Translator, cog_i18n
from redbot.core.utils.chat_formatting import bold, box, pagify 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 from redbot.core.utils.predicates import MessagePredicate
_ = Translator("ModLog", __file__) _ = Translator("ModLog", __file__)
@ -84,7 +84,7 @@ class ModLog(commands.Cog):
) )
rendered_cases.append(message) rendered_cases.append(message)
await menu(ctx, rendered_cases, DEFAULT_CONTROLS) await menu(ctx, rendered_cases)
@commands.command() @commands.command()
@commands.guild_only() @commands.guild_only()
@ -119,7 +119,7 @@ class ModLog(commands.Cog):
) )
for page in pagify(message, ["\n\n", "\n"], priority=True): for page in pagify(message, ["\n\n", "\n"], priority=True):
rendered_cases.append(page) rendered_cases.append(page)
await menu(ctx, rendered_cases, DEFAULT_CONTROLS) await menu(ctx, rendered_cases)
@commands.command() @commands.command()
@commands.guild_only() @commands.guild_only()

View File

@ -19,7 +19,7 @@ from redbot.core.commands import UserInputOptional
from redbot.core.i18n import Translator, cog_i18n from redbot.core.i18n import Translator, cog_i18n
from redbot.core.utils import AsyncIter from redbot.core.utils import AsyncIter
from redbot.core.utils.chat_formatting import warning, pagify 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__) _ = Translator("Warnings", __file__)
@ -324,7 +324,7 @@ class Warnings(commands.Cog):
).format(reason_name=r, **v) ).format(reason_name=r, **v)
) )
if msg_list: if msg_list:
await menu(ctx, msg_list, DEFAULT_CONTROLS) await menu(ctx, msg_list)
else: else:
await ctx.send(_("There are no reasons configured!")) await ctx.send(_("There are no reasons configured!"))
@ -359,7 +359,7 @@ class Warnings(commands.Cog):
).format(**r) ).format(**r)
) )
if msg_list: if msg_list:
await menu(ctx, msg_list, DEFAULT_CONTROLS) await menu(ctx, msg_list)
else: else:
await ctx.send(_("There are no actions configured!")) await ctx.send(_("There are no actions configured!"))

View File

@ -18,7 +18,7 @@ import pip
import traceback import traceback
from pathlib import Path from pathlib import Path
from redbot.core import data_manager 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 redbot.core.commands import GuildConverter, RawUserIdConverter
from string import ascii_letters, digits from string import ascii_letters, digits
from typing import TYPE_CHECKING, Union, Tuple, List, Optional, Iterable, Sequence, Dict, Set 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: if len(pages) == 1:
await ctx.send(pages[0]) await ctx.send(pages[0])
else: else:
await menu(ctx, pages, DEFAULT_CONTROLS) await menu(ctx, pages)
@commands.command(require_var_positional=True) @commands.command(require_var_positional=True)
@checks.is_owner() @checks.is_owner()

View File

@ -6,7 +6,7 @@ import asyncio
import contextlib import contextlib
import functools import functools
from types import MappingProxyType 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 import discord
@ -22,7 +22,7 @@ _ControlCallable = Callable[[commands.Context, _PageList, discord.Message, int,
async def menu( async def menu(
ctx: commands.Context, ctx: commands.Context,
pages: _PageList, pages: _PageList,
controls: Mapping[str, _ControlCallable], controls: Optional[Mapping[str, _ControlCallable]] = None,
message: discord.Message = None, message: discord.Message = None,
page: int = 0, page: int = 0,
timeout: float = 30.0, timeout: float = 30.0,
@ -45,10 +45,12 @@ async def menu(
The command context The command context
pages: `list` of `str` or `discord.Embed` pages: `list` of `str` or `discord.Embed`
The pages of the menu. 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 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 emoji. The signature of the function should be the same as of this function
and should additionally accept an ``emoji`` parameter of type `str`. 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 message: discord.Message
The message representing the menu. Usually :code:`None` when first opening The message representing the menu. Usually :code:`None` when first opening
the menu the menu
@ -68,6 +70,11 @@ async def menu(
isinstance(x, str) for x in pages isinstance(x, str) for x in pages
): ):
raise RuntimeError("All pages must be of the same type") 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(): for key, value in controls.items():
maybe_coro = value maybe_coro = value
if isinstance(value, functools.partial): if isinstance(value, functools.partial):
@ -144,6 +151,10 @@ async def next_page(
timeout: float, timeout: float,
emoji: str, emoji: str,
) -> _T: ) -> _T:
"""
Function for showing next page which is suitable
for use in ``controls`` mapping that is passed to `menu()`.
"""
if page == len(pages) - 1: if page == len(pages) - 1:
page = 0 # Loop around to the first item page = 0 # Loop around to the first item
else: else:
@ -160,6 +171,10 @@ async def prev_page(
timeout: float, timeout: float,
emoji: str, emoji: str,
) -> _T: ) -> _T:
"""
Function for showing previous page which is suitable
for use in ``controls`` mapping that is passed to `menu()`.
"""
if page == 0: if page == 0:
page = len(pages) - 1 # Loop around to the last item page = len(pages) - 1 # Loop around to the last item
else: else:
@ -176,6 +191,10 @@ async def close_menu(
timeout: float, timeout: float,
emoji: str, emoji: str,
) -> None: ) -> None:
"""
Function for closing (deleting) menu which is suitable
for use in ``controls`` mapping that is passed to `menu()`.
"""
with contextlib.suppress(discord.NotFound): with contextlib.suppress(discord.NotFound):
await message.delete() await message.delete()
@ -191,7 +210,7 @@ def start_adding_reactions(
This is particularly useful if you wish to start waiting for a This is particularly useful if you wish to start waiting for a
reaction whilst the reactions are still being added - in fact, 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 Parameters
---------- ----------
@ -216,6 +235,8 @@ def start_adding_reactions(
return asyncio.create_task(task()) 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( DEFAULT_CONTROLS: Mapping[str, _ControlCallable] = MappingProxyType(
{ {
"\N{LEFTWARDS BLACK ARROW}\N{VARIATION SELECTOR-16}": prev_page, "\N{LEFTWARDS BLACK ARROW}\N{VARIATION SELECTOR-16}": prev_page,