diff --git a/README.md b/README.md index 464bb8f..a4e6d3a 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,76 @@ # Frank -Frank is a modular tool for quickly creating Discord bots. +## Description +Playing around with creating a Discord bot is a fun pass-time, and a good way to learn a programming language. Sadly, +however, discord.py can be a little hard to work with at times. That's when I got the idea to create Frank. The goal of +Frank is to make creating Discord bots easier. It handles all the bot-related stuff in the background, so you can focus +on writing the functionality of the bot itself, not how the bot works/interacts with Discord. + +Frank works by dividing the bot into modules. Each module has its own prefix, commands, and daemons. Frank handles +routing the Discord commands to their respective functions. + +## Example Module +In this section, I've written an example module for you, to understand the basic mechanics behind Frank. + +```python +import frank + +class ExampleMod(frank.Module): + PREFIX = 'examp' + NAME = 'example' + HELP = 'an example module' +``` + +This first part shows the three important variables in any module. +- PREFIX defines the string used to use the commands defined in this module. This means you can use the module as such + inside your Discord server: + ``` + fr examp [NAME_OF_COMMAND] [ARGS] + ``` + With fr being the default prefix for Frank (can be overwritten). As you define more modules, they should all have a + unique prefix. This is how Frank's modular system works, and any modules added to the list will automatically be + picked up by Frank. The PREFIX value can also be list, allowing for multiple prefixes: for example a long, + description one, and a short, easy to type one (e.g. minecraft and mc). + +```python + def pre_start(self): + self.some_var = 'a value needed for working' +``` +The pre_start function is where you define any variables which should be created before any daemons are started or +commands are run. I don't recommend overwriting `__init__`, as this might break compatibility with future versions of +Frank. + +```python + @frank.command('command', help_str='a small description of the command') + async def some_command(self, cmd, author, channel, mid): + # do some stuff + pass + + @frank.daemon() + async def some_daemon(self): + while True: + # do some stuff + pass + + @frank.default() + async def default_cmd(prefix, author, channel, mid): + # do some default action + pass +``` + +These three decorators are the bread and butter of Frank. Let's break them down: +- `frank.command` defines a command. The first argument is its keyword, which will be used to execute the command. The + help_str value is used in the help command, to show some information about the module. The syntax is the same as + before: + ``` + fr examp command [ARGS] + ``` + This is how you can define as many Discord commands as you want, without needing to know how to parse the messages + etc. Each command gets the `author`, `channel`, and `id` of the message. The `cmd` variable contains all the arguments passed + to the command. +- `frank.daemon` defines a daemon, a process that should run in the background for as long as the bot is active. It + should contain a while loop and preferably a sleep function using `asyncio.sleep()` (there are plans to improve this + behavior). Because a daemon is just a method of the module class, it has access to all class variables, including + those defined in `pre_start`. +- `frank.default` defines the command that should be run if the module is called without explicitely giving a command. + For example, if you call `fr examp` without specifying a command, it will run the default command. This is useful for + making a command that's used very often easier to execute. diff --git a/frank/module/meta.py b/frank/module/meta.py index 1d906a4..c78aac6 100644 --- a/frank/module/meta.py +++ b/frank/module/meta.py @@ -17,6 +17,7 @@ if TYPE_CHECKING: class ModuleMeta: def _filter_attrs(self, condition: callable[[Any], bool]) -> List[Any]: + # This prevents an infinite loop of getting the attribute illegal_names = ['commands', 'daemons', 'default'] output = [] diff --git a/requirements-dev.txt b/requirements-dev.txt index 60a2136..67889f9 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,4 +1,4 @@ -jedi~=0.17.2 +jedi>=0.17.2,<1.0.0 flake8~=3.8.3 flake8-bugbear~=20.1.4 flake8-builtins~=1.5.3 diff --git a/tests/module_tester.py b/tests/module_tester.py index e3a4c6d..cc66ca2 100644 --- a/tests/module_tester.py +++ b/tests/module_tester.py @@ -1,7 +1,9 @@ -from frank import Module, command, default +from frank import Module, command, default, daemon class ModuleTester(Module): + PREFIX = 'tester' + @command('test') async def test(cmd, author, channel, mid): pass @@ -9,3 +11,7 @@ class ModuleTester(Module): @default() async def test2(prefix, cmd, author, channel, mid): pass + + @daemon() + async def test_daemon(self): + pass diff --git a/tests/test_module.py b/tests/test_module.py index c7c19bb..75aab8c 100644 --- a/tests/test_module.py +++ b/tests/test_module.py @@ -5,6 +5,10 @@ import pytest def test_property_types(): + """ + Test wether the cached_property's return the expected value + """ + test_mod = ModuleTester(None) assert isinstance(test_mod.default, Default) @@ -15,7 +19,7 @@ def test_property_types(): isinstance(item, Command) for item in test_mod.commands )) - assert isinstance(test_mod.commands, list) + assert isinstance(test_mod.daemons, list) assert all(( isinstance(item, Daemon) for item in test_mod.daemons )) @@ -28,3 +32,10 @@ async def test_invalid_command(): with pytest.raises(InvalidCommand): # None is just a placeholder here await test_mod('aninvalidcommand', None, None, None) + + +def test_match(): + test_mod = ModuleTester(None) + + assert test_mod.match('tester') + assert not test_mod.match('testerrr')