Passed
Push — main ( a2b0a1...a2405b )
by
unknown
01:44
created

pincer.client.Client.event()   A

Complexity

Conditions 4

Size

Total Lines 74
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 14
dl 0
loc 74
rs 9.7
c 0
b 0
f 0
cc 4
nop 1

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
# -*- coding: utf-8 -*-
0 ignored issues
show
introduced by
Missing module docstring
Loading history...
2
# MIT License
3
#
4
# Copyright (c) 2021 Pincer
5
#
6
# Permission is hereby granted, free of charge, to any person obtaining
7
# a copy of this software and associated documentation files
8
# (the "Software"), to deal in the Software without restriction,
9
# including without limitation the rights to use, copy, modify, merge,
10
# publish, distribute, sublicense, and/or sell copies of the Software,
11
# and to permit persons to whom the Software is furnished to do so,
12
# subject to the following conditions:
13
#
14
# The above copyright notice and this permission notice shall be
15
# included in all copies or substantial portions of the Software.
16
#
17
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
18
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
19
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
20
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
21
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
22
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
23
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
24
25
from __future__ import annotations
26
27
import logging
28
from asyncio import iscoroutinefunction, run
29
from typing import Optional, Any, Union, Dict, Tuple, List
30
31
from . import __package__
32
from ._config import events
33
from .commands import ChatCommandHandler
0 ignored issues
show
introduced by
Cannot import 'commands' due to syntax error 'invalid syntax (<unknown>, line 89)'
Loading history...
34
from .core.dispatch import GatewayDispatch
35
from .core.gateway import Dispatcher
36
from .core.http import HTTPClient
37
from .exceptions import InvalidEventName
38
from .middleware import middleware
39
from .objects import User, Intents
40
from .utils import get_index, should_pass_cls, Coro
41
42
_log = logging.getLogger(__package__)
43
44
MiddlewareType = Optional[Union[Coro, Tuple[str, List[Any], Dict[str, Any]]]]
45
46
_events: Dict[str, Optional[Union[str, Coro]]] = {}
47
48
for event in events:
49
    event_final_executor = f"on_{event}"
50
51
    # Event middleware for the library.
52
    # Function argument is a payload (GatewayDispatch).
53
54
    # The function must return a string which
55
    # contains the main event key.
56
57
    # As second value a list with arguments,
58
    # and the third value value must be a dictionary.
59
    # The last two are passed on as *args and **kwargs.
60
61
    # NOTE: These return values must be passed as a tuple!
62
    _events[event] = event_final_executor
63
64
    # The registered event by the client. Do not manually overwrite.
65
    _events[event_final_executor] = None
66
67
68
def event_middleware(call: str, *, override: bool = False):
69
    """
70
    Middleware are methods which can be registered with this decorator.
71
    These methods are invoked before any ``on_`` event.
72
    As the ``on_`` event is the final call.
73
74
    A default call exists for all events, but some might already be in
75
    use by the library.
76
77
    If you know what you are doing, you can override these default
78
    middleware methods by passing the override parameter.
79
80
    The method to which this decorator is registered must be a coroutine,
81
    and it must return a tuple with the following format:
82
83
    .. code-block:: python
84
85
        tuple(
86
            key for next middleware or final event [str],
87
            args for next middleware/event which will be passed as *args
88
                [list(Any)],
89
            kwargs for next middleware/event which will be passed as
90
                **kwargs [dict(Any)]
91
        )
92
93
    Two parameters are passed to the middleware. The first parameter is
94
    the current socket connection with and the second one is the payload
95
    parameter which is of type :class:`~.core.dispatch.GatewayDispatch`.
96
    This contains the response from the discord API.
97
98
    :Implementation example:
99
100
    .. code-block:: pycon
101
102
        >>> @event_middleware("ready", override=True)
103
        >>> async def custom_ready(_, payload: GatewayDispatch):
104
        >>>     return "on_ready", [User.from_dict(payload.data.get("user"))]
105
106
        >>> @Client.event
107
        >>> async def on_ready(bot: User):
108
        >>>     print(f"Signed in as {bot}")
109
110
111
    :param call:
112
        The call where the method should be registered.
113
114
    Keyword Arguments:
115
116
    :param override:
117
        Setting this to True will allow you to override existing
118
        middleware. Usage of this is discouraged, but can help you out
119
        of some situations.
120
    """
121
122
    def decorator(func: Coro):
123
        if override:
124
            _log.warning(
125
                "Middleware overriding has been enabled for `%s`."
126
                " This might cause unexpected behavior.", call
127
            )
128
129
        if not override and callable(_events.get(call)):
130
            raise RuntimeError(
131
                f"Middleware event with call `{call}` has "
132
                "already been registered"
133
            )
134
135
        async def wrapper(cls, payload: GatewayDispatch):
136
            _log.debug("`%s` middleware has been invoked", call)
137
138
            print(func, should_pass_cls(func))
139
140
            return await (
141
                func(cls, payload)
142
                if should_pass_cls(func)
143
                else func(payload)
144
            )
145
146
        _events[call] = wrapper
147
        return wrapper
148
149
    return decorator
150
151
152
for event, middleware in middleware.items():
153
    event_middleware(event)(middleware)
154
155
156
class Client(Dispatcher):
0 ignored issues
show
introduced by
Missing class docstring
Loading history...
157
    def __init__(
158
            self,
159
            token: str, *,
160
            received: str = None,
161
            intents: Intents = None
162
    ):
163
        """
164
        The client is the main instance which is between the programmer
165
            and the discord API.
166
167
        This client represents your bot.
168
169
        :param token:
170
            The secret bot token which can be found in
171
            `<https://discord.com/developers/applications/<bot_id>/bot>`_
172
173
        :param received:
174
            The default message which will be sent when no response is
175
            given.
176
177
        :param intents:
178
            The discord intents for your client.
179
        """
180
        super().__init__(
181
            token,
182
            handlers={
183
                # Use this event handler for opcode 0.
184
                0: self.event_handler
185
            },
186
            intents=intents or Intents.NONE
187
        )
188
189
        self.bot: Optional[User] = None
190
        self.__received = received or "Command arrived successfully!"
191
        self.http = HTTPClient(token)
192
193
    @property
194
    def chat_commands(self):
195
        """
196
        Get a list of chat command calls which have been registered in
197
        the ChatCommandHandler.
198
        """
199
        return [cmd.app.name for cmd in ChatCommandHandler.register.values()]
200
201
    @staticmethod
202
    def event(coroutine: Coro):
203
        """
204
        Register a Discord gateway event listener. This event will get
205
        called when the client receives a new event update from Discord
206
        which matches the event name.
207
208
        The event name gets pulled from your method name, and this must
209
        start with ``on_``. This forces you to write clean and consistent
210
        code.
211
212
        This decorator can be used in and out of a class, and all
213
        event methods must be coroutines. *(async)*
214
215
        :Example usage:
216
217
        .. code-block:: pycon
218
219
            >>> # Function based
220
            >>> from pincer import Client
221
            >>>
222
            >>> client = Client("token")
223
            >>>
224
            >>> @client.event
225
            >>> async def on_ready():
226
            ...     print(f"Signed in as {client.bot}")
227
            >>>
228
            >>> if __name__ == "__main__":
229
            ...     client.run()
230
231
        .. code-block :: pycon
232
233
            >>> # Class based
234
            >>> from pincer import Client
235
            >>>
236
            >>> class BotClient(Client):
237
            ...     @Client.event
238
            ...     async def on_ready(self):
239
            ...         print(f"Signed in as {self.bot}")
240
            >>>
241
            >>> if __name__ == "__main__":
242
            ...     BotClient("token").run()
243
244
245
        :param coroutine: # TODO: add info
246
247
        :raises TypeError:
248
            If the method is not a coroutine.
249
250
        :raises InvalidEventName:
251
            If the event name does not start with ``on_``, has already
252
            been registered or is not a valid event name.
253
        """
254
255
        if not iscoroutinefunction(coroutine):
256
            raise TypeError(
257
                "Any event which is registered must be a coroutine function"
258
            )
259
260
        name: str = coroutine.__name__.lower()
261
262
        if not name.startswith("on_"):
263
            raise InvalidEventName(
264
                f"The event named `{name}` must start with `on_`"
265
            )
266
267
        if _events.get(name) is not None:
268
            raise InvalidEventName(
269
                f"The event `{name}` has already been registered or is not "
270
                f"a valid event name."
271
            )
272
273
        _events[name] = coroutine
274
        return coroutine
275
276
    def run(self):
277
        """Start the event listener"""
278
        self.start_loop()
279
        run(self.http.close())
280
281
    async def handle_middleware(
282
            self,
283
            payload: GatewayDispatch,
284
            key: str,
285
            *args,
286
            **kwargs
287
    ) -> Tuple[Optional[Coro], List[Any], Dict[str, Any]]:
288
        """
289
        Handles all middleware recursively. Stops when it has found an
290
        event name which starts with ``on_``.
291
292
        :param payload:
293
            The original payload for the event.
294
295
        :param key:
296
            The index of the middleware in ``_events``.
297
298
        :param \\*args:
299
            The arguments which will be passed to the middleware.
300
301
        :param \\*\\*kwargs:
302
            The named arguments which will be passed to the middleware.
303
304
        :return:
305
            A tuple where the first element is the final executor
306
            (so the event) its index in ``_events``. The second and third
307
            element are the ``*args`` and ``**kwargs`` for the event.
308
        """
309
        ware: MiddlewareType = _events.get(key)
310
        next_call, arguments, params = ware, [], {}
311
312
        if iscoroutinefunction(ware):
313
            extractable = await ware(self, payload, *args, **kwargs)
314
315
            if not isinstance(extractable, tuple):
316
                raise RuntimeError(
317
                    f"Return type from `{key}` middleware must be tuple. "
318
                )
319
320
            next_call = get_index(extractable, 0, "")
321
            arguments = get_index(extractable, 1, [])
322
            params = get_index(extractable, 2, {})
323
324
        if next_call is None:
325
            raise RuntimeError(f"Middleware `{key}` has not been registered.")
326
327
        return (
328
            (next_call, arguments, params)
329
            if next_call.startswith("on_")
330
            else await self.handle_middleware(
331
                payload, next_call, *arguments, **params
332
            )
333
        )
334
335
    async def event_handler(self, _, payload: GatewayDispatch):
336
        """
337
        Handles all payload events with opcode 0.
338
339
        :param _:
340
            Socket param, but this isn't required for this handler. So
341
            its just a filler parameter, doesn't matter what is passed.
342
343
        :param payload:
344
            The payload sent from the Discord gateway, this contains the
345
            required data for the client to know what event it is and
346
            what specifically happened.
347
        """
348
        event_name = payload.event_name.lower()
349
350
        key, args, kwargs = await self.handle_middleware(payload, event_name)
351
352
        call = _events.get(key)
353
354
        if iscoroutinefunction(call):
355
            if should_pass_cls(call):
356
                await call(self, *args, **kwargs)
357
            else:
358
                await call(*args, **kwargs)
359
360
361
Bot = Client
362