There have been a few breaking changes involving the way Client
deals with the asyncio
event loop.
Originally, you could pass a loop
object and retrieve it using the Client.loop
attribute. This design made sense for the older 3.4-3.7 Python. Likewise, discord.py had a method named Client.run
to abstract all the worries of clean up for you.
At around Python 3.7, a new function named asyncio.run
was added to the standard library to help with clean up. Unfortunately, this function is still buggy on Windows and leads to some rather noisy clean up code. However, from a maintenance perspective it was desirable to switch to asyncio.run
instead of Client.run
.
The issue with supporting asyncio.run
is that it creates a new event loop and maintains ownership of it. The problem with creating a new event loop is that if two objects have different event loops then things error out. Compounded by this, in Python 3.8, asyncio deprecated explicit loop passing and made it rather cumbersome to work with. This means that the old approach of creating an implicit loop using asyncio.get_event_loop()
with asyncio.run
means that it'll always result in an error due to different event loops.
In order to facilitate these changes, breaking changes had to be made to the Client to allow it to work with asyncio.run
.
Note that Python 3.10 removed the loop parameter, therefore discord.py is merely adapting to modern asyncio standards in its design.
While Client.run
still works, accessing the Client.loop
attribute will now result in an error if it's accessed outside of an async
context. In order to do any sort of asynchronous initialisation, it is recommended to refactor your code out into a "main" function. For example:
# Before
bot = commands.Bot(...)
bot.loop.create_task(background_task())
bot.run('token')
# After
bot = commands.Bot(...)
async def main():
async with bot:
bot.loop.create_task(background_task())
await bot.start('token')
asyncio.run(main())
While this is more lines of code to do the same thing, this gives you the greatest amount of flexibility in terms of asynchronous initialisation. If you desire something simpler, you can use the brand new Client.setup_hook
in your subclass. For example:
class MyBot(commands.Bot):
async def setup_hook(self):
self.loop.create_task(background_task())
bot = MyBot()
bot.run('token')
Warning It is important to note that using
wait_until_ready
inside asetup_hook
can cause a deadlock (i.e. your bot hangs).
Note that ext.tasks
also requires it to start inside an asynchronous context now as well. This means that this code used to be valid but no longer is:
# Before
@tasks.loop(minutes=5)
async def my_task():
...
my_task.start()
bot.run('token')
In order to fix this, your choices are the same as mentioned above. Either move it into an asynchronous main function or use the new setup_hook
. For example:
# After
@tasks.loop(minutes=5)
async def my_task():
...
async def main():
async with bot:
my_task.start()
await bot.start('token')
asyncio.run(main())
Creating an aiohttp.ClientSession
inside __init__
or outside Client.run
or asyncio.run
will suffer from the same problems mentioned above. The fix for it is the same.
# Before
bot.session = aiohttp.ClientSession()
bot.run('token')
# After
async def main():
async with aiohttp.ClientSession() as session:
async with bot:
bot.session = session
await bot.start('token')
asyncio.run(main())
Because of the removal of Client.loop
in non-async contexts, some use cases involving asynchronous initialisation within cogs and extensions broke due to this change. As a result, cog and extension loading and unloading were made asynchronous to facilitate asynchronous initialisation.
For example:
# Before
class MyCog(commands.Cog):
def __init__(self, bot):
bot.loop.create_task(self.async_init())
self.bot = bot
async def async_init(self):
...
def setup(bot):
bot.add_cog(MyCog(bot))
Can now be done doing:
# After
class MyCog(commands.Cog):
def __init__(self, bot):
self.bot = bot
# New async cog_load special method is automatically called
async def cog_load(self):
...
async def setup(bot):
await bot.add_cog(MyCog(bot))
Note that to load and unload an extension you now need await
. Putting it all together, you can get a complete program that is more or less like this:
from discord.ext import commands, tasks
class MyBot(commands.Bot):
def __init__(self):
super().__init__(command_prefix='$')
self.initial_extensions = [
'cogs.admin',
'cogs.foo',
'cogs.bar',
]
async def setup_hook(self):
self.background_task.start()
self.session = aiohttp.ClientSession()
for ext in self.initial_extensions:
await self.load_extension(ext)
async def close(self):
await super().close()
await self.session.close()
@tasks.loop(minutes=10)
async def background_task(self):
print('Running background task...')
async def on_ready(self):
print('Ready!')
bot = MyBot()
bot.run('token')
Client.loop
is now invalid outside of async contexts.Client
is now an asynchronous context manager.- [commands] Extensions and cogs are now fully async.
- Add new
Cog.cog_load
async init - Change extension
setup
andteardown
to be async. - Change
Bot.load_extension
,Bot.reload_extension
, andBot.remove_extension
to be async. - Change
Cog.cog_unload
to be maybe async. - Change
Bot.add_cog
andBot.remove_cog
to be async.
- Add new
Wow