Compare commits
19 Commits
Author | SHA1 | Date |
---|---|---|
Jef Roosens | 2a9323b5f5 | |
Jef Roosens | 32ab8538a4 | |
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/
|
*.egg-info/
|
||||||
*.eggs/
|
*.eggs/
|
||||||
|
|
||||||
# Docs output
|
# Docs
|
||||||
docs/build
|
docs/build
|
||||||
|
docs/source/apidoc/
|
||||||
|
|
||||||
# Caches
|
# Caches
|
||||||
__pycache__/
|
__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
|
### Added
|
||||||
- Prefix can now be passed as argument to init
|
- Prefix can now be passed as argument to init
|
||||||
- Pre-made help module
|
- Pre-made help module
|
||||||
|
|
32
Makefile
32
Makefile
|
@ -10,10 +10,10 @@ DOCS=docs
|
||||||
# Interpreter to create venv with
|
# Interpreter to create venv with
|
||||||
INTERPRETER=python3.8
|
INTERPRETER=python3.8
|
||||||
|
|
||||||
all: run
|
all: build-venv
|
||||||
|
|
||||||
|
|
||||||
# Re-create venv when needed
|
# =====VENV=====
|
||||||
$(VENV)/bin/activate: requirements.txt requirements-dev.txt
|
$(VENV)/bin/activate: requirements.txt requirements-dev.txt
|
||||||
@ echo "Rebuilding venv..."
|
@ echo "Rebuilding venv..."
|
||||||
@ [ ! -e "$(VENV)" ] || rm -rf "$(VENV)"
|
@ [ ! -e "$(VENV)" ] || rm -rf "$(VENV)"
|
||||||
|
@ -22,8 +22,9 @@ $(VENV)/bin/activate: requirements.txt requirements-dev.txt
|
||||||
|
|
||||||
build-venv: $(VENV)/bin/activate
|
build-venv: $(VENV)/bin/activate
|
||||||
|
|
||||||
|
|
||||||
# =====CLEANING=====
|
# =====CLEANING=====
|
||||||
clean: clean-venv clean-cache clean-docs
|
clean: clean-venv clean-cache clean-docs clean-setup
|
||||||
|
|
||||||
# Remove venv
|
# Remove venv
|
||||||
clean-venv:
|
clean-venv:
|
||||||
|
@ -37,24 +38,41 @@ clean-cache:
|
||||||
@ echo "Removing caches..."
|
@ echo "Removing caches..."
|
||||||
@ find . -type d \( -name "__pycache__" -o -name ".pytest_cache" \) -exec rm -r "{}" +
|
@ find . -type d \( -name "__pycache__" -o -name ".pytest_cache" \) -exec rm -r "{}" +
|
||||||
|
|
||||||
|
# Removed generation documentation
|
||||||
clean-docs:
|
clean-docs:
|
||||||
@ echo "Removing documentation build..."
|
@ echo "Removing documentation build..."
|
||||||
@ [ ! -e "$(DOCS)/build" ] || rm -r "$(DOCS)/build"
|
@ [ ! -e "$(DOCS)/build" ] || rm -r "$(DOCS)/build"
|
||||||
|
@ [ ! -e "$(DOCS)/source/apidoc" ] || rm -r "$(DOCS)/source/apidoc"
|
||||||
|
|
||||||
|
# Remove build leftovers (not dist)
|
||||||
clean-setup:
|
clean-setup:
|
||||||
@ echo 'Removing build artifacts...'
|
@ echo 'Removing build artifacts...'
|
||||||
@ [ ! -e "build" ] || rm -rf build
|
@ [ ! -e "build" ] || rm -rf build
|
||||||
@ find . -maxdepth 1 -type d -name '*.egg-info' -exec rm -rf "{}" \;
|
@ find . -maxdepth 1 -type d -name '*.egg-info' -exec rm -rf "{}" \;
|
||||||
|
|
||||||
|
|
||||||
# =====DOCS=====
|
# =====DOCS=====
|
||||||
docs: build-venv
|
# # Generate documentation
|
||||||
@ "$(VENV)/bin/sphinx-apidoc" -o "$(DOCS)/source" "$(SRC)"
|
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"
|
@ "$(VENV)/bin/sphinx-build" "$(DOCS)/source" "$(DOCS)/build"
|
||||||
|
|
||||||
|
|
||||||
# =====TESTS=====
|
# =====TESTS=====
|
||||||
|
# Run tests
|
||||||
test: pytest.ini build-venv
|
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=====
|
# =====PACKAGING=====
|
||||||
|
@ -62,7 +80,7 @@ package: README.md LICENSE setup.py test clean-setup
|
||||||
@ echo "Removing build..."
|
@ echo "Removing build..."
|
||||||
@ [ ! -e "dist" ] || rm -r "dist"
|
@ [ ! -e "dist" ] || rm -r "dist"
|
||||||
@ echo "Running setup.py..."
|
@ echo "Running setup.py..."
|
||||||
@ "$(VENV)/bin/python" setup.py sdist bdist_wheel
|
@ "$(VENV)/bin/python" setup.py bdist_wheel
|
||||||
|
|
||||||
publish: package
|
publish: package
|
||||||
@ [ "$$(git symbolic-ref HEAD --short)" = master ] && { \
|
@ [ "$$(git symbolic-ref HEAD --short)" = master ] && { \
|
||||||
|
|
37
README.md
37
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
|
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.
|
routing the Discord commands to their respective functions.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
You can install the `frank-discord` package from PyPi:
|
||||||
|
|
||||||
|
```
|
||||||
|
pip install frank-discord
|
||||||
|
```
|
||||||
|
|
||||||
## Example Module
|
## Example Module
|
||||||
In this section, I've written an example module for you, to understand the basic mechanics behind Frank.
|
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
|
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
|
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,
|
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
|
```python
|
||||||
def pre_start(self):
|
def pre_start(self):
|
||||||
|
@ -46,14 +53,14 @@ Frank.
|
||||||
# do some stuff
|
# do some stuff
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@frank.daemon()
|
# Interval defines how many seconds are between each call
|
||||||
|
@frank.daemon(interval=5)
|
||||||
async def some_daemon(self):
|
async def some_daemon(self):
|
||||||
while True:
|
# do some stuff
|
||||||
# do some stuff
|
pass
|
||||||
pass
|
|
||||||
|
|
||||||
@frank.default()
|
@frank.default()
|
||||||
async def default_cmd(self):
|
async def default_cmd(self, author, channel, mid):
|
||||||
# do some default action
|
# do some default action
|
||||||
pass
|
pass
|
||||||
```
|
```
|
||||||
|
@ -66,12 +73,24 @@ These three decorators are the bread and butter of Frank. Let's break them down:
|
||||||
fr examp command [ARGS]
|
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
|
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
|
etc. Each command gets the `author`, `channel`, and `id` of the message. The `cmd` variable contains all the
|
||||||
to the command.
|
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
|
- `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
|
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
|
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`.
|
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
|
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.
|
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 .frank import Frank
|
||||||
from .module import (
|
from .module import (
|
||||||
Module, command, Command, default, Default, daemon, Daemon,
|
Module,
|
||||||
regex_command, RegexCommand,
|
command,
|
||||||
|
Command,
|
||||||
|
default,
|
||||||
|
Default,
|
||||||
|
daemon,
|
||||||
|
Daemon,
|
||||||
|
regex_command,
|
||||||
|
RegexCommand,
|
||||||
)
|
)
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
'Frank',
|
"Frank",
|
||||||
'Module',
|
"Module",
|
||||||
'command',
|
"command",
|
||||||
'Command',
|
"Command",
|
||||||
'default',
|
"default",
|
||||||
'Default',
|
"Default",
|
||||||
'daemon',
|
"daemon",
|
||||||
'Daemon',
|
"Daemon",
|
||||||
'regex_command',
|
"regex_command",
|
||||||
'RegexCommand',
|
"RegexCommand",
|
||||||
]
|
]
|
||||||
|
|
|
@ -11,6 +11,7 @@ import discord
|
||||||
|
|
||||||
# Typing imports
|
# Typing imports
|
||||||
from typing import TYPE_CHECKING, List
|
from typing import TYPE_CHECKING, List
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
# Own imports
|
# Own imports
|
||||||
from .module import Module
|
from .module import Module
|
||||||
|
@ -23,8 +24,12 @@ class Frank(discord.Client):
|
||||||
their own behavior.
|
their own behavior.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, modules: List[Module], config_file: str = 'frank.yaml',
|
def __init__(
|
||||||
prefix: str = 'fr'):
|
self,
|
||||||
|
modules: List[Module],
|
||||||
|
config_file: str = "frank.yaml",
|
||||||
|
prefix: str = "fr",
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
Args:
|
Args:
|
||||||
modules: modules to load
|
modules: modules to load
|
||||||
|
@ -39,7 +44,7 @@ class Frank(discord.Client):
|
||||||
self.PREFIX = prefix
|
self.PREFIX = prefix
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with open(config_file, 'r') as f:
|
with open(config_file, "r") as f:
|
||||||
self._config = yaml.load(f, Loader=yaml.FullLoader)
|
self._config = yaml.load(f, Loader=yaml.FullLoader)
|
||||||
|
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
|
@ -50,7 +55,7 @@ class Frank(discord.Client):
|
||||||
Runs when the bot has succesfully connected to Discord
|
Runs when the bot has succesfully connected to Discord
|
||||||
"""
|
"""
|
||||||
|
|
||||||
print('Connected')
|
print("Connected")
|
||||||
|
|
||||||
# Startup all modules
|
# Startup all modules
|
||||||
for module in self._modules:
|
for module in self._modules:
|
||||||
|
@ -63,7 +68,7 @@ class Frank(discord.Client):
|
||||||
await loaded._start()
|
await loaded._start()
|
||||||
self._loaded_modules.append(loaded)
|
self._loaded_modules.append(loaded)
|
||||||
|
|
||||||
print('All modules loaded')
|
print("All modules loaded")
|
||||||
|
|
||||||
async def stop(self):
|
async def stop(self):
|
||||||
"""
|
"""
|
||||||
|
@ -88,10 +93,18 @@ class Frank(discord.Client):
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return
|
return
|
||||||
|
|
||||||
if cmd and cmd[0] == self.PREFIX:
|
# Exit if no commands are given or prefix is wrong
|
||||||
module = next((mod for mod in self._loaded_modules
|
if not (cmd and cmd[0] == self.PREFIX):
|
||||||
if mod.match(cmd[1])), None)
|
return
|
||||||
|
|
||||||
if module:
|
module = next(
|
||||||
await module(cmd=cmd[2:], author=message.author,
|
(mod for mod in self._loaded_modules if mod.match(cmd[1])), None
|
||||||
channel=message.channel, mid=message.id)
|
)
|
||||||
|
|
||||||
|
if module:
|
||||||
|
await module(
|
||||||
|
cmd=cmd[2:],
|
||||||
|
author=message.author,
|
||||||
|
channel=message.channel,
|
||||||
|
mid=message.id,
|
||||||
|
)
|
||||||
|
|
|
@ -1,18 +1,24 @@
|
||||||
from .module import Module
|
from .module import Module
|
||||||
from .decorators import (
|
from .decorators import (
|
||||||
command, Command, default, Default, daemon, Daemon, regex_command,
|
command,
|
||||||
|
Command,
|
||||||
|
default,
|
||||||
|
Default,
|
||||||
|
daemon,
|
||||||
|
Daemon,
|
||||||
|
regex_command,
|
||||||
RegexCommand,
|
RegexCommand,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
'Module',
|
"Module",
|
||||||
'command',
|
"command",
|
||||||
'Command',
|
"Command",
|
||||||
'default',
|
"default",
|
||||||
'Default',
|
"Default",
|
||||||
'daemon',
|
"daemon",
|
||||||
'Daemon',
|
"Daemon",
|
||||||
'regex_command',
|
"regex_command",
|
||||||
'RegexCommand',
|
"RegexCommand",
|
||||||
]
|
]
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
from .classes import Command, RegexCommand, Daemon, Default
|
from .classes import RegularCommand, RegexCommand, Daemon, Default
|
||||||
from .functions import command, regex_command, daemon, default
|
from .functions import command, regex_command, daemon, default
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
'command',
|
"command",
|
||||||
'Command',
|
"RegularCommand",
|
||||||
'regex_command',
|
"regex_command",
|
||||||
'RegexCommand',
|
"RegexCommand",
|
||||||
'default',
|
"default",
|
||||||
'Default',
|
"Default",
|
||||||
'daemon',
|
"daemon",
|
||||||
'Daemon',
|
"Daemon",
|
||||||
]
|
]
|
||||||
|
|
|
@ -4,6 +4,15 @@ from __future__ import annotations
|
||||||
|
|
||||||
# Built-in imports
|
# Built-in imports
|
||||||
import re
|
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:
|
class Simple:
|
||||||
|
@ -45,10 +54,18 @@ class Simple:
|
||||||
|
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
@property
|
||||||
|
def client(self):
|
||||||
|
"""
|
||||||
|
Returns the Frank client instance.
|
||||||
|
"""
|
||||||
|
|
||||||
class Command(Simple):
|
return self._obj._client
|
||||||
|
|
||||||
|
|
||||||
|
class SimpleCommand(Simple):
|
||||||
"""
|
"""
|
||||||
Represents a command of the module.
|
Base class for the various command types.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, func: callable, cmd: str, help_str: str = None):
|
def __init__(self, func: callable, cmd: str, help_str: str = None):
|
||||||
|
@ -64,47 +81,170 @@ class Command(Simple):
|
||||||
self.cmd = cmd
|
self.cmd = cmd
|
||||||
self.help_str = help_str
|
self.help_str = help_str
|
||||||
|
|
||||||
def match(self, prefix: str) -> bool:
|
def match(self, message: str) -> Tuple[bool, List[str]]:
|
||||||
"""
|
"""
|
||||||
Returns wether the command matches the given prefix.
|
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:
|
Args:
|
||||||
prefix: string to match own prefix against
|
message: message to check
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return self.cmd == prefix
|
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 RegexCommand(Command):
|
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
|
A subclass of Command that can use a regex pattern instead of a fixed
|
||||||
prefix.
|
prefix.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def match(self, prefix: str) -> bool:
|
def match(self, message: str) -> Tuple[str, List[str]]:
|
||||||
"""
|
"""
|
||||||
Returns wether the regex pattern matches the given prefix.
|
Returns wether the regex pattern matches the given prefix.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
prefix: string to match pattern against; Pattern must match entire
|
prefix: string to match pattern against; Pattern must match entire
|
||||||
prefix
|
prefix
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return bool(re.fullmatch(self.cmd, 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):
|
class Daemon(Simple):
|
||||||
"""
|
"""
|
||||||
Represents a daemon. Currently, it's only used as its own type, but writing
|
Represents a daemon a.k.a. a background process.
|
||||||
it this way allows us to easily expand upon its functionality later.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
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):
|
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.
|
command.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
|
@ -1,19 +1,37 @@
|
||||||
# =====IMPORTS=====
|
# =====IMPORTS=====
|
||||||
|
# Future imports
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
# Own imports
|
# Own imports
|
||||||
from .classes import Command, RegexCommand, Daemon, Default
|
from .classes import RegularCommand, RegexCommand, Daemon, Default
|
||||||
|
|
||||||
|
# Typing imports
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
# Built-in imports
|
||||||
|
from typing import Union, List
|
||||||
|
|
||||||
|
|
||||||
def command(cmd, help_str: str = None) -> callable:
|
def command(
|
||||||
|
cmd,
|
||||||
|
help_str: str = None,
|
||||||
|
alias: Union[str, List[str]] = None,
|
||||||
|
requires_prefix: bool = False,
|
||||||
|
) -> callable:
|
||||||
"""
|
"""
|
||||||
Converts a method into a command by replacing it with a Command object.
|
Converts a method into a command by replacing it with a Command object.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
cmd: keyword used to call this function
|
cmd: keyword used to call this function
|
||||||
help_str: short description of the command
|
help_str: short description of the command
|
||||||
|
alias: alias(es) for the command
|
||||||
|
requires_prefix: defines wether the command needs the Frank prefix for
|
||||||
|
its aliases to work or not
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def inner(func):
|
def inner(func):
|
||||||
return Command(func, cmd, help_str)
|
return RegularCommand(func, cmd, help_str, aliases)
|
||||||
|
|
||||||
return inner
|
return inner
|
||||||
|
|
||||||
|
@ -33,14 +51,14 @@ def regex_command(pattern: str, help_str: str = None) -> callable:
|
||||||
return inner
|
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
|
Converts the method into a Daemon, which will then be run when the module
|
||||||
is started.
|
is started.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def inner(func):
|
def inner(func):
|
||||||
return Daemon(func)
|
return Daemon(func, interval)
|
||||||
|
|
||||||
return inner
|
return inner
|
||||||
|
|
||||||
|
|
|
@ -7,4 +7,4 @@ class DuplicateCommand(Exception):
|
||||||
|
|
||||||
|
|
||||||
class MultipleDefaults(Exception):
|
class MultipleDefaults(Exception):
|
||||||
message = 'Multiple default commands detected'
|
message = "Multiple default commands detected"
|
||||||
|
|
|
@ -6,10 +6,11 @@ from __future__ import annotations
|
||||||
from functools import cached_property
|
from functools import cached_property
|
||||||
|
|
||||||
# Own imports
|
# Own imports
|
||||||
from .decorators import Command, Daemon, Default, RegexCommand
|
from .decorators import SimpleCommand, Daemon, Default, RegexCommand
|
||||||
|
|
||||||
# Typing imports
|
# Typing imports
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
# Built-in imports
|
# Built-in imports
|
||||||
from typing import List, Any
|
from typing import List, Any
|
||||||
|
@ -18,7 +19,7 @@ if TYPE_CHECKING:
|
||||||
class ModuleMeta:
|
class ModuleMeta:
|
||||||
def _filter_attrs(self, condition: callable[[Any], bool]) -> List[Any]:
|
def _filter_attrs(self, condition: callable[[Any], bool]) -> List[Any]:
|
||||||
# This prevents an infinite loop of getting the attribute
|
# This prevents an infinite loop of getting the attribute
|
||||||
illegal_names = ['commands', 'daemons', 'default']
|
illegal_names = ["commands", "daemons", "default"]
|
||||||
|
|
||||||
output = []
|
output = []
|
||||||
|
|
||||||
|
@ -31,13 +32,15 @@ class ModuleMeta:
|
||||||
return output
|
return output
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def commands(self) -> List[Command]:
|
def commands(self) -> List[SimpleCommand]:
|
||||||
# This also matches RegexCommand objects
|
# This also matches RegexCommand objects
|
||||||
# The sort puts all the RegexCommand objects at the back, making them
|
# The sort puts all the RegexCommand objects at the back,making
|
||||||
# be matched last
|
# them be matched last
|
||||||
|
|
||||||
return sorted(self._filter_attrs(lambda val: isinstance(val, Command)),
|
return sorted(
|
||||||
key=lambda x: isinstance(x, RegexCommand))
|
self._filter_attrs(lambda val: isinstance(val, SimpleCommand)),
|
||||||
|
key=lambda x: isinstance(x, RegexCommand),
|
||||||
|
)
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def daemons(self) -> List[Daemon]:
|
def daemons(self) -> List[Daemon]:
|
||||||
|
@ -45,5 +48,7 @@ class ModuleMeta:
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def default(self) -> Default:
|
def default(self) -> Default:
|
||||||
return next(iter(self._filter_attrs(
|
return next(
|
||||||
lambda val: isinstance(val, Default))), None)
|
iter(self._filter_attrs(lambda val: isinstance(val, Default))),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
|
|
@ -12,6 +12,7 @@ from .decorators import RegexCommand
|
||||||
|
|
||||||
# Typing imports
|
# Typing imports
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
# Built-in imports
|
# Built-in imports
|
||||||
from typing import List, Dict
|
from typing import List, Dict
|
||||||
|
@ -33,13 +34,13 @@ class Module(ModuleMeta):
|
||||||
Prefix to activate this module.
|
Prefix to activate this module.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
NAME = ''
|
NAME = ""
|
||||||
"""
|
"""
|
||||||
The name is used in various places, such as the config file and the
|
The name is used in various places, such as the config file and the
|
||||||
help function.
|
help function.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
HELP = ''
|
HELP = ""
|
||||||
"""
|
"""
|
||||||
Short description of the module to use in the help function.
|
Short description of the module to use in the help function.
|
||||||
"""
|
"""
|
||||||
|
@ -84,8 +85,9 @@ class Module(ModuleMeta):
|
||||||
for task in self._tasks:
|
for task in self._tasks:
|
||||||
task.cancel()
|
task.cancel()
|
||||||
|
|
||||||
async def __call__(self, cmd: List[str], author: User,
|
async def __call__(
|
||||||
channel: Messageable, mid: int):
|
self, cmd: List[str], author: User, channel: Messageable, mid: int
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
Execute the command, if found.
|
Execute the command, if found.
|
||||||
|
|
||||||
|
@ -97,21 +99,28 @@ class Module(ModuleMeta):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if cmd:
|
if cmd:
|
||||||
func = next((func for func in self.commands
|
func = next(
|
||||||
if func.match(cmd[0])), None)
|
(func for func in self.commands if func.match(cmd[0])), None
|
||||||
|
)
|
||||||
|
|
||||||
if func:
|
# Throw error if no function is found
|
||||||
# A RegexCommand can use the prefix, as it's not a fixed string
|
if not func:
|
||||||
if isinstance(func, RegexCommand):
|
raise InvalidCommand(f"Unknown command: {cmd}")
|
||||||
await func(prefix=cmd[0], cmd=cmd[1:], author=author,
|
|
||||||
channel=channel, mid=mid)
|
|
||||||
|
|
||||||
else:
|
# A RegexCommand can use the prefix, as it's not a fixed string
|
||||||
await func(cmd=cmd[1:], author=author, channel=channel,
|
if isinstance(func, RegexCommand):
|
||||||
mid=mid)
|
await func(
|
||||||
|
prefix=cmd[0],
|
||||||
|
cmd=cmd[1:],
|
||||||
|
author=author,
|
||||||
|
channel=channel,
|
||||||
|
mid=mid,
|
||||||
|
)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
raise InvalidCommand(f'Unknown command: {cmd}')
|
await func(
|
||||||
|
cmd=cmd[1:], author=author, channel=channel, mid=mid
|
||||||
|
)
|
||||||
|
|
||||||
elif self.default:
|
elif self.default:
|
||||||
await self.default(author=author, channel=channel, mid=mid)
|
await self.default(author=author, channel=channel, mid=mid)
|
||||||
|
@ -125,12 +134,12 @@ class Module(ModuleMeta):
|
||||||
prefix: prefix to check
|
prefix: prefix to check
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if cls.PREFIX:
|
# Always return False if there's no PREFIX defined
|
||||||
if isinstance(cls.PREFIX, list):
|
if not cls.PREFIX:
|
||||||
return prefix in cls.PREFIX
|
return False
|
||||||
|
|
||||||
else:
|
if isinstance(cls.PREFIX, list):
|
||||||
return prefix == cls.PREFIX
|
return prefix in cls.PREFIX
|
||||||
|
|
||||||
else:
|
else:
|
||||||
return False
|
return prefix == cls.PREFIX
|
||||||
|
|
|
@ -2,5 +2,5 @@ from .help import HelpMod
|
||||||
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
'HelpMod',
|
"HelpMod",
|
||||||
]
|
]
|
||||||
|
|
|
@ -10,6 +10,7 @@ from .. import Module, default, regex_command
|
||||||
|
|
||||||
# Typing imports
|
# Typing imports
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
# Built-in imports
|
# Built-in imports
|
||||||
from typing import List
|
from typing import List
|
||||||
|
@ -24,11 +25,11 @@ class HelpMod(Module):
|
||||||
other modules.
|
other modules.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
PREFIX = 'help'
|
PREFIX = "help"
|
||||||
NAME = 'help'
|
NAME = "help"
|
||||||
HELP = 'Shows help info about all modules'
|
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):
|
async def send_all(self, author: User, channel: Messageable, mid: int):
|
||||||
embed = Embed()
|
embed = Embed()
|
||||||
|
|
||||||
|
@ -37,20 +38,33 @@ class HelpMod(Module):
|
||||||
|
|
||||||
await channel.send(embed=embed)
|
await channel.send(embed=embed)
|
||||||
|
|
||||||
@regex_command(cmd='.+', help_str='Show help about a certain module.')
|
@regex_command(pattern=".+", help_str="Show help about a certain module.")
|
||||||
async def show_module_help(self, prefix: str, cmd: List[str], author: User,
|
async def show_module_help(
|
||||||
channel: Messageable, mid: int):
|
self,
|
||||||
|
prefix: str,
|
||||||
|
cmd: List[str],
|
||||||
|
author: User,
|
||||||
|
channel: Messageable,
|
||||||
|
mid: int,
|
||||||
|
):
|
||||||
# Yes, this command just ignores cmd at the moment
|
# Yes, this command just ignores cmd at the moment
|
||||||
mod_name = prefix.lower()
|
mod_name = prefix.lower()
|
||||||
mod = next((mod for mod in self._client._modules
|
mod = next(
|
||||||
if mod.NAME.lower() == mod_name), None)
|
(
|
||||||
|
mod
|
||||||
|
for mod in self._client._modules
|
||||||
|
if mod.NAME.lower() == mod_name
|
||||||
|
),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
|
||||||
if mod:
|
if mod:
|
||||||
embed = Embed()
|
embed = Embed()
|
||||||
|
|
||||||
if mod.default:
|
if mod.default:
|
||||||
embed.add_field(name='default', value=mod.default.help_str,
|
embed.add_field(
|
||||||
inline=False)
|
name="default", value=mod.default.help_str, inline=False
|
||||||
|
)
|
||||||
|
|
||||||
for cmd in mod._COMMANDS:
|
for cmd in mod._COMMANDS:
|
||||||
embed.add_field(name=cmd.cmd, value=mod.help_str, inline=False)
|
embed.add_field(name=cmd.cmd, value=mod.help_str, inline=False)
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
[tool.black]
|
||||||
|
line-length = 79
|
||||||
|
target-version = ['py38']
|
||||||
|
include = '\.pyi?$'
|
||||||
|
exclude = '''
|
||||||
|
|
||||||
|
(
|
||||||
|
/(
|
||||||
|
\.eggs
|
||||||
|
| \.git
|
||||||
|
| venv
|
||||||
|
| build
|
||||||
|
| dist
|
||||||
|
)/
|
||||||
|
)
|
||||||
|
'''
|
|
@ -11,3 +11,6 @@ setuptools~=49.6.0
|
||||||
pytest>=6.0.1,<7.0.0
|
pytest>=6.0.1,<7.0.0
|
||||||
pytest-asyncio>=0.14.0,<1.0.0
|
pytest-asyncio>=0.14.0,<1.0.0
|
||||||
twine>=3.2.0,<4.0.0
|
twine>=3.2.0,<4.0.0
|
||||||
|
Sphinx==3.2.1
|
||||||
|
sphinx-rtd-theme==0.5.0
|
||||||
|
black
|
||||||
|
|
2
setup.py
2
setup.py
|
@ -2,6 +2,8 @@
|
||||||
# Third-party imports
|
# Third-party imports
|
||||||
import setuptools
|
import setuptools
|
||||||
|
|
||||||
|
# TODO: switch to alternative that uses pyproject.toml (e.g. poetry)
|
||||||
|
|
||||||
|
|
||||||
with open('requirements.txt', 'r') as reqs_file:
|
with open('requirements.txt', 'r') as reqs_file:
|
||||||
reqs = [line.strip() for line in reqs_file.readlines()]
|
reqs = [line.strip() for line in reqs_file.readlines()]
|
||||||
|
|
|
@ -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