diff options
-rw-r--r-- | bot/bot.py | 14 | ||||
-rw-r--r-- | bot/decorators.py | 10 | ||||
-rw-r--r-- | bot/pagination.py | 75 | ||||
-rw-r--r-- | bot/seasons/season.py | 102 |
4 files changed, 95 insertions, 106 deletions
@@ -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): |