Passed
Push — main ( 1ee03a...dea977 )
by
unknown
01:38
created

pincer.client.Client._http()   A

Complexity

Conditions 1

Size

Total Lines 19
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 3
dl 0
loc 19
rs 10
c 0
b 0
f 0
cc 1
nop 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
import logging
25
from asyncio import iscoroutinefunction
26
from typing import Optional, Any, Union, Dict, Tuple, List
27
28
from pincer import __package__
1 ignored issue
show
Bug Best Practice introduced by
This seems to re-define the built-in __package__.

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

Loading history...
29
from pincer._config import GatewayConfig, events
0 ignored issues
show
Unused Code introduced by
Unused GatewayConfig imported from pincer._config
Loading history...
30
from pincer.core.dispatch import GatewayDispatch
31
from pincer.core.gateway import Dispatcher
32
from pincer.core.http import HTTPClient
33
from pincer.exceptions import InvalidEventName
34
from pincer.objects.user import User
35
from pincer.utils.extraction import get_index
36
from pincer.utils.insertion import should_pass_cls
37
from pincer.utils.types import Coro
38
39
_log = logging.getLogger(__package__)
40
41
middleware_type = Optional[Union[Coro, Tuple[str, List[Any], Dict[str, Any]]]]
1 ignored issue
show
Coding Style Naming introduced by
Class name "middleware_type" doesn't conform to PascalCase naming style ('[^\\W\\da-z][^\\W_]+$' pattern)

This check looks for invalid names for a range of different identifiers.

You can set regular expressions to which the identifiers must conform if the defaults do not match your requirements.

If your project includes a Pylint configuration file, the settings contained in that file take precedence.

To find out more about Pylint, please refer to their site.

Loading history...
42
43
_events: Dict[str, Optional[Union[str, Coro]]] = {}
44
45
for event in events:
46
    event_final_executor = f"on_{event}"
47
48
    # Event middleware for the library. Function argument is a payload
49
    # (GatewayDispatch). The function must return a string which
50
    # contains the main event key. As second value a list with arguments,
51
    # and thee third value value must be a dictionary. The last two are
52
    # passed on as *args and **kwargs.
53
    #
54
    # NOTE: These return values must be passed as a tuple!
55
    _events[event] = event_final_executor
56
57
    # The registered event by the client. Do not manually overwrite.
58
    _events[event_final_executor] = None
59
60
61
def middleware(call: str, *, override: bool = False):
62
    """
63
    Middleware are methods which can be registered with this decorator.
64
    These methods are invoked before any `on_` event. As the `on_` event
65
    is the final call.
66
67
    A default call exists for all events, but some might already be in
68
    use by the library. If you know what you are doing, you can override
69
    these default middleware methods by passing the override parameter.
70
71
    The method to which this decorator is registered must be a coroutine,
72
    and it must return a tuple with the following format:
73
    ```
74
        tuple(
75
            key for next middleware or final event [str],
76
            args for next middleware/event which will be passed as *args
77
                [list(Any)],
78
            kwargs for next middleware/event which will be passed as
79
                **kwargs [dict(Any)]
80
        )
81
    ```
82
83
    One parameter is passed to the middleware. This parameter is the
84
    payload parameter which is of type GatewayDispatch. This contains
85
    the response from the discord API.
86
87
    Implementation example:
88
    ```py
89
    >>> @middleware("ready", override=True)
90
    >>> async def custom_ready(payload: GatewayDispatch):
91
    >>>     return "on_ready", [User.from_dict(payload.data.get("user"))]
92
93
    >>> @Client.event
94
    >>> async def on_ready(bot: User):
95
    >>>     print(f"Signed in as {bot}")
96
    ```
97
98
    :param call:
99
        The call where the method should be registered.
100
101
    Keyword Arguments:
102
103
    :param override:
104
        Setting this to True will allow you to override existing
105
        middleware. Usage of this is discouraged, but can help you out
106
        of some situations.
107
    """
108
    def decorator(func: Coro):
109
        if override:
110
            _log.warning(f"Middleware overriding has been enabled for `{call}`."
0 ignored issues
show
introduced by
Use lazy % formatting in logging functions
Loading history...
111
                         " This might cause unexpected behaviour.")
112
113
        if not override and callable(_events.get(call)):
2 ignored issues
show
Comprehensibility Best Practice introduced by
The variable _events does not seem to be defined.
Loading history...
Comprehensibility Best Practice introduced by
The variable call does not seem to be defined.
Loading history...
114
            raise RuntimeError(f"Middleware event with call `{call}` has "
115
                               "already been registered")
116
117
        async def wrapper(cls, payload: GatewayDispatch):
118
            _log.debug("`%s` middleware has been invoked", call)
119
            return await func(cls, payload) \
120
                if should_pass_cls(func) \
121
                else await func(payload)
122
123
        _events[call] = wrapper
124
        return wrapper
125
126
    return decorator
127
128
129
class Client(Dispatcher):
0 ignored issues
show
introduced by
Missing class docstring
Loading history...
130
    def __init__(self, token: str):
131
        """
132
        The client is the main instance which is between the programmer and the
133
        discord API. This client represents your bot.
134
135
        :param token:
136
            The secret bot token which can be found in
137
            https://discord.com/developers/applications/<bot_id>/bot
138
        """
139
        # TODO: Implement intents
0 ignored issues
show
Coding Style introduced by
TODO and FIXME comments should generally be avoided.
Loading history...
140
        super().__init__(
141
            token,
142
            handlers={
143
                # Use this event handler for opcode 0.
144
                0: self.event_handler
145
            }
146
        )
147
148
        self.bot: Optional[User] = None
149
        self.__token = token
150
151
    @property
152
    def _http(self):
153
        """
154
        Returns a http client with the current client its
155
        authentication credentials.
156
157
        Usage example:
158
        ```py
159
        >>> async with self._http as client:
160
        >>>     await client.post(
161
        >>>         '<endpoint>',
162
        >>>         {
163
        >>>             "foo": "bar",
164
        >>>             "bar": "baz",
165
        >>>             "baz": "foo"
166
        >>>         })
167
        ```
168
        """
169
        return HTTPClient(self.__token)
170
171
    @staticmethod
172
    def event(coroutine: Coro):
173
        """
174
        Register a Discord gateway event listener. This event will get
175
        called when the client receives a new event update from Discord
176
        which matches the event name.
177
178
        The event name gets pulled from your method name, and this must
179
        start with `on_`. This forces you to write clean and consistent
180
        code.
181
182
        This decorator can be used in and out of a class, and all
183
        event methods must be coroutines. *(async)*
184
185
        Example usage:
186
        ```py
187
        >>> # Function based
188
        >>> from pincer import Client
189
        >>>
190
        >>> client = Client("token")
191
        >>>
192
        >>> @client.event
193
        >>> async def on_ready():
194
        >>>     print(f"Signed in as {client.bot}")
195
        >>>
196
        >>> if __name__ == "__main__":
197
        >>>     client.run()
198
        ```
199
        ```py
200
        >>> # Class based
201
        >>> from pincer import Client
202
        >>>
203
        >>> class BotClient(Client):
204
        >>>     @Client.event
205
        >>>     async def on_ready(self):
206
        >>>         print(f"Signed in as {self.bot}")
207
        >>>
208
        >>> if __name__ == "__main__":
209
        >>>     BotClient("token").run()
210
        ```
211
212
        :raises TypeError:
213
            If the method is not a coroutine.
214
215
        :raises InvalidEventName:
216
            If the event name does not start with 'on_', has already
217
            been registered or is not a valid event name.
218
        """
219
220
        if not iscoroutinefunction(coroutine):
221
            raise TypeError(
222
                "Any event which is registered must be a coroutine function"
223
            )
224
225
        name: str = coroutine.__name__.lower()
226
227
        if not name.startswith("on_"):
228
            raise InvalidEventName(
229
                f"The event `{name}` its name must start with `on_`"
230
            )
231
232
        if _events.get(name) is not None:
1 ignored issue
show
Comprehensibility Best Practice introduced by
The variable _events does not seem to be defined.
Loading history...
233
            raise InvalidEventName(
234
                f"The event `{name}` has already been registered or is not "
235
                f"a valid event name."
236
            )
237
238
        _events[name] = coroutine
239
        return coroutine
240
241
    async def handle_middleware(
242
            self,
243
            payload: GatewayDispatch,
244
            key: str,
245
            *args,
246
            **kwargs
247
    ) -> tuple[Optional[Coro], List[Any], Dict[str, Any]]:
0 ignored issues
show
introduced by
Value 'tuple' is unsubscriptable
Loading history...
248
        """
249
        Handles all middleware recursively. Stops when it has found an
250
        event name which starts with "on_".
251
252
        :param payload:
253
            The original payload for the event.
254
255
        :param key:
256
            The index of the middleware in `_events`.
257
258
        :param *args:
259
            The arguments which will be passed to the middleware.
260
261
        :param **kwargs:
262
            The named arguments which will be passed to the middleware.
263
264
        :return:
265
            A tuple where the first element is the final executor
266
            (so the event) its index in `_events`. The second and third
267
            element are the `*args` and `**kwargs` for the event.
268
        """
269
        ware: middleware_type = _events.get(key)
270
        next_call, arguments, params = ware, list(), dict()
271
272
        if iscoroutinefunction(ware):
273
            extractable = await ware(self, payload, *args, **kwargs)
274
275
            if not isinstance(extractable, tuple):
276
                raise RuntimeError(f"Return type from `{key}` middleware must "
277
                                   f"be tuple. ")
278
279
            next_call = get_index(extractable, 0, "")
280
            arguments = get_index(extractable, 1, list())
281
            params = get_index(extractable, 2, dict())
282
283
        if next_call is None:
284
            raise RuntimeError(f"Middleware `{key}` has not been registered.")
285
286
        return (next_call, arguments, params) \
287
            if next_call.startswith("on_") \
288
            else await self.handle_middleware(payload, next_call,
289
                                              *arguments, **params)
290
291
    async def event_handler(self, _, payload: GatewayDispatch):
292
        """
293
        Handles all payload events with opcode 0.
294
295
        :param _:
296
            Socket param, but this isn't required for this handler. So
297
            its just a filler parameter, doesn't matter what is passed.
298
299
        :param payload:
300
            The payload sent from the Discord gateway, this contains the
301
            required data for the client to know what event it is and
302
            what specifically happened.
303
        """
304
        event_name = payload.event_name.lower()
305
306
        key, args, kwargs = await self.handle_middleware(payload, event_name)
307
308
        call = _events.get(key)
309
310
        if iscoroutinefunction(call):
311
            if should_pass_cls(call):
312
                await call(self, *args, **kwargs)
313
            else:
314
                await call(*args, **kwargs)
315
316
    @middleware("ready")
317
    async def on_ready_middleware(self, payload: GatewayDispatch):
318
        """
319
        Middleware for `on_ready` event.
320
321
        :param payload: The data received from the ready event.
322
        """
323
        self.bot = User.from_dict(payload.data.get("user"))
324
        return "on_ready",
1 ignored issue
show
Unused Code introduced by
Disallow trailing comma tuple
Loading history...
325
326
327
Bot = Client
328