1# -*- coding: utf-8 -*-
2
3"""
4The MIT License (MIT)
5
6Copyright (c) 2015-present Rapptz
7
8Permission is hereby granted, free of charge, to any person obtaining a
9copy of this software and associated documentation files (the "Software"),
10to deal in the Software without restriction, including without limitation
11the rights to use, copy, modify, merge, publish, distribute, sublicense,
12and/or sell copies of the Software, and to permit persons to whom the
13Software is furnished to do so, subject to the following conditions:
14
15The above copyright notice and this permission notice shall be included in
16all copies or substantial portions of the Software.
17
18THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
19OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
23FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
24DEALINGS IN THE SOFTWARE.
25"""
26
27import inspect
28import copy
29from ._types import _BaseCommand
30
31__all__ = (
32    'CogMeta',
33    'Cog',
34)
35
36class CogMeta(type):
37    """A metaclass for defining a cog.
38
39    Note that you should probably not use this directly. It is exposed
40    purely for documentation purposes along with making custom metaclasses to intermix
41    with other metaclasses such as the :class:`abc.ABCMeta` metaclass.
42
43    For example, to create an abstract cog mixin class, the following would be done.
44
45    .. code-block:: python3
46
47        import abc
48
49        class CogABCMeta(commands.CogMeta, abc.ABCMeta):
50            pass
51
52        class SomeMixin(metaclass=abc.ABCMeta):
53            pass
54
55        class SomeCogMixin(SomeMixin, commands.Cog, metaclass=CogABCMeta):
56            pass
57
58    .. note::
59
60        When passing an attribute of a metaclass that is documented below, note
61        that you must pass it as a keyword-only argument to the class creation
62        like the following example:
63
64        .. code-block:: python3
65
66            class MyCog(commands.Cog, name='My Cog'):
67                pass
68
69    Attributes
70    -----------
71    name: :class:`str`
72        The cog name. By default, it is the name of the class with no modification.
73    description: :class:`str`
74        The cog description. By default, it is the cleaned docstring of the class.
75
76        .. versionadded:: 1.6
77
78    command_attrs: :class:`dict`
79        A list of attributes to apply to every command inside this cog. The dictionary
80        is passed into the :class:`Command` options at ``__init__``.
81        If you specify attributes inside the command attribute in the class, it will
82        override the one specified inside this attribute. For example:
83
84        .. code-block:: python3
85
86            class MyCog(commands.Cog, command_attrs=dict(hidden=True)):
87                @commands.command()
88                async def foo(self, ctx):
89                    pass # hidden -> True
90
91                @commands.command(hidden=False)
92                async def bar(self, ctx):
93                    pass # hidden -> False
94    """
95
96    def __new__(cls, *args, **kwargs):
97        name, bases, attrs = args
98        attrs['__cog_name__'] = kwargs.pop('name', name)
99        attrs['__cog_settings__'] = kwargs.pop('command_attrs', {})
100
101        description = kwargs.pop('description', None)
102        if description is None:
103            description = inspect.cleandoc(attrs.get('__doc__', ''))
104        attrs['__cog_description__'] = description
105
106        commands = {}
107        listeners = {}
108        no_bot_cog = 'Commands or listeners must not start with cog_ or bot_ (in method {0.__name__}.{1})'
109
110        new_cls = super().__new__(cls, name, bases, attrs, **kwargs)
111        for base in reversed(new_cls.__mro__):
112            for elem, value in base.__dict__.items():
113                if elem in commands:
114                    del commands[elem]
115                if elem in listeners:
116                    del listeners[elem]
117
118                is_static_method = isinstance(value, staticmethod)
119                if is_static_method:
120                    value = value.__func__
121                if isinstance(value, _BaseCommand):
122                    if is_static_method:
123                        raise TypeError('Command in method {0}.{1!r} must not be staticmethod.'.format(base, elem))
124                    if elem.startswith(('cog_', 'bot_')):
125                        raise TypeError(no_bot_cog.format(base, elem))
126                    commands[elem] = value
127                elif inspect.iscoroutinefunction(value):
128                    try:
129                        getattr(value, '__cog_listener__')
130                    except AttributeError:
131                        continue
132                    else:
133                        if elem.startswith(('cog_', 'bot_')):
134                            raise TypeError(no_bot_cog.format(base, elem))
135                        listeners[elem] = value
136
137        new_cls.__cog_commands__ = list(commands.values()) # this will be copied in Cog.__new__
138
139        listeners_as_list = []
140        for listener in listeners.values():
141            for listener_name in listener.__cog_listener_names__:
142                # I use __name__ instead of just storing the value so I can inject
143                # the self attribute when the time comes to add them to the bot
144                listeners_as_list.append((listener_name, listener.__name__))
145
146        new_cls.__cog_listeners__ = listeners_as_list
147        return new_cls
148
149    def __init__(self, *args, **kwargs):
150        super().__init__(*args)
151
152    @classmethod
153    def qualified_name(cls):
154        return cls.__cog_name__
155
156def _cog_special_method(func):
157    func.__cog_special_method__ = None
158    return func
159
160class Cog(metaclass=CogMeta):
161    """The base class that all cogs must inherit from.
162
163    A cog is a collection of commands, listeners, and optional state to
164    help group commands together. More information on them can be found on
165    the :ref:`ext_commands_cogs` page.
166
167    When inheriting from this class, the options shown in :class:`CogMeta`
168    are equally valid here.
169    """
170
171    def __new__(cls, *args, **kwargs):
172        # For issue 426, we need to store a copy of the command objects
173        # since we modify them to inject `self` to them.
174        # To do this, we need to interfere with the Cog creation process.
175        self = super().__new__(cls)
176        cmd_attrs = cls.__cog_settings__
177
178        # Either update the command with the cog provided defaults or copy it.
179        self.__cog_commands__ = tuple(c._update_copy(cmd_attrs) for c in cls.__cog_commands__)
180
181        lookup = {
182            cmd.qualified_name: cmd
183            for cmd in self.__cog_commands__
184        }
185
186        # Update the Command instances dynamically as well
187        for command in self.__cog_commands__:
188            setattr(self, command.callback.__name__, command)
189            parent = command.parent
190            if parent is not None:
191                # Get the latest parent reference
192                parent = lookup[parent.qualified_name]
193
194                # Update our parent's reference to our self
195                parent.remove_command(command.name)
196                parent.add_command(command)
197
198        return self
199
200    def get_commands(self):
201        r"""
202        Returns
203        --------
204        List[:class:`.Command`]
205            A :class:`list` of :class:`.Command`\s that are
206            defined inside this cog.
207
208            .. note::
209
210                This does not include subcommands.
211        """
212        return [c for c in self.__cog_commands__ if c.parent is None]
213
214    @property
215    def qualified_name(self):
216        """:class:`str`: Returns the cog's specified name, not the class name."""
217        return self.__cog_name__
218
219    @property
220    def description(self):
221        """:class:`str`: Returns the cog's description, typically the cleaned docstring."""
222        return self.__cog_description__
223
224    @description.setter
225    def description(self, description):
226        self.__cog_description__ = description
227
228    def walk_commands(self):
229        """An iterator that recursively walks through this cog's commands and subcommands.
230
231        Yields
232        ------
233        Union[:class:`.Command`, :class:`.Group`]
234            A command or group from the cog.
235        """
236        from .core import GroupMixin
237        for command in self.__cog_commands__:
238            if command.parent is None:
239                yield command
240                if isinstance(command, GroupMixin):
241                    yield from command.walk_commands()
242
243    def get_listeners(self):
244        """Returns a :class:`list` of (name, function) listener pairs that are defined in this cog.
245
246        Returns
247        --------
248        List[Tuple[:class:`str`, :ref:`coroutine <coroutine>`]]
249            The listeners defined in this cog.
250        """
251        return [(name, getattr(self, method_name)) for name, method_name in self.__cog_listeners__]
252
253    @classmethod
254    def _get_overridden_method(cls, method):
255        """Return None if the method is not overridden. Otherwise returns the overridden method."""
256        return getattr(method.__func__, '__cog_special_method__', method)
257
258    @classmethod
259    def listener(cls, name=None):
260        """A decorator that marks a function as a listener.
261
262        This is the cog equivalent of :meth:`.Bot.listen`.
263
264        Parameters
265        ------------
266        name: :class:`str`
267            The name of the event being listened to. If not provided, it
268            defaults to the function's name.
269
270        Raises
271        --------
272        TypeError
273            The function is not a coroutine function or a string was not passed as
274            the name.
275        """
276
277        if name is not None and not isinstance(name, str):
278            raise TypeError('Cog.listener expected str but received {0.__class__.__name__!r} instead.'.format(name))
279
280        def decorator(func):
281            actual = func
282            if isinstance(actual, staticmethod):
283                actual = actual.__func__
284            if not inspect.iscoroutinefunction(actual):
285                raise TypeError('Listener function must be a coroutine function.')
286            actual.__cog_listener__ = True
287            to_assign = name or actual.__name__
288            try:
289                actual.__cog_listener_names__.append(to_assign)
290            except AttributeError:
291                actual.__cog_listener_names__ = [to_assign]
292            # we have to return `func` instead of `actual` because
293            # we need the type to be `staticmethod` for the metaclass
294            # to pick it up but the metaclass unfurls the function and
295            # thus the assignments need to be on the actual function
296            return func
297        return decorator
298
299    def has_error_handler(self):
300        """:class:`bool`: Checks whether the cog has an error handler.
301
302        .. versionadded:: 1.7
303        """
304        return not hasattr(self.cog_command_error.__func__, '__cog_special_method__')
305
306    @_cog_special_method
307    def cog_unload(self):
308        """A special method that is called when the cog gets removed.
309
310        This function **cannot** be a coroutine. It must be a regular
311        function.
312
313        Subclasses must replace this if they want special unloading behaviour.
314        """
315        pass
316
317    @_cog_special_method
318    def bot_check_once(self, ctx):
319        """A special method that registers as a :meth:`.Bot.check_once`
320        check.
321
322        This function **can** be a coroutine and must take a sole parameter,
323        ``ctx``, to represent the :class:`.Context`.
324        """
325        return True
326
327    @_cog_special_method
328    def bot_check(self, ctx):
329        """A special method that registers as a :meth:`.Bot.check`
330        check.
331
332        This function **can** be a coroutine and must take a sole parameter,
333        ``ctx``, to represent the :class:`.Context`.
334        """
335        return True
336
337    @_cog_special_method
338    def cog_check(self, ctx):
339        """A special method that registers as a :func:`commands.check`
340        for every command and subcommand in this cog.
341
342        This function **can** be a coroutine and must take a sole parameter,
343        ``ctx``, to represent the :class:`.Context`.
344        """
345        return True
346
347    @_cog_special_method
348    async def cog_command_error(self, ctx, error):
349        """A special method that is called whenever an error
350        is dispatched inside this cog.
351
352        This is similar to :func:`.on_command_error` except only applying
353        to the commands inside this cog.
354
355        This **must** be a coroutine.
356
357        Parameters
358        -----------
359        ctx: :class:`.Context`
360            The invocation context where the error happened.
361        error: :class:`CommandError`
362            The error that happened.
363        """
364        pass
365
366    @_cog_special_method
367    async def cog_before_invoke(self, ctx):
368        """A special method that acts as a cog local pre-invoke hook.
369
370        This is similar to :meth:`.Command.before_invoke`.
371
372        This **must** be a coroutine.
373
374        Parameters
375        -----------
376        ctx: :class:`.Context`
377            The invocation context.
378        """
379        pass
380
381    @_cog_special_method
382    async def cog_after_invoke(self, ctx):
383        """A special method that acts as a cog local post-invoke hook.
384
385        This is similar to :meth:`.Command.after_invoke`.
386
387        This **must** be a coroutine.
388
389        Parameters
390        -----------
391        ctx: :class:`.Context`
392            The invocation context.
393        """
394        pass
395
396    def _inject(self, bot):
397        cls = self.__class__
398
399        # realistically, the only thing that can cause loading errors
400        # is essentially just the command loading, which raises if there are
401        # duplicates. When this condition is met, we want to undo all what
402        # we've added so far for some form of atomic loading.
403        for index, command in enumerate(self.__cog_commands__):
404            command.cog = self
405            if command.parent is None:
406                try:
407                    bot.add_command(command)
408                except Exception as e:
409                    # undo our additions
410                    for to_undo in self.__cog_commands__[:index]:
411                        if to_undo.parent is None:
412                            bot.remove_command(to_undo.name)
413                    raise e
414
415        # check if we're overriding the default
416        if cls.bot_check is not Cog.bot_check:
417            bot.add_check(self.bot_check)
418
419        if cls.bot_check_once is not Cog.bot_check_once:
420            bot.add_check(self.bot_check_once, call_once=True)
421
422        # while Bot.add_listener can raise if it's not a coroutine,
423        # this precondition is already met by the listener decorator
424        # already, thus this should never raise.
425        # Outside of, memory errors and the like...
426        for name, method_name in self.__cog_listeners__:
427            bot.add_listener(getattr(self, method_name), name)
428
429        return self
430
431    def _eject(self, bot):
432        cls = self.__class__
433
434        try:
435            for command in self.__cog_commands__:
436                if command.parent is None:
437                    bot.remove_command(command.name)
438
439            for _, method_name in self.__cog_listeners__:
440                bot.remove_listener(getattr(self, method_name))
441
442            if cls.bot_check is not Cog.bot_check:
443                bot.remove_check(self.bot_check)
444
445            if cls.bot_check_once is not Cog.bot_check_once:
446                bot.remove_check(self.bot_check_once, call_once=True)
447        finally:
448            try:
449                self.cog_unload()
450            except Exception:
451                pass
452