From de49cf507944bff25fa3cf53b6b0e4c2067a8128 Mon Sep 17 00:00:00 2001 From: DA-344 <108473820+DA-344@users.noreply.github.com> Date: Mon, 11 Nov 2024 18:25:23 +0100 Subject: [PATCH 01/15] fix: client.run not allowing bots to start --- discord/client.py | 27 ++------------------------- 1 file changed, 2 insertions(+), 25 deletions(-) diff --git a/discord/client.py b/discord/client.py index cbc22813fd..71e8b860b4 100644 --- a/discord/client.py +++ b/discord/client.py @@ -778,13 +778,6 @@ def run(self, *args: Any, **kwargs: Any) -> None: 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: @@ -793,26 +786,10 @@ async def runner(): if not self.is_closed(): await self.close() - def stop_loop_on_completion(f): - loop.stop() - - future = asyncio.ensure_future(runner(), loop=loop) - future.add_done_callback(stop_loop_on_completion) try: - loop.run_forever() + asyncio.run(runner()) except KeyboardInterrupt: - _log.info("Received signal to terminate bot and event loop.") - finally: - future.remove_done_callback(stop_loop_on_completion) - _log.info("Cleaning up tasks.") - _cleanup_loop(loop) - - if not future.cancelled(): - try: - return future.result() - except KeyboardInterrupt: - # I am unsure why this gets raised here but suppress it anyway - return None + return # properties From 4fe559fe47373b3186be4b25ec0cd9d7fdaeef52 Mon Sep 17 00:00:00 2001 From: DA-344 <108473820+DA-344@users.noreply.github.com> Date: Mon, 11 Nov 2024 20:35:51 +0100 Subject: [PATCH 02/15] chore: Update more things --- discord/client.py | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/discord/client.py b/discord/client.py index 71e8b860b4..f21303bfb0 100644 --- a/discord/client.py +++ b/discord/client.py @@ -27,7 +27,6 @@ import asyncio import logging -import signal import sys import traceback from types import TracebackType @@ -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]]]] = ( {} ) @@ -780,16 +777,26 @@ def run(self, *args: Any, **kwargs: Any) -> None: """ async def runner(): + # Update the bot loop to replace MISSING + self.loop = asyncio.get_event_loop() try: await self.start(*args, **kwargs) finally: if not self.is_closed(): await self.close() + run = asyncio.run + + if self.loop is not MISSING: + run = self.loop.run_until_complete + try: - asyncio.run(runner()) - except KeyboardInterrupt: - return + run(runner()) + finally: + if not self.is_closed(): + self.loop.run_until_complete(self.close()) + + _cleanup_loop(self.loop) # properties From 74863c5424fa6d296bd5a3ae7f73f83faa443f9d Mon Sep 17 00:00:00 2001 From: DA-344 <108473820+DA-344@users.noreply.github.com> Date: Mon, 11 Nov 2024 20:36:35 +0100 Subject: [PATCH 03/15] chore: Move the loop update to .start --- discord/client.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/discord/client.py b/discord/client.py index f21303bfb0..a1556d1358 100644 --- a/discord/client.py +++ b/discord/client.py @@ -748,6 +748,9 @@ 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() await self.login(token) await self.connect(reconnect=reconnect) @@ -777,8 +780,6 @@ def run(self, *args: Any, **kwargs: Any) -> None: """ async def runner(): - # Update the bot loop to replace MISSING - self.loop = asyncio.get_event_loop() try: await self.start(*args, **kwargs) finally: From 72f904c486d27340bdff7b3733c2f90f6d92802c Mon Sep 17 00:00:00 2001 From: DA-344 <108473820+DA-344@users.noreply.github.com> Date: Mon, 11 Nov 2024 21:31:13 +0100 Subject: [PATCH 04/15] chore: Added logging and updated `run` to specify the arguments --- discord/client.py | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/discord/client.py b/discord/client.py index a1556d1358..74267bcabe 100644 --- a/discord/client.py +++ b/discord/client.py @@ -751,10 +751,13 @@ async def start(self, token: str, *, reconnect: bool = True) -> None: # 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. @@ -765,12 +768,17 @@ 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. + 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). .. warning:: @@ -781,7 +789,7 @@ def run(self, *args: Any, **kwargs: Any) -> None: async def runner(): try: - await self.start(*args, **kwargs) + await self.start(token, reconnect=reconnect) finally: if not self.is_closed(): await self.close() @@ -794,9 +802,11 @@ async def runner(): try: run(runner()) finally: + # Ensure the bot is closed if not self.is_closed(): self.loop.run_until_complete(self.close()) + _log.info('Cleaning up tasks.') _cleanup_loop(self.loop) # properties From 7e7f1e959c0be329cc3f9bf85ab875a82fb6a6f3 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 11 Nov 2024 20:31:44 +0000 Subject: [PATCH 05/15] style(pre-commit): auto fixes from pre-commit.com hooks --- discord/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/client.py b/discord/client.py index 74267bcabe..d023eac7dd 100644 --- a/discord/client.py +++ b/discord/client.py @@ -806,7 +806,7 @@ async def runner(): if not self.is_closed(): self.loop.run_until_complete(self.close()) - _log.info('Cleaning up tasks.') + _log.info("Cleaning up tasks.") _cleanup_loop(self.loop) # properties From f289f74a2880e23af40fe31f96a09459f57ffea2 Mon Sep 17 00:00:00 2001 From: DA-344 <108473820+DA-344@users.noreply.github.com> Date: Mon, 11 Nov 2024 21:32:46 +0100 Subject: [PATCH 06/15] chore: Updated CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bc995ffd76..b6b0727815 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -60,6 +60,7 @@ 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 From 3d235eebb0b3014b90575e643aa4e6c49b3b0598 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 11 Nov 2024 20:36:27 +0000 Subject: [PATCH 07/15] style(pre-commit): auto fixes from pre-commit.com hooks --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b6b0727815..3ca1a883ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -60,7 +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)) +- Fixed Async I/O errors that could be raised when using `Client.run` + ([#2645](https://github.com/Pycord-Development/pycord/pull/2645)) ### Changed From 07ccbc97f14c16603fee2a117e2bad28c1f0778c Mon Sep 17 00:00:00 2001 From: DA-344 <108473820+DA-344@users.noreply.github.com> Date: Mon, 11 Nov 2024 21:37:05 +0100 Subject: [PATCH 08/15] dot --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ca1a883ab..a34bee9eb9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -60,7 +60,7 @@ 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` +- Fixed Async I/O errors that could be raised when using `Client.run`. ([#2645](https://github.com/Pycord-Development/pycord/pull/2645)) ### Changed From 5e8a322ab9335e152430b623d1dec60d82f3c153 Mon Sep 17 00:00:00 2001 From: DA344 <108473820+DA-344@users.noreply.github.com> Date: Tue, 19 Nov 2024 08:23:39 +0100 Subject: [PATCH 09/15] Update docstrings Co-authored-by: Dorukyum <53639936+Dorukyum@users.noreply.github.com> Signed-off-by: DA344 <108473820+DA-344@users.noreply.github.com> --- discord/client.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/discord/client.py b/discord/client.py index d023eac7dd..4742bce52c 100644 --- a/discord/client.py +++ b/discord/client.py @@ -777,8 +777,10 @@ def run(self, token: str, *, reconnect: bool = True) -> None: token: :class:`str` The authentication token. Do not prefix this token with anything as the library will do it for you. 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). + 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). .. warning:: From de5156c2ae8eb7d687b09d7f75f65c6054230473 Mon Sep 17 00:00:00 2001 From: DA344 <108473820+DA-344@users.noreply.github.com> Date: Tue, 19 Nov 2024 08:23:53 +0100 Subject: [PATCH 10/15] Update docstrings Co-authored-by: Dorukyum <53639936+Dorukyum@users.noreply.github.com> Signed-off-by: DA344 <108473820+DA-344@users.noreply.github.com> --- discord/client.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/discord/client.py b/discord/client.py index 4742bce52c..137c1e1de2 100644 --- a/discord/client.py +++ b/discord/client.py @@ -775,7 +775,8 @@ def run(self, token: str, *, reconnect: bool = True) -> None: Parameters ---------- token: :class:`str` - The authentication token. Do not prefix this token with anything as the library will do it for you. + The authentication token. Do not prefix this token with + anything as the library will do it for you. 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 From 88484e221c486a01d6f0eabdbd3034e03d9c757c Mon Sep 17 00:00:00 2001 From: DA-344 <108473820+DA-344@users.noreply.github.com> Date: Wed, 11 Dec 2024 16:19:54 +0100 Subject: [PATCH 11/15] chore: Update Client.__aenter__ and Client.run --- discord/client.py | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/discord/client.py b/discord/client.py index 137c1e1de2..78c1ba5afc 100644 --- a/discord/client.py +++ b/discord/client.py @@ -267,10 +267,20 @@ def __init__( self._tasks = set() async def __aenter__(self) -> Client: - loop = asyncio.get_running_loop() - self.loop = loop - self.http.loop = loop - self._connection.loop = loop + if self.loop is MISSING: + try: + self.loop = asyncio.get_running_loop() + except RuntimeError: + # No event loop was found, this should not happen + # because entering on this context manager means a + # loop is already active, but we need to handle it + # anyways just to prevent future errors. + + # Maybe handle different system event loop policies? + self.loop = asyncio.new_event_loop() + + self.http.loop = self.loop + self._connection.loop = self.loop self._ready = asyncio.Event() @@ -749,11 +759,6 @@ async def start(self, token: str, *, reconnect: bool = True) -> None: 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) @@ -791,11 +796,8 @@ def run(self, token: str, *, reconnect: bool = True) -> None: """ async def runner(): - try: - await self.start(token, reconnect=reconnect) - finally: - if not self.is_closed(): - await self.close() + async with self: + await self.start(token=token, reconnect=reconnect) run = asyncio.run From ba81ebeb469053385882b129e782c48d34c7f08b Mon Sep 17 00:00:00 2001 From: DA-344 <108473820+DA-344@users.noreply.github.com> Date: Wed, 11 Dec 2024 16:20:52 +0100 Subject: [PATCH 12/15] feat: Add operations container to Client docstring --- discord/client.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/discord/client.py b/discord/client.py index 78c1ba5afc..19053c2eb4 100644 --- a/discord/client.py +++ b/discord/client.py @@ -121,6 +121,12 @@ class Client: A number of options can be passed to the :class:`Client`. + .. container:: operations + + .. describe:: async with x + + Asynchronously initializes the client. + Parameters ----------- max_messages: Optional[:class:`int`] From 28fab357755bf85cfce07c5d696adf8c47551e4d Mon Sep 17 00:00:00 2001 From: DA-344 <108473820+DA-344@users.noreply.github.com> Date: Wed, 11 Dec 2024 16:30:57 +0100 Subject: [PATCH 13/15] chore: Update Client.close to prevent double closing and race conditions --- discord/client.py | 35 +++++++++++++++++++---------------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/discord/client.py b/discord/client.py index e6832231b6..8c64fe447b 100644 --- a/discord/client.py +++ b/discord/client.py @@ -259,7 +259,8 @@ def __init__( self._enable_debug_events: bool = options.pop("enable_debug_events", False) self._connection: ConnectionState = self._get_state(**options) self._connection.shard_count = self.shard_count - self._closed: bool = False + self._closed: asyncio.Event = asyncio.Event() + self._closing_task: asyncio.Lock = asyncio.Lock() self._ready: asyncio.Event = asyncio.Event() self._connection._get_websocket = self._get_websocket self._connection._get_client = lambda: self @@ -289,6 +290,7 @@ async def __aenter__(self) -> Client: self._connection.loop = self.loop self._ready = asyncio.Event() + self._closed = asyncio.Event() return self @@ -725,23 +727,24 @@ async def close(self) -> None: Closes the connection to Discord. """ - if self._closed: + if self.is_closed(): return - await self.http.close() - self._closed = True + async with self._closing_task: + await self.http.close() - for voice in self.voice_clients: - try: - await voice.disconnect(force=True) - except Exception: - # if an error happens during disconnects, disregard it. - pass + for voice in self.voice_clients: + try: + await voice.disconnect(force=True) + except Exception: + # if an error happens during disconnects, disregard it. + pass - if self.ws is not None and self.ws.open: - await self.ws.close(code=1000) + if self.ws is not None and self.ws.open: + await self.ws.close(code=1000) - self._ready.clear() + self._ready.clear() + self._closed.set() def clear(self) -> None: """Clears the internal state of the bot. @@ -818,14 +821,14 @@ async def runner(): if not self.is_closed(): self.loop.run_until_complete(self.close()) - _log.info("Cleaning up tasks.") - _cleanup_loop(self.loop) + _log.info("Cleaning up tasks.") + _cleanup_loop(self.loop) # properties def is_closed(self) -> bool: """Indicates if the WebSocket connection is closed.""" - return self._closed + return self._closed.is_set() @property def activity(self) -> ActivityTypes | None: From c378d41f9896263dcf4cdb63af6f1466c54f354c Mon Sep 17 00:00:00 2001 From: DA-344 <108473820+DA-344@users.noreply.github.com> Date: Wed, 11 Dec 2024 16:32:36 +0100 Subject: [PATCH 14/15] fix: Indentation error --- discord/client.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/discord/client.py b/discord/client.py index 8c64fe447b..dcbcd05959 100644 --- a/discord/client.py +++ b/discord/client.py @@ -821,8 +821,8 @@ async def runner(): if not self.is_closed(): self.loop.run_until_complete(self.close()) - _log.info("Cleaning up tasks.") - _cleanup_loop(self.loop) + _log.info("Cleaning up tasks.") + _cleanup_loop(self.loop) # properties From e963341ca9fc0e38e3d4e3ac20b4dc0825598864 Mon Sep 17 00:00:00 2001 From: DA-344 <108473820+DA-344@users.noreply.github.com> Date: Sat, 1 Feb 2025 08:33:22 +0100 Subject: [PATCH 15/15] chore: Update Client.close and Client.clear to correctly update and use Client._closed asyncio.Event object --- discord/client.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/discord/client.py b/discord/client.py index dcbcd05959..092c0a5163 100644 --- a/discord/client.py +++ b/discord/client.py @@ -727,10 +727,10 @@ async def close(self) -> None: Closes the connection to Discord. """ - if self.is_closed(): - return - async with self._closing_task: + if self.is_closed(): + return + await self.http.close() for voice in self.voice_clients: @@ -753,7 +753,7 @@ def clear(self) -> None: and :meth:`is_ready` both return ``False`` along with the bot's internal cache cleared. """ - self._closed = False + self._closed.clear() self._ready.clear() self._connection.clear() self.http.recreate()