Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: Update Client.run to have a better async I/O usage #2645

Open
wants to merge 20 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ These changes are available on the `master` branch, but have not yet been releas
([#2627](https://github.com/Pycord-Development/pycord/issues/2627))
- Fixed `AttributeError` when sending polls with `PartialWebook`.
([#2624](https://github.com/Pycord-Development/pycord/pull/2624))
- Fixed Async I/O errors that could be raised when using `Client.run`.
([#2645](https://github.com/Pycord-Development/pycord/pull/2645))

### Changed

Expand Down
65 changes: 30 additions & 35 deletions discord/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@

import asyncio
import logging
import signal
import sys
import traceback
from types import TracebackType
Expand Down Expand Up @@ -221,14 +220,12 @@ class Client:
def __init__(
self,
*,
loop: asyncio.AbstractEventLoop | None = None,
loop: asyncio.AbstractEventLoop = MISSING,
**options: Any,
):
# self.ws is set in the connect method
self.ws: DiscordWebSocket = None # type: ignore
self.loop: asyncio.AbstractEventLoop = (
asyncio.get_event_loop() if loop is None else loop
)
self.loop: asyncio.AbstractEventLoop = loop
self._listeners: dict[str, list[tuple[asyncio.Future, Callable[..., bool]]]] = (
{}
)
Expand Down Expand Up @@ -751,10 +748,16 @@ async def start(self, token: str, *, reconnect: bool = True) -> None:
TypeError
An unexpected keyword argument was received.
"""
# Update the loop to get the running one in case the one set is MISSING
if self.loop is MISSING:
self.loop = asyncio.get_event_loop()
self.http.loop = self.loop
self._connection.loop = self.loop

await self.login(token)
await self.connect(reconnect=reconnect)

def run(self, *args: Any, **kwargs: Any) -> None:
def run(self, token: str, *, reconnect: bool = True) -> None:
"""A blocking call that abstracts away the event loop
initialisation from you.

Expand All @@ -765,54 +768,46 @@ def run(self, *args: Any, **kwargs: Any) -> None:
Roughly Equivalent to: ::

try:
loop.run_until_complete(start(*args, **kwargs))
asyncio.run(start(token))
except KeyboardInterrupt:
loop.run_until_complete(close())
# cancel all tasks lingering
finally:
loop.close()
return

Parameters
----------
token: :class:`str`
The authentication token. Do not prefix this token with anything as the library will do it for you.
DA-344 marked this conversation as resolved.
Show resolved Hide resolved
reconnect: :class:`bool`
If we should attempt reconnecting to the gateway, either due to internet failure or a specific failure on Discord's part.
Certain disconnects that lead to bad state will not be handled (such as invalid sharding payloads or bad tokens).
DA-344 marked this conversation as resolved.
Show resolved Hide resolved

.. warning::

This function must be the last function to call due to the fact that it
is blocking. That means that registration of events or anything being
called after this function call will not execute until it returns.
"""
loop = self.loop

try:
loop.add_signal_handler(signal.SIGINT, loop.stop)
loop.add_signal_handler(signal.SIGTERM, loop.stop)
except (NotImplementedError, RuntimeError):
pass

async def runner():
try:
await self.start(*args, **kwargs)
await self.start(token, reconnect=reconnect)
finally:
if not self.is_closed():
await self.close()

def stop_loop_on_completion(f):
loop.stop()
run = asyncio.run

if self.loop is not MISSING:
run = self.loop.run_until_complete

future = asyncio.ensure_future(runner(), loop=loop)
future.add_done_callback(stop_loop_on_completion)
try:
loop.run_forever()
except KeyboardInterrupt:
_log.info("Received signal to terminate bot and event loop.")
run(runner())
finally:
future.remove_done_callback(stop_loop_on_completion)
_log.info("Cleaning up tasks.")
_cleanup_loop(loop)
# Ensure the bot is closed
if not self.is_closed():
self.loop.run_until_complete(self.close())

if not future.cancelled():
try:
return future.result()
except KeyboardInterrupt:
# I am unsure why this gets raised here but suppress it anyway
return None
_log.info("Cleaning up tasks.")
_cleanup_loop(self.loop)

# properties

Expand Down