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