diff --git a/CHANGELOG.md b/CHANGELOG.md index 6188ae0..eae002c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## Unreleased +### Added +- Daemons can now accept an interval value, removing the need for a manual while True loop + ## v0.1 (2020/08/26) ### Added diff --git a/frank/frank.py b/frank/frank.py index 62c52ab..2a8a01e 100644 --- a/frank/frank.py +++ b/frank/frank.py @@ -88,10 +88,13 @@ class Frank(discord.Client): except ValueError: return - if cmd and cmd[0] == self.PREFIX: - module = next((mod for mod in self._loaded_modules - if mod.match(cmd[1])), None) + # Exit if no commands are given or prefix is wrong + if not (cmd and cmd[0] == self.PREFIX): + return - if module: - await module(cmd=cmd[2:], author=message.author, - channel=message.channel, mid=message.id) + module = next((mod for mod in self._loaded_modules + if mod.match(cmd[1])), None) + + if module: + await module(cmd=cmd[2:], author=message.author, + channel=message.channel, mid=message.id) diff --git a/frank/module/decorators/classes.py b/frank/module/decorators/classes.py index d14e398..97f31bd 100644 --- a/frank/module/decorators/classes.py +++ b/frank/module/decorators/classes.py @@ -4,6 +4,13 @@ from __future__ import annotations # Built-in imports import re +import asyncio + +# Typing imports +from typing import TYPE_CHECKING +if TYPE_CHECKING: + # Built-in imports + from typing import Union class Simple: @@ -95,16 +102,37 @@ class RegexCommand(Command): class Daemon(Simple): """ - Represents a daemon. Currently, it's only used as its own type, but writing - it this way allows us to easily expand upon its functionality later. + Represents a daemon a.k.a. a background process. """ - pass + 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 + Represents a default command a.k.a. when the module is called without a command. """ diff --git a/frank/module/decorators/functions.py b/frank/module/decorators/functions.py index 205cd87..130bcd0 100644 --- a/frank/module/decorators/functions.py +++ b/frank/module/decorators/functions.py @@ -1,7 +1,16 @@ # =====IMPORTS===== +# Future imports +from __future__ import annotations + # Own imports from .classes import Command, RegexCommand, Daemon, Default +# Typing imports +from typing import TYPE_CHECKING +if TYPE_CHECKING: + # Built-in imports + from typing import Union + def command(cmd, help_str: str = None) -> callable: """ @@ -33,14 +42,14 @@ def regex_command(pattern: str, help_str: str = None) -> callable: return inner -def daemon() -> callable: +def daemon(interval: Union[int, float] = 0) -> callable: """ Converts the method into a Daemon, which will then be run when the module is started. """ def inner(func): - return Daemon(func) + return Daemon(func, interval) return inner diff --git a/frank/module/module.py b/frank/module/module.py index 5facd13..3c9dbf4 100644 --- a/frank/module/module.py +++ b/frank/module/module.py @@ -100,18 +100,18 @@ class Module(ModuleMeta): func = next((func for func in self.commands if func.match(cmd[0])), None) - if func: - # A RegexCommand can use the prefix, as it's not a fixed string - if isinstance(func, RegexCommand): - await func(prefix=cmd[0], cmd=cmd[1:], author=author, - channel=channel, mid=mid) + # Throw error if no function is found + if not func: + raise InvalidCommand(f'Unknown command: {cmd}') - else: - await func(cmd=cmd[1:], author=author, channel=channel, - mid=mid) + # A RegexCommand can use the prefix, as it's not a fixed string + if isinstance(func, RegexCommand): + await func(prefix=cmd[0], cmd=cmd[1:], author=author, + channel=channel, mid=mid) else: - raise InvalidCommand(f'Unknown command: {cmd}') + await func(cmd=cmd[1:], author=author, channel=channel, + mid=mid) elif self.default: await self.default(author=author, channel=channel, mid=mid) @@ -125,12 +125,12 @@ class Module(ModuleMeta): prefix: prefix to check """ - if cls.PREFIX: - if isinstance(cls.PREFIX, list): - return prefix in cls.PREFIX + # Always return False if there's no PREFIX defined + if not cls.PREFIX: + return False - else: - return prefix == cls.PREFIX + if isinstance(cls.PREFIX, list): + return prefix in cls.PREFIX else: - return False + return prefix == cls.PREFIX diff --git a/tests/test_decorators.py b/tests/test_decorators.py index 2c9f400..c9aa530 100644 --- a/tests/test_decorators.py +++ b/tests/test_decorators.py @@ -1,4 +1,7 @@ # =====IMPORTS===== +# Third-party imports +import pytest + # Own imports from frank import default, command, daemon @@ -27,7 +30,7 @@ class TestDecorators: return 'daemon' @daemon() - def daemon_dec(self): + async def daemon_dec(self): return self.daemon_no_dec() def test_default(self): @@ -36,5 +39,6 @@ class TestDecorators: def test_command(self): assert self.command_no_dec() == self.command_dec() - def test_daemon(self): - assert self.daemon_no_dec() == self.daemon_dec() + @pytest.mark.asyncio + async def test_daemon(self): + assert self.daemon_no_dec() == await self.daemon_dec()