diff --git a/discord/bot.py b/discord/bot.py index aabb6e472c..9b16a63cb5 100644 --- a/discord/bot.py +++ b/discord/bot.py @@ -31,13 +31,21 @@ import traceback from .commands.errors import CheckFailure -from typing import List, Optional, Union +from typing import ( + Any, + Callable, + Coroutine, + List, + Optional, + TypeVar, + Union, +) import sys from .client import Client from .shard import AutoShardedClient -from .utils import get, async_all +from .utils import MISSING, get, async_all from .commands import ( SlashCommand, SlashCommandGroup, @@ -52,6 +60,8 @@ from .errors import Forbidden, DiscordException from .interactions import Interaction +CoroFunc = Callable[..., Coroutine[Any, Any, Any]] +CFT = TypeVar('CFT', bound=CoroFunc) class ApplicationCommandMixin: """A mixin that implements common functionality for classes that need @@ -692,6 +702,102 @@ async def can_run( # type-checker doesn't distinguish between functions and methods return await async_all(f(ctx) for f in data) # type: ignore + # listener registration + + def add_listener(self, func: CoroFunc, name: str = MISSING) -> None: + """The non decorator alternative to :meth:`.listen`. + + Parameters + ----------- + func: :ref:`coroutine ` + The function to call. + name: :class:`str` + The name of the event to listen for. Defaults to ``func.__name__``. + + Example + -------- + + .. code-block:: python3 + + async def on_ready(): pass + async def my_message(message): pass + + bot.add_listener(on_ready) + bot.add_listener(my_message, 'on_message') + """ + name = func.__name__ if name is MISSING else name + + if not asyncio.iscoroutinefunction(func): + raise TypeError('Listeners must be coroutines') + + if name in self.extra_events: + self.extra_events[name].append(func) + else: + self.extra_events[name] = [func] + + def remove_listener(self, func: CoroFunc, name: str = MISSING) -> None: + """Removes a listener from the pool of listeners. + + Parameters + ----------- + func + The function that was used as a listener to remove. + name: :class:`str` + The name of the event we want to remove. Defaults to + ``func.__name__``. + """ + + name = func.__name__ if name is MISSING else name + + if name in self.extra_events: + try: + self.extra_events[name].remove(func) + except ValueError: + pass + + def listen(self, name: str = MISSING) -> Callable[[CFT], CFT]: + """A decorator that registers another function as an external + event listener. Basically this allows you to listen to multiple + events from different places e.g. such as :func:`.on_ready` + + The functions being listened to must be a :ref:`coroutine `. + + Example + -------- + + .. code-block:: python3 + + @bot.listen() + async def on_message(message): + print('one') + + # in some other file... + + @bot.listen('on_message') + async def my_message(message): + print('two') + + Would print one and two in an unspecified order. + + Raises + ------- + TypeError + The function being listened to is not a coroutine. + """ + + def decorator(func: CFT) -> CFT: + self.add_listener(func, name) + return func + + return decorator + + def dispatch(self, event_name: str, *args: Any, **kwargs: Any) -> None: + # super() will resolve to Client + super().dispatch(event_name, *args, **kwargs) # type: ignore + ev = 'on_' + event_name + for event in self.extra_events.get(ev, []): + self._schedule_event(event, ev, *args, **kwargs) # type: ignore + def before_invoke(self, coro): """A decorator that registers a coroutine as a pre-invoke hook. A pre-invoke hook is called directly before the command is diff --git a/discord/ext/commands/bot.py b/discord/ext/commands/bot.py index fae1165212..4c36943796 100644 --- a/discord/ext/commands/bot.py +++ b/discord/ext/commands/bot.py @@ -133,15 +133,6 @@ def __init__(self, command_prefix=when_mentioned, help_command=_default, **optio else: self.help_command = help_command - # internal helpers - - def dispatch(self, event_name: str, *args: Any, **kwargs: Any) -> None: - # super() will resolve to Client - super().dispatch(event_name, *args, **kwargs) # type: ignore - ev = 'on_' + event_name - for event in self.extra_events.get(ev, []): - self._schedule_event(event, ev, *args, **kwargs) # type: ignore - @discord.utils.copy_doc(discord.Client.close) async def close(self) -> None: for extension in tuple(self.__extensions): @@ -404,95 +395,6 @@ def after_invoke(self, coro: CFT) -> CFT: self._after_invoke = coro return coro - # listener registration - - def add_listener(self, func: CoroFunc, name: str = MISSING) -> None: - """The non decorator alternative to :meth:`.listen`. - - Parameters - ----------- - func: :ref:`coroutine ` - The function to call. - name: :class:`str` - The name of the event to listen for. Defaults to ``func.__name__``. - - Example - -------- - - .. code-block:: python3 - - async def on_ready(): pass - async def my_message(message): pass - - bot.add_listener(on_ready) - bot.add_listener(my_message, 'on_message') - - """ - name = func.__name__ if name is MISSING else name - - if not asyncio.iscoroutinefunction(func): - raise TypeError('Listeners must be coroutines') - - if name in self.extra_events: - self.extra_events[name].append(func) - else: - self.extra_events[name] = [func] - - def remove_listener(self, func: CoroFunc, name: str = MISSING) -> None: - """Removes a listener from the pool of listeners. - - Parameters - ----------- - func - The function that was used as a listener to remove. - name: :class:`str` - The name of the event we want to remove. Defaults to - ``func.__name__``. - """ - - name = func.__name__ if name is MISSING else name - - if name in self.extra_events: - try: - self.extra_events[name].remove(func) - except ValueError: - pass - - def listen(self, name: str = MISSING) -> Callable[[CFT], CFT]: - """A decorator that registers another function as an external - event listener. Basically this allows you to listen to multiple - events from different places e.g. such as :func:`.on_ready` - - The functions being listened to must be a :ref:`coroutine `. - - Example - -------- - - .. code-block:: python3 - - @bot.listen() - async def on_message(message): - print('one') - - # in some other file... - - @bot.listen('on_message') - async def my_message(message): - print('two') - - Would print one and two in an unspecified order. - - Raises - ------- - TypeError - The function being listened to is not a coroutine. - """ - - def decorator(func: CFT) -> CFT: - self.add_listener(func, name) - return func - - return decorator # cogs diff --git a/docs/api.rst b/docs/api.rst index 6b2bc1ce4f..7a489dc560 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -66,7 +66,7 @@ Bot .. autoclass:: Bot :members: :inherited-members: - :exclude-members: command, event, message_command, slash_command, user_command + :exclude-members: command, event, message_command, slash_command, user_command, listen .. automethod:: Bot.command(**kwargs) :decorator: @@ -83,6 +83,9 @@ Bot .. automethod:: Bot.user_command(**kwargs) :decorator: + .. automethod:: Bot.listen(name=None) + :decorator: + AutoShardedBot ~~~~~~~~~~~~~~~ .. attributetable:: AutoShardedBot