Compare commits
17 Commits
Author | SHA1 | Date |
---|---|---|
Jef Roosens | 7a7bd6fed4 | |
Jef Roosens | 6f2d9d1dde | |
Jef Roosens | 593fe189cd | |
Jef Roosens | 5855c931ca | |
Jef Roosens | c9700618f7 | |
Jef Roosens | 346f39f243 | |
Jef Roosens | 70fc8dd5f4 | |
Jef Roosens | 3c7259ce08 | |
Jef Roosens | 75be358ff6 | |
Jef Roosens | 6f0af5dfdf | |
Jef Roosens | 9c1a56445b | |
Jef Roosens | e41d9780a4 | |
Jef Roosens | 5648f1a9de | |
Jef Roosens | e7637a1bce | |
Jef Roosens | f6f081bfba | |
Jef Roosens | beb5bf49cc | |
Jef Roosens | d1dc46c17b |
|
@ -0,0 +1,11 @@
|
|||
#!/bin/sh
|
||||
#
|
||||
# This hooks runs tests and checks if the linter doesn't hate you
|
||||
|
||||
# Linting
|
||||
printf "black......"
|
||||
make format > /dev/null 2>&1 && printf "OK!\n" || { printf "Fail." && exit 1; }
|
||||
|
||||
# Running tests
|
||||
printf "pytest....."
|
||||
make test > /dev/null 2>&1 && printf "OK!\n" || { printf "Fail." && exit 1; }
|
|
@ -8,8 +8,9 @@ build/
|
|||
*.egg-info/
|
||||
*.eggs/
|
||||
|
||||
# Docs output
|
||||
# Docs
|
||||
docs/build
|
||||
docs/source/apidoc/
|
||||
|
||||
# Caches
|
||||
__pycache__/
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
# Changelog
|
||||
## Unreleased
|
||||
### Added
|
||||
- Daemons can now accept an interval value, removing the need for a manual while True loop
|
||||
|
||||
## v0.1 (2020/08/26)
|
||||
|
||||
## 0.1 (2020/08/26)
|
||||
### Added
|
||||
- Prefix can now be passed as argument to init
|
||||
- Pre-made help module
|
||||
|
|
32
Makefile
32
Makefile
|
@ -10,10 +10,10 @@ DOCS=docs
|
|||
# Interpreter to create venv with
|
||||
INTERPRETER=python3.8
|
||||
|
||||
all: run
|
||||
all: build-venv
|
||||
|
||||
|
||||
# Re-create venv when needed
|
||||
# =====VENV=====
|
||||
$(VENV)/bin/activate: requirements.txt requirements-dev.txt
|
||||
@ echo "Rebuilding venv..."
|
||||
@ [ ! -e "$(VENV)" ] || rm -rf "$(VENV)"
|
||||
|
@ -22,8 +22,9 @@ $(VENV)/bin/activate: requirements.txt requirements-dev.txt
|
|||
|
||||
build-venv: $(VENV)/bin/activate
|
||||
|
||||
|
||||
# =====CLEANING=====
|
||||
clean: clean-venv clean-cache clean-docs
|
||||
clean: clean-venv clean-cache clean-docs clean-setup
|
||||
|
||||
# Remove venv
|
||||
clean-venv:
|
||||
|
@ -37,24 +38,41 @@ clean-cache:
|
|||
@ echo "Removing caches..."
|
||||
@ find . -type d \( -name "__pycache__" -o -name ".pytest_cache" \) -exec rm -r "{}" +
|
||||
|
||||
# Removed generation documentation
|
||||
clean-docs:
|
||||
@ echo "Removing documentation build..."
|
||||
@ [ ! -e "$(DOCS)/build" ] || rm -r "$(DOCS)/build"
|
||||
@ [ ! -e "$(DOCS)/source/apidoc" ] || rm -r "$(DOCS)/source/apidoc"
|
||||
|
||||
# Remove build leftovers (not dist)
|
||||
clean-setup:
|
||||
@ echo 'Removing build artifacts...'
|
||||
@ [ ! -e "build" ] || rm -rf build
|
||||
@ find . -maxdepth 1 -type d -name '*.egg-info' -exec rm -rf "{}" \;
|
||||
|
||||
|
||||
# =====DOCS=====
|
||||
docs: build-venv
|
||||
@ "$(VENV)/bin/sphinx-apidoc" -o "$(DOCS)/source" "$(SRC)"
|
||||
# # Generate documentation
|
||||
docs: docs/source/conf.py docs/source/index.rst build-venv
|
||||
@ "$(VENV)/bin/sphinx-apidoc" -f -o "$(DOCS)/source/apidoc" "$(SRC)"
|
||||
@ "$(VENV)/bin/sphinx-build" "$(DOCS)/source" "$(DOCS)/build"
|
||||
|
||||
|
||||
# =====TESTS=====
|
||||
# Run tests
|
||||
test: pytest.ini build-venv
|
||||
@ "$(VENV)/bin/pytest" --color=yes
|
||||
@ "$(VENV)/bin/pytest" --color=auto
|
||||
|
||||
|
||||
# =====LINTING=====
|
||||
# Run flake8
|
||||
lint: build-venv
|
||||
@ "$(VENV)/bin/flake8" "$(SRC)"/**/*.py
|
||||
|
||||
# =====FORMATTING=====
|
||||
# Run black
|
||||
format: build-venv
|
||||
@ "$(VENV)/bin/black" '$(SRC)'
|
||||
|
||||
|
||||
# =====PACKAGING=====
|
||||
|
@ -62,7 +80,7 @@ package: README.md LICENSE setup.py test clean-setup
|
|||
@ echo "Removing build..."
|
||||
@ [ ! -e "dist" ] || rm -r "dist"
|
||||
@ echo "Running setup.py..."
|
||||
@ "$(VENV)/bin/python" setup.py sdist bdist_wheel
|
||||
@ "$(VENV)/bin/python" setup.py bdist_wheel
|
||||
|
||||
publish: package
|
||||
@ [ "$$(git symbolic-ref HEAD --short)" = master ] && { \
|
||||
|
|
33
README.md
33
README.md
|
@ -8,6 +8,13 @@ on writing the functionality of the bot itself, not how the bot works/interacts
|
|||
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.
|
||||
|
||||
## Installation
|
||||
You can install the `frank-discord` package from PyPi:
|
||||
|
||||
```
|
||||
pip install frank-discord
|
||||
```
|
||||
|
||||
## Example Module
|
||||
In this section, I've written an example module for you, to understand the basic mechanics behind Frank.
|
||||
|
||||
|
@ -29,7 +36,7 @@ This first part shows the three important variables in any module.
|
|||
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).
|
||||
descriptive one, and a short, easy to type one (e.g. minecraft and mc).
|
||||
|
||||
```python
|
||||
def pre_start(self):
|
||||
|
@ -46,14 +53,14 @@ Frank.
|
|||
# do some stuff
|
||||
pass
|
||||
|
||||
@frank.daemon()
|
||||
# Interval defines how many seconds are between each call
|
||||
@frank.daemon(interval=5)
|
||||
async def some_daemon(self):
|
||||
while True:
|
||||
# do some stuff
|
||||
pass
|
||||
|
||||
@frank.default()
|
||||
async def default_cmd(self):
|
||||
async def default_cmd(self, author, channel, mid):
|
||||
# do some default action
|
||||
pass
|
||||
```
|
||||
|
@ -66,12 +73,24 @@ These three decorators are the bread and butter of Frank. Let's break them down:
|
|||
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.
|
||||
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.
|
||||
- `frank.default` defines the command that should be run if the module is called without explicitly 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.
|
||||
|
||||
In the end, all you need to do is add the following in your main script:
|
||||
|
||||
```python
|
||||
from frank import Frank
|
||||
# Or whatever your package containing your modules is called
|
||||
from modules import ExampleMod
|
||||
|
||||
if __name__ == '__main__':
|
||||
client = Frank([ExampleMod])
|
||||
client.run(YOUR_DISCORD_TOKEN)
|
||||
```
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
# Minimal makefile for Sphinx documentation
|
||||
#
|
||||
|
||||
# You can set these variables from the command line, and also
|
||||
# from the environment for the first two.
|
||||
SPHINXOPTS ?=
|
||||
SPHINXBUILD ?= sphinx-build
|
||||
SOURCEDIR = source
|
||||
BUILDDIR = build
|
||||
|
||||
# Put it first so that "make" without argument is like "make help".
|
||||
help:
|
||||
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
||||
|
||||
.PHONY: help Makefile
|
||||
|
||||
# Catch-all target: route all unknown targets to Sphinx using the new
|
||||
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
|
||||
%: Makefile
|
||||
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
|
@ -0,0 +1,35 @@
|
|||
@ECHO OFF
|
||||
|
||||
pushd %~dp0
|
||||
|
||||
REM Command file for Sphinx documentation
|
||||
|
||||
if "%SPHINXBUILD%" == "" (
|
||||
set SPHINXBUILD=sphinx-build
|
||||
)
|
||||
set SOURCEDIR=source
|
||||
set BUILDDIR=build
|
||||
|
||||
if "%1" == "" goto help
|
||||
|
||||
%SPHINXBUILD% >NUL 2>NUL
|
||||
if errorlevel 9009 (
|
||||
echo.
|
||||
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
|
||||
echo.installed, then set the SPHINXBUILD environment variable to point
|
||||
echo.to the full path of the 'sphinx-build' executable. Alternatively you
|
||||
echo.may add the Sphinx directory to PATH.
|
||||
echo.
|
||||
echo.If you don't have Sphinx installed, grab it from
|
||||
echo.http://sphinx-doc.org/
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
|
||||
goto end
|
||||
|
||||
:help
|
||||
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
|
||||
|
||||
:end
|
||||
popd
|
|
@ -0,0 +1,57 @@
|
|||
# Configuration file for the Sphinx documentation builder.
|
||||
#
|
||||
# This file only contains a selection of the most common options. For a full
|
||||
# list see the documentation:
|
||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html
|
||||
|
||||
# -- Path setup --------------------------------------------------------------
|
||||
|
||||
# If extensions (or modules to document with autodoc) are in another directory,
|
||||
# add these directories to sys.path here. If the directory is relative to the
|
||||
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
||||
#
|
||||
import os
|
||||
import sys
|
||||
sys.path.insert(0, os.path.abspath('../..'))
|
||||
|
||||
|
||||
# -- Project information -----------------------------------------------------
|
||||
|
||||
project = 'Frank'
|
||||
author = 'Jef Roosens'
|
||||
copyright = f'2020, {author}' # noqa: A001,VNE003
|
||||
|
||||
# The full version, including alpha/beta/rc tags
|
||||
release = '0.1'
|
||||
|
||||
|
||||
# -- General configuration ---------------------------------------------------
|
||||
|
||||
# Add any Sphinx extension module names here, as strings. They can be
|
||||
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
||||
# ones.
|
||||
extensions = [
|
||||
'sphinx.ext.napoleon',
|
||||
'sphinx_rtd_theme',
|
||||
]
|
||||
|
||||
# Add any paths that contain templates here, relative to this directory.
|
||||
templates_path = ['_templates']
|
||||
|
||||
# List of patterns, relative to source directory, that match files and
|
||||
# directories to ignore when looking for source files.
|
||||
# This pattern also affects html_static_path and html_extra_path.
|
||||
exclude_patterns = []
|
||||
|
||||
|
||||
# -- Options for HTML output -------------------------------------------------
|
||||
|
||||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||
# a list of builtin themes.
|
||||
#
|
||||
html_theme = 'sphinx_rtd_theme'
|
||||
|
||||
# Add any paths that contain custom static files (such as style sheets) here,
|
||||
# relative to this directory. They are copied after the builtin static files,
|
||||
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||
html_static_path = ['_static']
|
|
@ -0,0 +1,20 @@
|
|||
Welcome to Frank's documentation!
|
||||
=================================
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
:caption: Contents:
|
||||
|
||||
Installation
|
||||
============
|
||||
|
||||
You can install Frank using pip::
|
||||
|
||||
pip install frank-discord
|
||||
|
||||
Indices and tables
|
||||
==================
|
||||
|
||||
* :ref:`genindex`
|
||||
* :ref:`modindex`
|
||||
* :ref:`search`
|
|
@ -1,18 +1,25 @@
|
|||
from .frank import Frank
|
||||
from .module import (
|
||||
Module, command, Command, default, Default, daemon, Daemon,
|
||||
regex_command, RegexCommand,
|
||||
Module,
|
||||
command,
|
||||
Command,
|
||||
default,
|
||||
Default,
|
||||
daemon,
|
||||
Daemon,
|
||||
regex_command,
|
||||
RegexCommand,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
'Frank',
|
||||
'Module',
|
||||
'command',
|
||||
'Command',
|
||||
'default',
|
||||
'Default',
|
||||
'daemon',
|
||||
'Daemon',
|
||||
'regex_command',
|
||||
'RegexCommand',
|
||||
"Frank",
|
||||
"Module",
|
||||
"command",
|
||||
"Command",
|
||||
"default",
|
||||
"Default",
|
||||
"daemon",
|
||||
"Daemon",
|
||||
"regex_command",
|
||||
"RegexCommand",
|
||||
]
|
||||
|
|
|
@ -11,6 +11,7 @@ import discord
|
|||
|
||||
# Typing imports
|
||||
from typing import TYPE_CHECKING, List
|
||||
|
||||
if TYPE_CHECKING:
|
||||
# Own imports
|
||||
from .module import Module
|
||||
|
@ -23,8 +24,12 @@ class Frank(discord.Client):
|
|||
their own behavior.
|
||||
"""
|
||||
|
||||
def __init__(self, modules: List[Module], config_file: str = 'frank.yaml',
|
||||
prefix: str = 'fr'):
|
||||
def __init__(
|
||||
self,
|
||||
modules: List[Module],
|
||||
config_file: str = "frank.yaml",
|
||||
prefix: str = "fr",
|
||||
):
|
||||
"""
|
||||
Args:
|
||||
modules: modules to load
|
||||
|
@ -39,7 +44,7 @@ class Frank(discord.Client):
|
|||
self.PREFIX = prefix
|
||||
|
||||
try:
|
||||
with open(config_file, 'r') as f:
|
||||
with open(config_file, "r") as f:
|
||||
self._config = yaml.load(f, Loader=yaml.FullLoader)
|
||||
|
||||
except FileNotFoundError:
|
||||
|
@ -50,7 +55,7 @@ class Frank(discord.Client):
|
|||
Runs when the bot has succesfully connected to Discord
|
||||
"""
|
||||
|
||||
print('Connected')
|
||||
print("Connected")
|
||||
|
||||
# Startup all modules
|
||||
for module in self._modules:
|
||||
|
@ -63,7 +68,7 @@ class Frank(discord.Client):
|
|||
await loaded._start()
|
||||
self._loaded_modules.append(loaded)
|
||||
|
||||
print('All modules loaded')
|
||||
print("All modules loaded")
|
||||
|
||||
async def stop(self):
|
||||
"""
|
||||
|
@ -88,10 +93,18 @@ 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
|
||||
|
||||
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)
|
||||
await module(
|
||||
cmd=cmd[2:],
|
||||
author=message.author,
|
||||
channel=message.channel,
|
||||
mid=message.id,
|
||||
)
|
||||
|
|
|
@ -1,18 +1,24 @@
|
|||
from .module import Module
|
||||
from .decorators import (
|
||||
command, Command, default, Default, daemon, Daemon, regex_command,
|
||||
command,
|
||||
Command,
|
||||
default,
|
||||
Default,
|
||||
daemon,
|
||||
Daemon,
|
||||
regex_command,
|
||||
RegexCommand,
|
||||
)
|
||||
|
||||
|
||||
__all__ = [
|
||||
'Module',
|
||||
'command',
|
||||
'Command',
|
||||
'default',
|
||||
'Default',
|
||||
'daemon',
|
||||
'Daemon',
|
||||
'regex_command',
|
||||
'RegexCommand',
|
||||
"Module",
|
||||
"command",
|
||||
"Command",
|
||||
"default",
|
||||
"Default",
|
||||
"daemon",
|
||||
"Daemon",
|
||||
"regex_command",
|
||||
"RegexCommand",
|
||||
]
|
||||
|
|
|
@ -2,12 +2,12 @@ from .classes import Command, RegexCommand, Daemon, Default
|
|||
from .functions import command, regex_command, daemon, default
|
||||
|
||||
__all__ = [
|
||||
'command',
|
||||
'Command',
|
||||
'regex_command',
|
||||
'RegexCommand',
|
||||
'default',
|
||||
'Default',
|
||||
'daemon',
|
||||
'Daemon',
|
||||
"command",
|
||||
"Command",
|
||||
"regex_command",
|
||||
"RegexCommand",
|
||||
"default",
|
||||
"Default",
|
||||
"daemon",
|
||||
"Daemon",
|
||||
]
|
||||
|
|
|
@ -4,6 +4,14 @@ 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 +103,38 @@ 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.
|
||||
"""
|
||||
|
||||
|
|
|
@ -1,7 +1,17 @@
|
|||
# =====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 +43,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
|
||||
|
||||
|
|
|
@ -7,4 +7,4 @@ class DuplicateCommand(Exception):
|
|||
|
||||
|
||||
class MultipleDefaults(Exception):
|
||||
message = 'Multiple default commands detected'
|
||||
message = "Multiple default commands detected"
|
||||
|
|
|
@ -10,6 +10,7 @@ from .decorators import Command, Daemon, Default, RegexCommand
|
|||
|
||||
# Typing imports
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
# Built-in imports
|
||||
from typing import List, Any
|
||||
|
@ -18,7 +19,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']
|
||||
illegal_names = ["commands", "daemons", "default"]
|
||||
|
||||
output = []
|
||||
|
||||
|
@ -36,8 +37,10 @@ class ModuleMeta:
|
|||
# The sort puts all the RegexCommand objects at the back, making them
|
||||
# be matched last
|
||||
|
||||
return sorted(self._filter_attrs(lambda val: isinstance(val, Command)),
|
||||
key=lambda x: isinstance(x, RegexCommand))
|
||||
return sorted(
|
||||
self._filter_attrs(lambda val: isinstance(val, Command)),
|
||||
key=lambda x: isinstance(x, RegexCommand),
|
||||
)
|
||||
|
||||
@cached_property
|
||||
def daemons(self) -> List[Daemon]:
|
||||
|
@ -45,5 +48,6 @@ class ModuleMeta:
|
|||
|
||||
@cached_property
|
||||
def default(self) -> Default:
|
||||
return next(iter(self._filter_attrs(
|
||||
lambda val: isinstance(val, Default))), None)
|
||||
return next(
|
||||
iter(self._filter_attrs(lambda val: isinstance(val, Default))), None
|
||||
)
|
||||
|
|
|
@ -12,6 +12,7 @@ from .decorators import RegexCommand
|
|||
|
||||
# Typing imports
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
# Built-in imports
|
||||
from typing import List, Dict
|
||||
|
@ -33,13 +34,13 @@ class Module(ModuleMeta):
|
|||
Prefix to activate this module.
|
||||
"""
|
||||
|
||||
NAME = ''
|
||||
NAME = ""
|
||||
"""
|
||||
The name is used in various places, such as the config file and the
|
||||
help function.
|
||||
"""
|
||||
|
||||
HELP = ''
|
||||
HELP = ""
|
||||
"""
|
||||
Short description of the module to use in the help function.
|
||||
"""
|
||||
|
@ -84,8 +85,9 @@ class Module(ModuleMeta):
|
|||
for task in self._tasks:
|
||||
task.cancel()
|
||||
|
||||
async def __call__(self, cmd: List[str], author: User,
|
||||
channel: Messageable, mid: int):
|
||||
async def __call__(
|
||||
self, cmd: List[str], author: User, channel: Messageable, mid: int
|
||||
):
|
||||
"""
|
||||
Execute the command, if found.
|
||||
|
||||
|
@ -97,21 +99,26 @@ class Module(ModuleMeta):
|
|||
"""
|
||||
|
||||
if cmd:
|
||||
func = next((func for func in self.commands
|
||||
if func.match(cmd[0])), None)
|
||||
func = next(
|
||||
(func for func in self.commands if func.match(cmd[0])), None
|
||||
)
|
||||
|
||||
# Throw error if no function is found
|
||||
if not func:
|
||||
raise InvalidCommand(f"Unknown command: {cmd}")
|
||||
|
||||
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)
|
||||
await func(
|
||||
prefix=cmd[0],
|
||||
cmd=cmd[1:],
|
||||
author=author,
|
||||
channel=channel,
|
||||
mid=mid,
|
||||
)
|
||||
|
||||
else:
|
||||
await func(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 +132,12 @@ class Module(ModuleMeta):
|
|||
prefix: prefix to check
|
||||
"""
|
||||
|
||||
if cls.PREFIX:
|
||||
# Always return False if there's no PREFIX defined
|
||||
if not cls.PREFIX:
|
||||
return False
|
||||
|
||||
if isinstance(cls.PREFIX, list):
|
||||
return prefix in cls.PREFIX
|
||||
|
||||
else:
|
||||
return prefix == cls.PREFIX
|
||||
|
||||
else:
|
||||
return False
|
||||
|
|
|
@ -2,5 +2,5 @@ from .help import HelpMod
|
|||
|
||||
|
||||
__all__ = [
|
||||
'HelpMod',
|
||||
"HelpMod",
|
||||
]
|
||||
|
|
|
@ -10,6 +10,7 @@ from .. import Module, default, regex_command
|
|||
|
||||
# Typing imports
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
# Built-in imports
|
||||
from typing import List
|
||||
|
@ -24,11 +25,11 @@ class HelpMod(Module):
|
|||
other modules.
|
||||
"""
|
||||
|
||||
PREFIX = 'help'
|
||||
NAME = 'help'
|
||||
HELP = 'Shows help info about all modules'
|
||||
PREFIX = "help"
|
||||
NAME = "help"
|
||||
HELP = "Shows help info about all modules"
|
||||
|
||||
@default(help_str='Show help about all modules.')
|
||||
@default(help_str="Show help about all modules.")
|
||||
async def send_all(self, author: User, channel: Messageable, mid: int):
|
||||
embed = Embed()
|
||||
|
||||
|
@ -37,20 +38,33 @@ class HelpMod(Module):
|
|||
|
||||
await channel.send(embed=embed)
|
||||
|
||||
@regex_command(cmd='.+', help_str='Show help about a certain module.')
|
||||
async def show_module_help(self, prefix: str, cmd: List[str], author: User,
|
||||
channel: Messageable, mid: int):
|
||||
@regex_command(pattern=".+", help_str="Show help about a certain module.")
|
||||
async def show_module_help(
|
||||
self,
|
||||
prefix: str,
|
||||
cmd: List[str],
|
||||
author: User,
|
||||
channel: Messageable,
|
||||
mid: int,
|
||||
):
|
||||
# Yes, this command just ignores cmd at the moment
|
||||
mod_name = prefix.lower()
|
||||
mod = next((mod for mod in self._client._modules
|
||||
if mod.NAME.lower() == mod_name), None)
|
||||
mod = next(
|
||||
(
|
||||
mod
|
||||
for mod in self._client._modules
|
||||
if mod.NAME.lower() == mod_name
|
||||
),
|
||||
None,
|
||||
)
|
||||
|
||||
if mod:
|
||||
embed = Embed()
|
||||
|
||||
if mod.default:
|
||||
embed.add_field(name='default', value=mod.default.help_str,
|
||||
inline=False)
|
||||
embed.add_field(
|
||||
name="default", value=mod.default.help_str, inline=False
|
||||
)
|
||||
|
||||
for cmd in mod._COMMANDS:
|
||||
embed.add_field(name=cmd.cmd, value=mod.help_str, inline=False)
|
||||
|
|
|
@ -11,3 +11,6 @@ setuptools~=49.6.0
|
|||
pytest>=6.0.1,<7.0.0
|
||||
pytest-asyncio>=0.14.0,<1.0.0
|
||||
twine>=3.2.0,<4.0.0
|
||||
Sphinx==3.2.1
|
||||
sphinx-rtd-theme==0.5.0
|
||||
black
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
# =====IMPORTS=====
|
||||
# Third-party imports
|
||||
import pytest
|
||||
|
||||
# Own imports
|
||||
from frank import default, command, daemon
|
||||
|
||||
|
||||
class TestDecorators:
|
||||
"""
|
||||
This test makes sure the decorated functions return the same result as
|
||||
their non-decorated counterparts
|
||||
"""
|
||||
|
||||
def default_no_dec(self):
|
||||
return 'default'
|
||||
|
||||
@default()
|
||||
def default_dec(self):
|
||||
return self.default_no_dec()
|
||||
|
||||
def command_no_dec(self):
|
||||
return 'command'
|
||||
|
||||
@command('cmd')
|
||||
def command_dec(self):
|
||||
return self.command_no_dec()
|
||||
|
||||
def daemon_no_dec(self):
|
||||
return 'daemon'
|
||||
|
||||
@daemon()
|
||||
async def daemon_dec(self):
|
||||
return self.daemon_no_dec()
|
||||
|
||||
def test_default(self):
|
||||
assert self.default_no_dec() == self.default_dec()
|
||||
|
||||
def test_command(self):
|
||||
assert self.command_no_dec() == self.command_dec()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_daemon(self):
|
||||
assert self.daemon_no_dec() == await self.daemon_dec()
|
Reference in New Issue