From d1dc46c17b0a1bb14c443ab2673c859341c21123 Mon Sep 17 00:00:00 2001 From: chewingbever Date: Wed, 26 Aug 2020 17:30:22 +0200 Subject: [PATCH 01/17] Added install instructions to README --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index 142d880..6a01a8f 100644 --- a/README.md +++ b/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. From beb5bf49ccc92ffcf8162840fc75e14d22086fc6 Mon Sep 17 00:00:00 2001 From: chewingbever Date: Thu, 27 Aug 2020 09:41:03 +0200 Subject: [PATCH 02/17] Added decorator tests; fixed README typo --- CHANGELOG.md | 3 +-- README.md | 22 +++++++++++++++++----- tests/test_decorators.py | 40 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 58 insertions(+), 7 deletions(-) create mode 100644 tests/test_decorators.py diff --git a/CHANGELOG.md b/CHANGELOG.md index bde1806..6188ae0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,5 @@ -# Changelog +## v0.1 (2020/08/26) -## 0.1 (2020/08/26) ### Added - Prefix can now be passed as argument to init - Pre-made help module diff --git a/README.md b/README.md index 6a01a8f..78e4fa7 100644 --- a/README.md +++ b/README.md @@ -36,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): @@ -60,7 +60,7 @@ Frank. pass @frank.default() - async def default_cmd(self): + async def default_cmd(self, author, channel, mid): # do some default action pass ``` @@ -73,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) +``` diff --git a/tests/test_decorators.py b/tests/test_decorators.py new file mode 100644 index 0000000..2c9f400 --- /dev/null +++ b/tests/test_decorators.py @@ -0,0 +1,40 @@ +# =====IMPORTS===== +# 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() + 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() + + def test_daemon(self): + assert self.daemon_no_dec() == self.daemon_dec() From f6f081bfbac034ba21258b616566d8cec2a91f2f Mon Sep 17 00:00:00 2001 From: chewingbever Date: Thu, 27 Aug 2020 11:59:15 +0200 Subject: [PATCH 03/17] Daemons now accept an interval value --- frank/module/decorators/classes.py | 33 +++++++++++++++++++++++++--- frank/module/decorators/functions.py | 13 +++++++++-- 2 files changed, 41 insertions(+), 5 deletions(-) diff --git a/frank/module/decorators/classes.py b/frank/module/decorators/classes.py index 3e93be1..3239893 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,11 +102,31 @@ 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 aka 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: + func(self, *args, **kwargs) + + await asyncio.sleep(interval) + + self.func = loop class Default(Simple): 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 From e7637a1bce918bd5d8b60bca1e3c7e0c51700537 Mon Sep 17 00:00:00 2001 From: chewingbever Date: Thu, 27 Aug 2020 13:06:22 +0200 Subject: [PATCH 04/17] Daemon now must be async (more consistent) --- CHANGELOG.md | 4 ++++ frank/module/decorators/classes.py | 2 +- tests/test_decorators.py | 10 +++++++--- 3 files changed, 12 insertions(+), 4 deletions(-) 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/module/decorators/classes.py b/frank/module/decorators/classes.py index 3239893..11f17a3 100644 --- a/frank/module/decorators/classes.py +++ b/frank/module/decorators/classes.py @@ -122,7 +122,7 @@ class Daemon(Simple): if interval > 0: async def loop(self, *args, **kwargs): while True: - func(self, *args, **kwargs) + await func(self, *args, **kwargs) await asyncio.sleep(interval) 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() From 5648f1a9dee78773c8fde4ce6b0a3b301679045f Mon Sep 17 00:00:00 2001 From: chewingbever Date: Thu, 27 Aug 2020 17:23:38 +0200 Subject: [PATCH 05/17] Flattened code a bit --- frank/frank.py | 15 +++++++++------ frank/module/decorators/classes.py | 5 +++-- frank/module/module.py | 30 +++++++++++++++--------------- 3 files changed, 27 insertions(+), 23 deletions(-) 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 11f17a3..3b1c187 100644 --- a/frank/module/decorators/classes.py +++ b/frank/module/decorators/classes.py @@ -102,7 +102,7 @@ class RegexCommand(Command): class Daemon(Simple): """ - Represents a daemon aka a background process. + Represents a daemon a.k.a. a background process. """ def __init__(self, func: callable, interval: Union[int, float] = 0): @@ -122,6 +122,7 @@ class Daemon(Simple): 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) @@ -131,7 +132,7 @@ class Daemon(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. """ 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 From e41d9780a4ee92137a04caefef96f8588a004fc3 Mon Sep 17 00:00:00 2001 From: chewingbever Date: Thu, 27 Aug 2020 17:34:08 +0200 Subject: [PATCH 06/17] Generated default Sphinx config --- docs/Makefile | 20 ++++++++++++++++ docs/make.bat | 35 +++++++++++++++++++++++++++ docs/source/conf.py | 55 +++++++++++++++++++++++++++++++++++++++++++ docs/source/index.rst | 20 ++++++++++++++++ requirements-dev.txt | 1 + 5 files changed, 131 insertions(+) create mode 100644 docs/Makefile create mode 100644 docs/make.bat create mode 100644 docs/source/conf.py create mode 100644 docs/source/index.rst diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..d0c3cbf --- /dev/null +++ b/docs/Makefile @@ -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) diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 0000000..9534b01 --- /dev/null +++ b/docs/make.bat @@ -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 diff --git a/docs/source/conf.py b/docs/source/conf.py new file mode 100644 index 0000000..ec7ed4e --- /dev/null +++ b/docs/source/conf.py @@ -0,0 +1,55 @@ +# 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' +copyright = '2020, Jef Roosens' +author = 'Jef Roosens' + +# 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 = [ +] + +# 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 = 'alabaster' + +# 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'] \ No newline at end of file diff --git a/docs/source/index.rst b/docs/source/index.rst new file mode 100644 index 0000000..08aa64f --- /dev/null +++ b/docs/source/index.rst @@ -0,0 +1,20 @@ +.. Frank documentation master file, created by + sphinx-quickstart on Thu Aug 27 17:28:57 2020. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Welcome to Frank's documentation! +================================= + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + + + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/requirements-dev.txt b/requirements-dev.txt index 3395ec6..abf950d 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -11,3 +11,4 @@ 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 From 9c1a56445b38cf13ab8f55a87a197aceeea2a2dd Mon Sep 17 00:00:00 2001 From: chewingbever Date: Thu, 27 Aug 2020 18:18:27 +0200 Subject: [PATCH 07/17] Docs now auto-generate from docstrings --- .gitignore | 4 +++- Makefile | 4 ++-- docs/source/conf.py | 9 +++++---- docs/source/index.rst | 5 ----- frank/modules/help.py | 2 +- 5 files changed, 11 insertions(+), 13 deletions(-) diff --git a/.gitignore b/.gitignore index a65b273..b4bfba1 100644 --- a/.gitignore +++ b/.gitignore @@ -8,8 +8,10 @@ build/ *.egg-info/ *.eggs/ -# Docs output +# Docs docs/build +docs/source/*.rst +!docs/source/index.rst # Caches __pycache__/ diff --git a/Makefile b/Makefile index 2f30a78..a7e6a75 100644 --- a/Makefile +++ b/Makefile @@ -47,8 +47,8 @@ clean-setup: @ find . -maxdepth 1 -type d -name '*.egg-info' -exec rm -rf "{}" \; # =====DOCS===== -docs: build-venv - @ "$(VENV)/bin/sphinx-apidoc" -o "$(DOCS)/source" "$(SRC)" +docs: build-venv docs/source/conf.py docs/source/index.rst + @ "$(VENV)/bin/sphinx-apidoc" -f -o "$(DOCS)/source" "$(SRC)" @ "$(VENV)/bin/sphinx-build" "$(DOCS)/source" "$(DOCS)/build" diff --git a/docs/source/conf.py b/docs/source/conf.py index ec7ed4e..386c386 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -10,9 +10,9 @@ # 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('.')) +import os +import sys +sys.path.insert(0, os.path.abspath('../..')) # -- Project information ----------------------------------------------------- @@ -31,6 +31,7 @@ release = '0.1' # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ + 'sphinx.ext.napoleon', ] # Add any paths that contain templates here, relative to this directory. @@ -52,4 +53,4 @@ html_theme = 'alabaster' # 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'] \ No newline at end of file +html_static_path = ['_static'] diff --git a/docs/source/index.rst b/docs/source/index.rst index 08aa64f..b2bd4a0 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -1,8 +1,3 @@ -.. Frank documentation master file, created by - sphinx-quickstart on Thu Aug 27 17:28:57 2020. - You can adapt this file completely to your liking, but it should at least - contain the root `toctree` directive. - Welcome to Frank's documentation! ================================= diff --git a/frank/modules/help.py b/frank/modules/help.py index b8a6c61..fa08f33 100644 --- a/frank/modules/help.py +++ b/frank/modules/help.py @@ -37,7 +37,7 @@ class HelpMod(Module): 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, channel: Messageable, mid: int): # Yes, this command just ignores cmd at the moment From 6f0af5dfdfe6fb2350e945caa88a135cf653b712 Mon Sep 17 00:00:00 2001 From: chewingbever Date: Thu, 27 Aug 2020 18:30:56 +0200 Subject: [PATCH 08/17] Switched to Read The Docs theme --- docs/source/conf.py | 3 ++- frank/module/decorators/classes.py | 2 +- requirements-dev.txt | 1 + 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 386c386..9a8dc9b 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -32,6 +32,7 @@ release = '0.1' # ones. extensions = [ 'sphinx.ext.napoleon', + 'sphinx_rtd_theme', ] # Add any paths that contain templates here, relative to this directory. @@ -48,7 +49,7 @@ exclude_patterns = [] # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # -html_theme = 'alabaster' +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, diff --git a/frank/module/decorators/classes.py b/frank/module/decorators/classes.py index 3e93be1..d14e398 100644 --- a/frank/module/decorators/classes.py +++ b/frank/module/decorators/classes.py @@ -87,7 +87,7 @@ class RegexCommand(Command): Args: prefix: string to match pattern against; Pattern must match entire - prefix + prefix """ return bool(re.fullmatch(self.cmd, prefix)) diff --git a/requirements-dev.txt b/requirements-dev.txt index abf950d..5059612 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -12,3 +12,4 @@ 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 From 75be358ff69f83fca740acb082c3dc8071b174ac Mon Sep 17 00:00:00 2001 From: chewingbever Date: Thu, 27 Aug 2020 18:40:54 +0200 Subject: [PATCH 09/17] apidoc now has its own directory --- .gitignore | 3 +-- Makefile | 2 +- docs/source/conf.py | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index b4bfba1..b20fe45 100644 --- a/.gitignore +++ b/.gitignore @@ -10,8 +10,7 @@ build/ # Docs docs/build -docs/source/*.rst -!docs/source/index.rst +docs/source/apidoc/ # Caches __pycache__/ diff --git a/Makefile b/Makefile index a7e6a75..423af24 100644 --- a/Makefile +++ b/Makefile @@ -48,7 +48,7 @@ clean-setup: # =====DOCS===== docs: build-venv docs/source/conf.py docs/source/index.rst - @ "$(VENV)/bin/sphinx-apidoc" -f -o "$(DOCS)/source" "$(SRC)" + @ "$(VENV)/bin/sphinx-apidoc" -f -o "$(DOCS)/source/apidoc" "$(SRC)" @ "$(VENV)/bin/sphinx-build" "$(DOCS)/source" "$(DOCS)/build" diff --git a/docs/source/conf.py b/docs/source/conf.py index 9a8dc9b..e31b07d 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -18,8 +18,8 @@ sys.path.insert(0, os.path.abspath('../..')) # -- Project information ----------------------------------------------------- project = 'Frank' -copyright = '2020, Jef Roosens' author = 'Jef Roosens' +copyright = f'2020, {author}' # noqa: A001,VNE003 # The full version, including alpha/beta/rc tags release = '0.1' From 3c7259ce08f55c9791caccdb4735e8db5d62953c Mon Sep 17 00:00:00 2001 From: chewingbever Date: Fri, 28 Aug 2020 09:38:04 +0200 Subject: [PATCH 10/17] Added install instructions --- docs/source/index.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/source/index.rst b/docs/source/index.rst index b2bd4a0..902759e 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -5,7 +5,12 @@ Welcome to Frank's documentation! :maxdepth: 2 :caption: Contents: +Installation +============ +You can install Frank using pip:: + + pip install frank-discord Indices and tables ================== From 70fc8dd5f491d64100afdc3c6a979b2b178b0c3a Mon Sep 17 00:00:00 2001 From: chewingbever Date: Fri, 28 Aug 2020 11:43:55 +0200 Subject: [PATCH 11/17] Remove tar sdist from setup.py --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 2f30a78..0f70aa5 100644 --- a/Makefile +++ b/Makefile @@ -62,7 +62,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 ] && { \ From 346f39f243a3ef39d276612f10e0f96c8d399dfc Mon Sep 17 00:00:00 2001 From: chewingbever Date: Fri, 28 Aug 2020 12:00:21 +0200 Subject: [PATCH 12/17] Tidied up Makefile --- Makefile | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 423af24..56d16ab 100644 --- a/Makefile +++ b/Makefile @@ -13,7 +13,7 @@ INTERPRETER=python3.8 all: run -# 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: @@ -46,8 +47,9 @@ clean-setup: @ [ ! -e "build" ] || rm -rf build @ find . -maxdepth 1 -type d -name '*.egg-info' -exec rm -rf "{}" \; + # =====DOCS===== -docs: build-venv docs/source/conf.py docs/source/index.rst +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" From 5855c931ca84088a5e7a6592d3143fc7ee05c69c Mon Sep 17 00:00:00 2001 From: chewingbever Date: Fri, 28 Aug 2020 13:15:08 +0200 Subject: [PATCH 13/17] Added pre-commit hook --- .githooks/pre-commit | 11 +++++++++++ Makefile | 15 +++++++++++++-- 2 files changed, 24 insertions(+), 2 deletions(-) create mode 100755 .githooks/pre-commit diff --git a/.githooks/pre-commit b/.githooks/pre-commit new file mode 100755 index 0000000..a4d593c --- /dev/null +++ b/.githooks/pre-commit @@ -0,0 +1,11 @@ +#!/bin/sh +# +# This hooks runs tests and checks if the linter doesn't hate you + +# Linting +printf "flake8....." +make lint > /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; } diff --git a/Makefile b/Makefile index 62bc4ff..e4b0b93 100644 --- a/Makefile +++ b/Makefile @@ -10,7 +10,7 @@ DOCS=docs # Interpreter to create venv with INTERPRETER=python3.8 -all: run +all: build-venv # =====VENV===== @@ -38,10 +38,13 @@ 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 @@ -49,14 +52,22 @@ clean-setup: # =====DOCS===== +# # 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 # =====PACKAGING===== From 6f2d9d1dde93e666a81bb723a57c2b3877f2d331 Mon Sep 17 00:00:00 2001 From: chewingbever Date: Mon, 31 Aug 2020 22:48:14 +0200 Subject: [PATCH 14/17] Updated daemon info in README --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 78e4fa7..bdb6600 100644 --- a/README.md +++ b/README.md @@ -53,11 +53,11 @@ 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 + # do some stuff + pass @frank.default() async def default_cmd(self, author, channel, mid): From 7a7bd6fed4f5ea54992c915e9b218ea34b7d1c85 Mon Sep 17 00:00:00 2001 From: chewingbever Date: Tue, 1 Sep 2020 09:17:09 +0200 Subject: [PATCH 15/17] Added black formatter to pre-commit hook --- .githooks/pre-commit | 4 ++-- Makefile | 5 ++++ frank/__init__.py | 31 ++++++++++++++---------- frank/frank.py | 28 +++++++++++++++------- frank/module/__init__.py | 26 ++++++++++++-------- frank/module/decorators/__init__.py | 16 ++++++------- frank/module/decorators/classes.py | 2 ++ frank/module/decorators/functions.py | 1 + frank/module/exceptions.py | 2 +- frank/module/meta.py | 14 +++++++---- frank/module/module.py | 29 +++++++++++++--------- frank/modules/__init__.py | 2 +- frank/modules/help.py | 36 +++++++++++++++++++--------- requirements-dev.txt | 1 + 14 files changed, 127 insertions(+), 70 deletions(-) diff --git a/.githooks/pre-commit b/.githooks/pre-commit index a4d593c..90f2230 100755 --- a/.githooks/pre-commit +++ b/.githooks/pre-commit @@ -3,8 +3,8 @@ # This hooks runs tests and checks if the linter doesn't hate you # Linting -printf "flake8....." -make lint > /dev/null 2>&1 && printf "OK!\n" || { printf "Fail." && exit 1; } +printf "black......" +make format > /dev/null 2>&1 && printf "OK!\n" || { printf "Fail." && exit 1; } # Running tests printf "pytest....." diff --git a/Makefile b/Makefile index e4b0b93..0431562 100644 --- a/Makefile +++ b/Makefile @@ -69,6 +69,11 @@ test: pytest.ini build-venv lint: build-venv @ "$(VENV)/bin/flake8" "$(SRC)"/**/*.py +# =====FORMATTING===== +# Run black +format: build-venv + @ "$(VENV)/bin/black" '$(SRC)' + # =====PACKAGING===== package: README.md LICENSE setup.py test clean-setup diff --git a/frank/__init__.py b/frank/__init__.py index 0468a36..e5cf103 100644 --- a/frank/__init__.py +++ b/frank/__init__.py @@ -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", ] diff --git a/frank/frank.py b/frank/frank.py index 2a8a01e..eb84d64 100644 --- a/frank/frank.py +++ b/frank/frank.py @@ -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): """ @@ -92,9 +97,14 @@ class Frank(discord.Client): if not (cmd and cmd[0] == self.PREFIX): return - module = next((mod for mod in self._loaded_modules - if mod.match(cmd[1])), None) + 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, + ) diff --git a/frank/module/__init__.py b/frank/module/__init__.py index 322a87d..459ef30 100644 --- a/frank/module/__init__.py +++ b/frank/module/__init__.py @@ -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", ] diff --git a/frank/module/decorators/__init__.py b/frank/module/decorators/__init__.py index 1c8ce8e..cc5e89d 100644 --- a/frank/module/decorators/__init__.py +++ b/frank/module/decorators/__init__.py @@ -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", ] diff --git a/frank/module/decorators/classes.py b/frank/module/decorators/classes.py index 97f31bd..f0fa326 100644 --- a/frank/module/decorators/classes.py +++ b/frank/module/decorators/classes.py @@ -8,6 +8,7 @@ import asyncio # Typing imports from typing import TYPE_CHECKING + if TYPE_CHECKING: # Built-in imports from typing import Union @@ -120,6 +121,7 @@ class Daemon(Simple): # 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? diff --git a/frank/module/decorators/functions.py b/frank/module/decorators/functions.py index 130bcd0..d60fcec 100644 --- a/frank/module/decorators/functions.py +++ b/frank/module/decorators/functions.py @@ -7,6 +7,7 @@ from .classes import Command, RegexCommand, Daemon, Default # Typing imports from typing import TYPE_CHECKING + if TYPE_CHECKING: # Built-in imports from typing import Union diff --git a/frank/module/exceptions.py b/frank/module/exceptions.py index 4502971..d7c541b 100644 --- a/frank/module/exceptions.py +++ b/frank/module/exceptions.py @@ -7,4 +7,4 @@ class DuplicateCommand(Exception): class MultipleDefaults(Exception): - message = 'Multiple default commands detected' + message = "Multiple default commands detected" diff --git a/frank/module/meta.py b/frank/module/meta.py index c488b7e..29f0977 100644 --- a/frank/module/meta.py +++ b/frank/module/meta.py @@ -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 + ) diff --git a/frank/module/module.py b/frank/module/module.py index 3c9dbf4..03eb053 100644 --- a/frank/module/module.py +++ b/frank/module/module.py @@ -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}') + raise InvalidCommand(f"Unknown command: {cmd}") # 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) + await func(cmd=cmd[1:], author=author, channel=channel, mid=mid) elif self.default: await self.default(author=author, channel=channel, mid=mid) diff --git a/frank/modules/__init__.py b/frank/modules/__init__.py index 801d111..f6055c9 100644 --- a/frank/modules/__init__.py +++ b/frank/modules/__init__.py @@ -2,5 +2,5 @@ from .help import HelpMod __all__ = [ - 'HelpMod', + "HelpMod", ] diff --git a/frank/modules/help.py b/frank/modules/help.py index fa08f33..76b6b83 100644 --- a/frank/modules/help.py +++ b/frank/modules/help.py @@ -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(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): + @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) diff --git a/requirements-dev.txt b/requirements-dev.txt index 5059612..30c0a62 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -13,3 +13,4 @@ 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 From 32ab8538a43ebb9f13d715cd439fa0db35fc9390 Mon Sep 17 00:00:00 2001 From: chewingbever Date: Fri, 4 Sep 2020 11:12:22 +0200 Subject: [PATCH 16/17] Unfinished changes, i gotta push --- .flake8 | 2 ++ frank/module/decorators/__init__.py | 4 +-- frank/module/decorators/classes.py | 42 ++++++++++++++++++++++------ frank/module/decorators/functions.py | 16 ++++++++--- frank/module/meta.py | 10 +++---- pyproject.toml | 16 +++++++++++ setup.py | 2 ++ 7 files changed, 73 insertions(+), 19 deletions(-) create mode 100644 .flake8 create mode 100644 pyproject.toml diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..dcc253a --- /dev/null +++ b/.flake8 @@ -0,0 +1,2 @@ +[flake8] +inline-quotes = double diff --git a/frank/module/decorators/__init__.py b/frank/module/decorators/__init__.py index cc5e89d..67c183b 100644 --- a/frank/module/decorators/__init__.py +++ b/frank/module/decorators/__init__.py @@ -1,9 +1,9 @@ -from .classes import Command, RegexCommand, Daemon, Default +from .classes import RegularCommand, RegexCommand, Daemon, Default from .functions import command, regex_command, daemon, default __all__ = [ "command", - "Command", + "RegularCommand", "regex_command", "RegexCommand", "default", diff --git a/frank/module/decorators/classes.py b/frank/module/decorators/classes.py index f0fa326..c35ce09 100644 --- a/frank/module/decorators/classes.py +++ b/frank/module/decorators/classes.py @@ -54,9 +54,9 @@ class Simple: return self -class Command(Simple): +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): @@ -72,18 +72,44 @@ class Command(Simple): self.cmd = cmd self.help_str = help_str - def match(self, prefix: str) -> bool: + def match(self, message: str) -> bool: """ - Returns wether the command matches the given prefix. + Returns wether the command matches the given message. Args: - prefix: string to match own prefix against + message: message to check """ - return self.cmd == prefix + return self.cmd == message.split(" ")[0] -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 + self.requires_prefix = requires_prefix + + def match(self, message: str) -> bool: + # This just makes it a bit easier to use in the function + module = self._obj + client = module._client + + words = [word for word in message.split(" ") if word] + + +class RegexCommand(SimpleCommand): """ A subclass of Command that can use a regex pattern instead of a fixed prefix. @@ -110,7 +136,7 @@ class Daemon(Simple): """ Args func: function to wrap - interval: time between calls of the function; if < 0, the function + 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 """ diff --git a/frank/module/decorators/functions.py b/frank/module/decorators/functions.py index d60fcec..60ef048 100644 --- a/frank/module/decorators/functions.py +++ b/frank/module/decorators/functions.py @@ -3,27 +3,35 @@ from __future__ import annotations # 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 + 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. Args: cmd: keyword used to call this function 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): - return Command(func, cmd, help_str) + return RegularCommand(func, cmd, help_str, aliases) return inner diff --git a/frank/module/meta.py b/frank/module/meta.py index 29f0977..4446092 100644 --- a/frank/module/meta.py +++ b/frank/module/meta.py @@ -6,7 +6,7 @@ from __future__ import annotations from functools import cached_property # Own imports -from .decorators import Command, Daemon, Default, RegexCommand +from .decorators import SimpleCommand, Daemon, Default, RegexCommand # Typing imports from typing import TYPE_CHECKING @@ -32,13 +32,13 @@ class ModuleMeta: return output @cached_property - def commands(self) -> List[Command]: + def commands(self) -> List[SimpleCommand]: # This also matches RegexCommand objects - # The sort puts all the RegexCommand objects at the back, making them - # be matched last + # 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)), + self._filter_attrs(lambda val: isinstance(val, SimpleCommand)), key=lambda x: isinstance(x, RegexCommand), ) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..f96e88f --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,16 @@ +[tool.black] +line-length = 80 +target-version = ['py38'] +include = '\.pyi?$' +exclude = ''' + +( + /( + \.eggs + | \.git + | venv + | build + | dist + )/ +) +''' diff --git a/setup.py b/setup.py index 9238b29..2cd43fa 100644 --- a/setup.py +++ b/setup.py @@ -2,6 +2,8 @@ # Third-party imports import setuptools +# TODO: switch to alternative that uses pyproject.toml (e.g. poetry) + with open('requirements.txt', 'r') as reqs_file: reqs = [line.strip() for line in reqs_file.readlines()] From 2a9323b5f58e94a85cc741fe3766f8d9630327f2 Mon Sep 17 00:00:00 2001 From: chewingbever Date: Tue, 15 Sep 2020 15:13:22 +0200 Subject: [PATCH 17/17] Unfinished changes --- frank/module/decorators/classes.py | 106 ++++++++++++++++++++++++++--- frank/module/meta.py | 3 +- frank/module/module.py | 4 +- pyproject.toml | 2 +- 4 files changed, 101 insertions(+), 14 deletions(-) diff --git a/frank/module/decorators/classes.py b/frank/module/decorators/classes.py index c35ce09..9d10a82 100644 --- a/frank/module/decorators/classes.py +++ b/frank/module/decorators/classes.py @@ -5,13 +5,14 @@ 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 + from typing import Union, List, Tuple class Simple: @@ -53,6 +54,14 @@ class Simple: return self + @property + def client(self): + """ + Returns the Frank client instance. + """ + + return self._obj._client + class SimpleCommand(Simple): """ @@ -72,15 +81,48 @@ class SimpleCommand(Simple): self.cmd = cmd self.help_str = help_str - def match(self, message: str) -> bool: + def match(self, message: str) -> Tuple[bool, List[str]]: """ - Returns wether the command matches the given message. + 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.cmd == message.split(" ")[0] + 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): @@ -99,14 +141,43 @@ class RegularCommand(SimpleCommand): super().__init__(self, func, cmd, help_str) self.alias = alias + + # This only matters for aliases self.requires_prefix = requires_prefix - def match(self, message: str) -> bool: - # This just makes it a bit easier to use in the function - module = self._obj - client = module._client + # TODO: make this return the right value + def match(self, message: str) -> Tuple[bool, List[str]]: + """ + Returns wether the message matches the current command. + """ - words = [word for word in message.split(" ") if word] + 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): @@ -115,7 +186,7 @@ class RegexCommand(SimpleCommand): 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. @@ -124,7 +195,20 @@ class RegexCommand(SimpleCommand): 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): diff --git a/frank/module/meta.py b/frank/module/meta.py index 4446092..c43f056 100644 --- a/frank/module/meta.py +++ b/frank/module/meta.py @@ -49,5 +49,6 @@ class ModuleMeta: @cached_property def default(self) -> Default: return next( - iter(self._filter_attrs(lambda val: isinstance(val, Default))), None + iter(self._filter_attrs(lambda val: isinstance(val, Default))), + None, ) diff --git a/frank/module/module.py b/frank/module/module.py index 03eb053..f666c25 100644 --- a/frank/module/module.py +++ b/frank/module/module.py @@ -118,7 +118,9 @@ class Module(ModuleMeta): ) else: - await func(cmd=cmd[1:], author=author, channel=channel, mid=mid) + await func( + cmd=cmd[1:], author=author, channel=channel, mid=mid + ) elif self.default: await self.default(author=author, channel=channel, mid=mid) diff --git a/pyproject.toml b/pyproject.toml index f96e88f..a7e0353 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [tool.black] -line-length = 80 +line-length = 79 target-version = ['py38'] include = '\.pyi?$' exclude = '''