Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: move listeners to the main library #917

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
101 changes: 99 additions & 2 deletions nextcord/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,12 +84,13 @@
from .message import Attachment, Message
from .permissions import Permissions
from .scheduled_events import ScheduledEvent
from .types.checks import CoroFunc
from .types.interactions import ApplicationCommand as ApplicationCommandPayload
from .voice_client import VoiceProtocol

__all__ = ("Client",)

Coro = TypeVar("Coro", bound=Callable[..., Coroutine[Any, Any, Any]])
Coro = TypeVar("Coro", bound="CoroFunc")
InterT = TypeVar("InterT", bound="Interaction")


Expand Down Expand Up @@ -292,6 +293,7 @@ def __init__(
self.ws: DiscordWebSocket = None # type: ignore
self.loop: asyncio.AbstractEventLoop = asyncio.get_event_loop() if loop is None else loop
self._listeners: Dict[str, List[Tuple[asyncio.Future, Callable[..., bool]]]] = {}
self.extra_events: Dict[str, List[CoroFunc]] = {}

self.shard_id: Optional[int] = shard_id
self.shard_count: Optional[int] = shard_count
Expand Down Expand Up @@ -559,6 +561,9 @@ def dispatch(self, event: str, *args: Any, **kwargs: Any) -> None:
else:
self._schedule_event(coro, method, *args, **kwargs)

for coro in self.extra_events.get(method, []):
self._schedule_event(coro, method, *args, **kwargs)

async def on_error(self, event_method: str, *args: Any, **kwargs: Any) -> None:
"""|coro|

Expand Down Expand Up @@ -1263,7 +1268,7 @@ def _check(*_args) -> bool:
listeners.append((future, check))
return asyncio.wait_for(future, timeout)

# event registration
# event/listener registration

def event(self, coro: Coro) -> Coro:
"""A decorator that registers an event to listen to.
Expand Down Expand Up @@ -1294,6 +1299,98 @@ async def on_ready():
_log.debug("%s has successfully been registered as an event", coro.__name__)
return coro

def add_listener(self, func: CoroFunc, name: str = MISSING) -> None:
"""The non decorator alternative to :meth:`.listen`.
EmreTech marked this conversation as resolved.
Show resolved Hide resolved

.. versionadded:: 3.0

Parameters
----------
func: :ref:`coroutine <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

client.add_listener(on_ready)
client.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.

.. versionadded:: 3.0

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:
with contextlib.suppress(ValueError):
self.extra_events[name].remove(func)

def listen(self, name: str = MISSING) -> Callable[[Coro], Coro]:
"""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 <coroutine>`.

.. versionadded:: 3.0

Example
-------

.. code-block:: python3

@client.listen()
async def on_message(message):
print('one')

# in some other file...

@client.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: Coro) -> Coro:
self.add_listener(func, name)
return func

return decorator

async def change_presence(
self,
*,
Expand Down
94 changes: 8 additions & 86 deletions nextcord/ext/commands/bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,8 @@ class MissingMessageContentIntentWarning(UserWarning):


class BotBase(GroupMixin):
extra_events: Dict[str, List[CoroFunc]]

def __init__(
self,
command_prefix: Union[
Expand All @@ -163,7 +165,6 @@ def __init__(
)

self.command_prefix = command_prefix if command_prefix is not MISSING else ()
self.extra_events: Dict[str, List[CoroFunc]] = {}
self.__cogs: Dict[str, Cog] = {}
self.__extensions: Dict[str, types.ModuleType] = {}
self._checks: List[Check] = []
Expand Down Expand Up @@ -210,12 +211,11 @@ def __init__(

# internal helpers

# kept in since BotBase isn't a direct subclass of client and thus doesn't know
# about inheriting this method
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
EmreTech marked this conversation as resolved.
Show resolved Hide resolved
for event in self.extra_events.get(ev, []):
self._schedule_event(event, ev, *args, **kwargs) # type: ignore

@nextcord.utils.copy_doc(nextcord.Client.close)
async def close(self) -> None:
Expand Down Expand Up @@ -475,91 +475,13 @@ def after_invoke(self, coro: CFT) -> CFT:

# listener registration

@nextcord.utils.copy_doc(nextcord.Client.add_listener)
EmreTech marked this conversation as resolved.
Show resolved Hide resolved
def add_listener(self, func: CoroFunc, name: str = MISSING) -> None:
"""The non decorator alternative to :meth:`.listen`.

Parameters
----------
func: :ref:`coroutine <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]
super().add_listener(func, name) # type: ignore

@nextcord.utils.copy_doc(nextcord.Client.remove_listener)
def remove_listener(self, func: CoroFunc, name: str = MISSING) -> None:
EmreTech marked this conversation as resolved.
Show resolved Hide resolved
"""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:
with contextlib.suppress(ValueError):
self.extra_events[name].remove(func)

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 <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
super().remove_listener(func, name) # type: ignore

# cogs

Expand Down