diff --git a/src/rez/data/tests/extensions/baz/.gitignore b/src/rez/data/tests/extensions/baz/.gitignore new file mode 100644 index 000000000..a66c47ad1 --- /dev/null +++ b/src/rez/data/tests/extensions/baz/.gitignore @@ -0,0 +1 @@ +*.egg-info \ No newline at end of file diff --git a/src/rez/data/tests/extensions/baz/__init__.py b/src/rez/data/tests/extensions/baz/__init__.py new file mode 100644 index 000000000..1ae756729 --- /dev/null +++ b/src/rez/data/tests/extensions/baz/__init__.py @@ -0,0 +1,38 @@ +""" +baz plugin +""" + +from rez.command import Command + +# This attribute is optional, default behavior will be applied if not present. +command_behavior = { + "hidden": False, # (bool): default False + "arg_mode": None, # (str): "passthrough", "grouped", default None +} + + +def setup_parser(parser, completions=False): + parser.add_argument( + "-m", "--message", action="store_true", help="Print message from world." + ) + + +def command(opts, parser=None, extra_arg_groups=None): + from baz import core + + if opts.message: + msg = core.get_message_from_tmp() + print(msg) + return + + print("Please use '-h' flag to see what you can do to this world !") + + +class BazCommand(Command): + @classmethod + def name(cls): + return "baz" + + +def register_plugin(): + return BazCommand diff --git a/src/rez/data/tests/extensions/baz/baz/__init__.py b/src/rez/data/tests/extensions/baz/baz/__init__.py new file mode 100644 index 000000000..e33708c1d --- /dev/null +++ b/src/rez/data/tests/extensions/baz/baz/__init__.py @@ -0,0 +1,38 @@ +""" +baz plugin +""" + +from rez.command import Command + +# This attribute is optional, default behavior will be applied if not present. +command_behavior = { + "hidden": False, # (bool): default False + "arg_mode": None, # (str): "passthrough", "grouped", default None +} + + +def setup_parser(parser, completions=False): + parser.add_argument( + "-m", "--message", action="store_true", help="Print message from world." + ) + + +def command(opts, parser=None, extra_arg_groups=None): + from baz import core + + if opts.message: + msg = core.get_message_from_baz() + print(msg) + return + + print("Please use '-h' flag to see what you can do to this world !") + + +class BazCommand(Command): + @classmethod + def name(cls): + return "baz" + + +def register_plugin(): + return BazCommand diff --git a/src/rez/data/tests/extensions/baz/baz/core.py b/src/rez/data/tests/extensions/baz/baz/core.py new file mode 100644 index 000000000..580795278 --- /dev/null +++ b/src/rez/data/tests/extensions/baz/baz/core.py @@ -0,0 +1,4 @@ +def get_message_from_baz(): + from rez.config import config + message = config.plugins.command.baz.message + return message diff --git a/src/rez/data/tests/extensions/baz/baz/rezconfig.py b/src/rez/data/tests/extensions/baz/baz/rezconfig.py new file mode 100644 index 000000000..d36f5806b --- /dev/null +++ b/src/rez/data/tests/extensions/baz/baz/rezconfig.py @@ -0,0 +1,3 @@ +baz = { + "message": "welcome to this world." +} diff --git a/src/rez/data/tests/extensions/baz/setup.py b/src/rez/data/tests/extensions/baz/setup.py new file mode 100644 index 000000000..53e160a2d --- /dev/null +++ b/src/rez/data/tests/extensions/baz/setup.py @@ -0,0 +1,17 @@ +from __future__ import print_function, with_statement +from setuptools import setup, find_packages + + +setup( + name="baz", + version="0.1.0", + package_dir={ + "baz": "baz" + }, + packages=find_packages(where="."), + entry_points={ + 'rez.plugins': [ + 'baz_cmd = baz', + ] + } +) diff --git a/src/rez/plugin_managers.py b/src/rez/plugin_managers.py index 64b1d8f09..92cea98e7 100644 --- a/src/rez/plugin_managers.py +++ b/src/rez/plugin_managers.py @@ -109,6 +109,10 @@ def register_plugin(self, plugin_name, plugin_class, plugin_module): self.plugin_modules[plugin_name] = plugin_module def load_plugins(self): + self.load_plugins_from_namespace() + self.load_plugins_from_entry_points() + + def load_plugins_from_namespace(self): import pkgutil from importlib import import_module type_module_name = 'rezplugins.' + self.type_name @@ -153,44 +157,15 @@ def load_plugins(self): if config.debug("plugins"): print_debug("loading %s plugin at %s: %s..." % (self.type_name, path, modname)) + try: - # https://github.com/AcademySoftwareFoundation/rez/pull/218 - # load_module will force reload the module if it's - # already loaded, so check for that plugin_module = sys.modules.get(modname) if plugin_module is None: loader = importer.find_module(modname) plugin_module = loader.load_module(modname) - elif os.path.dirname(plugin_module.__file__) != path: - if config.debug("plugins"): - # this should not happen but if it does, tell why. - print_warning( - "plugin module %s is not loaded from current " - "load path but reused from previous imported " - "path: %s" % (modname, plugin_module.__file__)) - - if (hasattr(plugin_module, "register_plugin") - and callable(plugin_module.register_plugin)): - - plugin_class = plugin_module.register_plugin() - if plugin_class is not None: - self.register_plugin(plugin_name, - plugin_class, - plugin_module) - else: - if config.debug("plugins"): - print_warning( - "'register_plugin' function at %s: %s did " - "not return a class." % (path, modname)) - else: - if config.debug("plugins"): - print_warning( - "no 'register_plugin' function at %s: %s" - % (path, modname)) - - # delete from sys.modules? - + self.register_plugin_module(plugin_name, plugin_module, path) + self.load_config_from_plugin(plugin_module) except Exception as e: nameish = modname.split('.')[-1] self.failed_plugins[nameish] = str(e) @@ -201,9 +176,55 @@ def load_plugins(self): traceback.print_exc(file=out) print_debug(out.getvalue()) - # load config - data, _ = _load_config_from_filepaths([os.path.join(path, "rezconfig")]) - deep_update(self.config_data, data) + def load_plugins_from_entry_points(self): + if sys.version_info.minor >= 8: + from importlib.metadata import entry_points + else: + from importlib_metadata import entry_points + + discovered_plugins = entry_points(group='rez.plugins') + for plugin in discovered_plugins: + plugin = plugin.load() + plugin_name = plugin.__name__.split('.')[-1] + plugin_path = os.path.dirname(plugin.__file__) + self.register_plugin_module(plugin_name, plugin, plugin_path) + self.load_config_from_plugin(plugin) + + def load_config_from_plugin(self, plugin): + plugin_path = os.path.dirname(plugin.__file__) + data, _ = _load_config_from_filepaths([os.path.join(plugin_path, "rezconfig")]) + deep_update(self.config_data, data) + + def register_plugin_module(self, plugin_name, plugin_module, plugin_path): + module_name = plugin_module.__name__ + if os.path.dirname(plugin_module.__file__) != plugin_path: + if config.debug("plugins"): + # this should not happen but if it does, tell why. + print_warning( + "plugin module %s is not loaded from current " + "load path but reused from previous imported " + "path: %s" % (module_name, plugin_module.__file__)) + + if (hasattr(plugin_module, "register_plugin") + and callable(plugin_module.register_plugin)): + + plugin_class = plugin_module.register_plugin() + if plugin_class is not None: + self.register_plugin( + plugin_name, + plugin_class, + plugin_module + ) + else: + if config.debug("plugins"): + print_warning( + "'register_plugin' function at %s: %s did " + "not return a class." % (plugin_path, module_name)) + else: + if config.debug("plugins"): + print_warning( + "no 'register_plugin' function at %s: %s" + % (plugin_path, module_name)) def get_plugin_class(self, plugin_name): """Returns the class registered under the given plugin name.""" diff --git a/src/rez/tests/test_plugin_manager.py b/src/rez/tests/test_plugin_manager.py index 670441fba..906d3d3ff 100644 --- a/src/rez/tests/test_plugin_manager.py +++ b/src/rez/tests/test_plugin_manager.py @@ -5,9 +5,10 @@ """ test rezplugins manager behaviors """ -from rez.tests.util import TestBase, TempdirMixin, restore_sys_path +from rez.tests.util import TestBase, TempdirMixin, restore_pip, restore_sys_path from rez.plugin_managers import plugin_manager, uncache_rezplugins_module_paths from rez.package_repository import package_repository_manager +import os import sys import unittest @@ -49,7 +50,7 @@ def setUp(self): TestBase.setUp(self) self._reset_plugin_manager() - def test_old_loading_style(self): + def test_load_plugin_from_plugin_path(self): """Test loading rez plugin from plugin_path""" self.update_settings(dict( plugin_path=[self.data_path("extensions", "foo")] @@ -59,7 +60,7 @@ def test_old_loading_style(self): "package_repository", "cloud") self.assertEqual(cloud_cls.name(), "cloud") - def test_new_loading_style(self): + def test_load_plugin_from_python_module(self): """Test loading rez plugin from python modules""" with restore_sys_path(): sys.path.append(self.data_path("extensions")) @@ -68,6 +69,13 @@ def test_new_loading_style(self): "package_repository", "cloud") self.assertEqual(cloud_cls.name(), "cloud") + def test_load_plugin_from_entry_points(self): + """Test loading rez plugin from setuptools entry points""" + with restore_pip("baz", os.path.join(self.data_path("extensions"), "baz")): + baz_cls = plugin_manager.get_plugin_class( + "command", "baz") + self.assertEqual(baz_cls.name(), "baz") + def test_plugin_override_1(self): """Test plugin from plugin_path can override the default""" self.update_settings(dict( diff --git a/src/rez/tests/util.py b/src/rez/tests/util.py index 60e2d865c..3a497e65f 100644 --- a/src/rez/tests/util.py +++ b/src/rez/tests/util.py @@ -303,6 +303,7 @@ def wrapper(self, *args, **kwargs): _restore_sys_path_lock = threading.Lock() _restore_os_environ_lock = threading.Lock() +_restore_pip_lock = threading.Lock() @contextmanager @@ -362,3 +363,15 @@ def restore_os_environ(): os.environ.clear() os.environ.update(original) + + +@contextmanager +def restore_pip(package_name, package_path): + from pip._internal import main as pipmain + + with _restore_pip_lock: + pipmain(['install', package_path]) + + yield True + + pipmain(['uninstall', package_name, "-y"])