Source code for curious.commands.decorators

# This file is part of curious.
#
# curious is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# curious is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with curious.  If not, see <http://www.gnu.org/licenses/>.

"""
Decorators to annotate function objects.

.. currentmodule:: curious.commands.decorators
"""
import inspect
import logging
from typing import Any, List, Type

from curious.commands import plugin as md_plugin
from curious.commands.ratelimit import BucketNamer, CommandRateLimit
from curious.commands.utils import get_description

logger = logging.getLogger(__name__)


[docs]def command(*, name: str = None, description: str = None, hidden: bool = False, aliases: List[str] = None, **kwargs): """ Marks a function as a command. This annotates the command with some attributes that allow it to be invoked as a command. This decorator can be invoked like this: .. code-block:: python3 @command() async def ping(self, ctx): await ctx.channel.messages.send("Ping!") :param name: The name of the command. If this is not specified, it will use the name of the \ function object. :param description: The description of the command. If this is not specified, it will use the \ first line of the docstring. :param hidden: If this command is hidden; i.e. it doesn't show up in the help listing. :param aliases: A list of aliases for this command. :param kwargs: Anything to annotate the command with. """ # wrapper function that actually marks the object def inner(func): def set(attr: str, value: Any): try: return getattr(func, attr) except AttributeError: setattr(func, attr, value) set("is_cmd", True) set("cmd_name", name or func.__name__) set("cmd_description", description or get_description(func)) set("cmd_aliases", aliases or []) set("cmd_subcommand", False) set("cmd_subcommands", []) set("cmd_parent", None) set("cmd_hidden", hidden) set("cmd_conditions", []) set("cmd_ratelimits", []) # annotate command object with any extra for ann_name, annotation in kwargs.items(): ann_name = "cmd_" + name set(ann_name, annotation) func.subcommand = _subcommand(func) return func return inner
[docs]def condition(cbl): """ Adds a condition to a command. This will add the callable to ``cmd_conditions`` on the function. """ def inner(func): if not hasattr(func, "cmd_conditions"): func.cmd_conditions = [] func.cmd_conditions.append(cbl) return func return inner
[docs]def ratelimit(*, limit: int, time: float, bucket_namer=BucketNamer.AUTHOR): """ Adds a ratelimit to a command. """ def inner(func): if not hasattr(func, "cmd_ratelimits"): func.cmd_ratelimits = [] rl = CommandRateLimit(limit=limit, time=time, bucket_namer=bucket_namer) rl.command = func func.cmd_ratelimits.append(rl) return func return inner
[docs]def _subcommand(parent): """ Decorator factory set on a command to produce subcommands. """ def inner(**kwargs): # MULTIPLE LAYERS def inner_2(func): if not hasattr(parent, "is_cmd"): raise TypeError("Cannot be a subcommand of a non-command") cmd = command(**kwargs)(func) cmd.cmd_subcommand = True cmd.cmd_parent = parent parent.cmd_subcommands.append(cmd) return cmd return inner_2 return inner
[docs]def autoplugin(plugin: 'Type[md_plugin.Plugin]' = None, *, startswith: str = "command") -> 'Type[md_plugin.Plugin]': """ Automatically assigns commands inside a plugin. This will scan a :class:`.Plugin` for functions matching the pattern ``command_[parent_]name``, and automatically decorate them with the command decorator and subcommand decorators. :param plugin: The :class:`.Plugin` subclass to autoplugin. :param startswith: Used to override what the command function prefix will be. :return: The edited plugin. """ # we were called like @autoplugin(startswith="something") # so return a lambda that provides the plugin if plugin is None: return lambda plugin: autoplugin(plugin, startswith=startswith) if not issubclass(plugin, md_plugin.Plugin): raise ValueError(f"Cannot autoplugin an object of type non-PluginMeta ({type(plugin)})") # we were called like @autoplugin or the lambda above completed logger.debug("Processing autocommand for plugin type %s", plugin.__name__) for name, member in plugin.__dict__.copy().items(): if not name.startswith(startswith + "_"): continue # unwrap all functions member = inspect.unwrap(member) parts = name.split("_", 2) if len(parts) == 2: # regular command, no parent # wrap it in a command and continue made_command = command(name=parts[1])(member) logger.debug("Made a top-level command %s on plugin %s", parts[1], plugin.__name__) setattr(plugin, name, made_command) else: # we have a parent, so call parent.subcommand() on it instead parent = parts[1] # o(n2) loop! def _pred(i): return hasattr(i, "cmd_name") and i.cmd_name == parent for _, found_command in inspect.getmembers(plugin, predicate=_pred): break else: raise AttributeError( f"When doing an autoplugin, could not locate parent command {parent}.\n" f"You need to make sure that the parent A) exists and B) is before the " f"current command in the class definition for the autoplugin to resolve the " f"name properly." ) from None made_command = found_command.subcommand(name=parts[2])(member) logger.debug("Made a subcommand %s (parent %s) on plugin %s", parts[2], parts[1], plugin.__name__) setattr(plugin, name, made_command) return plugin