Repository: attzonko/mmpy_bot (language: python)

Issue:
Implement PluginManager (3 of 3)
Reopened #212 targeting `main` instead of `master`.


Discussion / hints:

Fix patch:
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
index ec82c4ae..5ec38d0b 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.md
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -18,6 +18,6 @@ A clear and concise description of what you expected to happen.
  - Python Version: [e.g. 3.4, 3.8]
  - Mattermost Version: [e.g. 5.1.0]
  - mmpy_bot Version: [e.g. 1.2.1]
- 
+
 **Additional context**
 Add any other context about the problem here [e.g. Settings for your bot, API Version]
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index 2c77ec1b..1c16ab59 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -1,6 +1,6 @@
 name: Linting
 
-# Controls when the action will run. 
+# Controls when the action will run.
 on:
   # Triggers the workflow on push or pull request events but only for the main branch
   push:
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index 0cdbeafc..0fbbbb3b 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -10,7 +10,7 @@ on:
 jobs:
   build:
     runs-on: ubuntu-latest
-  
+
     steps:
       - uses: actions/checkout@v2
       - name: Set up Python 3.8
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
new file mode 100644
index 00000000..edc48283
--- /dev/null
+++ b/.pre-commit-config.yaml
@@ -0,0 +1,55 @@
+---
+# See https://pre-commit.com for more information
+# See https://pre-commit.com/hooks.html for more hooks
+repos:
+  - repo: https://github.com/pre-commit/pre-commit-hooks
+    rev: v3.4.0
+    hooks:
+      - id: end-of-file-fixer
+      - id: fix-byte-order-marker
+      - id: check-symlinks
+      - id: check-docstring-first
+      - id: check-added-large-files
+      - id: check-merge-conflict
+  - repo: https://github.com/pycqa/isort
+    # Formants, sorts and reorganizes imports
+    rev: 5.8.0
+    hooks:
+      - id: isort
+        args:
+          - "--profile"
+          - "black"
+          - "--filter-files"
+  - repo: https://github.com/psf/black
+    # Code style formatting
+    rev: 21.5b1
+    hooks:
+      - id: black
+  - repo: https://gitlab.com/pycqa/flake8
+    # Checks the code for PEP8 violations and common pitfals
+    rev: 3.9.0
+    hooks:
+      - id: flake8
+  - repo: https://github.com/mattseymour/pre-commit-pytype
+    rev: '2020.10.8'
+    hooks:
+      - id: pytype
+        args:
+          - "-j"
+          - "auto"
+          - "."
+  - repo: https://github.com/myint/docformatter
+    # Formats docstrings following PEP 257
+    rev: v1.3.1
+    hooks:
+      - id: docformatter
+        args:
+          - "--wrap-summaries"
+          - "88"
+          - "--wrap-descriptions"
+          - "88"
+  - repo: https://github.com/pycqa/doc8
+    # sphinx rst style checker
+    rev: 0.9.0a1
+    hooks:
+      - id: doc8
diff --git a/AUTHORS.rst b/AUTHORS.rst
index 27c90a53..e1193f87 100644
--- a/AUTHORS.rst
+++ b/AUTHORS.rst
@@ -3,14 +3,16 @@ Credits
 =======
 
 Active Contributors
-------------
+-------------------
+
 * Alex Tzonkov <alex.tzonkov@gmail.com>
 * Jelmer Neeven <jelmer@neeven.tech>
 * Renato Alves <alves.rjc@gmail.com>
 * Thomas Tuffin <ttuffin@redhat.com>
 
 Past Contributors
-------------------
+-----------------
+
 * Victor Hu <selain@nature.ee.ncku.edu.tw>
 * tgly307 <tgly307@163.com>
 * GoTLiuM InSPiRiT <gotlium@gmail.com>
diff --git a/README.md b/README.md
index 1b74ae4e..4e09e586 100644
--- a/README.md
+++ b/README.md
@@ -25,7 +25,7 @@ For Python 2 and Python3 < 3.8 support, please use versions v1.3.9 or lower.
 ##### Additional features added in v2.x:
 - Multi-threading and asyncio execution
 - Integrated webhook server
-- Support for click functions 
+- Support for click functions
 - Job scheduling
 
 ## Compatibility
diff --git a/docs/contributing.rst b/docs/contributing.rst
index 1c3e8b36..fef756e8 100644
--- a/docs/contributing.rst
+++ b/docs/contributing.rst
@@ -35,26 +35,32 @@ We recommend using `venv <https://docs.python.org/3.8/library/venv.html>`_ to ke
 Testing
 =======
 
-mmpy_bot develops all tests based on pytest. If you need to add your own tests and run tests, please install the dev requirements.
+mmpy_bot develops all tests based on pytest. If you need to add your own
+tests and run tests, please install the dev requirements.
 
     .. code-block:: bash
 
         $ pip install -r dev-requirements.txt
 
 All the tests are put in `mmpy_bot\tests`.
-There are two test packages: :code:`unit_tests` and :code:`integration_tests`.
+There are two test packages: :code:`unit_tests` and
+:code:`integration_tests`.
 
-Tests which can be performed by a single bot without requiring a server or interaction with other bots should be kept in the :code:`unit_tests` package.
-Tests that require interactions between bots on a mattermost server belong to the :code:`integration_tests` package.
+Tests which can be performed by a single bot without requiring a server or
+interaction with other bots should be kept in the :code:`unit_tests` package.
+Tests that require interactions between bots on a mattermost server belong to
+the :code:`integration_tests` package.
 
 
 Adding unit tests
 -----------------
 
-There are multiple test modules inside unit_tests package, one for each module in the code.
-The naming convention of these modules is *modulename_test*.
-Inside each module, there will be several test functions with naming convention *test_methodname*, grouped into classes for each corresponding class in the code.
-If you need to add more unit tests, please consider following these conventions.
+There are multiple test modules inside unit_tests package, one for each
+module in the code. The naming convention of these modules is
+*modulename_test*. Inside each module, there will be several test functions
+with naming convention *test_methodname*, grouped into classes for each
+corresponding class in the code. If you need to add more unit tests, please
+consider following these conventions.
 
 
 Running the unit tests
@@ -70,13 +76,18 @@ To run the unit tests (in parallel), simply execute:
 Adding integration tests
 ------------------------
 
-The integration tests are run on the `jneeven:mattermost-bot-test` docker image, for which dockerfiles are provided in the `tests/intergration_tests` folder.
-The tests are defined as interactions between a bot (the responder) and a driver (the one sending test messages), which live inside the docker image.
-Their respective tokens are available in `tests/integration_tests/utils.py`, and the two bots are available as pytest fixtures so they can be easily re-used.
-Note that while the bot is also a fixture, it should not be used in any functions.
-It will simply be started whenever the integration tests are executed.
+The integration tests are run on the `jneeven:mattermost-bot-test` docker
+image, for which dockerfiles are provided in the `tests/intergration_tests`
+folder. The tests are defined as interactions between a bot (the responder)
+and a driver (the one sending test messages), which live inside the docker
+image. Their respective tokens are available in
+`tests/integration_tests/utils.py`, and the two bots are available as pytest
+fixtures so they can be easily re-used. Note that while the bot is also a
+fixture, it should not be used in any functions. It will simply be started
+whenever the integration tests are executed.
 
-An integration test might look like this (also have a look at the actual code in `tests/integration_tests/test_example_plugin.py`):
+An integration test might look like this (also have a look at the actual code
+in `tests/integration_tests/test_example_plugin.py`):
 
     .. code-block:: python
 
@@ -94,17 +105,23 @@ An integration test might look like this (also have a look at the actual code in
             # Checks whether the bot has sent us the expected reply
             assert expect_reply(driver, post)["message"] == "Bring it on!"
 
-In this test, the driver sends a message in the "off-topic" channel, and waits for the bot to reply 'Bring it on!'.
-If no reply occurs within a default response timeout (15 seconds by default, but this can be passed as an argument to `expect_reply`), an exception will be raised.
-The driver fixture is imported from the utils and can be re-used in every test function simply by adding it as a function argument.
+In this test, the driver sends a message in the "off-topic" channel, and
+waits for the bot to reply 'Bring it on!'. If no reply occurs within a
+default response timeout (15 seconds by default, but this can be passed as an
+argument to `expect_reply`), an exception will be raised. The driver fixture
+is imported from the utils and can be re-used in every test function simply
+by adding it as a function argument.
 
 
 
 Running the integration_tests
 -----------------------------
 
-Running the integration_tests is easy: simply `cd` into `tests/integration_tests`, and run `docker-compose up -d` to start a local mattermost server.
-Then run `pytest -n auto .` to start the tests! For more info about the integration tests an the docker server, have a look at `tests/integration_tests/README.md`.
+Running the integration_tests is easy: simply `cd` into
+`tests/integration_tests`, and run `docker-compose up -d` to start a local
+mattermost server. Then run `pytest -n auto .` to start the tests! For more
+info about the integration tests an the docker server, have a look at
+`tests/integration_tests/README.md`.
 
 Test coverage:
 --------------
@@ -123,8 +140,9 @@ Set necessary configuration as described above, and run:
 
         $ py.test --cov=mmpy_bot tests\
 
-It automatically runs tests and measures code coverage of modules under mmpy_bot root dir.
-Using "--cov-report" parameter to write report into "cov_html" folder by html format.
+It automatically runs tests and measures code coverage of modules under
+mmpy_bot root dir. Using "--cov-report" parameter to write report into
+"cov_html" folder by html format.
 
     .. code-block:: bash
 
diff --git a/docs/getting_started.rst b/docs/getting_started.rst
index 7ff930df..fce58fad 100644
--- a/docs/getting_started.rst
+++ b/docs/getting_started.rst
@@ -1,7 +1,7 @@
 .. _getting-started:
 
 Getting Started
-=================
+===============
 
 Compatibility
 -------------
@@ -36,7 +36,9 @@ Git Repo
 
 Running the bot
 ---------------
-We recommend creating an `entrypoint` file for executing the bot, which will look something like this:
+
+We recommend creating an `entrypoint` file for executing the bot, which will
+look something like this:
 
     .. code-block:: python
 
@@ -62,8 +64,11 @@ For more information on configuring bot settings and plugins, please see `settin
 
 Container
 #########
-A container image is available at `jneeven/mmpy_bot <https://hub.docker.com/r/jneeven/mmpy_bot>`_.
-Using your preferred container management software (Docker/Podman), you can pull the image and run it using the following steps:
+
+A container image is available at `jneeven/mmpy_bot
+<https://hub.docker.com/r/jneeven/mmpy_bot>`_.
+Using your preferred container management software (Docker/Podman), you can
+pull the image and run it using the following steps:
 
 #. Pull the image from the Docker repository:
 
@@ -77,15 +82,21 @@ Using your preferred container management software (Docker/Podman), you can pull
 
         $ podman run -d --name=mmpy_bot --network=host -e MATTERMOST_URL=<mattermost_server_url> -e MATTERMOST_PORT=<mattermost_server_port> -e BOT_TOKEN=<bot_token> docker.io/jneeven/mmpy_bot
 
-You can also find an example `docker-compose.yml` file `here <https://github.com/attzonko/mmpy_bot/blob/main/docker-compose.yml>`_.
+You can also find an example `docker-compose.yml` file `here
+<https://github.com/attzonko/mmpy_bot/blob/master/docker-compose.yml>`_.
 
 Customizing your bot
 ####################
-Getting your bot running is only the beginning. The real fun begins with writing plugins to get it functioning exactly how you want it! Head on over to the :ref:`plugins <plugins>` page to get started.
+
+Getting your bot running is only the beginning. The real fun begins with
+writing plugins to get it functioning exactly how you want it! Head on over
+to the :ref:`plugins <plugins>` page to get started.
 
 Fetch mmpy_bot version
-####################
-To check your installed version of `mmpy_bot`, simply open a Python interpreter and run the following commands:
+######################
+
+To check your installed version of `mmpy_bot`, simply open a Python
+interpreter and run the following commands:
 
     .. code-block:: python
 
diff --git a/docs/plugins.rst b/docs/plugins.rst
index 0787fb12..3dd48c06 100644
--- a/docs/plugins.rst
+++ b/docs/plugins.rst
@@ -1,12 +1,15 @@
 Plugins
 =======
 
-A chat bot is meaningless unless you can extend/customize it to fit your own use cases, which can be achieved through custom plugins.
+A chat bot is meaningless unless you can extend/customize it to fit your own
+use cases, which can be achieved through custom plugins.
 
 Writing your first plugin
 -------------------------
 
-#. To demonstrate how easy it is to create a plugin for `mmpy_bot`, let's write a basic plugin and run it. Start with an empty Python file and import these three `mmpy_bot` modules:
+#. To demonstrate how easy it is to create a plugin for `mmpy_bot`, let's
+   write a basic plugin and run it. Start with an empty Python file and
+   import these three `mmpy_bot` modules:
 
     .. code-block:: python
 
@@ -19,7 +22,9 @@ Writing your first plugin
 
         class MyPlugin(Plugin):
 
-#. Now we can write the methods that control how the bot will respond to certain messages. Let's start with a basic one that will simply trigger a response from the bot:
+#. Now we can write the methods that control how the bot will respond to
+   certain messages. Let's start with a basic one that will simply trigger a
+   response from the bot:
 
     .. code-block:: python
 
@@ -27,9 +32,10 @@ Writing your first plugin
         async def wake_up(self, message: Message):
             self.driver.reply_to(message, "I'm awake!")
 
-    In the above code block, the `@listen_to` decorator tells the bot to listen on any channel for the string "wake up", and respond with "I'm awake!".
+    In the above code block, the ``@listen_to`` decorator tells the bot to listen on any channel for the string "wake up", and respond with "I'm awake!".
 
-#. Save your plugin file and open a fresh Python file which will be the entrypoint to start the bot and include your plugin:
+#. Save your plugin file and open a fresh Python file which will be the
+   entrypoint to start the bot and include your plugin:
 
     .. code-block:: python
 
@@ -58,7 +64,8 @@ Writing your first plugin
 
         $ ./my_bot.py
 
-If everything went as planned you can now start your bot, send the message "wake up" and expect the appropriate reply.
+If everything went as planned you can now start your bot, send the message
+"wake up" and expect the appropriate reply.
 
 Further configuration
 ---------------------
@@ -83,10 +90,13 @@ Implementing regular expression
 
 
 Only accept messages that mention the bot
-----------------------------------
+-----------------------------------------
+
+If you want the bot to only respond to messages containing a mention (e.g.
+"hey @bot_name !"), you can use the `needs_mention` flag. Note that this will
+also trigger if you send the bot a direct message without mentioning its
+name!
 
-If you want the bot to only respond to messages containing a mention (e.g. "hey @bot_name !"), you can use the `needs_mention` flag.
-Note that this will also trigger if you send the bot a direct message without mentioning its name!
     .. code-block:: python
 
         @listen_to("hey", needs_mention=True)
@@ -95,9 +105,10 @@ Note that this will also trigger if you send the bot a direct message without me
 
 
 Only accept direct messages
-----------------------------------
+---------------------------
 
-Using `direct_only=True`, the bot will only respond if you send it a direct message.
+Using `direct_only=True`, the bot will only respond if you send it a direct
+message.
 
     .. code-block:: python
 
@@ -107,7 +118,7 @@ Using `direct_only=True`, the bot will only respond if you send it a direct mess
 
 
 Restrict messages to specific users
-----------------------------------
+-----------------------------------
 
     .. code-block:: python
 
@@ -129,32 +140,33 @@ Restrict messages to specific channels
 
 Click support
 -------------
-    `mmpy_bot` now supports `click <https://click.palletsprojects.com/en/7.x/>`_ commands, so you can build a robust CLI-like experience if you need it.
-    The example below registers a `hello_click` command that takes a positional argument, a keyword argument and a toggleable flag, which are automatically converted to the correct type.
-    For example, it can be called with `hello_click my_argument --keyword-arg=3 -f` and will parse the arguments accordingly.
-    A nice benefit of `click` commands is that they also automatically generate nicely formatted help strings.
-    Try sending "help" to the `ExamplePlugin` to see what it looks like!
 
-    .. code-block:: python
+`mmpy_bot` now supports `click <https://click.palletsprojects.com/en/7.x/>`_ commands, so you can build a robust CLI-like experience if you need it.
+The example below registers a `hello_click` command that takes a positional argument, a keyword argument and a toggleable flag, which are automatically converted to the correct type.
+For example, it can be called with `hello_click my_argument --keyword-arg=3 -f` and will parse the arguments accordingly.
+A nice benefit of `click` commands is that they also automatically generate nicely formatted help strings.
+Try sending "help" to the `ExamplePlugin` to see what it looks like!
 
-        @listen_to("hello_click", needs_mention=True)
-        @click.command(help="An example click command with various arguments.")
-        @click.argument("POSITIONAL_ARG", type=str)
-        @click.option("--keyword-arg", type=float, default=5.0, help="A keyword arg.")
-        @click.option("-f", "--flag", is_flag=True, help="Can be toggled.")
-        def hello_click(
-            self, message: Message, positional_arg: str, keyword_arg: float, flag: bool
-        ):
-            response = (
-                "Received the following arguments:\n"
-                f"- positional_arg: {positional_arg}\n"
-                f"- keyword_arg: {keyword_arg}\n"
-                f"- flag: {flag}\n"
-            )
-            self.driver.reply_to(message, response)
+.. code-block:: python
+
+    @listen_to("hello_click", needs_mention=True)
+    @click.command(help="An example click command with various arguments.")
+    @click.argument("POSITIONAL_ARG", type=str)
+    @click.option("--keyword-arg", type=float, default=5.0, help="A keyword arg.")
+    @click.option("-f", "--flag", is_flag=True, help="Can be toggled.")
+    def hello_click(
+        self, message: Message, positional_arg: str, keyword_arg: float, flag: bool
+    ):
+        response = (
+            "Received the following arguments:\n"
+            f"- positional_arg: {positional_arg}\n"
+            f"- keyword_arg: {keyword_arg}\n"
+            f"- flag: {flag}\n"
+        )
+        self.driver.reply_to(message, response)
 
 File upload
-------------------
+-----------
 
 .. code-block:: python
 
@@ -168,8 +180,10 @@ File upload
 
 Plugin startup and shutdown
 ---------------------------
-The `Plugin` class comes with an `on_start` and `on_stop` function, which will be called when the bot starts up or shuts down.
-They can be used as follows:
+
+The `Plugin` class comes with an `on_start` and `on_stop` function, which
+will be called when the bot starts up or shuts down. They can be used as
+follows:
 
 .. code-block:: python
 
@@ -183,7 +197,8 @@ They can be used as follows:
 
 
 Webhook listener
----------------------
+----------------
+
 If you want to interact with your bot not only through chat messages but also through web requests (for example to implement an `interactive dialog <https://docs.mattermost.com/developer/interactive-dialogs.html>`_), you can use enable the built-in `WebHookServer`.
 In your `Settings`, make sure to set `WEBHOOK_HOST_ENABLED=True` and provide a value for `WEBHOOK_HOST_URL` and `WEBHOOK_HOST_PORT` (see `settings.py <https://github.com/attzonko/mmpy_bot/blob/main/mmpy_bot/settings.py>`_ for more info).
 Then, on your custom `Plugin` you can create a function like this:
@@ -201,7 +216,8 @@ Then, on your custom `Plugin` you can create a function like this:
             event.body["channel_id"], f"Webhook {event.webhook_id} triggered!"
         )
 
-And if you want to send a web response back to the incoming HTTP POST request, you can use `Driver.respond_to_web`:
+And if you want to send a web response back to the incoming HTTP POST
+request, you can use `Driver.respond_to_web`:
 
 .. code-block:: python
 
diff --git a/pyproject.toml b/pyproject.toml
index 895f4d68..228ff322 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -18,4 +18,3 @@ classifiers = [
 'Topic :: Internet :: WWW/HTTP',
 'Topic :: Software Development :: Libraries :: Python Modules',
 ]
-
diff --git a/requirements.txt b/requirements.txt
index 4665969f..bc335381 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -2,4 +2,4 @@ aiohttp>=3.7.4.post0
 click>=7.0
 mattermostdriver>=7.3.0
 schedule>=0.6.0
-Sphinx>=1.3.3
\ No newline at end of file
+Sphinx>=1.3.3


Test patch:
diff --git a/tests/unit_tests/snapshots/snap_plugins_test.py b/tests/unit_tests/snapshots/snap_plugins_test.py
index a925d2cc..faa0502a 100644
--- a/tests/unit_tests/snapshots/snap_plugins_test.py
+++ b/tests/unit_tests/snapshots/snap_plugins_test.py
@@ -4,7 +4,6 @@
 
 from snapshottest import Snapshot
 
-
 snapshots = Snapshot()
 
 snapshots['TestPlugin.test_help_string 1'] = '''Plugin FakePlugin has the following functions: