This repository has been archived on 2021-03-28. You can view files and clone it, but cannot push or open issues/pull-requests.
frank/frank/module/decorators/classes.py

261 lines
6.6 KiB
Python

# =====IMPORTS=====
# Future imports
from __future__ import annotations
# Built-in imports
import re
import asyncio
import shlex
# Typing imports
from typing import TYPE_CHECKING
if TYPE_CHECKING:
# Built-in imports
from typing import Union, List, Tuple
class Simple:
"""
Acts as a base class for all other types; behaves like the given
function
"""
def __init__(self, func: callable):
"""
Args:
func: function to mimic
"""
self.func = func
def __call__(self, *args, **kwargs):
"""
All this call does is call the wrapped function. Because we overwrote
__get__, we can pass self to the function, making it behave as a class
method of the instance calling it.
"""
return self.func.__call__(self._obj, *args, **kwargs)
def __get__(self, instance, owner) -> Simple:
"""
We use __get__ to get the class calling the function. This allows us to
pass 'self' to the wrapped function, effectively making this class
fully behave as a class method.
Args:
instance: instance calling the function
owner: type of the function
"""
self._cls = owner
self._obj = instance
return self
@property
def client(self):
"""
Returns the Frank client instance.
"""
return self._obj._client
class SimpleCommand(Simple):
"""
Base class for the various command types.
"""
def __init__(self, func: callable, cmd: str, help_str: str = None):
"""
Args:
func: function to wrap
cmd: keyword used to call this function
help_str: short description of the command
"""
super().__init__(func)
self.cmd = cmd
self.help_str = help_str
def match(self, message: str) -> Tuple[bool, List[str]]:
"""
Returns wether the command matches the given message. If the arguments
can't be parsed (e.g. unmatched quotes), it will return False as well.
Args:
message: message to check
"""
return self._match_full(message.split(" "))
def _match_full(self, parts: List[str]) -> Tuple[bool, List[str]]:
"""
Returns wether the message matches the full command.
Args:
parts: parts of the message
"""
# Can't match without 3 or more parts
if len(parts) < 3:
return False, None
# Return False if it doesn't match
if not all(
(
parts[0] == self.client.PREFIX,
parts[1] in self._obj.PREFIX,
parts[2] == self.cmd,
)
):
return False, None
# Parse the output, and return True with the parsed items if it works,
# otherwise return False
try:
parsed = shlex.split(" ".join(parts[3:]))
return True, parsed
except ValueError:
return False, None
class RegularCommand(SimpleCommand):
"""
Defines a regular command; Handles command aliases as well.
"""
def __init__(
self,
func: callable,
cmd: str,
help_str: str = None,
alias: Union[str, List[str]] = None,
requires_prefix: bool = False,
):
super().__init__(self, func, cmd, help_str)
self.alias = alias
# This only matters for aliases
self.requires_prefix = requires_prefix
# TODO: make this return the right value
def match(self, message: str) -> Tuple[bool, List[str]]:
"""
Returns wether the message matches the current command.
"""
parts = message.split(" ")
# If the alias doesn't match, return the full match, otherwise return
# alias
matches, parts = self._match_alias(parts)
if matches:
return matches, parts
return self._match_full(parts)
# TODO: make this return the right value
def _match_alias(self, parts: List[str]) -> Tuple[bool, List[str]]:
"""
Returns wether the message matches an alias.
"""
# Return False if there's only one part but a prefix is required
if self.requires_prefix and len(parts) == 1:
return False
# Match with prefix
if self.requires_prefix:
return parts[0] == self.client.PREFIX and parts[1] in self.alias
# Match without prefix
return parts[0] in self.alias
class RegexCommand(SimpleCommand):
"""
A subclass of Command that can use a regex pattern instead of a fixed
prefix.
"""
def match(self, message: str) -> Tuple[str, List[str]]:
"""
Returns wether the regex pattern matches the given prefix.
Args:
prefix: string to match pattern against; Pattern must match entire
prefix
"""
parts = message.split(" ")
matches = bool(re.fullmatch(self.cmd, parts[0]))
# If it doesn't match, just return False, don't parse the rest
if not matches:
return False, None
try:
parsed = shlex.split(" ".join(parts))
return True, parsed
except ValueError:
return False, None
class Daemon(Simple):
"""
Represents a daemon a.k.a. a background process.
"""
def __init__(self, func: callable, interval: Union[int, float] = 0):
"""
Args
func: function to wrap
interval: time between calls of the function; if <= 0, the function
is assumed to contain its own infinite loop, allowing for
fine-grained control of the daemon, if desired
"""
super().__init__(func)
self.interval = interval
# If an interval > 0 is given, it wraps the function inside an infinite
# loop with the desired delay
if interval > 0:
async def loop(self, *args, **kwargs):
while True:
# TODO: does this make func and sleep run at the same time?
await func(self, *args, **kwargs)
await asyncio.sleep(interval)
self.func = loop
class Default(Simple):
"""
Represents a default command a.k.a. when the module is called without a
command.
"""
def __init__(self, func: callable, help_str: str = None):
"""
Args:
func: function to wrap
help_str: short description of the default command
"""
super().__init__(func)
self.help_str = help_str