aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--bot/cogs/help.py264
-rw-r--r--bot/cogs/watchchannels/__init__.py5
-rw-r--r--bot/cogs/watchchannels/bigbrother.py4
-rw-r--r--bot/cogs/watchchannels/talentpool.py6
-rw-r--r--bot/cogs/watchchannels/watchchannel.py9
-rw-r--r--bot/converters.py73
-rw-r--r--bot/decorators.py57
-rw-r--r--bot/pagination.py126
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,