Cog-Creators / Red-DiscordBot

A multi-function Discord bot
https://docs.discord.red
GNU General Public License v3.0
4.73k stars 2.3k forks source link

[Feature] Boilerplate functions or doc for boilerplate #3629

Open Stonedestroyer opened 4 years ago

Stonedestroyer commented 4 years ago

Feature request

Select the type of feature you are requesting:

Describe your requested feature

As discussed on Discord there should be either documentation or helper functions for various things. An example is some user may wanna load a task before loading cog. For example can be to load a file. Audio and downloader uses start task as this example. Now if we had a special method called __start_task__ or something similar it would help cut down on a lot of code in cogs and also make it more uniform. There is more examples but one was this start task.

Jackenmen commented 4 years ago

About the boilerplate that is needed for start tasks, depending on how much you want (or need) to handle, it can definitely end up being quite big:

__init__.py file:

from .cogname import CogName

def setup(bot):
    cog = CogName()
    bot.add_cog(cog)
    cog.create_init_task()

cogname.py file:

import asyncio

class CogName:
    def __init__(self, bot):
        self._ready = asyncio.Event()
        self._init_task = None
        self._ready_raised = False

    def create_init_task(self):
        def _done_callback(task):
            exc = task.exception()
            if exc is not None:
                log.error(
                    "An unexpected error occurred during CogName's initialization.",
                    exc_info=exc
                )
                self._ready_raised = True
            self._ready.set()

        self._init_task = asyncio.create_task(self.initialize())
        self._init_task.add_done_callback(_done_callback)

    async def initialize(self):
        # alternatively use wait_until_red_ready()
        # if you need some stuff that happens > in our post-connection startup
        await self.bot.wait_until_ready()
        # do what you need

    def cog_unload(self):
        if self._init_task is not None:
            self._init_task.cancel()

    async def cog_before_invoke(self, ctx):
        # use if commands need initialize() to finish
        async with ctx.typing():
            await self._ready.wait()
        if self._ready_raised:
            await ctx.send(
                "There was an error during CogName's initialization.
                " Check logs for > more information."
            )
            raise commands.CheckFailure()

    @commands.command()
    async def example(self, ctx):
        # commands don't have to care about initialize()
        # cause we have cog-wide before_invoke
        pass

    @commands.Cog.listener()
    async def on_message(self, message):
        # use if listener needs initialize() to finish
        await self._ready.wait()
        if self._ready_raised:
            return
        # do everything

If we would want to make it easier for cog creators, we would of course need to make its behaviour configurable while not making it overcomplicated. Making decorators or reserving method names that could be used for init task could certainly make it a lot easier.

The way that made the most sense to me is using decorators. This way, everything related to initializing can be referred to with red_initialize.some_name_here which is in my opinion rather friendly to developers:

class CogName(commands.Cog):
    # name to be determined
    @commands.Cog.red_init_task()
    async def red_initialize(self):
        """
        When cog is added with `bot.add_cog()`, this coroutine would be
        scheduled as a task and before any command is ran, it would have to wait
        for this scheduled task to finish.
        """

    # name to be determined
    @red_initialize.error
    async def red_initialize_error(self, ctx):
        """
        If the init task fails, running any cog's command will pre-emptively fail.
        If the init task failed and this function is defined,
        it will be called each time any command from this cog is called.

        This can for example be used to let the user know that there was an initialization error:
        """
        await ctx.send(
            "There was an error during CogName's initialization. Check logs for more information."
        )

    @commands.Cog.listener()
    # name to be determined
    @red_initialize.require
    async def on_message(self, message):
        """
        If there is a function that requires the init task to finish
        and it isn't a command nor is it called by a command
        (for example a listener or background loop), it is possible
        to make it wait for the init task using a special decorator.

        Alternatively, developers would be able to use wait method:
        """
        if not await self.red_initialize.wait():
            # init task failed
            return

To not overcomplicate handling required for this, I think it would be fair to limit this decorator to one per cog class.

The alternative that I was initially thinking of were using special method names. I don't have compelling reason to choose this over decorators, using special method names seems less explicit and makes it harder to know what's going on. But since this is the way I was experimenting with when I was looking at how this could be handled internally, I think it's fair to at least include it here:

class CogName(commands.Cog):
    # name to be determined
    async def red_initialize(self):
        # same as in the approach above, with the use of special method name instead

    # name to be determined
    async def red_initialize_error(self, ctx):
        # same as in the approach above, with the use of special method name instead

    @commands.Cog.listener()
    # name to be determined
    @commands.Cog.require_init_task()
    async def on_message(self, message):
        # same as in the approach above, but different decorator

I can definitely see how this could be helpful for Red developers, so I wanted to share some of my own ideas on this since I had some time, but I haven't talked about this with anyone in the team yet, so for now this is all just a (cool) concept for now.