diff --git a/CHANGELOG.md b/CHANGELOG.md index 97d1d56853..791df31a1f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,8 @@ These changes are available on the `master` branch, but have not yet been releas - Added new events `on_bridge_command`, `on_bridge_command_completion`, and `on_bridge_command_error`. ([#1916](https://github.com/Pycord-Development/pycord/pull/1916)) +- Added the `@client.once()` decorator, which serves as a one-time event listener. + ([#1940](https://github.com/Pycord-Development/pycord/pull/1940)) ### Fixed diff --git a/discord/client.py b/discord/client.py index b8b1057e67..07bba3bf41 100644 --- a/discord/client.py +++ b/discord/client.py @@ -1286,6 +1286,64 @@ async def on_ready(): _log.debug("%s has successfully been registered as an event", coro.__name__) return coro + def once( + self, name: str = MISSING, check: Callable[..., bool] | None = None + ) -> Coro: + """A decorator that registers an event to listen to only once. + + You can find more info about the events on the :ref:`documentation below `. + + The events must be a :ref:`coroutine `, if not, :exc:`TypeError` is raised. + + Parameters + ---------- + name: :class:`str` + The name of the event we want to listen to. This is passed to + :py:meth:`~discord.Client.wait_for`. Defaults to ``func.__name__``. + check: Optional[Callable[..., :class:`bool`]] + A predicate to check what to wait for. The arguments must meet the + parameters of the event being waited for. + + Raises + ------ + TypeError + The coroutine passed is not actually a coroutine. + + Example + ------- + + .. code-block:: python3 + + @client.once() + async def ready(): + print('Ready!') + """ + + def decorator(func: Coro) -> Coro: + if not asyncio.iscoroutinefunction(func): + raise TypeError("event registered must be a coroutine function") + + async def wrapped() -> None: + nonlocal name + nonlocal check + + name = func.__name__ if name is MISSING else name + + args = await self.wait_for(name, check=check) + + arg_len = func.__code__.co_argcount + if arg_len == 0 and args is None: + await func() + elif arg_len == 1: + await func(args) + else: + await func(*args) + + self.loop.create_task(wrapped()) + return func + + return decorator + async def change_presence( self, *, diff --git a/docs/api/clients.rst b/docs/api/clients.rst index 8bf10cff4f..aeae87cbcb 100644 --- a/docs/api/clients.rst +++ b/docs/api/clients.rst @@ -10,7 +10,7 @@ Bots .. autoclass:: Bot :members: :inherited-members: - :exclude-members: command, event, message_command, slash_command, user_command, listen + :exclude-members: command, event, message_command, slash_command, user_command, listen, once .. automethod:: Bot.command(**kwargs) :decorator: @@ -30,6 +30,9 @@ Bots .. automethod:: Bot.listen(name=None) :decorator: + .. automethod:: Bot.once(name=None, check=None) + :decorator: + .. attributetable:: AutoShardedBot .. autoclass:: AutoShardedBot :members: @@ -41,7 +44,7 @@ Clients .. attributetable:: Client .. autoclass:: Client :members: - :exclude-members: fetch_guilds, event + :exclude-members: fetch_guilds, event, once .. automethod:: Client.event() :decorator: @@ -49,6 +52,9 @@ Clients .. automethod:: Client.fetch_guilds :async-for: + .. automethod:: Client.once(name=None, check=None) + :decorator: + .. attributetable:: AutoShardedClient .. autoclass:: AutoShardedClient :members: diff --git a/docs/api/events.rst b/docs/api/events.rst index 10e2039c7d..4903ae8f92 100644 --- a/docs/api/events.rst +++ b/docs/api/events.rst @@ -7,10 +7,11 @@ Event Reference This section outlines the different types of events listened by :class:`Client`. -There are 3 ways to register an event, the first way is through the use of +There are 4 ways to register an event, the first way is through the use of :meth:`Client.event`. The second way is through subclassing :class:`Client` and -overriding the specific events. The third way is through the use of :meth:`Client.listen`, which can be used to assign multiple -event handlers instead of only one like in :meth:`Client.event`. For example: +overriding the specific events. The third way is through the use of :meth:`Client.listen`, +which can be used to assign multiple event handlers instead of only one like in :meth:`Client.event`. +The fourth way is through the use of :meth:`Client.once`, which serves as a one-time event listener. For example: .. code-block:: python :emphasize-lines: 17, 22 @@ -40,6 +41,11 @@ event handlers instead of only one like in :meth:`Client.event`. For example: async def on_message(message: discord.Message): print(f"Received {message.content}") + # Runs only for the 1st 'on_message' event. Can be useful for listening to 'on_ready' + @client.once() + async def message(message: discord.Message): + print(f"Received {message.content}") + If an event handler raises an exception, :func:`on_error` will be called to handle it, which defaults to print a traceback and ignoring the exception. diff --git a/docs/ext/commands/api.rst b/docs/ext/commands/api.rst index 355a8a1020..119a8de10c 100644 --- a/docs/ext/commands/api.rst +++ b/docs/ext/commands/api.rst @@ -23,7 +23,7 @@ Bot .. autoclass:: discord.ext.commands.Bot :members: :inherited-members: - :exclude-members: after_invoke, before_invoke, check, check_once, command, event, group, listen + :exclude-members: after_invoke, before_invoke, check, check_once, command, event, group, listen, once .. automethod:: Bot.after_invoke() :decorator: @@ -49,6 +49,9 @@ Bot .. automethod:: Bot.listen(name=None) :decorator: + .. automethod:: Bot.once(name=None, check=None) + :decorator: + AutoShardedBot ~~~~~~~~~~~~~~