diff options
-rw-r--r-- | bot/cogs/help.py | 264 | ||||
-rw-r--r-- | bot/cogs/watchchannels/__init__.py | 5 | ||||
-rw-r--r-- | bot/cogs/watchchannels/bigbrother.py | 4 | ||||
-rw-r--r-- | bot/cogs/watchchannels/talentpool.py | 6 | ||||
-rw-r--r-- | bot/cogs/watchchannels/watchchannel.py | 9 | ||||
-rw-r--r-- | bot/converters.py | 73 | ||||
-rw-r--r-- | bot/decorators.py | 57 | ||||
-rw-r--r-- | bot/pagination.py | 126 |
8 files changed, 187 insertions, 357 deletions
diff --git a/bot/cogs/help.py b/bot/cogs/help.py index 20ed08f07..68c59d326 100644 --- a/bot/cogs/help.py +++ b/bot/cogs/help.py @@ -3,10 +3,11 @@ import inspect import itertools from collections import namedtuple from contextlib import suppress +from typing import Union -from discord import Colour, Embed, HTTPException +from discord import Colour, Embed, HTTPException, Message, Reaction, User from discord.ext import commands -from discord.ext.commands import CheckFailure +from discord.ext.commands import Bot, CheckFailure, Command, Context from fuzzywuzzy import fuzz, process from bot import constants @@ -43,7 +44,7 @@ class HelpQueryNotFound(ValueError): The likeness match scores are the values. """ - def __init__(self, arg, possible_matches=None): + def __init__(self, arg: str, possible_matches: dict = None): super().__init__(arg) self.possible_matches = possible_matches @@ -68,7 +69,10 @@ class HelpSession: Where the help message is to be sent to. """ - def __init__(self, ctx, *command, cleanup=False, only_can_run=True, show_hidden=False, max_lines=15): + def __init__( + self, ctx: Context, *command, cleanup: bool = False, only_can_run: bool = True, + show_hidden: bool = False, max_lines: int = 15 + ): """ Creates an instance of the HelpSession class. @@ -93,7 +97,6 @@ class HelpSession: single page. Defaults to 20. """ - self._ctx = ctx self._bot = ctx.bot self.title = "Command Help" @@ -122,20 +125,8 @@ class HelpSession: self._timeout_task = None self.reset_timeout() - def _get_query(self, query): - """ - Attempts to match the provided query with a valid command or cog. - - Parameters - ---------- - query: str - The joined string representing the session query. - - Returns - ------- - Union[:class:`discord.ext.commands.Command`, :class:`Cog`] - """ - + def _get_query(self, query: str) -> Union[Command, Cog]: + """Attempts to match the provided query with a valid command or cog.""" command = self._bot.get_command(query) if command: return command @@ -150,12 +141,11 @@ class HelpSession: self._handle_not_found(query) - def _handle_not_found(self, query): + def _handle_not_found(self, query: str) -> None: """ Handles when a query does not match a valid command or cog. - Will pass on possible close matches along with the - ``HelpQueryNotFound`` exception. + Will pass on possible close matches along with the ``HelpQueryNotFound`` exception. Parameters ---------- @@ -166,7 +156,6 @@ class HelpSession: ------ HelpQueryNotFound """ - # combine command and cog names choices = list(self._bot.all_commands) + list(self._bot.cogs) @@ -174,7 +163,7 @@ class HelpSession: raise HelpQueryNotFound(f'Query "{query}" not found.', dict(result)) - async def timeout(self, seconds=30): + async def timeout(self, seconds: int = 30) -> None: """ Waits for a set number of seconds, then stops the help session. @@ -183,15 +172,11 @@ class HelpSession: seconds: int Number of seconds to wait. """ - await asyncio.sleep(seconds) await self.stop() - def reset_timeout(self): - """ - Cancels the original timeout task and sets it again from the start. - """ - + def reset_timeout(self) -> None: + """Cancels the original timeout task and sets it again from the start.""" # cancel original if it exists if self._timeout_task: if not self._timeout_task.cancelled(): @@ -200,7 +185,7 @@ class HelpSession: # recreate the timeout task self._timeout_task = self._bot.loop.create_task(self.timeout()) - async def on_reaction_add(self, reaction, user): + async def on_reaction_add(self, reaction: Reaction, user: User) -> None: """ Event handler for when reactions are added on the help message. @@ -211,7 +196,6 @@ class HelpSession: user: :class:`discord.User` The user who added the reaction. """ - # ensure it was the relevant session message if reaction.message.id != self.message.id: return @@ -237,24 +221,13 @@ class HelpSession: with suppress(HTTPException): await self.message.remove_reaction(reaction, user) - async def on_message_delete(self, message): - """ - Closes the help session when the help message is deleted. - - Parameters - ---------- - message: :class:`discord.Message` - The message that was deleted. - """ - + async def on_message_delete(self, message: Message) -> None: + """Closes the help session when the help message is deleted.""" if message.id == self.message.id: await self.stop() - async def prepare(self): - """ - Sets up the help session pages, events, message and reactions. - """ - + async def prepare(self) -> None: + """Sets up the help session pages, events, message and reactions.""" # create paginated content await self.build_pages() @@ -266,12 +239,8 @@ class HelpSession: await self.update_page() self.add_reactions() - def add_reactions(self): - """ - Adds the relevant reactions to the help message based on if - pagination is required. - """ - + def add_reactions(self) -> None: + """Adds the relevant reactions to the help message based on if pagination is required.""" # if paginating if len(self._pages) > 1: for reaction in REACTIONS: @@ -281,44 +250,22 @@ class HelpSession: else: self._bot.loop.create_task(self.message.add_reaction(DELETE_EMOJI)) - def _category_key(self, cmd): + def _category_key(self, cmd: Command) -> str: """ - Returns a cog name of a given command. Used as a key for - ``sorted`` and ``groupby``. - - A zero width space is used as a prefix for results with no cogs - to force them last in ordering. - - Parameters - ---------- - cmd: :class:`discord.ext.commands.Command` - The command object being checked. + Returns a cog name of a given command for use as a key for ``sorted`` and ``groupby``. - Returns - ------- - str + A zero width space is used as a prefix for results with no cogs to force them last in ordering. """ - cog = cmd.cog_name return f'**{cog}**' if cog else f'**\u200bNo Category:**' - def _get_command_params(self, cmd): + def _get_command_params(self, cmd: Command) -> str: """ Returns the command usage signature. - This is a custom implementation of ``command.signature`` in - order to format the command signature without aliases. - - Parameters - ---------- - cmd: :class:`discord.ext.commands.Command` - The command object to get the parameters of. - - Returns - ------- - str + This is a custom implementation of ``command.signature`` in order to format the command + signature without aliases. """ - results = [] for name, param in cmd.clean_params.items(): @@ -346,16 +293,8 @@ class HelpSession: return f"{cmd.name} {' '.join(results)}" - async def build_pages(self): - """ - Builds the list of content pages to be paginated through in the - help message. - - Returns - ------- - list[str] - """ - + async def build_pages(self) -> None: + """Builds the list of content pages to be paginated through in the help message, as a list of str.""" # Use LinePaginator to restrict embed line height paginator = LinePaginator(prefix='', suffix='', max_lines=self._max_lines) @@ -482,20 +421,8 @@ class HelpSession: # save organised pages to session self._pages = paginator.pages - def embed_page(self, page_number=0): - """ - Returns an Embed with the requested page formatted within. - - Parameters - ---------- - page_number: int - The page to be retrieved. Zero indexed. - - Returns - ------- - :class:`discord.Embed` - """ - + def embed_page(self, page_number: int = 0) -> Embed: + """Returns an Embed with the requested page formatted within.""" embed = Embed() # if command or cog, add query to title for pages other than first @@ -514,17 +441,8 @@ class HelpSession: return embed - async def update_page(self, page_number=0): - """ - Sends the intial message, or changes the existing one to the - given page number. - - Parameters - ---------- - page_number: int - The page number to show in the help message. - """ - + async def update_page(self, page_number: int = 0) -> None: + """Sends the intial message, or changes the existing one to the given page number.""" self._current_page = page_number embed_page = self.embed_page(page_number) @@ -534,10 +452,9 @@ class HelpSession: await self.message.edit(embed=embed_page) @classmethod - async def start(cls, ctx, *command, **options): + async def start(cls, ctx: Context, *command, **options) -> "HelpSession": """ - Create and begin a help session based on the given command - context. + Create and begin a help session based on the given command context. Parameters ---------- @@ -558,23 +475,14 @@ class HelpSession: Sets the max number of lines the paginator will add to a single page. Defaults to 20. - - Returns - ------- - :class:`HelpSession` """ - session = cls(ctx, *command, **options) await session.prepare() return session - async def stop(self): - """ - Stops the help session, removes event listeners and attempts to - delete the help message. - """ - + async def stop(self) -> None: + """Stops the help session, removes event listeners and attempts to delete the help message.""" self._bot.remove_listener(self.on_reaction_add) self._bot.remove_listener(self.on_message_delete) @@ -586,80 +494,47 @@ class HelpSession: await self.message.clear_reactions() @property - def is_first_page(self): - """ - A bool reflecting if session is currently showing the first page. - - Returns - ------- - bool - """ - + def is_first_page(self) -> bool: + """Check if session is currently showing the first page.""" return self._current_page == 0 @property - def is_last_page(self): - """ - A bool reflecting if the session is currently showing the last page. - - Returns - ------- - bool - """ - + def is_last_page(self) -> bool: + """Check if the session is currently showing the last page.""" return self._current_page == (len(self._pages)-1) - async def do_first(self): - """ - Event that is called when the user requests the first page. - """ - + async def do_first(self) -> None: + """Event that is called when the user requests the first page.""" if not self.is_first_page: await self.update_page(0) - async def do_back(self): - """ - Event that is called when the user requests the previous page. - """ - + async def do_back(self) -> None: + """Event that is called when the user requests the previous page.""" if not self.is_first_page: await self.update_page(self._current_page-1) - async def do_next(self): - """ - Event that is called when the user requests the next page. - """ - + async def do_next(self) -> None: + """Event that is called when the user requests the next page.""" if not self.is_last_page: await self.update_page(self._current_page+1) - async def do_end(self): - """ - Event that is called when the user requests the last page. - """ - + async def do_end(self) -> None: + """Event that is called when the user requests the last page.""" if not self.is_last_page: await self.update_page(len(self._pages)-1) - async def do_stop(self): - """ - Event that is called when the user requests to stop the help session. - """ - + async def do_stop(self) -> None: + """Event that is called when the user requests to stop the help session.""" await self.message.delete() class Help: - """ - Custom Embed Pagination Help feature - """ + """Custom Embed Pagination Help feature.""" + @commands.command('help') @redirect_output(destination_channel=Channels.bot, bypass_roles=STAFF_ROLES) - async def new_help(self, ctx, *commands): - """ - Shows Command Help. - """ - + async def new_help(self, ctx: Context, *commands) -> None: + """Shows Command Help.""" try: await HelpSession.start(ctx, *commands) except HelpQueryNotFound as error: @@ -674,24 +549,17 @@ class Help: await ctx.send(embed=embed) -def unload(bot): +def unload(bot: Bot) -> None: """ Reinstates the original help command. - This is run if the cog raises an exception on load, or if the - extension is unloaded. - - Parameters - ---------- - bot: :class:`discord.ext.commands.Bot` - The discord bot client. + This is run if the cog raises an exception on load, or if the extension is unloaded. """ - bot.remove_command('help') bot.add_command(bot._old_help) -def setup(bot): +def setup(bot: Bot) -> None: """ The setup for the help extension. @@ -703,13 +571,7 @@ def setup(bot): If an exception is raised during the loading of the cog, ``unload`` will be called in order to reinstate the original help command. - - Parameters - ---------- - bot: `discord.ext.commands.Bot` - The discord bot client. """ - bot._old_help = bot.get_command('help') bot.remove_command('help') @@ -720,18 +582,12 @@ def setup(bot): raise -def teardown(bot): +def teardown(bot: Bot) -> None: """ The teardown for the help extension. This is called automatically on `bot.unload_extension` being run. Calls ``unload`` in order to reinstate the original help command. - - Parameters - ---------- - bot: `discord.ext.commands.Bot` - The discord bot client. """ - unload(bot) diff --git a/bot/cogs/watchchannels/__init__.py b/bot/cogs/watchchannels/__init__.py index ac7713803..86e1050fa 100644 --- a/bot/cogs/watchchannels/__init__.py +++ b/bot/cogs/watchchannels/__init__.py @@ -1,5 +1,7 @@ import logging +from discord.ext.commands import Bot + from .bigbrother import BigBrother from .talentpool import TalentPool @@ -7,7 +9,8 @@ from .talentpool import TalentPool log = logging.getLogger(__name__) -def setup(bot): +def setup(bot: Bot) -> None: + """Monitoring cogs load.""" bot.add_cog(BigBrother(bot)) log.info("Cog loaded: BigBrother") diff --git a/bot/cogs/watchchannels/bigbrother.py b/bot/cogs/watchchannels/bigbrother.py index e7b3d70bc..a4c95d8ad 100644 --- a/bot/cogs/watchchannels/bigbrother.py +++ b/bot/cogs/watchchannels/bigbrother.py @@ -3,7 +3,7 @@ from collections import ChainMap from typing import Union from discord import User -from discord.ext.commands import Context, group +from discord.ext.commands import Bot, Context, group from bot.constants import Channels, Roles, Webhooks from bot.decorators import with_role @@ -16,7 +16,7 @@ log = logging.getLogger(__name__) class BigBrother(WatchChannel): """Monitors users by relaying their messages to a watch channel to assist with moderation.""" - def __init__(self, bot) -> None: + def __init__(self, bot: Bot) -> None: super().__init__( bot, destination=Channels.big_brother_logs, diff --git a/bot/cogs/watchchannels/talentpool.py b/bot/cogs/watchchannels/talentpool.py index 47d207d05..bea0a8b0a 100644 --- a/bot/cogs/watchchannels/talentpool.py +++ b/bot/cogs/watchchannels/talentpool.py @@ -4,7 +4,7 @@ from collections import ChainMap from typing import Union from discord import Color, Embed, Member, User -from discord.ext.commands import Context, group +from discord.ext.commands import Bot, Context, group from bot.api import ResponseCodeError from bot.constants import Channels, Guild, Roles, Webhooks @@ -19,7 +19,7 @@ STAFF_ROLES = Roles.owner, Roles.admin, Roles.moderator, Roles.helpers # <- I class TalentPool(WatchChannel): """Relays messages of helper candidates to a watch channel to observe them.""" - def __init__(self, bot) -> None: + def __init__(self, bot: Bot) -> None: super().__init__( bot, destination=Channels.talent_pool, @@ -33,7 +33,6 @@ class TalentPool(WatchChannel): @with_role(Roles.owner, Roles.admin, Roles.moderator) async def nomination_group(self, ctx: Context) -> None: """Highlights the activity of helper nominees by relaying their messages to the talent pool channel.""" - await ctx.invoke(self.bot.get_command("help"), "talentpool") @nomination_group.command(name='watched', aliases=('all', 'list')) @@ -156,7 +155,6 @@ class TalentPool(WatchChannel): @with_role(Roles.owner, Roles.admin, Roles.moderator) async def nomination_edit_group(self, ctx: Context) -> None: """Commands to edit nominations.""" - await ctx.invoke(self.bot.get_command("help"), "talentpool", "edit") @nomination_edit_group.command(name='reason') diff --git a/bot/cogs/watchchannels/watchchannel.py b/bot/cogs/watchchannels/watchchannel.py index 3a24e3f21..5ca819955 100644 --- a/bot/cogs/watchchannels/watchchannel.py +++ b/bot/cogs/watchchannels/watchchannel.py @@ -42,6 +42,8 @@ def proxy_user(user_id: str) -> Object: @dataclass class MessageHistory: + """Represent the watch channel's message history.""" + last_author: Optional[int] = None last_channel: Optional[int] = None message_count: int = 0 @@ -51,7 +53,10 @@ class WatchChannel(ABC): """ABC with functionality for relaying users' messages to a certain channel.""" @abstractmethod - def __init__(self, bot: Bot, destination, webhook_id, api_endpoint, api_default_params, logger) -> None: + def __init__( + self, bot: Bot, destination: int, webhook_id: int, + api_endpoint: str, api_default_params: dict, logger: logging.Logger + ) -> None: self.bot = bot self.destination = destination # E.g., Channels.big_brother_logs @@ -271,7 +276,7 @@ class WatchChannel(ABC): self.message_history.message_count += 1 - async def send_header(self, msg) -> None: + async def send_header(self, msg: Message) -> None: """Sends a header embed with information about the relayed messages to the watch channel.""" user_id = msg.author.id diff --git a/bot/converters.py b/bot/converters.py index 30ea7ca0f..af7ecd107 100644 --- a/bot/converters.py +++ b/bot/converters.py @@ -1,6 +1,7 @@ import logging from datetime import datetime from ssl import CertificateError +from typing import Union import dateparser import discord @@ -15,17 +16,16 @@ class ValidPythonIdentifier(Converter): """ A converter that checks whether the given string is a valid Python identifier. - This is used to have package names - that correspond to how you would use - the package in your code, e.g. - `import package`. Raises `BadArgument` - if the argument is not a valid Python - identifier, and simply passes through + This is used to have package names that correspond to how you would use the package in your + code, e.g. `import package`. + + Raises `BadArgument` if the argument is not a valid Python identifier, and simply passes through the given argument otherwise. """ @staticmethod - async def convert(ctx, argument: str): + async def convert(ctx: Context, argument: str) -> str: + """Checks whether the given string is a valid Python identifier.""" if not argument.isidentifier(): raise BadArgument(f"`{argument}` is not a valid Python identifier") return argument @@ -35,14 +35,15 @@ class ValidURL(Converter): """ Represents a valid webpage URL. - This converter checks whether the given - URL can be reached and requesting it returns - a status code of 200. If not, `BadArgument` - is raised. Otherwise, it simply passes through the given URL. + This converter checks whether the given URL can be reached and requesting it returns a status + code of 200. If not, `BadArgument` is raised. + + Otherwise, it simply passes through the given URL. """ @staticmethod - async def convert(ctx, url: str): + async def convert(ctx: Context, url: str) -> str: + """This converter checks whether the given URL can be reached with a status code of 200.""" try: async with ctx.bot.http_session.get(url) as resp: if resp.status != 200: @@ -63,12 +64,11 @@ class ValidURL(Converter): class InfractionSearchQuery(Converter): - """ - A converter that checks if the argument is a Discord user, and if not, falls back to a string. - """ + """A converter that checks if the argument is a Discord user, and if not, falls back to a string.""" @staticmethod - async def convert(ctx, arg): + async def convert(ctx: Context, arg: str) -> Union[discord.Member, str]: + """Check if the argument is a Discord user, and if not, falls back to a string.""" try: maybe_snowflake = arg.strip("<@!>") return await ctx.bot.get_user_info(maybe_snowflake) @@ -77,12 +77,15 @@ class InfractionSearchQuery(Converter): class Subreddit(Converter): - """ - Forces a string to begin with "r/" and checks if it's a valid subreddit. - """ + """Forces a string to begin with "r/" and checks if it's a valid subreddit.""" @staticmethod - async def convert(ctx, sub: str): + async def convert(ctx: Context, sub: str) -> str: + """ + Force sub to begin with "r/" and check if it's a valid subreddit. + + If sub is a valid subreddit, return it prepended with "r/" + """ sub = sub.lower() if not sub.startswith("r/"): @@ -103,9 +106,21 @@ class Subreddit(Converter): class TagNameConverter(Converter): + """ + Ensure that a proposed tag name is valid. + + Valid tag names meet the following conditions: + * All ASCII characters + * Has at least one non-whitespace character + * Not solely numeric + * Shorter than 127 characters + """ + @staticmethod - async def convert(ctx: Context, tag_name: str): - def is_number(value): + async def convert(ctx: Context, tag_name: str) -> str: + """Lowercase & strip whitespace from proposed tag_name & ensure it's valid.""" + def is_number(value: str) -> bool: + """Check to see if the input string is numeric.""" try: float(value) except ValueError: @@ -142,8 +157,15 @@ class TagNameConverter(Converter): class TagContentConverter(Converter): + """Ensure proposed tag content is not empty and contains at least one non-whitespace character.""" + @staticmethod - async def convert(ctx: Context, tag_content: str): + async def convert(ctx: Context, tag_content: str) -> str: + """ + Ensure tag_content is non-empty and contains at least one non-whitespace character. + + If tag_content is valid, return the stripped version. + """ tag_content = tag_content.strip() # The tag contents should not be empty, or filled with whitespace. @@ -156,13 +178,16 @@ class TagContentConverter(Converter): class ExpirationDate(Converter): + """Convert relative expiration date into UTC datetime using dateparser.""" + DATEPARSER_SETTINGS = { 'PREFER_DATES_FROM': 'future', 'TIMEZONE': 'UTC', 'TO_TIMEZONE': 'UTC' } - async def convert(self, ctx, expiration_string: str): + async def convert(self, ctx: Context, expiration_string: str) -> datetime: + """Convert relative expiration date into UTC datetime.""" expiry = dateparser.parse(expiration_string, settings=self.DATEPARSER_SETTINGS) if expiry is None: raise BadArgument(f"Failed to parse expiration date from `{expiration_string}`") diff --git a/bot/decorators.py b/bot/decorators.py index 1ba2cd59e..3600be3bb 100644 --- a/bot/decorators.py +++ b/bot/decorators.py @@ -1,9 +1,9 @@ import logging import random -import typing from asyncio import Lock, sleep from contextlib import suppress from functools import wraps +from typing import Callable, Container, Union from weakref import WeakValueDictionary from discord import Colour, Embed @@ -18,14 +18,15 @@ log = logging.getLogger(__name__) class InChannelCheckFailure(CheckFailure): + """In channel check failure exception.""" + pass -def in_channel(*channels: int, bypass_roles: typing.Container[int] = None): - """ - Checks that the message is in a whitelisted channel or optionally has a bypass role. - """ - def predicate(ctx: Context): +def in_channel(*channels: int, bypass_roles: Container[int] = None) -> Callable: + """Checks that the message is in a whitelisted channel or optionally has a bypass role.""" + def predicate(ctx: Context) -> bool: + """In-channel checker predicate.""" if ctx.channel.id in channels: log.debug(f"{ctx.author} tried to call the '{ctx.command.name}' command. " f"The command was used in a whitelisted channel.") @@ -49,42 +50,34 @@ def in_channel(*channels: int, bypass_roles: typing.Container[int] = None): return commands.check(predicate) -def with_role(*role_ids: int): - """ - Returns True if the user has any one - of the roles in role_ids. - """ - - async def predicate(ctx: Context): +def with_role(*role_ids: int) -> Callable: + """Returns True if the user has any one of the roles in role_ids.""" + async def predicate(ctx: Context) -> bool: + """With role checker predicate.""" return with_role_check(ctx, *role_ids) return commands.check(predicate) -def without_role(*role_ids: int): - """ - Returns True if the user does not have any - of the roles in role_ids. - """ - - async def predicate(ctx: Context): +def without_role(*role_ids: int) -> Callable: + """Returns True if the user does not have any of the roles in role_ids.""" + async def predicate(ctx: Context) -> bool: return without_role_check(ctx, *role_ids) return commands.check(predicate) -def locked(): +def locked() -> Union[Callable, None]: """ Allows the user to only run one instance of the decorated command at a time. - Subsequent calls to the command from the same author are - ignored until the command has completed invocation. + + Subsequent calls to the command from the same author are ignored until the command has completed invocation. This decorator has to go before (below) the `command` decorator. """ - - def wrap(func): + def wrap(func: Callable) -> Union[Callable, None]: func.__locks = WeakValueDictionary() @wraps(func) - async def inner(self, ctx, *args, **kwargs): + async def inner(self: Callable, ctx: Context, *args, **kwargs) -> Union[Callable, None]: lock = func.__locks.setdefault(ctx.author.id, Lock()) if lock.locked(): embed = Embed() @@ -104,15 +97,15 @@ def locked(): return wrap -def redirect_output(destination_channel: int, bypass_roles: typing.Container[int] = None): - """ - Changes the channel in the context of the command to redirect the output - to a certain channel, unless the author has a role to bypass redirection +def redirect_output(destination_channel: int, bypass_roles: Container[int] = None) -> Callable: """ + Changes the channel in the context of the command to redirect the output to a certain channel. - def wrap(func): + Redirect is bypassed if the author has a role to bypass redirection. + """ + def wrap(func: Callable) -> Callable: @wraps(func) - async def inner(self, ctx, *args, **kwargs): + async def inner(self: Callable, ctx: Context, *args, **kwargs) -> Callable: if ctx.channel.id == destination_channel: log.trace(f"Command {ctx.command.name} was invoked in destination_channel, not redirecting") return await func(self, ctx, *args, **kwargs) diff --git a/bot/pagination.py b/bot/pagination.py index 0ad5b81f1..10ef6c407 100644 --- a/bot/pagination.py +++ b/bot/pagination.py @@ -18,6 +18,8 @@ log = logging.getLogger(__name__) class EmptyPaginatorEmbed(Exception): + """Empty paginator embed exception.""" + pass @@ -37,13 +39,13 @@ class LinePaginator(Paginator): The maximum amount of lines allowed in a page. """ - def __init__(self, prefix='```', suffix='```', - max_size=2000, max_lines=None): + def __init__( + self, prefix: str = '```', suffix: str = '```', max_size: int = 2000, max_lines: int = None + ) -> None: """ - This function overrides the Paginator.__init__ - from inside discord.ext.commands. - It overrides in order to allow us to configure - the maximum number of lines per page. + This function overrides the Paginator.__init__ from inside discord.ext.commands. + + It overrides in order to allow us to configure the maximum number of lines per page. """ self.prefix = prefix self.suffix = suffix @@ -54,28 +56,15 @@ class LinePaginator(Paginator): 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. + def add_line(self, line: str = '', *, empty: bool = False) -> None: + """ + Adds a line to the current page. - This function overrides the Paginator.add_line - from inside discord.ext.commands. - It overrides in order to allow us to configure - the maximum number of lines per page. + If the line exceeds the `self.max_size` then an exception is raised. - Parameters - ----------- - line: str - The line to add. - empty: bool - Indicates if another empty line should be added. + This function overrides the `Paginator.add_line` from inside `discord.ext.commands`. - Raises - ------ - RuntimeError - The line was too big for the current :attr:`max_size`. + It overrides in order to allow us to configure the maximum number of lines per page. """ 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)) @@ -100,39 +89,24 @@ class LinePaginator(Paginator): async def paginate(cls, lines: Iterable[str], ctx: Context, embed: Embed, prefix: str = "", suffix: str = "", max_lines: Optional[int] = None, max_size: int = 500, empty: bool = True, restrict_to_user: User = None, timeout: int = 300, - footer_text: str = None, url: str = None, exception_on_empty_embed: bool = False): + footer_text: str = None, url: str = None, exception_on_empty_embed: bool = False) -> None: """ - Use a paginator and set of reactions to provide pagination over a set of lines. The reactions are used to - switch page, or to finish with pagination. + Use a paginator and set of reactions to provide pagination over a set of lines. + + The reactions are used to switch page, or to finish with pagination. + When used, this will send a message using `ctx.send()` and apply a set of reactions to it. These reactions may - be used to change page, or to remove pagination from the message. Pagination will also be removed automatically - if no reaction is added for five minutes (300 seconds). + be used to change page, or to remove pagination from the message. + + Pagination will also be removed automatically if no reaction is added for five minutes (300 seconds). + + Example: >>> embed = Embed() >>> embed.set_author(name="Some Operation", url=url, icon_url=icon) - >>> await LinePaginator.paginate( - ... (line for line in lines), - ... ctx, embed - ... ) - :param lines: The lines to be paginated - :param ctx: Current context object - :param embed: A pre-configured embed to be used as a template for each page - :param prefix: Text to place before each page - :param suffix: Text to place after each page - :param max_lines: The maximum number of lines on each page - :param max_size: The maximum number of characters on each page - :param empty: Whether to place an empty line between each given line - :param restrict_to_user: A user to lock pagination operations to for this message, if supplied - :param exception_on_empty_embed: Should there be an exception if the embed is empty? - :param url: the url to use for the embed headline - :param timeout: The amount of time in seconds to disable pagination of no reaction is added - :param footer_text: Text to prefix the page number in the footer with + >>> await LinePaginator.paginate((line for line in lines), ctx, embed) """ - - def event_check(reaction_: Reaction, user_: Member): - """ - Make sure that this reaction is what we want to operate on - """ - + def event_check(reaction_: Reaction, user_: Member) -> bool: + """Make sure that this reaction is what we want to operate on.""" no_restrictions = ( # Pagination is not restricted not restrict_to_user @@ -301,24 +275,20 @@ class LinePaginator(Paginator): class ImagePaginator(Paginator): """ Helper class that paginates images for embeds in messages. + Close resemblance to LinePaginator, except focuses on images over text. Refer to ImagePaginator.paginate for documentation on how to use. """ - def __init__(self, prefix="", suffix=""): + def __init__(self, prefix: str = "", suffix: str = ""): super().__init__(prefix, suffix) self._current_page = [prefix] self.images = [] self._pages = [] def add_line(self, line: str = '', *, empty: bool = False) -> None: - """ - Adds a line to each page, usually just 1 line in this context - :param line: str to be page content / title - :param empty: if there should be new lines between entries - """ - + """Adds a line to each page.""" if line: self._count = len(line) else: @@ -327,11 +297,7 @@ class ImagePaginator(Paginator): self.close_page() def add_image(self, image: str = None) -> None: - """ - Adds an image to a page - :param image: image url to be appended - """ - + """Adds an image to a page.""" self.images.append(image) @classmethod @@ -339,38 +305,22 @@ class ImagePaginator(Paginator): prefix: str = "", suffix: str = "", timeout: int = 300, exception_on_empty_embed: bool = False): """ - Use a paginator and set of reactions to provide - pagination over a set of title/image pairs.The reactions are - used to switch page, or to finish with pagination. + Use a paginator and set of reactions to provide pagination over a set of title/image pairs. + + The reactions are used to switch page, or to finish with pagination. - When used, this will send a message using `ctx.send()` and - apply a set of reactions to it. These reactions may + When used, this will send a message using `ctx.send()` and apply a set of reactions to it. These reactions may be used to change page, or to remove pagination from the message. - Note: Pagination will be removed automatically - if no reaction is added for five minutes (300 seconds). + Note: Pagination will be removed automatically if no reaction is added for five minutes (300 seconds). + Example: >>> embed = Embed() >>> embed.set_author(name="Some Operation", url=url, icon_url=icon) >>> await ImagePaginator.paginate(pages, ctx, embed) - - Parameters - ----------- - :param pages: An iterable of tuples with title for page, and img url - :param ctx: ctx for message - :param embed: base embed to modify - :param prefix: prefix of message - :param suffix: suffix of message - :param timeout: timeout for when reactions get auto-removed """ - def check_event(reaction_: Reaction, member: Member) -> bool: - """ - Checks each reaction added, if it matches our conditions pass the wait_for - :param reaction_: reaction added - :param member: reaction added by member - """ - + """Checks each reaction added, if it matches our conditions pass the wait_for.""" return all(( # Reaction is on the same message sent reaction_.message.id == message.id, |