refactor: Update ``Client.run`` to have a better async I/O usage
Summary
This PR refactors the Client.run logic to fix problems involving asyncio due to how the library used the loop:
Needs testing
Exception
File "/home/container/main.py", line 23, in <module>
bot.run(BOT_TOKEN)
File "/home/container/.local/lib/python3.11/site-packages/discord/client.py", line 772, in run
loop.run_forever()
File "/usr/local/lib/python3.11/asyncio/base_events.py", line 608, in run_forever
self._run_once()
File "/usr/local/lib/python3.11/asyncio/base_events.py", line 1936, in _run_once
handle._run()
File "/usr/local/lib/python3.11/asyncio/events.py", line 84, in _run
self._context.run(self._callback, *self._args)
File "/usr/local/lib/python3.11/asyncio/selector_events.py", line 956, in _read_ready
self._read_ready_cb()
File "/usr/local/lib/python3.11/asyncio/selector_events.py", line 988, in _read_ready__get_buffer
self._protocol.buffer_updated(nbytes)
File "/usr/local/lib/python3.11/asyncio/sslproto.py", line 439, in buffer_updated
self._do_handshake()
File "/usr/local/lib/python3.11/asyncio/sslproto.py", line 560, in _do_handshake
self._sslobj.do_handshake()
File "/usr/local/lib/python3.11/ssl.py", line 979, in do_handshake
self._sslobj.do_handshake()
Information
- [x] This PR fixes an issue.
- [ ] This PR adds something new (e.g. new method or parameters).
- [ ] This PR is a breaking change (e.g. methods or parameters removed/renamed).
- [ ] This PR is not a code change (e.g. documentation, README, typehinting, examples, ...).
Checklist
- [x] I have searched the open pull requests for duplicates.
- [ ] If code changes were made then they have been tested.
- [x] I have updated the documentation to reflect the changes.
- [ ] If
type: ignorecomments were used, a comment is also left explaining why. - [x] I have updated the changelog to include these changes.
Applied all the changes Dorukyum requested.
Before merging, I would like some feedback on this discussion message
@DA-344 can u fix merge conflicts for the changelog? @Pycord-Development/contributors can we get some testing here :D
@DA-344 can u fix merge conflicts for the changelog?
Okay, it should be fixed now. 👍
i tested this pr on my prod bot for some hours and nothing to issue to signal
I've done some testing and it has been working as expected. Can this pr get any review more/merge ?
This is required if we ever want py 3.14 support, see my pr about that
Not just for 3.14 support, but to fix all asyncio related issues that can be presented in any version due to how the AbstractEventLoop is currently handled.
Could be neat if we could get this merged for 2.7
Also please @DA-344 fix conflicts (sry 😅)
@DA-344 The test code below crashes, can you take a look ? I think it has to do with self.loop now being a property, maybe self._loop should be passed to HTTPClient but I am not 100% sure.
https://github.com/Pycord-Development/pycord/blob/2ae01b796647466861eead7da7cd1c7065a1543a/discord/client.py#L249
Traceback (most recent call last):
File "C:\Users\Jérémie\Documents\GitHub\pycord\thing.py", line 12, in <module>
bot = discord.Bot(intents=discord.Intents.all())
File "C:\Users\Jérémie\Documents\GitHub\pycord\discord\bot.py", line 1171, in __init__
super().__init__(*args, **options)
~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^
File "C:\Users\Jérémie\Documents\GitHub\pycord\discord\bot.py", line 97, in __init__
super().__init__(*args, **kwargs)
~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^
File "C:\Users\Jérémie\Documents\GitHub\pycord\discord\cog.py", line 610, in __init__
super().__init__(*args, **kwargs)
~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^
File "C:\Users\Jérémie\Documents\GitHub\pycord\discord\client.py", line 250, in __init__
loop=self.loop,
^^^^^^^^^
File "C:\Users\Jérémie\Documents\GitHub\pycord\discord\client.py", line 330, in loop
raise RuntimeError("loop is not set")
RuntimeError: loop is not set
Test Code
import os
import discord
import asyncio
import logging
from dotenv import load_dotenv
logging.basicConfig(level=logging.INFO)
load_dotenv()
bot = discord.Bot(intents=discord.Intents.all())
@bot.event
async def on_ready():
print(f"Logged in as {bot.user} ({bot.user.id})")
options = [
"Option 1",
"Option 2",
"Option 3",
"Option 4",
]
async def my_autocomplete(ctx: discord.AutocompleteContext):
"""Autocomplete function for the command"""
query = ctx.value.lower()
return [option for option in options if query in option.lower()]
class MyCog(discord.Cog):
"""Example cog for the bot"""
def __init__(self, bot: discord.Bot):
self.bot: discord.Bot = bot
@discord.command(name="hello")
@discord.option(
name="option",
description="An example option",
required=True,
autocomplete=my_autocomplete,
)
async def hello(self, ctx: discord.ApplicationContext, option: str):
await ctx.respond(f"Hello! You selected: {option}")
async def load_cogs():
bot.add_cog(MyCog(bot))
async def main():
await load_cogs()
try:
await bot.start(os.getenv("TOKEN_3", ""))
except KeyboardInterrupt:
await bot.close()
if __name__ == "__main__":
asyncio.run(main())
@DA-344 The test code below crashes, can you take a look ? I think it has to do with
self.loopnow being a property, maybeself._loopshould be passed toHTTPClientbut I am not 100% sure.
Yup, I forgot to update that to use _loop instead of loop, should be fixed now.
Traceback (most recent call last):
File "C:\Users\Jérémie\Documents\GitHub\pycord\thing.py", line 12, in <module>
bot = discord.Bot(intents=discord.Intents.all())
File "C:\Users\Jérémie\Documents\GitHub\pycord\discord\bot.py", line 1171, in __init__
super().__init__(*args, **options)
~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^
File "C:\Users\Jérémie\Documents\GitHub\pycord\discord\bot.py", line 97, in __init__
super().__init__(*args, **kwargs)
~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^
File "C:\Users\Jérémie\Documents\GitHub\pycord\discord\cog.py", line 610, in __init__
super().__init__(*args, **kwargs)
~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^
File "C:\Users\Jérémie\Documents\GitHub\pycord\discord\client.py", line 262, in __init__
self._connection: ConnectionState = self._get_state(**options)
~~~~~~~~~~~~~~~^^^^^^^^^^^
File "C:\Users\Jérémie\Documents\GitHub\pycord\discord\client.py", line 321, in _get_state
loop=self.loop,
^^^^^^^^^
File "C:\Users\Jérémie\Documents\GitHub\pycord\discord\client.py", line 332, in loop
raise RuntimeError("loop is not set")
RuntimeError: loop is not set
Still getting the same @DA-344. I tried replacing self.loop with self._loop in line 321 there but that lead to some other issues...
will review soon
Kk no stress
Okay, everything should be now fixed
Tested and works.
Tested Code
Code is basically simple, because everything involving loop happens at startup.
import discord
intents = discord.Intents.default()
intents.members = True
intents.message_content = True
bot = discord.Bot(intents=intents, chunk_guilds_at_startup=False, debug_guilds=[...])
@bot.check
async def check(ctx):
if ctx.guild.id not in bot.debug_guilds:
return False
if not ctx.guild.chunked:
await ctx.guild.chunk()
return True
@bot.command(name='test-loop')
async def test_loop(ctx: discord.ApplicationContext) -> None:
await ctx.respond(content='pong')
if __name__ == '__main__':
bot.run('token')
Pls dont merge before I test this
raising RuntimeError: loop is not set
Traceback (most recent call last):
File "/Users/luma/GitHub/pycord-fork/test.py", line 36, in <module>
bot.loop.run_until_complete(main())
^^^^^^^^
File "/Users/luma/GitHub/pycord-fork/discord/client.py", line 334, in loop
raise RuntimeError("loop is not set")
RuntimeError: loop is not set
async def main():
try:
await init()
await bot.start(BOT_TOKEN)
finally:
await shutdown()
if not bot.is_closed():
await bot.close()
if __name__ == "__main__":
bot.loop.run_until_complete(main())
its also blocking all @tasks.loop() (from my testing)
@tasks.loop(seconds=5)
async def test():
print("start", test.current_loop)
await asyncio.sleep(60)
print("stop", test.current_loop)
test.start()
bot.run()
Pls fix Luma's issue
This has been fixed on the latest commit.
I'll review and test that around 1pm
tested and no issue with #2771
@Lulalaby when your merge this (once review is good) pls make sure to squash so that we can put it in rc2 and then revert it easily if problems
Can't seem to manually call login + connect:
Error
Traceback (most recent call last):
File "C:\xxx\pr2645.py", line 19, in <module>
asyncio.run(main())
~~~~~~~~~~~^^^^^^^^
File "C:xxx\uv\python\cpython-3.14.0-windows-x86_64-none\Lib\asyncio\runners.py", line 204, in run
return runner.run(main)
~~~~~~~~~~^^^^^^
File "C:\xxx\uv\python\cpython-3.14.0-windows-x86_64-none\Lib\asyncio\runners.py", line 127, in run
return self._loop.run_until_complete(task)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^
File "C:\xxx\uv\python\cpython-3.14.0-windows-x86_64-none\Lib\asyncio\base_events.py", line 719, in run_until_complete
return future.result()
~~~~~~~~~~~~~^^
File "C:\xxx\pr2645.py", line 16, in main
await client.connect()
File "xxx\.venv\Lib\site-packages\discord\client.py", line 761, in connect
self.ws = await asyncio.wait_for(coro, timeout=60.0)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "C:\xxx\uv\python\cpython-3.14.0-windows-x86_64-none\Lib\asyncio\tasks.py", line 488, in wait_for
return await fut
^^^^^^^^^
File "C:\xxx\.venv\Lib\site-packages\discord\gateway.py", line 348, in from_client
ws = cls(socket, loop=client.loop)
^^^^^^^^^^^
File "C:xxx\.venv\Lib\site-packages\discord\client.py", line 369, in loop
raise RuntimeError("loop is not set")
RuntimeError: loop is not set
Unclosed client session
Code
import asyncio
import discord
client = discord.Client()
@client.event
async def on_ready():
print(f"We have logged in as {client.user}")
async def main():
await client.login("xxx")
await client.connect()
asyncio.run(main())
ext.tasks seems to be broken and the changes to it aren't in the changelog?
- This task runs automatically
@tasks.loop(seconds=1)
# @tasks.loop(seconds=1, create_loop=False) # <- default
async def my_background_task():
print("Background task is running...")
# my_background_task.start() <- does nothing
client.run(...)
- This task does not run
@tasks.loop(seconds=1)
# @tasks.loop(seconds=1, create_loop=False) # <- default
async def my_background_task():
print("Background task is running...")
# optional: my_background_task.start()
- This task does not run
@tasks.loop(seconds=1, create_loop=True)
async def my_background_task():
print("Background task is running...")
client.run(...)
- This task does not ran and logs a warning
@tasks.loop(seconds=1, create_loop=True)
async def my_background_task():
print("Background task is running...")
my_background_task.start()
# console:
# Task was destroyed but it is pending!
# task: <Task pending name='pycord-ext-task (0x1f4fb9d3380): my_background_task' coro=<Loop._loop() running at C:\Users\Sohea\OneDrive\Documents\test\pycord\.venv\Lib\site-packages\discord\ext\tasks\__init__.py:213>>
# <sys>:0: RuntimeWarning: coroutine 'Loop._loop' was never awaited
This is all super confusing and so is the doc of the create_loop parameter.
- This task does not run
Isn't that the same code as task number one ?
Isn't that the same code as task number one ?
Yes but without running the bot, which is what I assumed create_loop allowed.
Added medium prio, this fixes multiple issues and is a much needed cleanup, will check everything out to try and find out more about those loop issues.
Can't seem to manually call login + connect:
Also got that same issue.
@DA-344 What do you think about this ? Is there any reason why we couldn't do that:
@property
def loop(self) -> asyncio.AbstractEventLoop:
"""The event loop that the client uses for asynchronous operations."""
if self._loop is None:
try:
self._loop = asyncio.get_running_loop()
except RuntimeError as e:
raise RuntimeError("loop is not set") from e
return self._loop
This should also normally allow you to remove the other places where it try: excepts asyncio.get_running_loop since it would be in the property itself
Note, Needs testing w/ async autocompletes and typing context manager as well as ext.loop