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 * Jelmer Neeven * Renato Alves * Thomas Tuffin Past Contributors ------------------- +----------------- + * Victor Hu * tgly307 * GoTLiuM InSPiRiT 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 `_ 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 `_. -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 +`_. +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= -e MATTERMOST_PORT= -e BOT_TOKEN= docker.io/jneeven/mmpy_bot -You can also find an example `docker-compose.yml` file `here `_. +You can also find an example `docker-compose.yml` file `here +`_. 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 ` 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 ` 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 `_ 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 `_ 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 `_), 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 `_ 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 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: