diff options
Diffstat (limited to '')
| -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): | 
