Skip to content
Open
Show file tree
Hide file tree
Changes from 41 commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
3c35a8c
Port #7528
elenakrittik Nov 6, 2023
d6877d6
Port #7545
elenakrittik Nov 6, 2023
c24dc14
fix(autoshard): Errors when trying to use AutoSharded client variants…
elenakrittik Nov 6, 2023
124890b
fix(test_bot): Update according to new constraints.
elenakrittik Nov 7, 2023
8f85551
refactor: Further reduce loop state sharing.
elenakrittik Nov 7, 2023
26c4663
misc(debug): Set a name for hearbeat thread.
elenakrittik Nov 7, 2023
213edba
compat: Add Client.loop property for compatibility.
elenakrittik Nov 7, 2023
e76ce01
compat: Bring back connector and revert behavior changes.
elenakrittik Nov 12, 2023
fb8b1f2
fix: ruff errors
elenakrittik Nov 12, 2023
f812d1c
docs: Remove syncio_debug param from Client.
elenakrittik Nov 12, 2023
c3988ff
refactor: Async extensions (& cogs).
elenakrittik Nov 12, 2023
2b30e94
fix: Revert setup to be a hook instead of an event.
elenakrittik Nov 12, 2023
ebea051
docs: Fix setup_hook docstring.
elenakrittik Nov 12, 2023
f37594e
fix: Self-review fixes
elenakrittik Nov 12, 2023
1900902
docs: Add changelogs.
elenakrittik Nov 12, 2023
1daebcd
docs: Add example usage of setup_hook.
elenakrittik Nov 12, 2023
3c1baa9
docs: Post-PR-open fixes. Of course.
elenakrittik Nov 12, 2023
5566e17
fix: Formatting.
elenakrittik Nov 12, 2023
416333e
docs: Minor wording fix.
elenakrittik Nov 12, 2023
f677880
fix: run codemod
elenakrittik Nov 12, 2023
8a873a5
Merge branch 'master' into refactor/async
elenakrittik Nov 12, 2023
abd0a1d
fix: propagate ignore_session_start_limit
elenakrittik Nov 14, 2023
b68f114
Merge branch 'refactor/async' of https://github.com/elenakrittik/disn…
elenakrittik Nov 14, 2023
1d740c3
docs: 3.0
elenakrittik Nov 16, 2023
94ceb0b
Merge branch 'master' into refactor/async
elenakrittik Nov 16, 2023
f38fed2
Merge branch 'master' into refactor/async
elenakrittik Nov 18, 2023
a078653
Merge branch 'master' into refactor/async
elenakrittik Jan 21, 2024
6cc4a6d
merge latest changes
elenakrittik May 9, 2024
d8e80b0
Merge branch 'master' of https://github.com/DisnakeDev/disnake into r…
elenakrittik May 9, 2024
0d34a70
Apply suggestions from code review
elenakrittik May 10, 2024
45382d6
Merge branch 'master' into refactor/async
elenakrittik May 10, 2024
a0c5a8e
fix voice sending
elenakrittik May 14, 2024
df499a7
improve error message
elenakrittik May 14, 2024
c0ab613
remove dependency
elenakrittik May 14, 2024
01fc5f2
merge
elenakrittik Apr 2, 2025
10c39cf
merge
elenakrittik Dec 6, 2025
6328931
merge
elenakrittik Dec 6, 2025
1f2dde0
merge
elenakrittik Dec 6, 2025
ead0604
merge
elenakrittik Dec 6, 2025
1e703e5
merge
elenakrittik Dec 6, 2025
1dacea2
merge
elenakrittik Dec 7, 2025
302f91f
merge
elenakrittik Dec 7, 2025
442bad8
merge
elenakrittik Dec 7, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog/1132.breaking.0.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Removed the ``loop`` and ``asyncio_debug`` parameters from :class:`Client`.
1 change: 1 addition & 0 deletions changelog/1132.breaking.1.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
The majority of the library now assumes that there is an :mod:`asyncio` event loop running.
1 change: 1 addition & 0 deletions changelog/1132.deprecate.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Deprecate :attr:`Client.loop`. Use :func:`asyncio.get_running_loop` instead.
1 change: 1 addition & 0 deletions changelog/1132.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add :meth:`Client.setup_hook`.
1 change: 1 addition & 0 deletions changelog/1132.misc.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
:meth:`Client.run` now uses :func:`asyncio.run` under-the-hood instead of custom runner logic.
1 change: 1 addition & 0 deletions changelog/641.breaking.0.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
|commands| Make :meth:`Bot.load_extensions <ext.commands.Bot.load_extensions>`, :meth:`Bot.load_extension <ext.commands.Bot.load_extension>`, :meth:`Bot.unload_extension <ext.commands.Bot.unload_extension>`, :meth:`Bot.reload_extension <ext.commands.Bot.reload_extension>`, :meth:`Bot.add_cog <ext.commands.Bot.add_cog>`, and :meth:`Bot.remove_cog <ext.commands.Bot.remove_cog>` asynchronous.
1 change: 1 addition & 0 deletions changelog/641.breaking.1.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
|commands| :meth:`Cog.cog_load <ext.commands.Cog.cog_load>` is now called *after* the cog has finished loading.
1 change: 1 addition & 0 deletions changelog/641.feature.0.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
|commands| :meth:`Cog.cog_load <ext.commands.Cog.cog_load>` and :meth:`Cog.cog_unload <ext.commands.Cog.cog_unload>` can now be either sync or async.
1 change: 1 addition & 0 deletions changelog/641.feature.1.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
|commands| The ``setup`` and ``teardown`` functions utilized by :ref:`ext_commands_extensions` can now be asynchronous.
183 changes: 80 additions & 103 deletions disnake/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@

import asyncio
import logging
import signal
import sys
import traceback
import types
import warnings
from collections.abc import Callable, Coroutine
from datetime import timedelta
from errno import ECONNRESET
Expand Down Expand Up @@ -117,41 +117,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.
Expand Down Expand Up @@ -233,6 +198,9 @@ class Client:

A number of options can be passed to the :class:`Client`.

.. versionchanged:: 3.0
The ``asyncio_debug`` parameter has been removed. Use :meth:`asyncio.loop.set_debug` directly.
Comment on lines +201 to +202
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be worth to add this back and only deprecate it?

Also, it might be better to upgrade to Sphinx 7.3 first and change the directive to .. versionremoved:: instead.


Parameters
----------
max_messages: :class:`int` | :data:`None`
Expand All @@ -241,13 +209,6 @@ class Client:

.. versionchanged:: 1.3
Allow disabling the message cache and change the default size to ``1000``.
loop: :class:`asyncio.AbstractEventLoop` | :data:`None`
The :class:`asyncio.AbstractEventLoop` to use for asynchronous operations.
Defaults to :data:`None`, in which case the current event loop is
used, or a new loop is created if there is none.
asyncio_debug: :class:`bool`
Whether to enable asyncio debugging when the client starts.
Defaults to False.
connector: :class:`aiohttp.BaseConnector` | :data:`None`
The connector to use for connection pooling.
proxy: :class:`str` | :data:`None`
Expand Down Expand Up @@ -365,8 +326,6 @@ class Client:
----------
ws
The websocket gateway the client is currently connected to. Could be :data:`None`.
loop: :class:`asyncio.AbstractEventLoop`
The event loop that the client uses for asynchronous operations.
session_start_limit: :class:`SessionStartLimit` | :data:`None`
Information about the current session start limit.
Only available after initiating the connection.
Expand All @@ -382,8 +341,6 @@ class Client:
def __init__(
self,
*,
asyncio_debug: bool = False,
loop: asyncio.AbstractEventLoop | None = None,
shard_id: int | None = None,
shard_count: int | None = None,
enable_debug_events: bool = False,
Expand All @@ -409,21 +366,26 @@ def __init__(
# self.ws is set in the connect method
self.ws: DiscordWebSocket = None # pyright: ignore[reportAttributeAccessIssue]

if loop is None:
self.loop: asyncio.AbstractEventLoop = utils.get_event_loop()
else:
self.loop: asyncio.AbstractEventLoop = loop

self.loop.set_debug(asyncio_debug)
self._listeners: dict[str, list[tuple[asyncio.Future, Callable[..., bool]]]] = {}
self.session_start_limit: SessionStartLimit | None = None

if connector:
try:
asyncio.get_running_loop()
except RuntimeError:
msg = (
"`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"
)
raise RuntimeError(msg) from None
Comment on lines +372 to +382
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will remove this as this check only works when you create a dedicated loop for the connector but forget to set it as the global running loop, which is a very specific condition. I would rather not have this check at all (and document the requirement that was actually already there the whole time) or do a proper loop equivalency check, but the latter requires accessing private fields (<connector>._loop). Thoughts?


self.http: HTTPClient = HTTPClient(
connector,
proxy=proxy,
proxy_auth=proxy_auth,
unsync_clock=assume_unsync_clock,
loop=self.loop,
)

self._handlers: dict[str, Callable[..., Any]] = {
Expand Down Expand Up @@ -510,7 +472,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,
Expand All @@ -531,6 +492,29 @@ 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:: 3.0
Use :func:`asyncio.get_running_loop` directly.
"""
warnings.warn(
"Accessing `Client.loop` is deprecated. Use `asyncio.get_running_loop()` instead.",
category=DeprecationWarning,
stacklevel=2,
)
return asyncio.get_running_loop()

@loop.setter
def loop(self, value: asyncio.AbstractEventLoop) -> None:
warnings.warn(
"Assigning to `Client.loop` is deprecated. Use `asyncio.set_event_loop()` instead.",
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't communicated in the docstring of @.loop because setters don't show up in the docs. Should the docstring be adjusted?

category=DeprecationWarning,
stacklevel=2,
)
asyncio.set_event_loop(value)

@property
def latency(self) -> float:
""":class:`float`: Measures latency between a HEARTBEAT and a HEARTBEAT_ACK in seconds.
Expand Down Expand Up @@ -1015,12 +999,34 @@ async def before_identify_hook(self, shard_id: int | None, *, initial: bool = Fa
if not initial:
await asyncio.sleep(5.0)

async def setup_hook(self) -> None:
"""A hook that allows you to perform asynchronous setup like
initiating database connections or loading cogs/extensions after
the bot has 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
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:: 3.0
"""

# login state management

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`.

.. versionchanged:: 3.0
Now also calls :meth:`.setup_hook`.

Parameters
----------
Expand All @@ -1045,6 +1051,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:
Expand Down Expand Up @@ -1259,62 +1267,26 @@ 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::

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
This function should be the last function to be called because it is blocking.
That means that registration of commands, events or any code after this function
call will not execute until it returns.

Parameters
----------
token: :class:`str`
The discord token of the bot that is being ran.
.. versionchanged:: 3.0
Changed to use :func:`asyncio.run` instead of custom logic.
"""
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:
future.result()
except KeyboardInterrupt:
# I am unsure why this gets raised here but suppress it anyway
pass
return

# properties

Expand Down Expand Up @@ -1750,6 +1722,9 @@ def wait_for(

This function returns the **first event that meets the requirements**.

.. important::
Requires an :mod:`asyncio` loop to be running.

Examples
--------
Waiting for a user reply: ::
Expand Down Expand Up @@ -1813,6 +1788,8 @@ def check(reaction, user):

Raises
------
RuntimeError
No running ``asyncio`` loop.
asyncio.TimeoutError
If a timeout is provided and it was reached.

Expand All @@ -1823,7 +1800,7 @@ def check(reaction, user):
arguments that mirrors the parameters passed in the
:ref:`event <disnake_api_events>`.
"""
future = self.loop.create_future()
future = asyncio.get_running_loop().create_future()
if check is None:

def _check(*args) -> bool:
Expand Down
3 changes: 1 addition & 2 deletions disnake/context_managers.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ def _typing_done_callback(fut: asyncio.Future) -> None:

class Typing:
def __init__(self, messageable: Messageable | ThreadOnlyGuildChannel) -> None:
self.loop: asyncio.AbstractEventLoop = messageable._state.loop
self.messageable: Messageable | ThreadOnlyGuildChannel = messageable

async def do_typing(self) -> None:
Expand All @@ -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

Expand Down
5 changes: 0 additions & 5 deletions disnake/ext/commands/bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
from .interaction_bot_base import InteractionBotBase

if TYPE_CHECKING:
import asyncio
from collections.abc import Callable, Sequence

import aiohttp
Expand Down Expand Up @@ -268,7 +267,6 @@ def __init__(
default_install_types: ApplicationInstallTypes | None = None,
default_contexts: InteractionContextTypes | None = None,
asyncio_debug: bool = False,
loop: asyncio.AbstractEventLoop | None = None,
shard_id: int | None = None,
shard_count: int | None = None,
enable_debug_events: bool = False,
Expand Down Expand Up @@ -321,7 +319,6 @@ def __init__(
default_install_types: ApplicationInstallTypes | None = None,
default_contexts: InteractionContextTypes | None = None,
asyncio_debug: bool = False,
loop: asyncio.AbstractEventLoop | None = None,
shard_ids: list[int] | None = None, # instead of shard_id
shard_count: int | None = None,
enable_debug_events: bool = False,
Expand Down Expand Up @@ -493,7 +490,6 @@ def __init__(
default_install_types: ApplicationInstallTypes | None = None,
default_contexts: InteractionContextTypes | None = None,
asyncio_debug: bool = False,
loop: asyncio.AbstractEventLoop | None = None,
shard_id: int | None = None,
shard_count: int | None = None,
enable_debug_events: bool = False,
Expand Down Expand Up @@ -539,7 +535,6 @@ def __init__(
default_install_types: ApplicationInstallTypes | None = None,
default_contexts: InteractionContextTypes | None = None,
asyncio_debug: bool = False,
loop: asyncio.AbstractEventLoop | None = None,
shard_ids: list[int] | None = None, # instead of shard_id
shard_count: int | None = None,
enable_debug_events: bool = False,
Expand Down
4 changes: 2 additions & 2 deletions disnake/ext/commands/bot_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -401,8 +401,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 and _is_submodule(name, cmd.module):
Expand Down
Loading