Open Stonedestroyer opened 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.
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.