diff --git a/docs/getting_started.rst b/docs/getting_started.rst index f6e892c8..5e9f33d1 100644 --- a/docs/getting_started.rst +++ b/docs/getting_started.rst @@ -64,7 +64,10 @@ Most configuration options reside in their relevant plugin, however there are th Overrides the list of default plugins. ``disable_plugins = []`` - List of any plugins to explicitly disable. + List of any plugins to explicitly disable. This takes priority over ``enable_plugins``. + +``enable_plugins = []`` + List of any plugins to explicitly enable. .. _library_path config option: diff --git a/moe/config.py b/moe/config.py index 43db51b8..5d05fb78 100644 --- a/moe/config.py +++ b/moe/config.py @@ -14,6 +14,7 @@ import logging import os import re +import sys from pathlib import Path from types import ModuleType from typing import NamedTuple, Optional, Union, cast @@ -182,6 +183,7 @@ def add_config_validator(settings: dynaconf.base.LazySettings): validators = [ dynaconf.Validator("DEFAULT_PLUGINS", default=DEFAULT_PLUGINS), dynaconf.Validator("DISABLE_PLUGINS", default=set()), + dynaconf.Validator("ENABLE_PLUGINS", default=set()), dynaconf.Validator("LIBRARY_PATH", default="~/Music"), dynaconf.Validator("ORIGINAL_DATE", default=False), ] @@ -360,9 +362,9 @@ def _setup_plugins(self, core_plugins: dict[str, str] = CORE_PLUGINS): self.pm.hook.add_config_validator(settings=self.settings) self._validate_settings() - config_plugins = set(self.settings.default_plugins) - set( - self.settings.disable_plugins - ) + config_plugins = ( + set(self.settings.default_plugins) | set(self.settings.enable_plugins) + ) - set(self.settings.disable_plugins) # the 'import' plugin maps to the 'moe_import' package if "import" in config_plugins: @@ -373,7 +375,13 @@ def _setup_plugins(self, core_plugins: dict[str, str] = CORE_PLUGINS): self.pm.register(importlib.import_module("moe.cli"), name="cli") # register plugin hookimpls for all enabled plugins - self._register_internal_plugins(config_plugins) + internal_plugin_dir = Path(__file__).resolve().parent / "plugins" + self._register_local_plugins( + config_plugins, internal_plugin_dir, "moe.plugins." + ) + if Path(self.config_dir / "plugins").exists(): + sys.path.append(str(self.config_dir / "plugins")) + self._register_local_plugins(config_plugins, self.config_dir / "plugins") # register plugin hookimpls for all extra plugins for extra_plugin in self._extra_plugins: @@ -387,13 +395,20 @@ def _setup_plugins(self, core_plugins: dict[str, str] = CORE_PLUGINS): log.debug(f"Registered plugins. [plugins={self.pm.list_name_plugin()}]") - def _register_internal_plugins(self, enabled_plugins): - """Registers all internal plugins in `enabled_plugins`.""" - plugin_dir = Path(__file__).resolve().parent / "plugins" + def _register_local_plugins( + self, enabled_plugins: set[str], plugin_dir: Path, pkg_name: str = "" + ): + """Registers all internal plugins in `enabled_plugins`. + Args: + enabled_plugins: All enabled plugins as specified by the config. + plugin_dir: Directory of plugins to register. + pkg_name: Optional common package name the plugins belong to. + Include the trailing '.'. + """ for plugin_path in plugin_dir.iterdir(): plugin_name = plugin_path.stem if plugin_path.stem in enabled_plugins: - plugin = importlib.import_module("moe.plugins." + plugin_name) + plugin = importlib.import_module(pkg_name + plugin_name) self.pm.register(plugin, plugin_name) diff --git a/tests/conftest.py b/tests/conftest.py index e4a86382..9ab9a996 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -76,6 +76,7 @@ def tmp_config( tmp_db: Whether or not to use a temporary (in-memory) database. If ``True``, the database will be initialized regardless of ``init_db``. extra_plugins: Any additional plugins to enable. + config_dir: Optionally specifiy a config directory to use. Yields: The configuration instance. @@ -86,8 +87,11 @@ def _tmp_config( init_db: bool = False, tmp_db: bool = False, extra_plugins: Optional[list[ExtraPlugin]] = None, + config_dir: Optional[Path] = None, ) -> Config: - config_dir = tmp_path_factory.mktemp("config") + config_dir = config_dir or tmp_path_factory.mktemp("config") + assert config_dir + if "library_path" not in settings: settings += f"\nlibrary_path = '{LIBRARY_PATH.resolve()}'" diff --git a/tests/test_config.py b/tests/test_config.py index 9d98011c..05240576 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -1,5 +1,6 @@ """Tests configuration.""" +import shutil from pathlib import Path from unittest.mock import patch @@ -71,12 +72,34 @@ def test_config_plugins(self, tmp_config): for plugin in plugins: assert config.CONFIG.pm.has_plugin(plugin) + def test_enable_plugins(self, tmp_config): + """We can explictly enable plugins.""" + tmp_config( + settings="""default_plugins = ["cli"] + enable_plugins = ["list"]""" + ) + + assert config.CONFIG.pm.has_plugin("list") + def test_extra_plugins(self, tmp_config): """Any given additional plugins are also registered.""" tmp_config(extra_plugins=[ExtraPlugin(TestPlugins, "config_plugin")]) assert config.CONFIG.pm.has_plugin("config_plugin") + def test_register_local_user_plugins(self, tmp_config, tmp_path_factory): + """We can register plugins in the user plugin directory.""" + config_dir = tmp_path_factory.mktemp("config") + plugin_dir = config_dir / "plugins" + plugin_dir.mkdir() + + list_path = Path(__file__).resolve().parent.parent / "moe/plugins/list.py" + shutil.copyfile(list_path, plugin_dir / "my_list.py") + + tmp_config(settings="enable_plugins = ['my_list']", config_dir=config_dir) + + assert config.CONFIG.pm.has_plugin("my_list") + class ConfigPlugin: """Plugin that implements the config hooks for testing."""