aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--bot/bot.py14
-rw-r--r--bot/decorators.py10
-rw-r--r--bot/pagination.py75
-rw-r--r--bot/seasons/season.py102
4 files changed, 95 insertions, 106 deletions
diff --git a/bot/bot.py b/bot/bot.py
index 3cc57c3f..2885379c 100644
--- a/bot/bot.py
+++ b/bot/bot.py
@@ -16,6 +16,8 @@ __all__ = ('SeasonalBot',)
class SeasonalBot(Bot):
+ """Base bot instance."""
+
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.http_session = ClientSession(
@@ -26,9 +28,7 @@ class SeasonalBot(Bot):
)
def load_extensions(self, exts: List[str]):
- """
- Unload all current cogs, then load in the ones passed into `cogs`
- """
+ """Unload all current cogs, then load in the ones passed into `cogs`."""
# Unload all cogs
extensions = list(self.extensions.keys())
@@ -46,9 +46,8 @@ class SeasonalBot(Bot):
log.error(f'Failed to load extension {cog}: {repr(e)} {format_exc()}')
async def send_log(self, title: str, details: str = None, *, icon: str = None):
- """
- Send an embed message to the devlog channel
- """
+ """Send an embed message to the devlog channel."""
+
devlog = self.get_channel(constants.Channels.devlog)
if not devlog:
@@ -64,7 +63,8 @@ class SeasonalBot(Bot):
await devlog.send(embed=embed)
async def on_command_error(self, context, exception):
- # Don't punish the user for getting the arguments wrong
+ """Check command errors for UserInputError and reset the cooldown if thrown."""
+
if isinstance(exception, commands.UserInputError):
context.command.reset_cooldown(context)
else:
diff --git a/bot/decorators.py b/bot/decorators.py
index f5ffadf4..15f7fed2 100644
--- a/bot/decorators.py
+++ b/bot/decorators.py
@@ -14,6 +14,8 @@ log = logging.getLogger(__name__)
def with_role(*role_ids: int):
+ """Check to see whether the invoking user has any of the roles specified in role_ids."""
+
async def predicate(ctx: Context):
if not ctx.guild: # Return False in a DM
log.debug(f"{ctx.author} tried to use the '{ctx.command.name}'command from a DM. "
@@ -32,6 +34,8 @@ def with_role(*role_ids: int):
def without_role(*role_ids: int):
+ """Check whether the invoking user does not have all of the roles specified in role_ids."""
+
async def predicate(ctx: Context):
if not ctx.guild: # Return False in a DM
log.debug(f"{ctx.author} tried to use the '{ctx.command.name}' command from a DM. "
@@ -47,6 +51,8 @@ def without_role(*role_ids: int):
def in_channel(channel_id):
+ """Check that the command invocation is in the channel specified by channel_id."""
+
async def predicate(ctx: Context):
check = ctx.channel.id == channel_id
log.debug(f"{ctx.author} tried to call the '{ctx.command.name}' command. "
@@ -58,8 +64,8 @@ def in_channel(channel_id):
def locked():
"""
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.
"""
diff --git a/bot/pagination.py b/bot/pagination.py
index 0ad5b81f..3916809d 100644
--- a/bot/pagination.py
+++ b/bot/pagination.py
@@ -18,6 +18,8 @@ log = logging.getLogger(__name__)
class EmptyPaginatorEmbed(Exception):
+ """Base Exception class for an empty paginator embed."""
+
pass
@@ -37,14 +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='```', suffix='```', max_size=2000, max_lines=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.
+ Overrides the Paginator.__init__ from inside discord.ext.commands.
+
+ Allows for configuration of the maximum number of lines per page.
"""
+
self.prefix = prefix
self.suffix = suffix
self.max_size = max_size - len(suffix)
@@ -55,15 +56,13 @@ class LinePaginator(Paginator):
self._pages = []
def add_line(self, line='', *, empty=False):
- """Adds a line to the current page.
+ """
+ Adds a line to the current page.
- If the line exceeds the :attr:`max_size` then an exception
- is raised.
+ If the line exceeds the :attr:`max_size` then an exception is raised.
- 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.
+ Overrides the Paginator.add_line from inside discord.ext.commands in order to allow
+ configuration of the maximum number of lines per page.
Parameters
-----------
@@ -77,6 +76,7 @@ class LinePaginator(Paginator):
RuntimeError
The line was too big for the current :attr:`max_size`.
"""
+
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))
@@ -98,21 +98,26 @@ class LinePaginator(Paginator):
@classmethod
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):
+ 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):
"""
- 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).
+ 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).
+
>>> 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
@@ -129,9 +134,7 @@ class LinePaginator(Paginator):
"""
def event_check(reaction_: Reaction, user_: Member):
- """
- Make sure that this reaction is what we want to operate on
- """
+ """Make sure that this reaction is what we want to operate on."""
no_restrictions = (
# Pagination is not restricted
@@ -301,6 +304,7 @@ 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.
@@ -314,7 +318,8 @@ class ImagePaginator(Paginator):
def add_line(self, line: str = '', *, empty: bool = False) -> None:
"""
- Adds a line to each page, usually just 1 line in this context
+ 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
"""
@@ -328,7 +333,8 @@ class ImagePaginator(Paginator):
def add_image(self, image: str = None) -> None:
"""
- Adds an image to a page
+ Adds an image to a page.
+
:param image: image url to be appended
"""
@@ -339,16 +345,14 @@ 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.
- 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.
+ The reactions are used to switch page, or to finish with pagination.
- Note: Pagination will be removed automatically
- if no reaction is added for five minutes (300 seconds).
+ 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).
>>> embed = Embed()
>>> embed.set_author(name="Some Operation", url=url, icon_url=icon)
@@ -366,7 +370,8 @@ class ImagePaginator(Paginator):
def check_event(reaction_: Reaction, member: Member) -> bool:
"""
- Checks each reaction added, if it matches our conditions pass the wait_for
+ Checks each reaction added, if it matches our conditions pass the wait_for.
+
:param reaction_: reaction added
:param member: reaction added by member
"""
diff --git a/bot/seasons/season.py b/bot/seasons/season.py
index ae12057f..9dac51e2 100644
--- a/bot/seasons/season.py
+++ b/bot/seasons/season.py
@@ -19,9 +19,7 @@ log = logging.getLogger(__name__)
def get_seasons() -> List[str]:
- """
- Returns all the Season objects located in bot/seasons/
- """
+ """Returns all the Season objects located in /bot/seasons/."""
seasons = []
@@ -32,9 +30,7 @@ def get_seasons() -> List[str]:
def get_season_class(season_name: str) -> Type["SeasonBase"]:
- """
- Get's the season class of the season module.
- """
+ """Gets the season class of the season module."""
season_lib = importlib.import_module(f"bot.seasons.{season_name}")
class_name = season_name.replace("_", " ").title().replace(" ", "")
@@ -42,9 +38,7 @@ def get_season_class(season_name: str) -> Type["SeasonBase"]:
def get_season(season_name: str = None, date: datetime.datetime = None) -> "SeasonBase":
- """
- Returns a Season object based on either a string or a date.
- """
+ """Returns a Season object based on either a string or a date."""
# If either both or neither are set, raise an error.
if not bool(season_name) ^ bool(date):
@@ -78,9 +72,7 @@ def get_season(season_name: str = None, date: datetime.datetime = None) -> "Seas
class SeasonBase:
- """
- Base class for Seasonal classes.
- """
+ """Base class for Seasonal classes."""
name: Optional[str] = "evergreen"
bot_name: str = "SeasonalBot"
@@ -96,9 +88,7 @@ class SeasonBase:
@staticmethod
def current_year() -> int:
- """
- Returns the current year.
- """
+ """Returns the current year."""
return datetime.date.today().year
@@ -107,8 +97,7 @@ class SeasonBase:
"""
Returns the start date using current year and start_date attribute.
- If no start_date was defined, returns the minimum datetime to ensure
- it's always below checked dates.
+ If no start_date was defined, returns the minimum datetime to ensure it's always below checked dates.
"""
if not cls.start_date:
@@ -120,8 +109,7 @@ class SeasonBase:
"""
Returns the start date using current year and end_date attribute.
- If no end_date was defined, returns the minimum datetime to ensure
- it's always above checked dates.
+ If no end_date was defined, returns the minimum datetime to ensure it's always above checked dates.
"""
if not cls.end_date:
@@ -130,37 +118,36 @@ class SeasonBase:
@classmethod
def is_between_dates(cls, date: datetime.datetime) -> bool:
- """
- Determines if the given date falls between the season's date range.
- """
+ """Determines if the given date falls between the season's date range."""
return cls.start() <= date <= cls.end()
@property
def name_clean(self) -> str:
+ """Return the Season's name with underscores replaced by whitespace."""
+
return self.name.replace("_", " ").title()
@property
def greeting(self) -> str:
"""
- Provides a default greeting based on the season name if one wasn't
- defined in the season class.
+ Provides a default greeting based on the season name if one wasn't defined in the season class.
- It's recommended to define one in most cases by overwriting this as a
- normal attribute in the inhertiting class.
+ It's recommended to define one in most cases by overwriting this as a normal attribute in the
+ inheriting class.
"""
return f"New Season, {self.name_clean}!"
async def get_icon(self, avatar: bool = False) -> bytes:
"""
- Retrieves the icon image from the branding repository, using the
- defined icon attribute for the season. If `avatar` is True, uses
- optional bot-only avatar icon if present.
+ Retrieve the season's icon from the branding repository using the Season's icon attribute.
+
+ If `avatar` is True, uses optional bot-only avatar icon if present.
- The icon attribute must provide the url path, starting from the master
- branch base url, including the starting slash:
- `https://raw.githubusercontent.com/python-discord/branding/master`
+ The icon attribute must provide the url path, starting from the master branch base url,
+ including the starting slash.
+ e.g. `/logos/logo_seasonal/valentines/loved_up.png`
"""
base_url = "https://raw.githubusercontent.com/python-discord/branding/master"
@@ -175,8 +162,9 @@ class SeasonBase:
async def apply_username(self, *, debug: bool = False) -> Union[bool, None]:
"""
- Applies the username for the current season. Only changes nickname if
- `bool` is False, otherwise only changes the nickname.
+ Applies the username for the current season.
+
+ Only changes nickname if `bool` is False, otherwise only changes the nickname.
Returns True if it successfully changed the username.
Returns False if it failed to change the username, falling back to nick.
@@ -216,7 +204,9 @@ class SeasonBase:
async def apply_avatar(self) -> bool:
"""
- Applies the avatar for the current season. Returns if it was successful.
+ Applies the avatar for the current season.
+
+ Returns True if successful.
"""
# track old avatar hash for later comparison
@@ -238,7 +228,9 @@ class SeasonBase:
async def apply_server_icon(self) -> bool:
"""
- Applies the server icon for the current season. Returns if it was successful.
+ Applies the server icon for the current season.
+
+ Returns True if was successful.
"""
guild = bot.get_guild(Client.guild)
@@ -265,8 +257,7 @@ class SeasonBase:
"""
Announces a change in season in the announcement channel.
- It will skip the announcement if the current active season is the
- "evergreen" default season.
+ It will skip the announcement if the current active season is the "evergreen" default season.
"""
# don't actually announce if reverting to normal season
@@ -353,9 +344,7 @@ class SeasonBase:
class SeasonManager:
- """
- A cog for managing seasons.
- """
+ """A cog for managing seasons."""
def __init__(self, bot):
self.bot = bot
@@ -375,6 +364,8 @@ class SeasonManager:
self.sleep_time = (midnight - datetime.datetime.now()).seconds + 60
async def load_seasons(self):
+ """Asynchronous timer loop to check for a new season every midnight."""
+
await self.bot.wait_until_ready()
await self.season.load()
@@ -390,9 +381,7 @@ class SeasonManager:
@with_role(Roles.moderator, Roles.admin, Roles.owner)
@commands.command(name="season")
async def change_season(self, ctx, new_season: str):
- """
- Changes the currently active season on the bot.
- """
+ """Changes the currently active season on the bot."""
self.season = get_season(season_name=new_season)
await self.season.load()
@@ -401,9 +390,7 @@ class SeasonManager:
@with_role(Roles.moderator, Roles.admin, Roles.owner)
@commands.command(name="seasons")
async def show_seasons(self, ctx):
- """
- Shows the available seasons and their dates.
- """
+ """Shows the available seasons and their dates."""
# sort by start order, followed by lower duration
def season_key(season_class: Type[SeasonBase]):
@@ -447,17 +434,13 @@ class SeasonManager:
@with_role(Roles.moderator, Roles.admin, Roles.owner)
@commands.group()
async def refresh(self, ctx):
- """
- Refreshes certain seasonal elements without reloading seasons.
- """
+ """Refreshes certain seasonal elements without reloading seasons."""
if not ctx.invoked_subcommand:
await ctx.invoke(bot.get_command("help"), "refresh")
@refresh.command(name="avatar")
async def refresh_avatar(self, ctx):
- """
- Re-applies the bot avatar for the currently loaded season.
- """
+ """Re-applies the bot avatar for the currently loaded season."""
# attempt the change
is_changed = await self.season.apply_avatar()
@@ -481,9 +464,7 @@ class SeasonManager:
@refresh.command(name="icon")
async def refresh_server_icon(self, ctx):
- """
- Re-applies the server icon for the currently loaded season.
- """
+ """Re-applies the server icon for the currently loaded season."""
# attempt the change
is_changed = await self.season.apply_server_icon()
@@ -507,9 +488,7 @@ class SeasonManager:
@refresh.command(name="username", aliases=("name",))
async def refresh_username(self, ctx):
- """
- Re-applies the bot username for the currently loaded season.
- """
+ """Re-applies the bot username for the currently loaded season."""
old_username = str(bot.user)
old_display_name = ctx.guild.me.display_name
@@ -549,9 +528,8 @@ class SeasonManager:
@with_role(Roles.moderator, Roles.admin, Roles.owner)
@commands.command()
async def announce(self, ctx):
- """
- Announces the currently loaded season.
- """
+ """Announces the currently loaded season."""
+
await self.season.announce_season()
def __unload(self):