From 791a08b4f86cb27b82c097bbfe68d29dcc47f5a6 Mon Sep 17 00:00:00 2001 From: Sijis Aviles Date: Wed, 13 Jul 2022 22:41:50 -0500 Subject: [PATCH] Feature: plugins detection using entrypoints (#1590) * feat: add entrypoint plugin lookup * feat: add entrypoint plugins * feat: add entrypoint plugin for backends * fix: ensure cli uses entrypoints to list backend plugins * refactor: use importlib instead of pkg_resources * fix: ensure plugin lookup works for py37 * test: basic entrypoint check * docs: refactor installing plugins section * docs: add info on using entrypoints * docs: add info to CHANGES --- CHANGES.rst | 4 ++ docs/user_guide/administration.rst | 51 +++++++++++++++---- docs/user_guide/plugin_development/basics.rst | 27 ++++++++++ errbot/backend_plugin_manager.py | 5 +- errbot/cli.py | 4 +- errbot/plugin_manager.py | 5 +- errbot/utils.py | 13 +++++ setup.py | 3 ++ tests/plugin_management_test.py | 7 ++- 9 files changed, 102 insertions(+), 17 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index b6e362c7f..17a4b688b 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,6 +1,10 @@ v9.9.9 (unreleased) ------------------- +features: + +- core/plugins: detect plugins using entrypoints (#1590) + fixes: - docs: add unreleased section (#1576) diff --git a/docs/user_guide/administration.rst b/docs/user_guide/administration.rst index 94d5b3a34..0ec0520ba 100644 --- a/docs/user_guide/administration.rst +++ b/docs/user_guide/administration.rst @@ -40,8 +40,20 @@ If you just wish to know more about a specific command you can issue:: Installing plugins ------------------ -Errbot plugins are typically published to and installed from `GitHub `_. -We periodically crawl GitHub for errbot plugin repositories and `publish the results `_ for people to browse. +Errbot plugins can be installed via these methods + +* `!repos install` bot commnand +* Cloning a `GitHub `_ repository +* Extracting a tar/zip file +* Using pip + + +Using a bot command +^^^^^^^^^^^^^^^^^^^ + +Plugins installed via the :code:`!repos` command are managed by errbot itself and stored inside the `BOT_DATA_DIR` you set in `config.py`. + +We periodically crawl GitHub for errbot plugin repositories and `publish the results `_ for people to browse. You can have your bot display the same list of repos by issuing:: @@ -72,6 +84,32 @@ This can be done with:: !repos update all +Cloning a repository or tar/zip install +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Using a git repository or tar/zip file to install plugins is setting up your plugins to be managed manually. + +Plugins installed from cloning a repository need to be placed inside the `BOT_EXTRA_PLUGIN_DIR` path specified in the `config.py` file. + +Assuming `BOT_EXTRA_PLUGIN_DIR` is set to `/opt/plugins`:: + + $ git clone https://github.com/errbotio/err-helloworld /opt/plugins/err-helloworld + $ tar -zxvf err-helloworld.tar.gz -C /opt/plugins/ + +.. note:: + If a repo is cloned and the git remote information is present, updating the plugin may be possible via `!repos update` + + +Using pip for plugins +^^^^^^^^^^^^^^^^^^^^^ + +Plugins published to to pypi.org can be installed using pip.:: + + $ pip install errbot-plugin-helloworld + +As part of the packaging configuration for the plugin, it should install all necessary dependencies for the plugin to work. + + Dependencies ^^^^^^^^^^^^ @@ -83,15 +121,6 @@ If you have installed Errbot in a virtualenv, this will run the equivalent of :c If no virtualenv is detected, the equivalent of :code:`pip install --user -r requirements.txt` is used to ensure the package(s) is/are only installed for the user running Err. -Extra plugin directory -^^^^^^^^^^^^^^^^^^^^^^ - -Plugins installed via the :code:`!repos` command are managed by errbot itself and stored inside the `BOT_DATA_DIR` you set in `config.py`. -If you want to manage your plugins manually for any reason then errbot allows you to load additional plugins from a directory you specify. -You can do so by specifying the setting `BOT_EXTRA_PLUGIN_DIR` in your `config.py` file. -See the :download:`config-template.py` file for more details. - - .. _disabling_plugins: Disabling plugins diff --git a/docs/user_guide/plugin_development/basics.rst b/docs/user_guide/plugin_development/basics.rst index 697b01476..021e8609e 100644 --- a/docs/user_guide/plugin_development/basics.rst +++ b/docs/user_guide/plugin_development/basics.rst @@ -197,6 +197,33 @@ the function `invert_string()`, the `helloworld` plugin can import it and use it """Say hello to the world""" return invert_string("Hello, world!") +Packaging +--------- + +A plugin can be packaged and distributed through pypi.org. The errbot plugin system uses entrypoints in setuptools to find available plugins. + +The two entrypoint avialable are + +* `errbot.plugins` - normal plugin and flows +* `errbot.backend_plugins` - backend plugins for collaboration providers + +To get this setup, add this block of code to `setup.py`. + +.. code-block:: python + + entry_points = { + "errbot.plugins": [ + "helloworld = helloWorld:HelloWorld", + ] + } + +Optionally, you may need to include a `MANIFEST.in` to include files of the repo + +.. code-block:: python + + include *.py *.plug + + Wrapping up ----------- diff --git a/errbot/backend_plugin_manager.py b/errbot/backend_plugin_manager.py index cf967d2dd..5a3a0bba1 100644 --- a/errbot/backend_plugin_manager.py +++ b/errbot/backend_plugin_manager.py @@ -5,7 +5,7 @@ from errbot.plugin_info import PluginInfo -from .utils import collect_roots +from .utils import collect_roots, entry_point_plugins log = logging.getLogger(__name__) @@ -44,7 +44,8 @@ def __init__( self._base_class = base_class self.plugin_info = None - all_plugins_paths = collect_roots((base_search_dir, extra_search_dirs)) + ep = entry_point_plugins(group="errbot.backend_plugins") + all_plugins_paths = collect_roots((base_search_dir, extra_search_dirs, ep)) for potential_plugin in enumerate_backend_plugins(all_plugins_paths): if potential_plugin.name == plugin_name: diff --git a/errbot/cli.py b/errbot/cli.py index 75c5fa8aa..b0f8585ae 100755 --- a/errbot/cli.py +++ b/errbot/cli.py @@ -27,7 +27,7 @@ from errbot.bootstrap import CORE_BACKENDS from errbot.logs import root_logger from errbot.plugin_wizard import new_plugin_wizard -from errbot.utils import collect_roots +from errbot.utils import collect_roots, entry_point_plugins from errbot.version import VERSION log = logging.getLogger(__name__) @@ -281,6 +281,8 @@ def main() -> None: extra_backend = getattr(config, "BOT_EXTRA_BACKEND_DIR", []) if isinstance(extra_backend, str): extra_backend = [extra_backend] + ep = entry_point_plugins(group="errbot.backend_plugins") + extra_backend.extend(ep) if args["list"]: from errbot.backend_plugin_manager import enumerate_backend_plugins diff --git a/errbot/plugin_manager.py b/errbot/plugin_manager.py index 7c6618fe4..49b02c39b 100644 --- a/errbot/plugin_manager.py +++ b/errbot/plugin_manager.py @@ -19,7 +19,7 @@ from .plugin_info import PluginInfo from .storage import StoreMixin from .templating import add_plugin_templates_path, remove_plugin_templates_path -from .utils import collect_roots, version2tuple +from .utils import collect_roots, entry_point_plugins, version2tuple from .version import VERSION PluginInstanceCallback = Callable[[str, Type[BotPlugin]], BotPlugin] @@ -334,7 +334,8 @@ def update_plugin_places(self, path_list: str) -> Dict[Path, str]: :param path_list: the path list where to search for plugins. :return: the feedback for any specific path in case of error. """ - repo_roots = (CORE_PLUGINS, self._extra_plugin_dir, path_list) + ep = entry_point_plugins(group="errbot.plugins") + repo_roots = (CORE_PLUGINS, self._extra_plugin_dir, path_list, ep) all_roots = collect_roots(repo_roots) diff --git a/errbot/utils.py b/errbot/utils.py index f9c0e87f0..fd7f2694b 100644 --- a/errbot/utils.py +++ b/errbot/utils.py @@ -7,6 +7,12 @@ import sys import time from functools import wraps + +try: + from importlib.metadata import entry_points +except ImportError: + from importlib_metadata import entry_points + from platform import system from typing import List, Tuple, Union @@ -196,6 +202,13 @@ def collect_roots(base_paths: List, file_sig: str = "*.plug") -> List: return list(collections.OrderedDict.fromkeys(result)) +def entry_point_plugins(group): + paths = [] + for entry_point in entry_points().get(group, []): + paths.append(entry_point.dist._path.parent) + return paths + + def global_restart() -> None: """Restart the current process.""" python = sys.executable diff --git a/setup.py b/setup.py index d63e089f4..071f15631 100755 --- a/setup.py +++ b/setup.py @@ -42,6 +42,9 @@ "deepmerge==1.0.1", ] +if py_version < (3, 8): + deps.append("importlib-metadata==4.12.0") + if py_version < (3, 9): deps.append("graphlib-backport==1.0.3") diff --git a/tests/plugin_management_test.py b/tests/plugin_management_test.py index 59f878121..c82f6df24 100644 --- a/tests/plugin_management_test.py +++ b/tests/plugin_management_test.py @@ -9,7 +9,7 @@ from errbot import plugin_manager from errbot.plugin_info import PluginInfo from errbot.plugin_manager import IncompatiblePluginException -from errbot.utils import collect_roots, find_roots +from errbot.utils import collect_roots, entry_point_plugins, find_roots CORE_PLUGINS = plugin_manager.CORE_PLUGINS @@ -143,3 +143,8 @@ def test_errbot_version_check(): plugin_manager.check_errbot_version(pi) finally: plugin_manager.VERSION = real_version + + +def test_entry_point_plugin(): + no_plugins_found = entry_point_plugins("errbot.no_plugins") + assert [] == no_plugins_found