Passed
Pull Request — main (#68)
by
unknown
01:44
created

pincer.client   A

Complexity

Total Complexity 21

Size/Duplication

Total Lines 366
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 21
eloc 113
dl 0
loc 366
rs 10
c 0
b 0
f 0

1 Function

Rating   Name   Duplication   Size   Complexity  
B event_middleware() 0 82 5

7 Methods

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

It is generally discouraged to redefine built-ins as this makes code very hard to read.

Loading history...
362
        # TODO: docs
0 ignored issues
show
Coding Style introduced by
TODO and FIXME comments should generally be avoided.
Loading history...
363
        return await Guild.from_id(self, id)
364
365
Bot = Client
366