From 3c35a8c36fa94c6ae379049360363f3694770ff2 Mon Sep 17 00:00:00 2001 From: elenakrittik Date: Mon, 6 Nov 2023 20:29:38 +0300 Subject: [PATCH 01/26] Port #7528 --- disnake/client.py | 81 ++++++++--------------------------- disnake/ext/tasks/__init__.py | 17 +------- disnake/http.py | 4 +- disnake/player.py | 4 +- disnake/shard.py | 6 +-- disnake/ui/view.py | 6 +-- disnake/voice_client.py | 5 +-- 7 files changed, 29 insertions(+), 94 deletions(-) diff --git a/disnake/client.py b/disnake/client.py index f71842c7b3..c8ae761787 100644 --- a/disnake/client.py +++ b/disnake/client.py @@ -4,11 +4,9 @@ import asyncio import logging -import signal import sys import traceback import types -import warnings from datetime import datetime, timedelta from errno import ECONNRESET from typing import ( @@ -221,15 +219,9 @@ class Client: .. versionchanged:: 1.3 Allow disabling the message cache and change the default size to ``1000``. - loop: Optional[:class:`asyncio.AbstractEventLoop`] - The :class:`asyncio.AbstractEventLoop` to use for asynchronous operations. - Defaults to ``None``, in which case the default event loop is used via - :func:`asyncio.get_event_loop()`. asyncio_debug: :class:`bool` Whether to enable asyncio debugging when the client starts. Defaults to False. - connector: Optional[:class:`aiohttp.BaseConnector`] - The connector to use for connection pooling. proxy: Optional[:class:`str`] Proxy URL. proxy_auth: Optional[:class:`aiohttp.BasicAuth`] @@ -345,8 +337,6 @@ class Client: ---------- ws The websocket gateway the client is currently connected to. Could be ``None``. - loop: :class:`asyncio.AbstractEventLoop` - The event loop that the client uses for asynchronous operations. session_start_limit: Optional[:class:`SessionStartLimit`] Information about the current session start limit. Only available after initiating the connection. @@ -363,7 +353,6 @@ def __init__( self, *, asyncio_debug: bool = False, - loop: Optional[asyncio.AbstractEventLoop] = None, shard_id: Optional[int] = None, shard_count: Optional[int] = None, enable_debug_events: bool = False, @@ -371,7 +360,6 @@ def __init__( localization_provider: Optional[LocalizationProtocol] = None, strict_localization: bool = False, gateway_params: Optional[GatewayParams] = None, - connector: Optional[aiohttp.BaseConnector] = None, proxy: Optional[str] = None, proxy_auth: Optional[aiohttp.BasicAuth] = None, assume_unsync_clock: bool = True, @@ -389,19 +377,12 @@ def __init__( # self.ws is set in the connect method self.ws: DiscordWebSocket = None # type: ignore - if loop is None: - with warnings.catch_warnings(): - warnings.simplefilter("ignore", DeprecationWarning) - self.loop: asyncio.AbstractEventLoop = asyncio.get_event_loop() - else: - self.loop: asyncio.AbstractEventLoop = loop + self.loop: asyncio.AbstractEventLoop = MISSING - self.loop.set_debug(asyncio_debug) self._listeners: Dict[str, List[Tuple[asyncio.Future, Callable[..., bool]]]] = {} self.session_start_limit: Optional[SessionStartLimit] = None self.http: HTTPClient = HTTPClient( - connector, proxy=proxy, proxy_auth=proxy_auth, unsync_clock=assume_unsync_clock, @@ -728,7 +709,7 @@ def _schedule_event( ) -> asyncio.Task: wrapped = self._run_event(coro, event_name, *args, **kwargs) # Schedules the task - return asyncio.create_task(wrapped, name=f"disnake: {event_name}") + return self.loop.create_task(wrapped, name=f"disnake: {event_name}") def dispatch(self, event: str, *args: Any, **kwargs: Any) -> None: _log.debug("Dispatching event %s", event) @@ -1201,6 +1182,7 @@ async def close(self) -> None: await self.http.close() self._ready.clear() + self.loop = MISSING def clear(self) -> None: """Clears the internal state of the bot. @@ -1226,10 +1208,15 @@ async def start( TypeError An unexpected keyword argument was received. """ - await self.login(token) - await self.connect( - reconnect=reconnect, ignore_session_start_limit=ignore_session_start_limit - ) + self.loop = asyncio.get_running_loop() + self.http.loop = self.loop + self._connection.loop = self.loop + try: + await self.login(token) + await self.connect(reconnect=reconnect) + finally: + if not self.is_closed(): + await self.close() def run(self, *args: Any, **kwargs: Any) -> None: """A blocking call that abstracts away the event loop @@ -1239,15 +1226,12 @@ def run(self, *args: Any, **kwargs: Any) -> None: function should not be used. Use :meth:`start` coroutine or :meth:`connect` + :meth:`login`. - Roughly Equivalent to: :: + Equivalent to: :: try: - loop.run_until_complete(start(*args, **kwargs)) + asyncio.run(start(*args, **kwargs)) except KeyboardInterrupt: - loop.run_until_complete(close()) - # cancel all tasks lingering - finally: - loop.close() + return .. warning:: @@ -1255,41 +1239,10 @@ 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, lambda: loop.stop()) - loop.add_signal_handler(signal.SIGTERM, lambda: loop.stop()) - except NotImplementedError: - pass - - async def runner() -> None: - try: - await self.start(*args, **kwargs) - finally: - if not self.is_closed(): - await self.close() - - def stop_loop_on_completion(f) -> None: - loop.stop() - - future = asyncio.ensure_future(runner(), loop=loop) - future.add_done_callback(stop_loop_on_completion) - try: - loop.run_forever() + asyncio.run(self.start(*args, **kwargs)) 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 diff --git a/disnake/ext/tasks/__init__.py b/disnake/ext/tasks/__init__.py index 1c23e0e912..b3c7a11113 100644 --- a/disnake/ext/tasks/__init__.py +++ b/disnake/ext/tasks/__init__.py @@ -7,7 +7,6 @@ import inspect import sys import traceback -import warnings from collections.abc import Sequence from typing import ( TYPE_CHECKING, @@ -90,14 +89,12 @@ def __init__( time: Union[datetime.time, Sequence[datetime.time]] = MISSING, count: Optional[int] = None, reconnect: bool = True, - loop: asyncio.AbstractEventLoop = MISSING, ) -> None: """.. note: If you overwrite ``__init__`` arguments, make sure to redefine .clone too. """ self.coro: LF = coro self.reconnect: bool = reconnect - self.loop: asyncio.AbstractEventLoop = loop self.count: Optional[int] = count self._current_loop = 0 self._handle: SleepHandle = MISSING @@ -139,7 +136,7 @@ async def _call_loop_function(self, name: str, *args: Any, **kwargs: Any) -> Non await coro(*args, **kwargs) def _try_sleep_until(self, dt: datetime.datetime): - self._handle = SleepHandle(dt=dt, loop=self.loop) + self._handle = SleepHandle(dt=dt, loop=asyncio.get_running_loop()) return self._handle.wait() async def _loop(self, *args: Any, **kwargs: Any) -> None: @@ -214,7 +211,6 @@ def clone(self) -> Self: time=self._time, count=self.count, reconnect=self.reconnect, - loop=self.loop, ) instance._before_loop = self._before_loop instance._after_loop = self._after_loop @@ -324,12 +320,7 @@ def start(self, *args: Any, **kwargs: Any) -> asyncio.Task[None]: if self._injected is not None: args = (self._injected, *args) - if self.loop is MISSING: - with warnings.catch_warnings(): - warnings.simplefilter("ignore", DeprecationWarning) - self.loop = asyncio.get_event_loop() - - self._task = self.loop.create_task(self._loop(*args, **kwargs)) + self._task = asyncio.create_task(self._loop(*args, **kwargs)) return self._task def stop(self) -> None: @@ -721,7 +712,6 @@ def loop( time: Union[datetime.time, Sequence[datetime.time]] = ..., count: Optional[int] = None, reconnect: bool = True, - loop: asyncio.AbstractEventLoop = ..., ) -> Callable[[LF], Loop[LF]]: ... @@ -775,9 +765,6 @@ def loop( Whether to handle errors and restart the task using an exponential back-off algorithm similar to the one used in :meth:`disnake.Client.connect`. - loop: :class:`asyncio.AbstractEventLoop` - The loop to use to register the task, if not given - defaults to :func:`asyncio.get_event_loop`. Raises ------ diff --git a/disnake/http.py b/disnake/http.py index f8c4b44694..23c65dc71e 100644 --- a/disnake/http.py +++ b/disnake/http.py @@ -227,7 +227,7 @@ def __init__( unsync_clock: bool = True, ) -> None: self.loop: asyncio.AbstractEventLoop = loop - self.connector = connector + self.connector = connector or MISSING self.__session: aiohttp.ClientSession = MISSING # filled in static_login self._locks: weakref.WeakValueDictionary[str, asyncio.Lock] = weakref.WeakValueDictionary() self._global_over: asyncio.Event = asyncio.Event() @@ -452,6 +452,8 @@ async def close(self) -> None: async def static_login(self, token: str) -> user.User: # Necessary to get aiohttp to stop complaining about session creation + if self.connector is MISSING: + self.connector = aiohttp.TCPConnector(loop=self.loop, limit=0) self.__session = aiohttp.ClientSession( connector=self.connector, ws_response_class=DiscordClientWebSocketResponse ) diff --git a/disnake/player.py b/disnake/player.py index 3ff094d4d6..83c5a9e866 100644 --- a/disnake/player.py +++ b/disnake/player.py @@ -782,6 +782,8 @@ def _set_source(self, source: AudioSource) -> None: def _speak(self, speaking: bool) -> None: try: - asyncio.run_coroutine_threadsafe(self.client.ws.speak(speaking), self.client.loop) + asyncio.run_coroutine_threadsafe( + self.client.ws.speak(speaking), self.client.client.loop + ) except Exception as e: _log.info("Speaking call in player failed: %s", e) diff --git a/disnake/shard.py b/disnake/shard.py index 102c66e4ae..7d7edf03f3 100644 --- a/disnake/shard.py +++ b/disnake/shard.py @@ -93,7 +93,6 @@ def __init__( self._client: Client = client self._dispatch: Callable[..., None] = client.dispatch self._queue_put: Callable[[EventItem], None] = queue_put - self.loop: asyncio.AbstractEventLoop = self._client.loop self._disconnect: bool = False self._reconnect = client._reconnect self._backoff: ExponentialBackoff = ExponentialBackoff() @@ -113,7 +112,7 @@ def id(self) -> int: return self.ws.shard_id # type: ignore def launch(self) -> None: - self._task = self.loop.create_task(self.worker()) + self._task = self._client.loop.create_task(self.worker()) def _cancel_task(self) -> None: if self._task is not None and not self._task.done(): @@ -329,13 +328,11 @@ def __init__( self, *, asyncio_debug: bool = False, - loop: Optional[asyncio.AbstractEventLoop] = None, shard_ids: Optional[List[int]] = None, # instead of Client's shard_id: Optional[int] shard_count: Optional[int] = None, enable_debug_events: bool = False, enable_gateway_error_handler: bool = True, gateway_params: Optional[GatewayParams] = None, - connector: Optional[aiohttp.BaseConnector] = None, proxy: Optional[str] = None, proxy_auth: Optional[aiohttp.BasicAuth] = None, assume_unsync_clock: bool = True, @@ -391,7 +388,6 @@ def _get_state(self, **options: Any) -> AutoShardedConnectionState: handlers=self._handlers, hooks=self._hooks, http=self.http, - loop=self.loop, **options, ) diff --git a/disnake/ui/view.py b/disnake/ui/view.py index 71c2965074..68a6fc4840 100644 --- a/disnake/ui/view.py +++ b/disnake/ui/view.py @@ -178,12 +178,11 @@ def __init__(self, *, timeout: Optional[float] = 180.0) -> None: self.children.append(item) self.__weights = _ViewWeights(self.children) - loop = asyncio.get_running_loop() self.id: str = os.urandom(16).hex() self.__cancel_callback: Optional[Callable[[View], None]] = None self.__timeout_expiry: Optional[float] = None self.__timeout_task: Optional[asyncio.Task[None]] = None - self.__stopped: asyncio.Future[bool] = loop.create_future() + self.__stopped: asyncio.Future[bool] = asyncio.get_running_loop().create_future() def __repr__(self) -> str: return f"<{self.__class__.__name__} timeout={self.timeout} children={len(self.children)}>" @@ -389,12 +388,11 @@ async def _scheduled_task(self, item: Item, interaction: MessageInteraction): def _start_listening_from_store(self, store: ViewStore) -> None: self.__cancel_callback = partial(store.remove_view) if self.timeout: - loop = asyncio.get_running_loop() if self.__timeout_task is not None: self.__timeout_task.cancel() self.__timeout_expiry = time.monotonic() + self.timeout - self.__timeout_task = loop.create_task(self.__timeout_task_impl()) + self.__timeout_task = asyncio.create_task(self.__timeout_task_impl()) def _dispatch_timeout(self) -> None: if self.__stopped.done(): diff --git a/disnake/voice_client.py b/disnake/voice_client.py index 52750ecebd..a49ad695dd 100644 --- a/disnake/voice_client.py +++ b/disnake/voice_client.py @@ -187,8 +187,6 @@ class VoiceClient(VoiceProtocol): The endpoint we are connecting to. channel: :class:`abc.Connectable` The voice channel connected to. - loop: :class:`asyncio.AbstractEventLoop` - The event loop that the voice client is running on. """ endpoint_ip: str @@ -206,7 +204,6 @@ def __init__(self, client: Client, channel: abc.Connectable) -> None: state = client._connection self.token: str = MISSING self.socket: socket.socket = MISSING - self.loop: asyncio.AbstractEventLoop = state.loop self._state: ConnectionState = state # this will be used in the AudioPlayer thread self._connected: threading.Event = threading.Event() @@ -376,7 +373,7 @@ async def connect(self, *, reconnect: bool, timeout: float) -> None: raise if self._runner is MISSING: - self._runner = self.loop.create_task(self.poll_voice_ws(reconnect)) + self._runner = self.client.loop.create_task(self.poll_voice_ws(reconnect)) async def potential_reconnect(self) -> bool: # Attempt to stop the player thread from playing early From d6877d61f20fbcb7b93f03ddb32a88a17286cd03 Mon Sep 17 00:00:00 2001 From: elenakrittik Date: Mon, 6 Nov 2023 20:50:39 +0300 Subject: [PATCH 02/26] Port #7545 --- disnake/client.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/disnake/client.py b/disnake/client.py index c8ae761787..c49a8b7655 100644 --- a/disnake/client.py +++ b/disnake/client.py @@ -1009,6 +1009,8 @@ async def login(self, token: str) -> None: data = await self.http.static_login(token.strip()) self._connection.user = ClientUser(state=self._connection, data=data) + await self.setup_hook() + async def connect( self, *, reconnect: bool = True, ignore_session_start_limit: bool = False ) -> None: @@ -1218,6 +1220,26 @@ async def start( if not self.is_closed(): await self.close() + async def setup_hook(self) -> None: + """A coroutine to be called to setup the bot. + + To perform asynchronous setup after the bot is logged in but before + it has connected to the websocket, override this. + + This is only called once, in :meth:`.login`, before any events are + dispatched, making it a better solution than doing such setup in + the :func:`disnake.on_ready` event. + + .. warning:: + Since this is called *before* the websocket connection is made, + anything that waits for the websocket will deadlock, which includes + methods like :meth:`.wait_for`, :meth:`.wait_until_ready` and + :meth:`.wait_until_first_connect`. + + .. versionadded:: 2.10 + """ + pass + def run(self, *args: Any, **kwargs: Any) -> None: """A blocking call that abstracts away the event loop initialisation from you. From c24dc14af4613a384846b13aea5e48159ed68b74 Mon Sep 17 00:00:00 2001 From: elenakrittik Date: Mon, 6 Nov 2023 21:44:39 +0300 Subject: [PATCH 03/26] fix(autoshard): Errors when trying to use AutoSharded client variants. Based on #7545 --- disnake/client.py | 9 +++++---- disnake/ext/commands/interaction_bot_base.py | 4 ++-- disnake/state.py | 4 ++-- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/disnake/client.py b/disnake/client.py index c49a8b7655..eaddf1b5d3 100644 --- a/disnake/client.py +++ b/disnake/client.py @@ -469,7 +469,6 @@ def _get_state( handlers=self._handlers, hooks=self._hooks, http=self.http, - loop=self.loop, max_messages=max_messages, application_id=application_id, heartbeat_timeout=heartbeat_timeout, @@ -1006,6 +1005,11 @@ async def login(self, token: str) -> None: if not isinstance(token, str): raise TypeError(f"token must be of type str, got {type(token).__name__} instead") + loop = asyncio.get_running_loop() + self.loop = loop + self.http.loop = loop + self._connection.loop = loop + data = await self.http.static_login(token.strip()) self._connection.user = ClientUser(state=self._connection, data=data) @@ -1210,9 +1214,6 @@ async def start( TypeError An unexpected keyword argument was received. """ - self.loop = asyncio.get_running_loop() - self.http.loop = self.loop - self._connection.loop = self.loop try: await self.login(token) await self.connect(reconnect=reconnect) diff --git a/disnake/ext/commands/interaction_bot_base.py b/disnake/ext/commands/interaction_bot_base.py index 25308c3649..b2b6336256 100644 --- a/disnake/ext/commands/interaction_bot_base.py +++ b/disnake/ext/commands/interaction_bot_base.py @@ -219,10 +219,10 @@ def __init__( @disnake.utils.copy_doc(disnake.Client.login) async def login(self, token: str) -> None: - self._schedule_app_command_preparation() - await super().login(token) + self._schedule_app_command_preparation() + @property def command_sync_flags(self) -> CommandSyncFlags: """:class:`~.CommandSyncFlags`: The command sync flags configured for this bot. diff --git a/disnake/state.py b/disnake/state.py index ca915aa33f..4cf7a6cedb 100644 --- a/disnake/state.py +++ b/disnake/state.py @@ -191,7 +191,6 @@ def __init__( handlers: Dict[str, Callable], hooks: Dict[str, Callable], http: HTTPClient, - loop: asyncio.AbstractEventLoop, max_messages: Optional[int] = 1000, application_id: Optional[int] = None, heartbeat_timeout: float = 60.0, @@ -203,7 +202,8 @@ def __init__( chunk_guilds_at_startup: Optional[bool] = None, member_cache_flags: Optional[MemberCacheFlags] = None, ) -> None: - self.loop: asyncio.AbstractEventLoop = loop + # Set after Client.login + self.loop: asyncio.AbstractEventLoop = MISSING self.http: HTTPClient = http self.max_messages: Optional[int] = max_messages if self.max_messages is not None and self.max_messages <= 0: From 124890bbe3ac19b75d7fbbff7e0823dd3ffbefca Mon Sep 17 00:00:00 2001 From: elenakrittik Date: Tue, 7 Nov 2023 12:40:21 +0300 Subject: [PATCH 04/26] fix(test_bot): Update according to new constraints. --- test_bot/__main__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test_bot/__main__.py b/test_bot/__main__.py index 37c5afa288..6833e42d2b 100644 --- a/test_bot/__main__.py +++ b/test_bot/__main__.py @@ -52,6 +52,9 @@ async def on_ready(self) -> None: ) # fmt: on + async def setup_hook(self) -> None: + bot.load_extensions(os.path.join(__package__, Config.cogs_folder)) + def add_cog(self, cog: commands.Cog, *, override: bool = False) -> None: logger.info("Loading cog %s", cog.qualified_name) return super().add_cog(cog, override=override) @@ -98,5 +101,4 @@ async def on_message_command_error( if __name__ == "__main__": bot = TestBot() - bot.load_extensions(os.path.join(__package__, Config.cogs_folder)) bot.run(Config.token) From 8f85551cbf0d1b5cf09beac92a74fe7e727b38b2 Mon Sep 17 00:00:00 2001 From: elenakrittik Date: Tue, 7 Nov 2023 17:01:31 +0300 Subject: [PATCH 05/26] refactor: Further reduce loop state sharing. --- disnake/client.py | 49 +------------------- disnake/context_managers.py | 3 +- disnake/ext/commands/bot.py | 14 ------ disnake/ext/commands/cog.py | 2 +- disnake/ext/commands/common_bot_base.py | 5 +- disnake/ext/commands/cooldowns.py | 5 +- disnake/ext/commands/interaction_bot_base.py | 7 ++- disnake/ext/tasks/__init__.py | 17 ++++--- disnake/gateway.py | 26 ++++++----- disnake/http.py | 6 +-- disnake/player.py | 2 +- disnake/shard.py | 5 +- disnake/state.py | 14 ++---- disnake/ui/modal.py | 3 +- disnake/voice_client.py | 2 +- examples/basic_voice.py | 9 ++-- test_bot/__main__.py | 2 +- tests/ext/tasks/test_loops.py | 1 - 18 files changed, 52 insertions(+), 120 deletions(-) diff --git a/disnake/client.py b/disnake/client.py index eaddf1b5d3..2e1b5fb229 100644 --- a/disnake/client.py +++ b/disnake/client.py @@ -106,41 +106,6 @@ _log = logging.getLogger(__name__) -def _cancel_tasks(loop: asyncio.AbstractEventLoop) -> None: - tasks = {t for t in asyncio.all_tasks(loop=loop) if not t.done()} - - if not tasks: - return - - _log.info("Cleaning up after %d tasks.", len(tasks)) - for task in tasks: - task.cancel() - - loop.run_until_complete(asyncio.gather(*tasks, return_exceptions=True)) - _log.info("All tasks finished cancelling.") - - for task in tasks: - if task.cancelled(): - continue - if task.exception() is not None: - loop.call_exception_handler( - { - "message": "Unhandled exception during Client.run shutdown.", - "exception": task.exception(), - "task": task, - } - ) - - -def _cleanup_loop(loop: asyncio.AbstractEventLoop) -> None: - try: - _cancel_tasks(loop) - loop.run_until_complete(loop.shutdown_asyncgens()) - finally: - _log.info("Closing the event loop.") - loop.close() - - class SessionStartLimit: """A class that contains information about the current session start limit, at the time when the client connected for the first time. @@ -352,7 +317,6 @@ class Client: def __init__( self, *, - asyncio_debug: bool = False, shard_id: Optional[int] = None, shard_count: Optional[int] = None, enable_debug_events: bool = False, @@ -377,8 +341,6 @@ def __init__( # self.ws is set in the connect method self.ws: DiscordWebSocket = None # type: ignore - self.loop: asyncio.AbstractEventLoop = MISSING - self._listeners: Dict[str, List[Tuple[asyncio.Future, Callable[..., bool]]]] = {} self.session_start_limit: Optional[SessionStartLimit] = None @@ -386,7 +348,6 @@ def __init__( proxy=proxy, proxy_auth=proxy_auth, unsync_clock=assume_unsync_clock, - loop=self.loop, ) self._handlers: Dict[str, Callable] = { @@ -708,7 +669,7 @@ def _schedule_event( ) -> asyncio.Task: wrapped = self._run_event(coro, event_name, *args, **kwargs) # Schedules the task - return self.loop.create_task(wrapped, name=f"disnake: {event_name}") + return asyncio.create_task(wrapped, name=f"disnake: {event_name}") def dispatch(self, event: str, *args: Any, **kwargs: Any) -> None: _log.debug("Dispatching event %s", event) @@ -1005,11 +966,6 @@ async def login(self, token: str) -> None: if not isinstance(token, str): raise TypeError(f"token must be of type str, got {type(token).__name__} instead") - loop = asyncio.get_running_loop() - self.loop = loop - self.http.loop = loop - self._connection.loop = loop - data = await self.http.static_login(token.strip()) self._connection.user = ClientUser(state=self._connection, data=data) @@ -1188,7 +1144,6 @@ async def close(self) -> None: await self.http.close() self._ready.clear() - self.loop = MISSING def clear(self) -> None: """Clears the internal state of the bot. @@ -1755,7 +1710,7 @@ def check(reaction, user): arguments that mirrors the parameters passed in the :ref:`event `. """ - future = self.loop.create_future() + future = asyncio.get_running_loop().create_future() if check is None: def _check(*args) -> bool: diff --git a/disnake/context_managers.py b/disnake/context_managers.py index 64f409183c..20b9f331c5 100644 --- a/disnake/context_managers.py +++ b/disnake/context_managers.py @@ -26,7 +26,6 @@ def _typing_done_callback(fut: asyncio.Future) -> None: class Typing: def __init__(self, messageable: Union[Messageable, ForumChannel]) -> None: - self.loop: asyncio.AbstractEventLoop = messageable._state.loop self.messageable: Union[Messageable, ForumChannel] = messageable async def do_typing(self) -> None: @@ -42,7 +41,7 @@ async def do_typing(self) -> None: await asyncio.sleep(5) def __enter__(self) -> Self: - self.task: asyncio.Task = self.loop.create_task(self.do_typing()) + self.task: asyncio.Task = asyncio.create_task(self.do_typing()) self.task.add_done_callback(_typing_done_callback) return self diff --git a/disnake/ext/commands/bot.py b/disnake/ext/commands/bot.py index 5c3ba59eac..1f608696f3 100644 --- a/disnake/ext/commands/bot.py +++ b/disnake/ext/commands/bot.py @@ -10,8 +10,6 @@ from .interaction_bot_base import InteractionBotBase if TYPE_CHECKING: - import asyncio - import aiohttp from typing_extensions import Self @@ -237,14 +235,11 @@ def __init__( sync_commands: bool = ..., sync_commands_debug: bool = ..., sync_commands_on_cog_unload: bool = ..., - asyncio_debug: bool = False, - loop: Optional[asyncio.AbstractEventLoop] = None, shard_id: Optional[int] = None, shard_count: Optional[int] = None, enable_debug_events: bool = False, enable_gateway_error_handler: bool = True, gateway_params: Optional[GatewayParams] = None, - connector: Optional[aiohttp.BaseConnector] = None, proxy: Optional[str] = None, proxy_auth: Optional[aiohttp.BasicAuth] = None, assume_unsync_clock: bool = True, @@ -289,14 +284,11 @@ def __init__( sync_commands: bool = ..., sync_commands_debug: bool = ..., sync_commands_on_cog_unload: bool = ..., - asyncio_debug: bool = False, - loop: Optional[asyncio.AbstractEventLoop] = None, shard_ids: Optional[List[int]] = None, # instead of shard_id shard_count: Optional[int] = None, enable_debug_events: bool = False, enable_gateway_error_handler: bool = True, gateway_params: Optional[GatewayParams] = None, - connector: Optional[aiohttp.BaseConnector] = None, proxy: Optional[str] = None, proxy_auth: Optional[aiohttp.BasicAuth] = None, assume_unsync_clock: bool = True, @@ -438,14 +430,11 @@ def __init__( sync_commands: bool = ..., sync_commands_debug: bool = ..., sync_commands_on_cog_unload: bool = ..., - asyncio_debug: bool = False, - loop: Optional[asyncio.AbstractEventLoop] = None, shard_id: Optional[int] = None, shard_count: Optional[int] = None, enable_debug_events: bool = False, enable_gateway_error_handler: bool = True, gateway_params: Optional[GatewayParams] = None, - connector: Optional[aiohttp.BaseConnector] = None, proxy: Optional[str] = None, proxy_auth: Optional[aiohttp.BasicAuth] = None, assume_unsync_clock: bool = True, @@ -483,14 +472,11 @@ def __init__( sync_commands: bool = ..., sync_commands_debug: bool = ..., sync_commands_on_cog_unload: bool = ..., - asyncio_debug: bool = False, - loop: Optional[asyncio.AbstractEventLoop] = None, shard_ids: Optional[List[int]] = None, # instead of shard_id shard_count: Optional[int] = None, enable_debug_events: bool = False, enable_gateway_error_handler: bool = True, gateway_params: Optional[GatewayParams] = None, - connector: Optional[aiohttp.BaseConnector] = None, proxy: Optional[str] = None, proxy_auth: Optional[aiohttp.BasicAuth] = None, assume_unsync_clock: bool = True, diff --git a/disnake/ext/commands/cog.py b/disnake/ext/commands/cog.py index a0305ccea7..d2d70a676e 100644 --- a/disnake/ext/commands/cog.py +++ b/disnake/ext/commands/cog.py @@ -772,7 +772,7 @@ def _inject(self, bot: AnyBot) -> Self: raise if not hasattr(self.cog_load.__func__, "__cog_special_method__"): - bot.loop.create_task(disnake.utils.maybe_coroutine(self.cog_load)) + asyncio.create_task(disnake.utils.maybe_coroutine(self.cog_load)) # check if we're overriding the default if cls.bot_check is not Cog.bot_check: diff --git a/disnake/ext/commands/common_bot_base.py b/disnake/ext/commands/common_bot_base.py index f0d8fd5566..154d149db5 100644 --- a/disnake/ext/commands/common_bot_base.py +++ b/disnake/ext/commands/common_bot_base.py @@ -110,12 +110,11 @@ async def close(self) -> None: async def login(self, token: str) -> None: await super().login(token=token) # type: ignore - loop: asyncio.AbstractEventLoop = self.loop # type: ignore if self.reload: - loop.create_task(self._watchdog()) + asyncio.create_task(self._watchdog()) # prefetch - loop.create_task(self._fill_owners()) + asyncio.create_task(self._fill_owners()) async def is_owner(self, user: Union[disnake.User, disnake.Member]) -> bool: """|coro| diff --git a/disnake/ext/commands/cooldowns.py b/disnake/ext/commands/cooldowns.py index 4268f76fff..55ca3a92fe 100644 --- a/disnake/ext/commands/cooldowns.py +++ b/disnake/ext/commands/cooldowns.py @@ -279,11 +279,10 @@ class _Semaphore: overkill for what is basically a counter. """ - __slots__ = ("value", "loop", "_waiters") + __slots__ = ("value", "_waiters") def __init__(self, number: int) -> None: self.value: int = number - self.loop: asyncio.AbstractEventLoop = asyncio.get_running_loop() self._waiters: Deque[asyncio.Future] = deque() def __repr__(self) -> str: @@ -308,7 +307,7 @@ async def acquire(self, *, wait: bool = False) -> bool: return False while self.value <= 0: - future = self.loop.create_future() + future = asyncio.get_running_loop().create_future() self._waiters.append(future) try: await future diff --git a/disnake/ext/commands/interaction_bot_base.py b/disnake/ext/commands/interaction_bot_base.py index b2b6336256..c7f92a8ced 100644 --- a/disnake/ext/commands/interaction_bot_base.py +++ b/disnake/ext/commands/interaction_bot_base.py @@ -790,7 +790,7 @@ async def _sync_application_commands(self) -> None: if not isinstance(self, disnake.Client): raise NotImplementedError("This method is only usable in disnake.Client subclasses") - if not self._command_sync_flags._sync_enabled or self._is_closed or self.loop.is_closed(): + if not self._command_sync_flags._sync_enabled or self._is_closed: return # We assume that all commands are already cached. @@ -894,7 +894,6 @@ async def _delayed_command_sync(self) -> None: or self._sync_queued.locked() or not self.is_ready() or self._is_closed - or self.loop.is_closed() ): return # We don't do this task on login or in parallel with a similar task @@ -907,7 +906,7 @@ def _schedule_app_command_preparation(self) -> None: if not isinstance(self, disnake.Client): raise NotImplementedError("Command sync is only possible in disnake.Client subclasses") - self.loop.create_task( + asyncio.create_task( self._prepare_application_commands(), name="disnake: app_command_preparation" ) @@ -915,7 +914,7 @@ def _schedule_delayed_command_sync(self) -> None: if not isinstance(self, disnake.Client): raise NotImplementedError("This method is only usable in disnake.Client subclasses") - self.loop.create_task(self._delayed_command_sync(), name="disnake: delayed_command_sync") + asyncio.create_task(self._delayed_command_sync(), name="disnake: delayed_command_sync") # Error handlers diff --git a/disnake/ext/tasks/__init__.py b/disnake/ext/tasks/__init__.py index b3c7a11113..fbeef886b3 100644 --- a/disnake/ext/tasks/__init__.py +++ b/disnake/ext/tasks/__init__.py @@ -49,18 +49,21 @@ class SleepHandle: - __slots__ = ("future", "loop", "handle") + __slots__ = ("future", "handle") - def __init__(self, dt: datetime.datetime, *, loop: asyncio.AbstractEventLoop) -> None: - self.loop = loop - self.future: asyncio.Future[bool] = loop.create_future() + def __init__(self, dt: datetime.datetime) -> None: + self.future: asyncio.Future[bool] = asyncio.get_running_loop().create_future() relative_delta = disnake.utils.compute_timedelta(dt) - self.handle = loop.call_later(relative_delta, self.future.set_result, True) + self.handle = asyncio.get_running_loop().call_later( + relative_delta, self.future.set_result, True + ) def recalculate(self, dt: datetime.datetime) -> None: self.handle.cancel() relative_delta = disnake.utils.compute_timedelta(dt) - self.handle = self.loop.call_later(relative_delta, self.future.set_result, True) + self.handle = asyncio.get_running_loop().call_later( + relative_delta, self.future.set_result, True + ) def wait(self) -> asyncio.Future[bool]: return self.future @@ -136,7 +139,7 @@ async def _call_loop_function(self, name: str, *args: Any, **kwargs: Any) -> Non await coro(*args, **kwargs) def _try_sleep_until(self, dt: datetime.datetime): - self._handle = SleepHandle(dt=dt, loop=asyncio.get_running_loop()) + self._handle = SleepHandle(dt=dt) return self._handle.wait() async def _loop(self, *args: Any, **kwargs: Any) -> None: diff --git a/disnake/gateway.py b/disnake/gateway.py index 2081493509..050bdb15f1 100644 --- a/disnake/gateway.py +++ b/disnake/gateway.py @@ -170,6 +170,7 @@ def __init__( *args: Any, ws: HeartbeatWebSocket, interval: float, + loop: asyncio.AbstractEventLoop, shard_id: Optional[int] = None, **kwargs: Any, ) -> None: @@ -177,6 +178,7 @@ def __init__( self.ws: HeartbeatWebSocket = ws self._main_thread_id: int = ws.thread_id self.interval: float = interval + self.loop = loop self.daemon: bool = True self.shard_id: Optional[int] = shard_id self.msg = "Keeping shard ID %s websocket alive with sequence %s." @@ -197,7 +199,7 @@ def run(self) -> None: self.shard_id, ) coro = self.ws.close(4000) - f = asyncio.run_coroutine_threadsafe(coro, loop=self.ws.loop) + f = asyncio.run_coroutine_threadsafe(coro, loop=self.loop) try: f.result() @@ -210,7 +212,7 @@ def run(self) -> None: data = self.get_payload() _log.debug(self.msg, self.shard_id, data["d"]) coro = self.ws.send_heartbeat(data) - f = asyncio.run_coroutine_threadsafe(coro, loop=self.ws.loop) + f = asyncio.run_coroutine_threadsafe(coro, loop=self.loop) try: # block until sending is complete total = 0 @@ -277,7 +279,6 @@ class HeartbeatWebSocket(Protocol): HEARTBEAT: Final[Literal[1, 3]] # type: ignore thread_id: int - loop: asyncio.AbstractEventLoop _max_heartbeat_timeout: float async def close(self, code: int) -> None: @@ -345,10 +346,10 @@ class DiscordWebSocket: GUILD_SYNC: Final[Literal[12]] = 12 def __init__( - self, socket: aiohttp.ClientWebSocketResponse, *, loop: asyncio.AbstractEventLoop + self, + socket: aiohttp.ClientWebSocketResponse, ) -> None: self.socket: aiohttp.ClientWebSocketResponse = socket - self.loop: asyncio.AbstractEventLoop = loop # an empty dispatcher to prevent crashes self._dispatch: DispatchFunc = lambda event, *args: None @@ -420,7 +421,7 @@ async def from_client( gateway = await client.http.get_gateway(encoding=params.encoding, zlib=params.zlib) socket = await client.http.ws_connect(gateway) - ws = cls(socket, loop=client.loop) + ws = cls(socket) # dynamically add attributes needed ws.token = client.http.token # type: ignore @@ -483,7 +484,7 @@ def wait_for( asyncio.Future A future to wait for. """ - future = self.loop.create_future() + future = asyncio.get_running_loop().create_future() entry = EventListener(event=event, predicate=predicate, result=result, future=future) self._dispatch_listeners.append(entry) return future @@ -587,7 +588,10 @@ async def received_message(self, raw_msg: Union[str, bytes], /) -> None: if op == self.HELLO: interval: float = data["heartbeat_interval"] / 1000.0 self._keep_alive = KeepAliveHandler( - ws=self, interval=interval, shard_id=self.shard_id + ws=self, + interval=interval, + shard_id=self.shard_id, + loop=asyncio.get_running_loop(), ) # send a heartbeat immediately await self.send_as_json(self._keep_alive.get_payload()) @@ -889,12 +893,10 @@ class DiscordVoiceWebSocket: def __init__( self, socket: aiohttp.ClientWebSocketResponse, - loop: asyncio.AbstractEventLoop, *, hook: Optional[HookFunc] = None, ) -> None: self.ws: aiohttp.ClientWebSocketResponse = socket - self.loop: asyncio.AbstractEventLoop = loop self._keep_alive: Optional[VoiceKeepAliveHandler] = None self._close_code: Optional[int] = None self.secret_key: Optional[List[int]] = None @@ -956,7 +958,7 @@ async def from_client( gateway = f"wss://{client.endpoint}/?v=4" http = client._state.http socket = await http.ws_connect(gateway, compress=15) - ws = cls(socket, loop=client.loop, hook=hook) + ws = cls(socket, hook=hook) ws.gateway = gateway ws._connection = client ws._max_heartbeat_timeout = 60.0 @@ -1026,7 +1028,7 @@ async def initial_connection(self, data: VoiceReadyPayload) -> None: struct.pack_into(">H", packet, 2, 70) # 70 = Length struct.pack_into(">I", packet, 4, state.ssrc) state.socket.sendto(packet, (state.endpoint_ip, state.voice_port)) - recv = await self.loop.sock_recv(state.socket, 74) + recv = await asyncio.get_running_loop().sock_recv(state.socket, 74) _log.debug("received packet in initial_connection: %s", recv) # the ip is ascii starting at the 8th byte and ending at the first null diff --git a/disnake/http.py b/disnake/http.py index 23c65dc71e..0f44015b4e 100644 --- a/disnake/http.py +++ b/disnake/http.py @@ -221,12 +221,10 @@ def __init__( self, connector: Optional[aiohttp.BaseConnector] = None, *, - loop: asyncio.AbstractEventLoop, proxy: Optional[str] = None, proxy_auth: Optional[aiohttp.BasicAuth] = None, unsync_clock: bool = True, ) -> None: - self.loop: asyncio.AbstractEventLoop = loop self.connector = connector or MISSING self.__session: aiohttp.ClientSession = MISSING # filled in static_login self._locks: weakref.WeakValueDictionary[str, asyncio.Lock] = weakref.WeakValueDictionary() @@ -360,7 +358,7 @@ async def request( delta, ) maybe_lock.defer() - self.loop.call_later(delta, lock.release) + asyncio.get_running_loop().call_later(delta, lock.release) # the request was successful so just return the text/json if 300 > response.status >= 200: @@ -453,7 +451,7 @@ async def close(self) -> None: async def static_login(self, token: str) -> user.User: # Necessary to get aiohttp to stop complaining about session creation if self.connector is MISSING: - self.connector = aiohttp.TCPConnector(loop=self.loop, limit=0) + self.connector = aiohttp.TCPConnector(limit=0) self.__session = aiohttp.ClientSession( connector=self.connector, ws_response_class=DiscordClientWebSocketResponse ) diff --git a/disnake/player.py b/disnake/player.py index 83c5a9e866..a1dc4d52b0 100644 --- a/disnake/player.py +++ b/disnake/player.py @@ -783,7 +783,7 @@ def _set_source(self, source: AudioSource) -> None: def _speak(self, speaking: bool) -> None: try: asyncio.run_coroutine_threadsafe( - self.client.ws.speak(speaking), self.client.client.loop + self.client.ws.speak(speaking), asyncio.get_running_loop() ) except Exception as e: _log.info("Speaking call in player failed: %s", e) diff --git a/disnake/shard.py b/disnake/shard.py index 7d7edf03f3..3ec418dba1 100644 --- a/disnake/shard.py +++ b/disnake/shard.py @@ -112,7 +112,7 @@ def id(self) -> int: return self.ws.shard_id # type: ignore def launch(self) -> None: - self._task = self._client.loop.create_task(self.worker()) + self._task = asyncio.create_task(self.worker()) def _cancel_task(self) -> None: if self._task is not None and not self._task.done(): @@ -515,7 +515,8 @@ async def close(self) -> None: pass to_close = [ - asyncio.ensure_future(shard.close(), loop=self.loop) for shard in self.__shards.values() + asyncio.ensure_future(shard.close(), loop=asyncio.get_running_loop()) + for shard in self.__shards.values() ] if to_close: await asyncio.wait(to_close) diff --git a/disnake/state.py b/disnake/state.py index 4cf7a6cedb..f9b3e4fa60 100644 --- a/disnake/state.py +++ b/disnake/state.py @@ -113,14 +113,12 @@ class ChunkRequest: def __init__( self, guild_id: int, - loop: asyncio.AbstractEventLoop, resolver: Callable[[int], Any], *, cache: bool = True, ) -> None: self.guild_id: int = guild_id self.resolver: Callable[[int], Any] = resolver - self.loop: asyncio.AbstractEventLoop = loop self.cache: bool = cache self.nonce: str = os.urandom(16).hex() self.buffer: List[Member] = [] @@ -139,7 +137,7 @@ def add_members(self, members: List[Member]) -> None: guild._add_member(member) async def wait(self) -> List[Member]: - future = self.loop.create_future() + future = asyncio.get_running_loop().create_future() self.waiters.append(future) try: return await future @@ -147,7 +145,7 @@ async def wait(self) -> List[Member]: self.waiters.remove(future) def get_future(self) -> asyncio.Future[List[Member]]: - future = self.loop.create_future() + future = asyncio.get_running_loop().create_future() self.waiters.append(future) return future @@ -202,8 +200,6 @@ def __init__( chunk_guilds_at_startup: Optional[bool] = None, member_cache_flags: Optional[MemberCacheFlags] = None, ) -> None: - # Set after Client.login - self.loop: asyncio.AbstractEventLoop = MISSING self.http: HTTPClient = http self.max_messages: Optional[int] = max_messages if self.max_messages is not None and self.max_messages <= 0: @@ -640,7 +636,7 @@ async def query_members( if ws is None: raise RuntimeError("Somehow do not have a websocket for this guild_id") - request = ChunkRequest(guild.id, self.loop, self._get_guild, cache=cache) + request = ChunkRequest(guild.id, self._get_guild, cache=cache) self._chunk_requests[request.nonce] = request try: @@ -1393,7 +1389,7 @@ async def chunk_guild( request = self._chunk_requests.get(guild.id) if request is None: self._chunk_requests[guild.id] = request = ChunkRequest( - guild.id, self.loop, self._get_guild, cache=cache + guild.id, self._get_guild, cache=cache ) await self.chunker(guild.id, nonce=request.nonce) @@ -2224,7 +2220,7 @@ async def _delay_ready(self) -> None: future = asyncio.ensure_future(self.chunk_guild(guild)) current_bucket.append(future) else: - future = self.loop.create_future() + future = asyncio.get_running_loop().create_future() future.set_result([]) processed.append((guild, future)) diff --git a/disnake/ui/modal.py b/disnake/ui/modal.py index a7a5503a28..b0c0ee7313 100644 --- a/disnake/ui/modal.py +++ b/disnake/ui/modal.py @@ -232,9 +232,8 @@ def __init__(self, state: ConnectionState) -> None: self._modals: Dict[Tuple[int, str], Modal] = {} def add_modal(self, user_id: int, modal: Modal) -> None: - loop = asyncio.get_running_loop() self._modals[(user_id, modal.custom_id)] = modal - loop.create_task(self.handle_timeout(user_id, modal.custom_id, modal.timeout)) + asyncio.create_task(self.handle_timeout(user_id, modal.custom_id, modal.timeout)) def remove_modal(self, user_id: int, modal_custom_id: str) -> Modal: return self._modals.pop((user_id, modal_custom_id)) diff --git a/disnake/voice_client.py b/disnake/voice_client.py index a49ad695dd..e093f3c66a 100644 --- a/disnake/voice_client.py +++ b/disnake/voice_client.py @@ -373,7 +373,7 @@ async def connect(self, *, reconnect: bool, timeout: float) -> None: raise if self._runner is MISSING: - self._runner = self.client.loop.create_task(self.poll_voice_ws(reconnect)) + self._runner = asyncio.create_task(self.poll_voice_ws(reconnect)) async def potential_reconnect(self) -> bool: # Attempt to stop the player thread from playing early diff --git a/examples/basic_voice.py b/examples/basic_voice.py index 6d224b21e5..478cf468c8 100644 --- a/examples/basic_voice.py +++ b/examples/basic_voice.py @@ -45,11 +45,8 @@ def __init__(self, source: disnake.AudioSource, *, data: Dict[str, Any], volume: self.title = data.get("title") @classmethod - async def from_url( - cls, url, *, loop: Optional[asyncio.AbstractEventLoop] = None, stream: bool = False - ): - loop = loop or asyncio.get_event_loop() - data: Any = await loop.run_in_executor( + async def from_url(cls, url, *, stream: bool = False): + data: Any = await asyncio.get_running_loop().run_in_executor( None, lambda: ytdl.extract_info(url, download=not stream) ) @@ -96,7 +93,7 @@ async def stream(self, ctx, *, url: str): async def _play_url(self, ctx, *, url: str, stream: bool): await self.ensure_voice(ctx) async with ctx.typing(): - player = await YTDLSource.from_url(url, loop=self.bot.loop, stream=stream) + player = await YTDLSource.from_url(url, stream=stream) ctx.voice_client.play( player, after=lambda e: print(f"Player error: {e}") if e else None ) diff --git a/test_bot/__main__.py b/test_bot/__main__.py index 6833e42d2b..1eb9e254b4 100644 --- a/test_bot/__main__.py +++ b/test_bot/__main__.py @@ -53,7 +53,7 @@ async def on_ready(self) -> None: # fmt: on async def setup_hook(self) -> None: - bot.load_extensions(os.path.join(__package__, Config.cogs_folder)) + self.load_extensions(os.path.join(__package__, Config.cogs_folder)) def add_cog(self, cog: commands.Cog, *, override: bool = False) -> None: logger.info("Loading cog %s", cog.qualified_name) diff --git a/tests/ext/tasks/test_loops.py b/tests/ext/tasks/test_loops.py index 796b16f0c5..c5103db92c 100644 --- a/tests/ext/tasks/test_loops.py +++ b/tests/ext/tasks/test_loops.py @@ -49,7 +49,6 @@ def clone(self): instance._time = self._time instance.count = self.count instance.reconnect = self.reconnect - instance.loop = self.loop instance._before_loop = self._before_loop instance._after_loop = self._after_loop instance._error = self._error From 26c4663d9a155ecd50afd29483bf2d92539b777c Mon Sep 17 00:00:00 2001 From: elenakrittik Date: Tue, 7 Nov 2023 17:14:06 +0300 Subject: [PATCH 06/26] misc(debug): Set a name for hearbeat thread. --- disnake/gateway.py | 1 + 1 file changed, 1 insertion(+) diff --git a/disnake/gateway.py b/disnake/gateway.py index 050bdb15f1..f188321ab0 100644 --- a/disnake/gateway.py +++ b/disnake/gateway.py @@ -593,6 +593,7 @@ async def received_message(self, raw_msg: Union[str, bytes], /) -> None: shard_id=self.shard_id, loop=asyncio.get_running_loop(), ) + self._keep_alive.name = "disnake heartbeat thread" # send a heartbeat immediately await self.send_as_json(self._keep_alive.get_payload()) self._keep_alive.start() From 213edbabba4e178fe6ec63e741fd1f5fc95704bc Mon Sep 17 00:00:00 2001 From: elenakrittik Date: Tue, 7 Nov 2023 19:17:21 +0300 Subject: [PATCH 07/26] compat: Add Client.loop property for compatibility. --- disnake/client.py | 22 ++++++++++++++++++++++ disnake/gateway.py | 2 +- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/disnake/client.py b/disnake/client.py index 2e1b5fb229..ad63a7542f 100644 --- a/disnake/client.py +++ b/disnake/client.py @@ -27,6 +27,7 @@ Union, overload, ) +import warnings import aiohttp @@ -77,6 +78,7 @@ from .widget import Widget if TYPE_CHECKING: + from typing_extensions import Never from .abc import GuildChannel, PrivateChannel, Snowflake, SnowflakeTime from .app_commands import APIApplicationCommand from .asset import AssetBytes @@ -450,6 +452,26 @@ def _handle_first_connect(self) -> None: return self._first_connect.set() + @property + def loop(self): + """:class:`asyncio.AbstractEventLoop`: Same as :func:`asyncio.get_running_loop`. + + .. deprecated:: 2.10 + Use :func:`asyncio.get_running_loop` directly. + """ + warnings.warn( + "Accessing `Client.loop` is deprecated. Use `asyncio.get_running_loop()` instead.", + category=DeprecationWarning, + ) + return asyncio.get_running_loop() + + @loop.setter + def loop(self, _value: Never): + warnings.warn( + "Setting `Client.loop` is deprecated and has no effect. Use `asyncio.get_running_loop()` instead.", + category=DeprecationWarning, + ) + @property def latency(self) -> float: """:class:`float`: Measures latency between a HEARTBEAT and a HEARTBEAT_ACK in seconds. diff --git a/disnake/gateway.py b/disnake/gateway.py index f188321ab0..6ed22185c0 100644 --- a/disnake/gateway.py +++ b/disnake/gateway.py @@ -591,7 +591,7 @@ async def received_message(self, raw_msg: Union[str, bytes], /) -> None: ws=self, interval=interval, shard_id=self.shard_id, - loop=asyncio.get_running_loop(), + loop=asyncio.get_running_loop(), # share loop to the thread ) self._keep_alive.name = "disnake heartbeat thread" # send a heartbeat immediately From e76ce01e1f015628e4eaf46c51b1abf2f16d49db Mon Sep 17 00:00:00 2001 From: elenakrittik Date: Sun, 12 Nov 2023 12:43:02 +0300 Subject: [PATCH 08/26] compat: Bring back connector and revert behavior changes. --- disnake/client.py | 21 ++++++++++++++++++-- disnake/ext/commands/bot.py | 4 ++++ disnake/ext/commands/interaction_bot_base.py | 4 ++-- disnake/http.py | 2 +- 4 files changed, 26 insertions(+), 5 deletions(-) diff --git a/disnake/client.py b/disnake/client.py index ad63a7542f..bafdbb41ee 100644 --- a/disnake/client.py +++ b/disnake/client.py @@ -7,6 +7,7 @@ import sys import traceback import types +import warnings from datetime import datetime, timedelta from errno import ECONNRESET from typing import ( @@ -27,7 +28,6 @@ Union, overload, ) -import warnings import aiohttp @@ -79,6 +79,7 @@ if TYPE_CHECKING: from typing_extensions import Never + from .abc import GuildChannel, PrivateChannel, Snowflake, SnowflakeTime from .app_commands import APIApplicationCommand from .asset import AssetBytes @@ -189,6 +190,8 @@ class Client: asyncio_debug: :class:`bool` Whether to enable asyncio debugging when the client starts. Defaults to False. + connector: Optional[:class:`aiohttp.BaseConnector`] + The connector to use for connection pooling. proxy: Optional[:class:`str`] Proxy URL. proxy_auth: Optional[:class:`aiohttp.BasicAuth`] @@ -326,6 +329,7 @@ def __init__( localization_provider: Optional[LocalizationProtocol] = None, strict_localization: bool = False, gateway_params: Optional[GatewayParams] = None, + connector: Optional[aiohttp.BaseConnector] = None, proxy: Optional[str] = None, proxy_auth: Optional[aiohttp.BasicAuth] = None, assume_unsync_clock: bool = True, @@ -346,7 +350,19 @@ def __init__( self._listeners: Dict[str, List[Tuple[asyncio.Future, Callable[..., bool]]]] = {} self.session_start_limit: Optional[SessionStartLimit] = None + if connector: + try: + asyncio.get_running_loop() + except RuntimeError: + raise RuntimeError( + ( + "`connector` was created outside of an asyncio loop consider moving bot class " + "instantiation to an async main function and then manually asyncio.run it" + ) + ) from None + self.http: HTTPClient = HTTPClient( + connector, proxy=proxy, proxy_auth=proxy_auth, unsync_clock=assume_unsync_clock, @@ -967,7 +983,8 @@ async def before_identify_hook(self, shard_id: Optional[int], *, initial: bool = async def login(self, token: str) -> None: """|coro| - Logs in the client with the specified credentials. + Logs in the client with the specified credentials and calls + :meth:`.setup_hook`. Parameters ---------- diff --git a/disnake/ext/commands/bot.py b/disnake/ext/commands/bot.py index 1f608696f3..5a9cd72ed9 100644 --- a/disnake/ext/commands/bot.py +++ b/disnake/ext/commands/bot.py @@ -240,6 +240,7 @@ def __init__( enable_debug_events: bool = False, enable_gateway_error_handler: bool = True, gateway_params: Optional[GatewayParams] = None, + connector: Optional[aiohttp.BaseConnector] = None, proxy: Optional[str] = None, proxy_auth: Optional[aiohttp.BasicAuth] = None, assume_unsync_clock: bool = True, @@ -289,6 +290,7 @@ def __init__( enable_debug_events: bool = False, enable_gateway_error_handler: bool = True, gateway_params: Optional[GatewayParams] = None, + connector: Optional[aiohttp.BaseConnector] = None, proxy: Optional[str] = None, proxy_auth: Optional[aiohttp.BasicAuth] = None, assume_unsync_clock: bool = True, @@ -435,6 +437,7 @@ def __init__( enable_debug_events: bool = False, enable_gateway_error_handler: bool = True, gateway_params: Optional[GatewayParams] = None, + connector: Optional[aiohttp.BaseConnector] = None, proxy: Optional[str] = None, proxy_auth: Optional[aiohttp.BasicAuth] = None, assume_unsync_clock: bool = True, @@ -477,6 +480,7 @@ def __init__( enable_debug_events: bool = False, enable_gateway_error_handler: bool = True, gateway_params: Optional[GatewayParams] = None, + connector: Optional[aiohttp.BaseConnector] = None, proxy: Optional[str] = None, proxy_auth: Optional[aiohttp.BasicAuth] = None, assume_unsync_clock: bool = True, diff --git a/disnake/ext/commands/interaction_bot_base.py b/disnake/ext/commands/interaction_bot_base.py index c7f92a8ced..cca8248314 100644 --- a/disnake/ext/commands/interaction_bot_base.py +++ b/disnake/ext/commands/interaction_bot_base.py @@ -219,10 +219,10 @@ def __init__( @disnake.utils.copy_doc(disnake.Client.login) async def login(self, token: str) -> None: - await super().login(token) - self._schedule_app_command_preparation() + await super().login(token) + @property def command_sync_flags(self) -> CommandSyncFlags: """:class:`~.CommandSyncFlags`: The command sync flags configured for this bot. diff --git a/disnake/http.py b/disnake/http.py index 0f44015b4e..891e09e8d8 100644 --- a/disnake/http.py +++ b/disnake/http.py @@ -449,9 +449,9 @@ async def close(self) -> None: # login management async def static_login(self, token: str) -> user.User: - # Necessary to get aiohttp to stop complaining about session creation if self.connector is MISSING: self.connector = aiohttp.TCPConnector(limit=0) + # Necessary to get aiohttp to stop complaining about session creation self.__session = aiohttp.ClientSession( connector=self.connector, ws_response_class=DiscordClientWebSocketResponse ) From fb8b1f2880fa44c376ec15c9328da5bbffc356e0 Mon Sep 17 00:00:00 2001 From: elenakrittik Date: Sun, 12 Nov 2023 12:59:00 +0300 Subject: [PATCH 09/26] fix: ruff errors --- disnake/client.py | 2 ++ examples/basic_voice.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/disnake/client.py b/disnake/client.py index bafdbb41ee..59994691e2 100644 --- a/disnake/client.py +++ b/disnake/client.py @@ -478,6 +478,7 @@ def loop(self): warnings.warn( "Accessing `Client.loop` is deprecated. Use `asyncio.get_running_loop()` instead.", category=DeprecationWarning, + stacklevel=2, ) return asyncio.get_running_loop() @@ -486,6 +487,7 @@ def loop(self, _value: Never): warnings.warn( "Setting `Client.loop` is deprecated and has no effect. Use `asyncio.get_running_loop()` instead.", category=DeprecationWarning, + stacklevel=2, ) @property diff --git a/examples/basic_voice.py b/examples/basic_voice.py index 478cf468c8..72f963f34d 100644 --- a/examples/basic_voice.py +++ b/examples/basic_voice.py @@ -9,7 +9,7 @@ import asyncio import os -from typing import Any, Dict, Optional +from typing import Any, Dict import disnake import youtube_dl # type: ignore From f812d1ce0e037055b2cc7747869990a43210ca83 Mon Sep 17 00:00:00 2001 From: elenakrittik Date: Sun, 12 Nov 2023 13:32:32 +0300 Subject: [PATCH 10/26] docs: Remove syncio_debug param from Client. --- disnake/client.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/disnake/client.py b/disnake/client.py index 59994691e2..c03a34d0e3 100644 --- a/disnake/client.py +++ b/disnake/client.py @@ -187,9 +187,6 @@ class Client: .. versionchanged:: 1.3 Allow disabling the message cache and change the default size to ``1000``. - asyncio_debug: :class:`bool` - Whether to enable asyncio debugging when the client starts. - Defaults to False. connector: Optional[:class:`aiohttp.BaseConnector`] The connector to use for connection pooling. proxy: Optional[:class:`str`] From c3988ff4fe8eaf2c1b00701b12733614b838184c Mon Sep 17 00:00:00 2001 From: elenakrittik Date: Sun, 12 Nov 2023 18:13:42 +0300 Subject: [PATCH 11/26] refactor: Async extensions (& cogs). --- disnake/client.py | 22 +--------- disnake/ext/commands/bot_base.py | 4 +- disnake/ext/commands/cog.py | 24 ++++------- disnake/ext/commands/common_bot_base.py | 55 +++++++++++++------------ docs/api/events.rst | 18 ++++++++ examples/basic_voice.py | 5 ++- examples/interactions/subcmd.py | 5 ++- test_bot/__main__.py | 8 ++-- test_bot/cogs/events.py | 4 +- test_bot/cogs/guild_scheduled_events.py | 4 +- test_bot/cogs/injections.py | 4 +- test_bot/cogs/localization.py | 4 +- test_bot/cogs/message_commands.py | 4 +- test_bot/cogs/misc.py | 4 +- test_bot/cogs/modals.py | 4 +- test_bot/cogs/slash_commands.py | 4 +- test_bot/cogs/user_commands.py | 4 +- tests/test_events.py | 13 +++--- 18 files changed, 96 insertions(+), 94 deletions(-) diff --git a/disnake/client.py b/disnake/client.py index c03a34d0e3..c69b2937d7 100644 --- a/disnake/client.py +++ b/disnake/client.py @@ -1007,7 +1007,7 @@ async def login(self, token: str) -> None: data = await self.http.static_login(token.strip()) self._connection.user = ClientUser(state=self._connection, data=data) - await self.setup_hook() + self.dispatch("setup") async def connect( self, *, reconnect: bool = True, ignore_session_start_limit: bool = False @@ -1214,26 +1214,6 @@ async def start( if not self.is_closed(): await self.close() - async def setup_hook(self) -> None: - """A coroutine to be called to setup the bot. - - To perform asynchronous setup after the bot is logged in but before - it has connected to the websocket, override this. - - This is only called once, in :meth:`.login`, before any events are - dispatched, making it a better solution than doing such setup in - the :func:`disnake.on_ready` event. - - .. warning:: - Since this is called *before* the websocket connection is made, - anything that waits for the websocket will deadlock, which includes - methods like :meth:`.wait_for`, :meth:`.wait_until_ready` and - :meth:`.wait_until_first_connect`. - - .. versionadded:: 2.10 - """ - pass - def run(self, *args: Any, **kwargs: Any) -> None: """A blocking call that abstracts away the event loop initialisation from you. diff --git a/disnake/ext/commands/bot_base.py b/disnake/ext/commands/bot_base.py index d55dc63490..7f0fea41bc 100644 --- a/disnake/ext/commands/bot_base.py +++ b/disnake/ext/commands/bot_base.py @@ -410,8 +410,8 @@ def after_invoke(self, coro: CFT) -> CFT: # extensions - def _remove_module_references(self, name: str) -> None: - super()._remove_module_references(name) + async def _remove_module_references(self, name: str) -> None: + await super()._remove_module_references(name) # remove all the commands from the module for cmd in self.all_commands.copy().values(): if cmd.module is not None and _is_submodule(name, cmd.module): diff --git a/disnake/ext/commands/cog.py b/disnake/ext/commands/cog.py index d2d70a676e..1bab324853 100644 --- a/disnake/ext/commands/cog.py +++ b/disnake/ext/commands/cog.py @@ -476,18 +476,12 @@ def has_message_error_handler(self) -> bool: @_cog_special_method async def cog_load(self) -> None: - """A special method that is called as a task when the cog is added.""" + """A special method that is called when the cog is added.""" pass @_cog_special_method - def cog_unload(self) -> None: - """A special method that is called when the cog gets removed. - - This function **cannot** be a coroutine. It must be a regular - function. - - Subclasses must replace this if they want special unloading behaviour. - """ + async def cog_unload(self) -> None: + """A special method that is called when the cog gets removed.""" pass @_cog_special_method @@ -724,7 +718,7 @@ async def cog_after_message_command_invoke(self, inter: ApplicationCommandIntera """Similar to :meth:`cog_after_slash_command_invoke` but for message commands.""" pass - def _inject(self, bot: AnyBot) -> Self: + async def _inject(self, bot: AnyBot) -> Self: from .bot import AutoShardedInteractionBot, InteractionBot cls = self.__class__ @@ -771,9 +765,6 @@ def _inject(self, bot: AnyBot) -> Self: bot.remove_message_command(to_undo.name) raise - if not hasattr(self.cog_load.__func__, "__cog_special_method__"): - asyncio.create_task(disnake.utils.maybe_coroutine(self.cog_load)) - # check if we're overriding the default if cls.bot_check is not Cog.bot_check: if isinstance(bot, (InteractionBot, AutoShardedInteractionBot)): @@ -830,9 +821,12 @@ def _inject(self, bot: AnyBot) -> Self: except NotImplementedError: pass + if not hasattr(self.cog_load.__func__, "__cog_special_method__"): + await disnake.utils.maybe_coroutine(self.cog_load) + return self - def _eject(self, bot: AnyBot) -> None: + async def _eject(self, bot: AnyBot) -> None: cls = self.__class__ try: @@ -896,7 +890,7 @@ def _eject(self, bot: AnyBot) -> None: except NotImplementedError: pass try: - self.cog_unload() + await disnake.utils.maybe_coroutine(self.cog_unload) except Exception as e: _log.error( "An error occurred while unloading the %s cog.", self.qualified_name, exc_info=e diff --git a/disnake/ext/commands/common_bot_base.py b/disnake/ext/commands/common_bot_base.py index 154d149db5..0f556e8275 100644 --- a/disnake/ext/commands/common_bot_base.py +++ b/disnake/ext/commands/common_bot_base.py @@ -11,6 +11,7 @@ import sys import time import types +from functools import partial from typing import TYPE_CHECKING, Any, Dict, Generic, List, Mapping, Optional, Set, TypeVar, Union import disnake @@ -92,14 +93,14 @@ async def close(self) -> None: for extension in tuple(self.__extensions): try: - self.unload_extension(extension) + await self.unload_extension(extension) except Exception as error: error.__suppress_context__ = True _log.error("Failed to unload extension %r", extension, exc_info=error) for cog in tuple(self.__cogs): try: - self.remove_cog(cog) + await self.remove_cog(cog) except Exception as error: error.__suppress_context__ = True _log.exception("Failed to remove cog %r", cog, exc_info=error) @@ -147,7 +148,7 @@ async def is_owner(self, user: Union[disnake.User, disnake.Member]) -> bool: else: return user.id in self.owner_ids - def add_cog(self, cog: Cog, *, override: bool = False) -> None: + async def add_cog(self, cog: Cog, *, override: bool = False) -> None: """Adds a "cog" to the bot. A cog is a class that has its own event listeners and commands. @@ -185,10 +186,10 @@ def add_cog(self, cog: Cog, *, override: bool = False) -> None: if existing is not None: if not override: raise disnake.ClientException(f"Cog named {cog_name!r} already loaded") - self.remove_cog(cog_name) + await self.remove_cog(cog_name) # NOTE: Should be covariant - cog = cog._inject(self) # type: ignore + cog = await cog._inject(self) # type: ignore self.__cogs[cog_name] = cog def get_cog(self, name: str) -> Optional[Cog]: @@ -210,7 +211,7 @@ def get_cog(self, name: str) -> Optional[Cog]: """ return self.__cogs.get(name) - def remove_cog(self, name: str) -> Optional[Cog]: + async def remove_cog(self, name: str) -> Optional[Cog]: """Removes a cog from the bot and returns it. All registered commands and event listeners that the @@ -236,7 +237,7 @@ def remove_cog(self, name: str) -> Optional[Cog]: if help_command and help_command.cog is cog: help_command.cog = None # NOTE: Should be covariant - cog._eject(self) # type: ignore + await cog._eject(self) # type: ignore return cog @@ -247,12 +248,12 @@ def cogs(self) -> Mapping[str, Cog]: # extensions - def _remove_module_references(self, name: str) -> None: + async def _remove_module_references(self, name: str) -> None: # find all references to the module # remove the cogs registered from the module for cogname, cog in self.__cogs.copy().items(): if _is_submodule(name, cog.__module__): - self.remove_cog(cogname) + await self.remove_cog(cogname) # remove all the listeners from the module for event_list in self.extra_events.copy().values(): remove = [ @@ -264,14 +265,14 @@ def _remove_module_references(self, name: str) -> None: for index in reversed(remove): del event_list[index] - def _call_module_finalizers(self, lib: types.ModuleType, key: str) -> None: + async def _call_module_finalizers(self, lib: types.ModuleType, key: str) -> None: try: func = lib.teardown except AttributeError: pass else: try: - func(self) + await disnake.utils.maybe_coroutine(partial(func, self)) except Exception as error: error.__suppress_context__ = True _log.error("Exception in extension finalizer %r", key, exc_info=error) @@ -283,7 +284,7 @@ def _call_module_finalizers(self, lib: types.ModuleType, key: str) -> None: if _is_submodule(name, module): del sys.modules[module] - def _load_from_module_spec(self, spec: importlib.machinery.ModuleSpec, key: str) -> None: + async def _load_from_module_spec(self, spec: importlib.machinery.ModuleSpec, key: str) -> None: # precondition: key not in self.__extensions lib = importlib.util.module_from_spec(spec) sys.modules[key] = lib @@ -300,11 +301,11 @@ def _load_from_module_spec(self, spec: importlib.machinery.ModuleSpec, key: str) raise errors.NoEntryPointError(key) from None try: - setup(self) + await disnake.utils.maybe_coroutine(partial(setup, self)) except Exception as e: del sys.modules[key] - self._remove_module_references(lib.__name__) - self._call_module_finalizers(lib, key) + await self._remove_module_references(lib.__name__) + await self._call_module_finalizers(lib, key) raise errors.ExtensionFailed(key, e) from e else: self.__extensions[key] = lib @@ -315,7 +316,7 @@ def _resolve_name(self, name: str, package: Optional[str]) -> str: except ImportError as e: raise errors.ExtensionNotFound(name) from e - def load_extension(self, name: str, *, package: Optional[str] = None) -> None: + async def load_extension(self, name: str, *, package: Optional[str] = None) -> None: """Loads an extension. An extension is a python module that contains commands, cogs, or @@ -359,9 +360,9 @@ def load_extension(self, name: str, *, package: Optional[str] = None) -> None: if spec is None: raise errors.ExtensionNotFound(name) - self._load_from_module_spec(spec, name) + await self._load_from_module_spec(spec, name) - def unload_extension(self, name: str, *, package: Optional[str] = None) -> None: + async def unload_extension(self, name: str, *, package: Optional[str] = None) -> None: """Unloads an extension. When the extension is unloaded, all commands, listeners, and cogs are @@ -398,10 +399,10 @@ def unload_extension(self, name: str, *, package: Optional[str] = None) -> None: if lib is None: raise errors.ExtensionNotLoaded(name) - self._remove_module_references(lib.__name__) - self._call_module_finalizers(lib, name) + await self._remove_module_references(lib.__name__) + await self._call_module_finalizers(lib, name) - def reload_extension(self, name: str, *, package: Optional[str] = None) -> None: + async def reload_extension(self, name: str, *, package: Optional[str] = None) -> None: """Atomically reloads an extension. This replaces the extension with the same extension, only refreshed. This is @@ -449,9 +450,9 @@ def reload_extension(self, name: str, *, package: Optional[str] = None) -> None: try: # Unload and then load the module... - self._remove_module_references(lib.__name__) - self._call_module_finalizers(lib, name) - self.load_extension(name) + await self._remove_module_references(lib.__name__) + await self._call_module_finalizers(lib, name) + await self.load_extension(name) except Exception: # if the load failed, the remnants should have been # cleaned from the load_extension function call @@ -463,7 +464,7 @@ def reload_extension(self, name: str, *, package: Optional[str] = None) -> None: sys.modules.update(modules) raise - def load_extensions(self, path: str) -> None: + async def load_extensions(self, path: str) -> None: """Loads all extensions in a directory. .. versionadded:: 2.4 @@ -474,7 +475,7 @@ def load_extensions(self, path: str) -> None: The path to search for extensions """ for extension in disnake.utils.search_directory(path): - self.load_extension(extension) + await self.load_extension(extension) @property def extensions(self) -> Mapping[str, types.ModuleType]: @@ -517,7 +518,7 @@ async def _watchdog(self) -> None: for name in extensions: try: - self.reload_extension(name) + await self.reload_extension(name) except errors.ExtensionError as e: reload_log.exception(e) else: diff --git a/docs/api/events.rst b/docs/api/events.rst index c92762c3b0..0eec6f61b3 100644 --- a/docs/api/events.rst +++ b/docs/api/events.rst @@ -195,6 +195,24 @@ This section documents events related to :class:`Client` and its connectivity to once. This library implements reconnection logic and thus will end up calling this event whenever a RESUME request fails. +.. function:: on_setup() + + An event that allows you to perform asyncronous setup like + initiating database connections after the bot is logged in but + before it has connected to the websocket. + + This is only called once, in :meth:`Client.login`, before any events are + dispatched, making it a better solution than doing such setup in + the :func:`disnake.on_ready` event. + + .. warning:: + Since this is called *before* the websocket connection is made, + anything that waits for the websocket will deadlock, which includes + methods like :meth:`Client.wait_for`, :meth:`Client.wait_until_ready` + and :meth:`Client.wait_until_first_connect`. + + .. versionadded:: 2.10 + .. function:: on_resumed() Called when the client has resumed a session. diff --git a/examples/basic_voice.py b/examples/basic_voice.py index 72f963f34d..92463e2afa 100644 --- a/examples/basic_voice.py +++ b/examples/basic_voice.py @@ -136,7 +136,10 @@ async def on_ready(): print(f"Logged in as {bot.user} (ID: {bot.user.id})\n------") -bot.add_cog(Music(bot)) +@bot.event +async def on_setup(): + await bot.add_cog(Music(bot)) + if __name__ == "__main__": bot.run(os.getenv("BOT_TOKEN")) diff --git a/examples/interactions/subcmd.py b/examples/interactions/subcmd.py index ef9da28c38..8e2a3d5a26 100644 --- a/examples/interactions/subcmd.py +++ b/examples/interactions/subcmd.py @@ -71,7 +71,10 @@ async def on_ready(): print(f"Logged in as {bot.user} (ID: {bot.user.id})\n------") -bot.add_cog(MyCog()) +@bot.event +async def on_setup(): + await bot.add_cog(MyCog()) + if __name__ == "__main__": bot.run(os.getenv("BOT_TOKEN")) diff --git a/test_bot/__main__.py b/test_bot/__main__.py index 1eb9e254b4..72671e4fb7 100644 --- a/test_bot/__main__.py +++ b/test_bot/__main__.py @@ -52,12 +52,12 @@ async def on_ready(self) -> None: ) # fmt: on - async def setup_hook(self) -> None: - self.load_extensions(os.path.join(__package__, Config.cogs_folder)) + async def on_setup(self) -> None: + await self.load_extensions(os.path.join(__package__, Config.cogs_folder)) - def add_cog(self, cog: commands.Cog, *, override: bool = False) -> None: + async def add_cog(self, cog: commands.Cog, *, override: bool = False) -> None: logger.info("Loading cog %s", cog.qualified_name) - return super().add_cog(cog, override=override) + return await super().add_cog(cog, override=override) async def _handle_error( self, ctx: Union[commands.Context, disnake.AppCommandInter], error: Exception, prefix: str diff --git a/test_bot/cogs/events.py b/test_bot/cogs/events.py index 50c4833b6d..33f1ac7dbd 100644 --- a/test_bot/cogs/events.py +++ b/test_bot/cogs/events.py @@ -28,5 +28,5 @@ async def on_guild_scheduled_event_unsubscribe(self, event, user) -> None: print("Scheduled event unsubscribe", event, user, sep="\n", end="\n\n") -def setup(bot) -> None: - bot.add_cog(EventListeners(bot)) +async def setup(bot) -> None: + await bot.add_cog(EventListeners(bot)) diff --git a/test_bot/cogs/guild_scheduled_events.py b/test_bot/cogs/guild_scheduled_events.py index 1ffcb92295..703a94bdd1 100644 --- a/test_bot/cogs/guild_scheduled_events.py +++ b/test_bot/cogs/guild_scheduled_events.py @@ -53,5 +53,5 @@ async def create_event( await inter.response.send_message(str(gse.image)) -def setup(bot: commands.Bot) -> None: - bot.add_cog(GuildScheduledEvents(bot)) +async def setup(bot: commands.Bot) -> None: + await bot.add_cog(GuildScheduledEvents(bot)) diff --git a/test_bot/cogs/injections.py b/test_bot/cogs/injections.py index 192ca10137..9b75adf25e 100644 --- a/test_bot/cogs/injections.py +++ b/test_bot/cogs/injections.py @@ -123,5 +123,5 @@ async def discerned_injections( await inter.response.send_message(f"```py\n{pformat(locals())}\n```") -def setup(bot) -> None: - bot.add_cog(InjectionSlashCommands(bot)) +async def setup(bot) -> None: + await bot.add_cog(InjectionSlashCommands(bot)) diff --git a/test_bot/cogs/localization.py b/test_bot/cogs/localization.py index 9bba8d1495..dae2ec2c20 100644 --- a/test_bot/cogs/localization.py +++ b/test_bot/cogs/localization.py @@ -79,5 +79,5 @@ async def cmd_msg(self, inter: disnake.AppCmdInter[commands.Bot], msg: disnake.M await inter.response.send_message(msg.content[::-1]) -def setup(bot) -> None: - bot.add_cog(Localizations(bot)) +async def setup(bot) -> None: + await bot.add_cog(Localizations(bot)) diff --git a/test_bot/cogs/message_commands.py b/test_bot/cogs/message_commands.py index d4101b8e41..a3f4e914fe 100644 --- a/test_bot/cogs/message_commands.py +++ b/test_bot/cogs/message_commands.py @@ -13,5 +13,5 @@ async def reverse(self, inter: disnake.MessageCommandInteraction[commands.Bot]) await inter.response.send_message(inter.target.content[::-1]) -def setup(bot) -> None: - bot.add_cog(MessageCommands(bot)) +async def setup(bot) -> None: + await bot.add_cog(MessageCommands(bot)) diff --git a/test_bot/cogs/misc.py b/test_bot/cogs/misc.py index c10081e967..a5b5ef2a46 100644 --- a/test_bot/cogs/misc.py +++ b/test_bot/cogs/misc.py @@ -50,5 +50,5 @@ async def attachment_desc_edit( await inter.response.send_message(".", view=view) -def setup(bot) -> None: - bot.add_cog(Misc(bot)) +async def setup(bot) -> None: + await bot.add_cog(Misc(bot)) diff --git a/test_bot/cogs/modals.py b/test_bot/cogs/modals.py index 13c84bddf2..841318b806 100644 --- a/test_bot/cogs/modals.py +++ b/test_bot/cogs/modals.py @@ -74,5 +74,5 @@ async def create_tag_low(self, inter: disnake.AppCmdInter[commands.Bot]) -> None await modal_inter.response.send_message(embed=embed) -def setup(bot: commands.Bot) -> None: - bot.add_cog(Modals(bot)) +async def setup(bot: commands.Bot) -> None: + await bot.add_cog(Modals(bot)) diff --git a/test_bot/cogs/slash_commands.py b/test_bot/cogs/slash_commands.py index e7a2437d56..60b546f3fd 100644 --- a/test_bot/cogs/slash_commands.py +++ b/test_bot/cogs/slash_commands.py @@ -73,5 +73,5 @@ async def largenumber( await inter.send(f"Is int: {isinstance(largenum, int)}") -def setup(bot) -> None: - bot.add_cog(SlashCommands(bot)) +async def setup(bot) -> None: + await bot.add_cog(SlashCommands(bot)) diff --git a/test_bot/cogs/user_commands.py b/test_bot/cogs/user_commands.py index e8c67efdca..cd588f8f40 100644 --- a/test_bot/cogs/user_commands.py +++ b/test_bot/cogs/user_commands.py @@ -15,5 +15,5 @@ async def avatar( await inter.response.send_message(user.display_avatar.url, ephemeral=True) -def setup(bot) -> None: - bot.add_cog(UserCommands(bot)) +async def setup(bot) -> None: + await bot.add_cog(UserCommands(bot)) diff --git a/tests/test_events.py b/tests/test_events.py index 15cc467151..4c558e8b31 100644 --- a/tests/test_events.py +++ b/tests/test_events.py @@ -43,8 +43,9 @@ async def on_message_edit(self, *args: Any) -> None: # Client.wait_for +@pytest.mark.asyncio @pytest.mark.parametrize("event", ["thread_create", Event.thread_create]) -def test_wait_for(client_or_bot: disnake.Client, event) -> None: +async def test_wait_for(client_or_bot: disnake.Client, event) -> None: coro = client_or_bot.wait_for(event) assert len(client_or_bot._listeners["thread_create"]) == 1 coro.close() # close coroutine to avoid warning @@ -95,22 +96,24 @@ async def on_guild_role_create(self, *args: Any) -> None: # @commands.Cog.listener +@pytest.mark.asyncio @pytest.mark.parametrize("event", ["on_automod_rule_update", Event.automod_rule_update]) -def test_listener(bot: commands.Bot, event) -> None: +async def test_listener(bot: commands.Bot, event) -> None: class Cog(commands.Cog): @commands.Cog.listener(event) async def callback(self, *args: Any) -> None: ... - bot.add_cog(Cog()) + await bot.add_cog(Cog()) assert len(bot.extra_events["on_automod_rule_update"]) == 1 -def test_listener__implicit(bot: commands.Bot) -> None: +@pytest.mark.asyncio +async def test_listener__implicit(bot: commands.Bot) -> None: class Cog(commands.Cog): @commands.Cog.listener() async def on_automod_rule_update(self, *args: Any) -> None: ... - bot.add_cog(Cog()) + await bot.add_cog(Cog()) assert len(bot.extra_events["on_automod_rule_update"]) == 1 From 2b30e94705025dbcd91161567b11115cc7acd738 Mon Sep 17 00:00:00 2001 From: elenakrittik Date: Sun, 12 Nov 2023 19:55:57 +0300 Subject: [PATCH 12/26] fix: Revert setup to be a hook instead of an event. --- disnake/client.py | 20 +++++++++++++++++++- docs/api/events.rst | 18 ------------------ examples/basic_voice.py | 5 +++-- examples/interactions/subcmd.py | 5 +++-- test_bot/__main__.py | 2 +- 5 files changed, 26 insertions(+), 24 deletions(-) diff --git a/disnake/client.py b/disnake/client.py index c69b2937d7..140f8a34a2 100644 --- a/disnake/client.py +++ b/disnake/client.py @@ -977,6 +977,24 @@ async def before_identify_hook(self, shard_id: Optional[int], *, initial: bool = if not initial: await asyncio.sleep(5.0) + async def setup_hook(self) -> None: + """An event that allows you to perform asyncronous setup like + initiating database connections after the bot is logged in but + before it has connected to the websocket. + + This is only called once, in :meth:`Client.login`, before any events are + dispatched, making it a better solution than doing such setup in + the :func:`disnake.on_ready` event. + + .. warning:: + Since this is called *before* the websocket connection is made, + anything that waits for the websocket will deadlock, which includes + methods like :meth:`Client.wait_for`, :meth:`Client.wait_until_ready` + and :meth:`Client.wait_until_first_connect`. + + .. versionadded:: 2.10 + """ + # login state management async def login(self, token: str) -> None: @@ -1007,7 +1025,7 @@ async def login(self, token: str) -> None: data = await self.http.static_login(token.strip()) self._connection.user = ClientUser(state=self._connection, data=data) - self.dispatch("setup") + await self.setup_hook() async def connect( self, *, reconnect: bool = True, ignore_session_start_limit: bool = False diff --git a/docs/api/events.rst b/docs/api/events.rst index 0eec6f61b3..c92762c3b0 100644 --- a/docs/api/events.rst +++ b/docs/api/events.rst @@ -195,24 +195,6 @@ This section documents events related to :class:`Client` and its connectivity to once. This library implements reconnection logic and thus will end up calling this event whenever a RESUME request fails. -.. function:: on_setup() - - An event that allows you to perform asyncronous setup like - initiating database connections after the bot is logged in but - before it has connected to the websocket. - - This is only called once, in :meth:`Client.login`, before any events are - dispatched, making it a better solution than doing such setup in - the :func:`disnake.on_ready` event. - - .. warning:: - Since this is called *before* the websocket connection is made, - anything that waits for the websocket will deadlock, which includes - methods like :meth:`Client.wait_for`, :meth:`Client.wait_until_ready` - and :meth:`Client.wait_until_first_connect`. - - .. versionadded:: 2.10 - .. function:: on_resumed() Called when the client has resumed a session. diff --git a/examples/basic_voice.py b/examples/basic_voice.py index 92463e2afa..1f1e25c817 100644 --- a/examples/basic_voice.py +++ b/examples/basic_voice.py @@ -136,10 +136,11 @@ async def on_ready(): print(f"Logged in as {bot.user} (ID: {bot.user.id})\n------") -@bot.event -async def on_setup(): +async def setup_hook(): await bot.add_cog(Music(bot)) +bot.setup_hook = setup_hook + if __name__ == "__main__": bot.run(os.getenv("BOT_TOKEN")) diff --git a/examples/interactions/subcmd.py b/examples/interactions/subcmd.py index 8e2a3d5a26..119dffa6fd 100644 --- a/examples/interactions/subcmd.py +++ b/examples/interactions/subcmd.py @@ -71,10 +71,11 @@ async def on_ready(): print(f"Logged in as {bot.user} (ID: {bot.user.id})\n------") -@bot.event -async def on_setup(): +async def setup_hook(): await bot.add_cog(MyCog()) +bot.setup_hook = setup_hook + if __name__ == "__main__": bot.run(os.getenv("BOT_TOKEN")) diff --git a/test_bot/__main__.py b/test_bot/__main__.py index 72671e4fb7..a58ea00739 100644 --- a/test_bot/__main__.py +++ b/test_bot/__main__.py @@ -52,7 +52,7 @@ async def on_ready(self) -> None: ) # fmt: on - async def on_setup(self) -> None: + async def setup_hook(self) -> None: await self.load_extensions(os.path.join(__package__, Config.cogs_folder)) async def add_cog(self, cog: commands.Cog, *, override: bool = False) -> None: From ebea05154e25c042ccae696de7f60cb960212129 Mon Sep 17 00:00:00 2001 From: elenakrittik Date: Sun, 12 Nov 2023 20:15:29 +0300 Subject: [PATCH 13/26] docs: Fix setup_hook docstring. --- disnake/client.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/disnake/client.py b/disnake/client.py index 140f8a34a2..9406d2b7b6 100644 --- a/disnake/client.py +++ b/disnake/client.py @@ -978,19 +978,19 @@ async def before_identify_hook(self, shard_id: Optional[int], *, initial: bool = await asyncio.sleep(5.0) async def setup_hook(self) -> None: - """An event that allows you to perform asyncronous setup like + """A hook that allows you to perform asynchronous setup like initiating database connections after the bot is logged in but before it has connected to the websocket. - This is only called once, in :meth:`Client.login`, before any events are + This is only called once, in :meth:`.login`, before any events are dispatched, making it a better solution than doing such setup in the :func:`disnake.on_ready` event. .. warning:: Since this is called *before* the websocket connection is made, anything that waits for the websocket will deadlock, which includes - methods like :meth:`Client.wait_for`, :meth:`Client.wait_until_ready` - and :meth:`Client.wait_until_first_connect`. + methods like :meth:`.wait_for`, :meth:`.wait_until_ready` + and :meth:`.wait_until_first_connect`. .. versionadded:: 2.10 """ From f37594e65199fdd717faf3db27e2d2e14376d278 Mon Sep 17 00:00:00 2001 From: elenakrittik Date: Sun, 12 Nov 2023 20:35:37 +0300 Subject: [PATCH 14/26] fix: Self-review fixes --- disnake/client.py | 2 +- disnake/shard.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/disnake/client.py b/disnake/client.py index 9406d2b7b6..d3da16345f 100644 --- a/disnake/client.py +++ b/disnake/client.py @@ -353,7 +353,7 @@ def __init__( except RuntimeError: raise RuntimeError( ( - "`connector` was created outside of an asyncio loop consider moving bot class " + "`connector` was created outside of an asyncio loop; consider moving bot class " "instantiation to an async main function and then manually asyncio.run it" ) ) from None diff --git a/disnake/shard.py b/disnake/shard.py index 3ec418dba1..b6ffd51779 100644 --- a/disnake/shard.py +++ b/disnake/shard.py @@ -333,6 +333,7 @@ def __init__( enable_debug_events: bool = False, enable_gateway_error_handler: bool = True, gateway_params: Optional[GatewayParams] = None, + connector: Optional[aiohttp.BaseConnector] = None, proxy: Optional[str] = None, proxy_auth: Optional[aiohttp.BasicAuth] = None, assume_unsync_clock: bool = True, From 190090210f6b58538c1eb58e332cd581f4f4f98e Mon Sep 17 00:00:00 2001 From: elenakrittik Date: Sun, 12 Nov 2023 21:10:24 +0300 Subject: [PATCH 15/26] docs: Add changelogs. --- changelog/1132.breaking.0.rst | 1 + changelog/1132.deprecate.rst | 1 + changelog/1132.feature.rst | 1 + changelog/1132.misc.rst | 1 + changelog/641.breaking.0.rst | 1 + changelog/641.breaking.1.rst | 1 + changelog/641.feature.0.rst | 1 + changelog/641.feature.1.rst | 1 + 8 files changed, 8 insertions(+) create mode 100644 changelog/1132.breaking.0.rst create mode 100644 changelog/1132.deprecate.rst create mode 100644 changelog/1132.feature.rst create mode 100644 changelog/1132.misc.rst create mode 100644 changelog/641.breaking.0.rst create mode 100644 changelog/641.breaking.1.rst create mode 100644 changelog/641.feature.0.rst create mode 100644 changelog/641.feature.1.rst diff --git a/changelog/1132.breaking.0.rst b/changelog/1132.breaking.0.rst new file mode 100644 index 0000000000..c8ef9bbf26 --- /dev/null +++ b/changelog/1132.breaking.0.rst @@ -0,0 +1 @@ +Removed ``loop`` and ``asyncio_debug`` parameters from :class:`Client`. diff --git a/changelog/1132.deprecate.rst b/changelog/1132.deprecate.rst new file mode 100644 index 0000000000..e8134a17e7 --- /dev/null +++ b/changelog/1132.deprecate.rst @@ -0,0 +1 @@ +Deprecated :attr:`Client.loop`. Use :func:`asyncio.get_running_loop` instead. diff --git a/changelog/1132.feature.rst b/changelog/1132.feature.rst new file mode 100644 index 0000000000..32470148e2 --- /dev/null +++ b/changelog/1132.feature.rst @@ -0,0 +1 @@ +Add :meth:`Client.setup_hook`. diff --git a/changelog/1132.misc.rst b/changelog/1132.misc.rst new file mode 100644 index 0000000000..9273c50aab --- /dev/null +++ b/changelog/1132.misc.rst @@ -0,0 +1 @@ +:meth:`Client.run` now uses :func:`asyncio.run` under-the-hood, instead of custom runner logic. diff --git a/changelog/641.breaking.0.rst b/changelog/641.breaking.0.rst new file mode 100644 index 0000000000..5dcd89c364 --- /dev/null +++ b/changelog/641.breaking.0.rst @@ -0,0 +1 @@ +|commands| Make :meth:`.ext.commands.Bot.load_extensions`, :meth:`.ext.commands.Bot.load_extension`, :meth:`.ext.commands.Bot.unload_extension`, :meth:`.ext.commands.Bot.reload_extension`, :meth:`.ext.commands.Bot.add_cog`, :meth:`.ext.commands.Bot.remove_cog` asynchronous. diff --git a/changelog/641.breaking.1.rst b/changelog/641.breaking.1.rst new file mode 100644 index 0000000000..9b3a0fb2fa --- /dev/null +++ b/changelog/641.breaking.1.rst @@ -0,0 +1 @@ +|commands| :meth:`.ext.commands.Cog.cog_load` is now called *after* the cog finished loading. diff --git a/changelog/641.feature.0.rst b/changelog/641.feature.0.rst new file mode 100644 index 0000000000..ee9e11b621 --- /dev/null +++ b/changelog/641.feature.0.rst @@ -0,0 +1 @@ +|commands| :meth:`.ext.commands.Cog.cog_load` and :meth:`.ext.commands.Cog.cog_unload` can now be either asynchronous or not. diff --git a/changelog/641.feature.1.rst b/changelog/641.feature.1.rst new file mode 100644 index 0000000000..7a56ba23fe --- /dev/null +++ b/changelog/641.feature.1.rst @@ -0,0 +1 @@ +|commands| The ``setup`` and ``teardown`` functions utilized by :ref:`ext_commands_extensions` can now be asynchronous. From 1daebcd63fac6ebfd280bf84614e0bf53aa5b6eb Mon Sep 17 00:00:00 2001 From: elenakrittik Date: Sun, 12 Nov 2023 21:11:55 +0300 Subject: [PATCH 16/26] docs: Add example usage of setup_hook. --- disnake/client.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/disnake/client.py b/disnake/client.py index d3da16345f..ee6cf07c47 100644 --- a/disnake/client.py +++ b/disnake/client.py @@ -979,8 +979,8 @@ async def before_identify_hook(self, shard_id: Optional[int], *, initial: bool = async def setup_hook(self) -> None: """A hook that allows you to perform asynchronous setup like - initiating database connections after the bot is logged in but - before it has connected to the websocket. + initiating database connections or loading cogs/extensions after + the bot is logged in but before it has connected to the websocket. This is only called once, in :meth:`.login`, before any events are dispatched, making it a better solution than doing such setup in From 3c1baa9248679ebb2c87d0e98dc054c6c3fc6250 Mon Sep 17 00:00:00 2001 From: elenakrittik Date: Sun, 12 Nov 2023 21:34:02 +0300 Subject: [PATCH 17/26] docs: Post-PR-open fixes. Of course. --- changelog/1132.breaking.1.rst | 1 + docs/ext/commands/cogs.rst | 4 ++-- docs/ext/commands/extensions.rst | 7 ++++++- 3 files changed, 9 insertions(+), 3 deletions(-) create mode 100644 changelog/1132.breaking.1.rst diff --git a/changelog/1132.breaking.1.rst b/changelog/1132.breaking.1.rst new file mode 100644 index 0000000000..11893430a2 --- /dev/null +++ b/changelog/1132.breaking.1.rst @@ -0,0 +1 @@ +The majority of the library now assumes that there is an asyncio event loop running. diff --git a/docs/ext/commands/cogs.rst b/docs/ext/commands/cogs.rst index 5a5f5389f0..024799704c 100644 --- a/docs/ext/commands/cogs.rst +++ b/docs/ext/commands/cogs.rst @@ -63,7 +63,7 @@ Once you have defined your cogs, you need to tell the bot to register the cogs t .. code-block:: python3 - bot.add_cog(Greetings(bot)) + await bot.add_cog(Greetings(bot)) This binds the cog to the bot, adding all commands and listeners to the bot automatically. @@ -71,7 +71,7 @@ Note that we reference the cog by name, which we can override through :ref:`ext_ .. code-block:: python3 - bot.remove_cog('Greetings') + await bot.remove_cog('Greetings') Using Cogs ---------- diff --git a/docs/ext/commands/extensions.rst b/docs/ext/commands/extensions.rst index d3db0436c5..d7699124ec 100644 --- a/docs/ext/commands/extensions.rst +++ b/docs/ext/commands/extensions.rst @@ -36,6 +36,11 @@ In this example we define a simple command, and when the extension is loaded thi Extensions are usually used in conjunction with cogs. To read more about them, check out the documentation, :ref:`ext_commands_cogs`. +.. admonition:: Async + :class: helpful + + ``setup`` (as well as ``teardown``, see below) can be ``async`` too! + .. note:: Extension paths are ultimately similar to the import mechanism. What this means is that if there is a folder, then it must be dot-qualified. For example to load an extension in ``plugins/hello.py`` then we use the string ``plugins.hello``. @@ -47,7 +52,7 @@ When you make a change to the extension and want to reload the references, the l .. code-block:: python3 - >>> bot.reload_extension('hello') + >>> await bot.reload_extension('hello') Once the extension reloads, any changes that we did will be applied. This is useful if we want to add or remove functionality without restarting our bot. If an error occurred during the reloading process, the bot will pretend as if the reload never happened. From 5566e17d87747f73e2d0e9494efd707c2208e188 Mon Sep 17 00:00:00 2001 From: elenakrittik Date: Sun, 12 Nov 2023 21:34:54 +0300 Subject: [PATCH 18/26] fix: Formatting. --- disnake/gateway.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/disnake/gateway.py b/disnake/gateway.py index 6ed22185c0..3f6e1716b2 100644 --- a/disnake/gateway.py +++ b/disnake/gateway.py @@ -591,7 +591,7 @@ async def received_message(self, raw_msg: Union[str, bytes], /) -> None: ws=self, interval=interval, shard_id=self.shard_id, - loop=asyncio.get_running_loop(), # share loop to the thread + loop=asyncio.get_running_loop(), # share loop to the thread ) self._keep_alive.name = "disnake heartbeat thread" # send a heartbeat immediately From 416333e02292864211ac966d44b11d569c6c6835 Mon Sep 17 00:00:00 2001 From: elenakrittik Date: Sun, 12 Nov 2023 21:37:43 +0300 Subject: [PATCH 19/26] docs: Minor wording fix. --- changelog/641.breaking.0.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog/641.breaking.0.rst b/changelog/641.breaking.0.rst index 5dcd89c364..b998f47a72 100644 --- a/changelog/641.breaking.0.rst +++ b/changelog/641.breaking.0.rst @@ -1 +1 @@ -|commands| Make :meth:`.ext.commands.Bot.load_extensions`, :meth:`.ext.commands.Bot.load_extension`, :meth:`.ext.commands.Bot.unload_extension`, :meth:`.ext.commands.Bot.reload_extension`, :meth:`.ext.commands.Bot.add_cog`, :meth:`.ext.commands.Bot.remove_cog` asynchronous. +|commands| Make :meth:`.ext.commands.Bot.load_extensions`, :meth:`.ext.commands.Bot.load_extension`, :meth:`.ext.commands.Bot.unload_extension`, :meth:`.ext.commands.Bot.reload_extension`, :meth:`.ext.commands.Bot.add_cog` and :meth:`.ext.commands.Bot.remove_cog` asynchronous. From f677880bb800eea3acfa660e374a05df9bbd2265 Mon Sep 17 00:00:00 2001 From: elenakrittik Date: Sun, 12 Nov 2023 21:46:50 +0300 Subject: [PATCH 20/26] fix: run codemod --- disnake/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/disnake/client.py b/disnake/client.py index ee6cf07c47..5bf55f3c05 100644 --- a/disnake/client.py +++ b/disnake/client.py @@ -480,7 +480,7 @@ def loop(self): return asyncio.get_running_loop() @loop.setter - def loop(self, _value: Never): + def loop(self, _value: Never) -> None: warnings.warn( "Setting `Client.loop` is deprecated and has no effect. Use `asyncio.get_running_loop()` instead.", category=DeprecationWarning, From abd0a1d445f78cc6bea5680dfc8d96caba91b839 Mon Sep 17 00:00:00 2001 From: elenakrittik Date: Tue, 14 Nov 2023 13:24:06 +0300 Subject: [PATCH 21/26] fix: propagate ignore_session_start_limit --- disnake/client.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/disnake/client.py b/disnake/client.py index 5bf55f3c05..3396fcbe57 100644 --- a/disnake/client.py +++ b/disnake/client.py @@ -1227,7 +1227,9 @@ async def start( """ try: await self.login(token) - await self.connect(reconnect=reconnect) + await self.connect( + reconnect=reconnect, ignore_session_start_limit=ignore_session_start_limit + ) finally: if not self.is_closed(): await self.close() From 1d740c3534a90f2625dee10e63ff6b1e0909e9c3 Mon Sep 17 00:00:00 2001 From: elenakrittik Date: Thu, 16 Nov 2023 22:49:17 +0300 Subject: [PATCH 22/26] docs: 3.0 --- disnake/client.py | 8 +++++++- disnake/ext/commands/cog.py | 14 ++++++++++++-- disnake/ext/commands/common_bot_base.py | 18 ++++++++++++++++++ 3 files changed, 37 insertions(+), 3 deletions(-) diff --git a/disnake/client.py b/disnake/client.py index 3396fcbe57..4fcd5f1c81 100644 --- a/disnake/client.py +++ b/disnake/client.py @@ -469,7 +469,7 @@ def _handle_first_connect(self) -> None: def loop(self): """:class:`asyncio.AbstractEventLoop`: Same as :func:`asyncio.get_running_loop`. - .. deprecated:: 2.10 + .. deprecated:: 3.0 Use :func:`asyncio.get_running_loop` directly. """ warnings.warn( @@ -1003,6 +1003,9 @@ async def login(self, token: str) -> None: Logs in the client with the specified credentials and calls :meth:`.setup_hook`. + .. versionchanged:: 3.0 + Now also calls :meth:`.setup_hook`. + Parameters ---------- token: :class:`str` @@ -1254,6 +1257,9 @@ def run(self, *args: Any, **kwargs: Any) -> None: 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. + + .. versionchanged:: 3.0 + Changed to use :func:`asyncio.run`, instead of custom logic. """ try: asyncio.run(self.start(*args, **kwargs)) diff --git a/disnake/ext/commands/cog.py b/disnake/ext/commands/cog.py index 9be5a133e5..7f07b093c2 100644 --- a/disnake/ext/commands/cog.py +++ b/disnake/ext/commands/cog.py @@ -476,12 +476,22 @@ def has_message_error_handler(self) -> bool: @_cog_special_method async def cog_load(self) -> None: - """A special method that is called when the cog is added.""" + """A special method that is called when the cog is added. + + .. versionchanged:: 3.0 + This is now ``await``ed directly instead of being scheduled as a task. + + This is now run when the cog fully finished loading. + """ pass @_cog_special_method async def cog_unload(self) -> None: - """A special method that is called when the cog gets removed.""" + """A special method that is called when the cog gets removed. + + .. versionchanged:: 3.0 + This can now be a coroutine. + """ pass @_cog_special_method diff --git a/disnake/ext/commands/common_bot_base.py b/disnake/ext/commands/common_bot_base.py index 0f556e8275..c4dc2b8ba7 100644 --- a/disnake/ext/commands/common_bot_base.py +++ b/disnake/ext/commands/common_bot_base.py @@ -158,6 +158,9 @@ async def add_cog(self, cog: Cog, *, override: bool = False) -> None: :exc:`.ClientException` is raised when a cog with the same name is already loaded. + .. versionchanged:: 3.0 + This is now a coroutine. + Parameters ---------- cog: :class:`.Cog` @@ -219,6 +222,9 @@ async def remove_cog(self, name: str) -> Optional[Cog]: If no cog is found then this method has no effect. + .. versionchanged:: 3.0 + This is now a coroutine. + Parameters ---------- name: :class:`str` @@ -326,6 +332,9 @@ async def load_extension(self, name: str, *, package: Optional[str] = None) -> N the entry point on what to do when the extension is loaded. This entry point must have a single argument, the ``bot``. + .. versionchanged:: 3.0 + This is now a coroutine. + Parameters ---------- name: :class:`str` @@ -373,6 +382,9 @@ async def unload_extension(self, name: str, *, package: Optional[str] = None) -> parameter, the ``bot``, similar to ``setup`` from :meth:`~.Bot.load_extension`. + .. versionchanged:: 3.0 + This is now a coroutine. + Parameters ---------- name: :class:`str` @@ -410,6 +422,9 @@ async def reload_extension(self, name: str, *, package: Optional[str] = None) -> except done in an atomic way. That is, if an operation fails mid-reload then the bot will roll-back to the prior working state. + .. versionchanged:: 3.0 + This is now a coroutine. + Parameters ---------- name: :class:`str` @@ -469,6 +484,9 @@ async def load_extensions(self, path: str) -> None: .. versionadded:: 2.4 + .. versionchanged:: 3.0 + This is now a coroutine. + Parameters ---------- path: :class:`str` From 0d34a70f3a3c4f51f49ed11c39dafe1104db4dff Mon Sep 17 00:00:00 2001 From: lena <77104725+elenakrittik@users.noreply.github.com> Date: Fri, 10 May 2024 23:21:30 +0300 Subject: [PATCH 23/26] Apply suggestions from code review Signed-off-by: lena <77104725+elenakrittik@users.noreply.github.com> --- disnake/client.py | 2 +- disnake/ext/commands/cog.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/disnake/client.py b/disnake/client.py index f37914916e..a3c88ac60e 100644 --- a/disnake/client.py +++ b/disnake/client.py @@ -1006,7 +1006,7 @@ async def setup_hook(self) -> None: methods like :meth:`.wait_for`, :meth:`.wait_until_ready` and :meth:`.wait_until_first_connect`. - .. versionadded:: 2.10 + .. versionadded:: 3.0 """ # login state management diff --git a/disnake/ext/commands/cog.py b/disnake/ext/commands/cog.py index ed3060b3e6..3d7b4a3ea2 100644 --- a/disnake/ext/commands/cog.py +++ b/disnake/ext/commands/cog.py @@ -481,7 +481,7 @@ async def cog_load(self) -> None: .. versionchanged:: 3.0 This is now ``await``\\ed directly instead of being scheduled as a task. - This is now run when the cog fully finished loading. + This is now run when the cog has fully finished loading. """ pass From a0c5a8e204b054f0b8d8d82f3689680e0d1f4591 Mon Sep 17 00:00:00 2001 From: elenakrittik Date: Tue, 14 May 2024 13:42:20 +0300 Subject: [PATCH 24/26] fix voice sending --- disnake/gateway.py | 4 +++- disnake/player.py | 14 ++++++++++---- disnake/voice_client.py | 3 ++- pyproject.toml | 1 + 4 files changed, 16 insertions(+), 6 deletions(-) diff --git a/disnake/gateway.py b/disnake/gateway.py index 7d73f7e00e..af7a6215d8 100644 --- a/disnake/gateway.py +++ b/disnake/gateway.py @@ -1014,7 +1014,9 @@ async def received_message(self, msg: VoicePayload) -> None: await self.load_secret_key(data) elif op == self.HELLO: interval: float = data["heartbeat_interval"] / 1000.0 - self._keep_alive = VoiceKeepAliveHandler(ws=self, interval=min(interval, 5.0)) + self._keep_alive = VoiceKeepAliveHandler( + ws=self, interval=min(interval, 5.0), loop=asyncio.get_running_loop() + ) self._keep_alive.start() await self._hook(self, msg) diff --git a/disnake/player.py b/disnake/player.py index 6e772c87ba..1248b1d00f 100644 --- a/disnake/player.py +++ b/disnake/player.py @@ -689,11 +689,19 @@ def read(self) -> bytes: class AudioPlayer(threading.Thread): DELAY: float = OpusEncoder.FRAME_LENGTH / 1000.0 - def __init__(self, source: AudioSource, client: VoiceClient, *, after=None) -> None: + def __init__( + self, + source: AudioSource, + client: VoiceClient, + loop: asyncio.AbstractEventLoop, + *, + after=None, + ) -> None: threading.Thread.__init__(self) self.daemon: bool = True self.source: AudioSource = source self.client: VoiceClient = client + self.loop = loop self.after: Optional[Callable[[Optional[Exception]], Any]] = after self._end: threading.Event = threading.Event() @@ -803,8 +811,6 @@ def _set_source(self, source: AudioSource) -> None: def _speak(self, speaking: bool) -> None: try: - asyncio.run_coroutine_threadsafe( - self.client.ws.speak(speaking), asyncio.get_running_loop() - ) + asyncio.run_coroutine_threadsafe(self.client.ws.speak(speaking), self.loop) except Exception as e: _log.info("Speaking call in player failed: %s", e) diff --git a/disnake/voice_client.py b/disnake/voice_client.py index 8890e8bcd4..2bd50d11a5 100644 --- a/disnake/voice_client.py +++ b/disnake/voice_client.py @@ -14,6 +14,7 @@ - When that's all done, we receive opcode 4 from the vWS. - Finally we can transmit data to endpoint:port. """ + from __future__ import annotations import asyncio @@ -582,7 +583,7 @@ def play( if not self.encoder and not source.is_opus(): self.encoder = opus.Encoder() - self._player = AudioPlayer(source, self, after=after) + self._player = AudioPlayer(source, self, asyncio.get_running_loop(), after=after) self._player.start() def is_playing(self) -> bool: diff --git a/pyproject.toml b/pyproject.toml index a184953fdd..5286dfe960 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,6 +16,7 @@ keywords = ["disnake", "discord", "discord api"] license = { text = "MIT" } dependencies = [ "aiohttp>=3.7.0,<4.0", + "youtube-dl @ git+https://github.com/ytdl-org/youtube-dl", ] classifiers = [ "Development Status :: 5 - Production/Stable", From df499a72791048ea68d5648a525872d30674d342 Mon Sep 17 00:00:00 2001 From: elenakrittik Date: Tue, 14 May 2024 13:51:27 +0300 Subject: [PATCH 25/26] improve error message --- disnake/client.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/disnake/client.py b/disnake/client.py index a3c88ac60e..c8b229839a 100644 --- a/disnake/client.py +++ b/disnake/client.py @@ -367,8 +367,10 @@ def __init__( except RuntimeError: raise RuntimeError( ( - "`connector` was created outside of an asyncio loop; consider moving bot class " - "instantiation to an async main function and then manually asyncio.run it" + "`connector` was created outside of an asyncio loop, which will likely cause" + "issues later down the line due to the client and `connector` running on" + "different asyncio loops; consider moving client instantiation to an '`async" + "main`' function and then manually asyncio.run it" ) ) from None From c0ab61316d10494827f52e6d4598fc160669fd1f Mon Sep 17 00:00:00 2001 From: elenakrittik Date: Tue, 14 May 2024 13:55:04 +0300 Subject: [PATCH 26/26] remove dependency --- pyproject.toml | 142 ++++++++++++++++++++----------------------------- 1 file changed, 58 insertions(+), 84 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 5286dfe960..c6d79ca6d9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,16 +8,11 @@ build-backend = "setuptools.build_meta" name = "disnake" description = "A Python wrapper for the Discord API" readme = "README.md" -authors = [ - { name = "Disnake Development" } -] +authors = [{ name = "Disnake Development" }] requires-python = ">=3.8" keywords = ["disnake", "discord", "discord api"] license = { text = "MIT" } -dependencies = [ - "aiohttp>=3.7.0,<4.0", - "youtube-dl @ git+https://github.com/ytdl-org/youtube-dl", -] +dependencies = ["aiohttp>=3.7.0,<4.0"] classifiers = [ "Development Status :: 5 - Production/Stable", "License :: OSI Approved :: MIT License", @@ -45,14 +40,11 @@ Repository = "https://github.com/DisnakeDev/disnake" [project.optional-dependencies] speed = [ "orjson~=3.6", - # taken from aiohttp[speedups] - "aiodns>=1.1", + # taken from aiohttp[speedups] "aiodns>=1.1", "Brotli", 'cchardet; python_version < "3.10"', ] -voice = [ - "PyNaCl>=1.3.0,<1.6", -] +voice = ["PyNaCl>=1.3.0,<1.6"] docs = [ "sphinx==7.0.1", "sphinxcontrib-trio~=1.1.2", @@ -65,9 +57,7 @@ docs = [ discord = ["discord-disnake"] [tool.pdm.dev-dependencies] -nox = [ - "nox==2022.11.21", -] +nox = ["nox==2022.11.21"] tools = [ "pre-commit~=3.0", "slotscheck~=0.16.4", @@ -75,9 +65,7 @@ tools = [ "check-manifest==0.49", "ruff==0.3.4", ] -changelog = [ - "towncrier==23.6.0", -] +changelog = ["towncrier==23.6.0"] codemod = [ # run codemods on the respository (mostly automated typing) "libcst~=1.1.0", @@ -98,11 +86,7 @@ test = [ "looptime~=0.2", "coverage[toml]~=6.5.0", ] -build = [ - "wheel~=0.40.0", - "build~=0.10.0", - "twine~=4.0.2", -] +build = ["wheel~=0.40.0", "build~=0.10.0", "twine~=4.0.2"] [tool.pdm.scripts] black = { composite = ["lint black"], help = "Run black" } @@ -110,7 +94,10 @@ docs = { cmd = "nox -Rs docs --", help = "Build the documentation for developmen lint = { cmd = "nox -Rs lint --", help = "Check all files for linting errors" } pyright = { cmd = "nox -Rs pyright --", help = "Run pyright" } setup_env = { cmd = "pdm install -d -G speed -G docs -G voice", help = "Set up the local environment and all dependencies" } -post_setup_env = { composite = ["python -m ensurepip --default-pip", "pre-commit install --install-hooks"] } +post_setup_env = { composite = [ + "python -m ensurepip --default-pip", + "pre-commit install --install-hooks", +] } test = { cmd = "nox -Rs test --", help = "Run pytest" } # legacy tasks for those who still type `task` @@ -137,14 +124,15 @@ target-version = "py38" select = [ # commented out codes are intended to be enabled in future prs "F", # pyflakes - "E", "W", # pycodestyle + "E", + "W", # pycodestyle # "D", # pydocstyle - "D2", # pydocstyle, docstring formatting - "D4", # pydocstyle, docstring structure/content + "D2", # pydocstyle, docstring formatting + "D4", # pydocstyle, docstring structure/content # "ANN", # flake8-annotations - "S", # flake8-bandit - "B", # flake8-bugbear - "C4", # flake8-comprehensions + "S", # flake8-bandit + "B", # flake8-bugbear + "C4", # flake8-comprehensions "DTZ", # flake8-datetimez # "EM", # flake8-errmsg "G", # flake8-logging-format @@ -162,8 +150,10 @@ select = [ "PLE", # pylint error # "PLR", # pylint refactor "PLW", # pylint warnings - "TRY002", "TRY004", "TRY201", # tryceratops - "I", # isort + "TRY002", + "TRY004", + "TRY201", # tryceratops + "I", # isort ] ignore = [ # star imports @@ -212,14 +202,14 @@ ignore = [ "PLE0237", # pyright seems to catch this already # temporary disables, to fix later - "D205", # blank line required between summary and description - "D401", # first line of docstring should be in imperative mood - "D417", # missing argument description in docstring - "B904", # within an except clause raise from error or from none - "B026", # backwards star-arg unpacking - "E501", # line too long - "E731", # assigning lambdas to variables - "T201", # print statements + "D205", # blank line required between summary and description + "D401", # first line of docstring should be in imperative mood + "D417", # missing argument description in docstring + "B904", # within an except clause raise from error or from none + "B026", # backwards star-arg unpacking + "E501", # line too long + "E731", # assigning lambdas to variables + "T201", # print statements ] [tool.ruff.lint.per-file-ignores] @@ -270,35 +260,35 @@ title_format = false underlines = "-~" issue_format = ":issue:`{issue}`" - [[tool.towncrier.type]] - directory = "breaking" - name = "Breaking Changes" - showcontent = true +[[tool.towncrier.type]] +directory = "breaking" +name = "Breaking Changes" +showcontent = true - [[tool.towncrier.type]] - directory = "deprecate" - name = "Deprecations" - showcontent = true +[[tool.towncrier.type]] +directory = "deprecate" +name = "Deprecations" +showcontent = true - [[tool.towncrier.type]] - directory = "feature" - name = "New Features" - showcontent = true +[[tool.towncrier.type]] +directory = "feature" +name = "New Features" +showcontent = true - [[tool.towncrier.type]] - directory = "bugfix" - name = "Bug Fixes" - showcontent = true +[[tool.towncrier.type]] +directory = "bugfix" +name = "Bug Fixes" +showcontent = true - [[tool.towncrier.type]] - directory = "doc" - name = "Documentation" - showcontent = true +[[tool.towncrier.type]] +directory = "doc" +name = "Documentation" +showcontent = true - [[tool.towncrier.type]] - directory = "misc" - name = "Miscellaneous" - showcontent = true +[[tool.towncrier.type]] +directory = "misc" +name = "Miscellaneous" +showcontent = true [tool.slotscheck] @@ -314,17 +304,8 @@ exclude-modules = ''' [tool.pyright] typeCheckingMode = "strict" -include = [ - "disnake", - "docs", - "examples", - "test_bot", - "tests", - "*.py", -] -ignore = [ - "disnake/ext/mypy_plugin", -] +include = ["disnake", "docs", "examples", "test_bot", "tests", "*.py"] +ignore = ["disnake/ext/mypy_plugin"] # this is one of the diagnostics that aren't enabled by default, even in strict mode reportUnnecessaryTypeIgnoreComment = true @@ -359,15 +340,8 @@ asyncio_mode = "strict" [tool.coverage.run] branch = true -include = [ - "disnake/*", - "tests/*", -] -omit = [ - "disnake/ext/mypy_plugin/*", - "disnake/types/*", - "disnake/__main__.py", -] +include = ["disnake/*", "tests/*"] +omit = ["disnake/ext/mypy_plugin/*", "disnake/types/*", "disnake/__main__.py"] [tool.coverage.report] precision = 1