diff --git a/InvenTree/InvenTree/settings.py b/InvenTree/InvenTree/settings.py index eb18342bd1..7a2b022968 100644 --- a/InvenTree/InvenTree/settings.py +++ b/InvenTree/InvenTree/settings.py @@ -935,17 +935,6 @@ PLUGINS_ENABLED = _is_true(get_setting( PLUGIN_FILE = get_plugin_file() -# Plugin Directories (local plugins will be loaded from these directories) -PLUGIN_DIRS = ['plugin.builtin', ] - -if not TESTING: - # load local deploy directory in prod - PLUGIN_DIRS.append('plugins') # pragma: no cover - -if DEBUG or TESTING: - # load samples in debug mode - PLUGIN_DIRS.append('plugin.samples') - # Plugin test settings PLUGIN_TESTING = get_setting('PLUGIN_TESTING', TESTING) # are plugins beeing tested? PLUGIN_TESTING_SETUP = get_setting('PLUGIN_TESTING_SETUP', False) # load plugins from setup hooks in testing? diff --git a/InvenTree/plugin/helpers.py b/InvenTree/plugin/helpers.py index 53326ab286..3036e6a9b6 100644 --- a/InvenTree/plugin/helpers.py +++ b/InvenTree/plugin/helpers.py @@ -172,10 +172,16 @@ class GitStatus: # region plugin finders -def get_modules(pkg): +def get_modules(pkg, path=None): """Get all modules in a package.""" context = {} - for loader, name, _ in pkgutil.walk_packages(pkg.__path__): + + if path is None: + path = pkg.__path__ + elif type(path) is not list: + path = [path] + + for loader, name, _ in pkgutil.walk_packages(path): try: module = loader.find_module(name).load_module(name) pkg_names = getattr(module, '__all__', None) @@ -199,7 +205,7 @@ def get_classes(module): return inspect.getmembers(module, inspect.isclass) -def get_plugins(pkg, baseclass): +def get_plugins(pkg, baseclass, path=None): """Return a list of all modules under a given package. - Modules must be a subclass of the provided 'baseclass' @@ -207,7 +213,7 @@ def get_plugins(pkg, baseclass): """ plugins = [] - modules = get_modules(pkg) + modules = get_modules(pkg, path=path) # Iterate through each module in the package for mod in modules: diff --git a/InvenTree/plugin/plugin.py b/InvenTree/plugin/plugin.py index 38d4982f6e..b59b42bc3b 100644 --- a/InvenTree/plugin/plugin.py +++ b/InvenTree/plugin/plugin.py @@ -275,7 +275,11 @@ class InvenTreePlugin(MixinBase, MetaBase): """Path to the plugin.""" if self._is_package: return self.__module__ # pragma: no cover - return pathlib.Path(self.def_path).relative_to(settings.BASE_DIR) + + try: + return pathlib.Path(self.def_path).relative_to(settings.BASE_DIR) + except ValueError: + return pathlib.Path(self.def_path) @property def settings_url(self): diff --git a/InvenTree/plugin/registry.py b/InvenTree/plugin/registry.py index efed23f6bb..2bde24c543 100644 --- a/InvenTree/plugin/registry.py +++ b/InvenTree/plugin/registry.py @@ -187,6 +187,53 @@ class PluginsRegistry: logger.info('Finished reloading plugins') + def plugin_dirs(self): + """Construct a list of directories from where plugins can be loaded""" + + dirs = ['plugin.builtin', ] + + if settings.TESTING or settings.DEBUG: + # If in TEST or DEBUG mode, load plugins from the 'samples' directory + dirs.append('plugin.samples') + + if settings.TESTING: + custom_dirs = os.getenv('INVENTREE_PLUGIN_TEST_DIR', None) + else: + custom_dirs = os.getenv('INVENTREE_PLUGIN_DIR', None) + + # Load from user specified directories (unless in testing mode) + dirs.append('plugins') + + if custom_dirs is not None: + # Allow multiple plugin directories to be specified + for pd_text in custom_dirs.split(','): + pd = pathlib.Path(pd_text.strip()).absolute() + + # Attempt to create the directory if it does not already exist + if not pd.exists(): + try: + pd.mkdir(exist_ok=True) + except Exception: + logger.error(f"Could not create plugin directory '{pd}'") + continue + + # Ensure the directory has an __init__.py file + init_filename = pd.joinpath('__init__.py') + + if not init_filename.exists(): + try: + init_filename.write_text("# InvenTree plugin directory\n") + except Exception: + logger.error(f"Could not create file '{init_filename}'") + continue + + if pd.exists() and pd.is_dir(): + # By this point, we have confirmed that the directory at least exists + logger.info(f"Added plugin directory: '{pd}'") + dirs.append(pd) + + return dirs + def collect_plugins(self): """Collect plugins from all possible ways of loading.""" if not settings.PLUGINS_ENABLED: @@ -196,8 +243,21 @@ class PluginsRegistry: self.plugin_modules = [] # clear # Collect plugins from paths - for plugin in settings.PLUGIN_DIRS: - modules = get_plugins(importlib.import_module(plugin), InvenTreePlugin) + for plugin in self.plugin_dirs(): + + print(f"Loading plugins from directory '{plugin}'") + + parent_path = None + parent_obj = pathlib.Path(plugin) + + # If a "path" is provided, some special handling is required + if parent_obj.name is not plugin and len(parent_obj.parts) > 1: + print("loading from a qualified path:", plugin) + parent_path = parent_obj.parent + plugin = parent_obj.name + + modules = get_plugins(importlib.import_module(plugin), InvenTreePlugin, path=parent_path) + if modules: [self.plugin_modules.append(item) for item in modules]