diff --git a/src/mapclient/application.py b/src/mapclient/application.py index 2746f76..ce4f09e 100755 --- a/src/mapclient/application.py +++ b/src/mapclient/application.py @@ -213,6 +213,7 @@ def windows_main(workflow, execute_now): splash.showMessage('Ready ...', 100) splash.finish(window) + return app.exec() diff --git a/src/mapclient/core/managers/pluginmanager.py b/src/mapclient/core/managers/pluginmanager.py index 109dd83..39b68af 100644 --- a/src/mapclient/core/managers/pluginmanager.py +++ b/src/mapclient/core/managers/pluginmanager.py @@ -15,8 +15,10 @@ import types from mapclient.application import get_app_path +from mapclient.core.pluginframework import discover_plugins, add_plugins from mapclient.core.utils import which, FileTypeObject, grep, is_frozen, determine_step_name, determine_step_class_name, \ stable_hash +from mapclient.mountpoints.workflowstep import WorkflowStepMountPoint from mapclient.settings.definitions import VIRTUAL_ENV_PATH, \ PLUGINS_PACKAGE_NAME, PLUGINS_PTH from mapclient.core.checks import getPipExecutable @@ -173,7 +175,7 @@ def getPluginDatabase(self): def _addPluginDir(self, directory): added = False - if isMapClientPluginsDir(directory): + if is_map_client_plugins_dir(directory): if directory not in sys.path: sys.path.append(directory) added = True @@ -192,7 +194,6 @@ def installPackage(self, uri): if not is_frozen(): subprocess.Popen([python_executable, "-m", "pip", "install", str(uri)], stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=my_env) - # importlib.reload(pkg_resources) def extractPluginDependencies(self, path): setup_dir, step_dir = os.path.split(path) @@ -218,7 +219,7 @@ def extractPluginDependencies(self, path): return [] - def load(self): + def load(self, initialise=True): self._reload_plugins = False len_package_modules_prior = len(sys.modules[PLUGINS_PACKAGE_NAME].__path__) if PLUGINS_PACKAGE_NAME in sys.modules else 0 @@ -237,15 +238,10 @@ def load(self): new_plugin_directories.append(os.path.join(directory, name)) if len_package_modules_prior == 0: - try: - package = import_module(PLUGINS_PACKAGE_NAME) - except ModuleNotFoundError: - return - else: - for d in new_plugin_directories: - sys.modules[PLUGINS_PACKAGE_NAME].__path__.append(os.path.join(d, PLUGINS_PACKAGE_NAME)) + discover_plugins(new_plugin_directories) - package = sys.modules[PLUGINS_PACKAGE_NAME] + add_plugins(new_plugin_directories) + package = sys.modules[PLUGINS_PACKAGE_NAME] self._import_errors = [] self._type_errors = [] @@ -254,10 +250,6 @@ def load(self): self._plugin_error_directories = {} self._plugin_error_names = [] - # a(pkg_resources.working_set) - # installed = [pkg.key for pkg in pkg_resources.working_set] - # print(installed) - for module_finder, modname, ispkg in pkgutil.iter_modules(package.__path__): if ispkg: try: @@ -314,8 +306,8 @@ def load(self): traceback.print_exception(exc_type, exc_value, exc_traceback, file=redirect_output) logger.warning(''.join(redirect_output.messages)) - # installed = [pkg.key for pkg in pkg_resources.working_set] - # print(installed) + if initialise: + WorkflowStepMountPoint.initialise_plugin_map() def get_plugin_error_names(self): return self._plugin_error_names @@ -444,20 +436,20 @@ def writeSettings(self, settings): settings.endGroup() -def isMapClientPluginsDir(plugin_dir): - result = False +def is_map_client_plugins_dir(plugin_dir): try: names = os.listdir(plugin_dir) - except: - return result + except OSError: + return False if PLUGINS_PACKAGE_NAME in names: - files = grep(os.path.join(plugin_dir, PLUGINS_PACKAGE_NAME), - r'(from|import) mapclient.mountpoints.workflowstep', one_only=True, file_endswith='.py') - if files: - result = True + return True + # files = grep(os.path.join(plugin_dir, PLUGINS_PACKAGE_NAME), + # r'(from|import) mapclient.mountpoints.workflowstep', one_only=True, file_endswith='.py') + # if files: + # result = True - return result + return False class PluginSiteManager(object): diff --git a/src/mapclient/core/pluginframework.py b/src/mapclient/core/pluginframework.py index 47e04b7..2c68c9f 100644 --- a/src/mapclient/core/pluginframework.py +++ b/src/mapclient/core/pluginframework.py @@ -18,12 +18,19 @@ along with MAP Client. If not, see .. """ import importlib +import importlib.util import logging import os +import pkgutil +import sys +import types + +from importlib.metadata import distributions +from pathlib import Path from PySide6.QtCore import QObject -from mapclient.settings.definitions import MAIN_MODULE +from mapclient.settings.definitions import MAIN_MODULE, PLUGINS_PACKAGE_NAME logger = logging.getLogger(__name__) @@ -74,22 +81,44 @@ class MetaPluginMountPoint(type): classes like so: MyPlugin = MetaPluginMountPoint('MyPlugin', (object, ), {}) """ - def __init__(cls, name, bases, attrs): - if not hasattr(cls, 'plugins'): + super(MetaPluginMountPoint, cls).__init__(name, bases, attrs) + if not hasattr(cls, '_plugins'): # This branch only executes when processing the mount point itself. # So, since this is a new plugin type, not an implementation, this # class shouldn't be registered as a plugin. Instead, it sets up a # list where plugins can be registered later. - cls.plugins = [] + cls._plugins = [] + cls._map = {} else: # This must be a plugin implementation, which should be registered. # Simply appending it to the list is all that's needed to keep # track of it later. - cls.plugins.append(cls) + cls._plugins.append(cls) + + def initialise_plugin_map(self): + plugins = [p("") for p in self._plugins] + self._map = {plugin.getName(): index for index, plugin in enumerate(plugins)} + return plugins + + def get_all_plugins(self, *args, **kwargs): + return self.initialise_plugin_map() - def getPlugins(self, *args, **kwargs): - return [p(*args, **kwargs) for p in self.plugins] + def get_plugin(self, name, *args, **kwargs): + index = self._map.get(name, -1) + if indexj >= 0: + return self._plugins[index](*args, **kwargs) + + return None + + def remove_plugin(self, step_module): + for cls in self._plugins: + if cls and step_module in cls.__module__: + print(cls.getName(), step_module) + index = self._plugins.index(cls) + self._plugins.pop(index) + if cls.getName() in self._map: + self._map.pop(cls.getName()) # Plugin mount points are defined below. @@ -143,3 +172,33 @@ def __init__(self, name, bases, attrs): ToolMountPoint = MetaPluginMountPoint('ToolMountPoint', (object,), {}) +def add_plugins(plugin_directories): + for d in plugin_directories: + sys.modules[PLUGINS_PACKAGE_NAME].__path__.append(os.path.join(d, PLUGINS_PACKAGE_NAME)) + +def discover_plugins(plugin_directories): + # setuptools stopped supporting pkg_resources, + # but we cannot know if every plugin has removed + # this old import device. So, we will create a shim + # if it is no longer available and protect against + # plugins still using it. + mock_pkg_resources = False + try: + import pkg_resources + if not hasattr(pkg_resources, 'declare_namespace'): + mock_pkg_resources = True + except ModuleNotFoundError: + mock_pkg_resources = True + + if mock_pkg_resources: + def declare_namespace(name): + pass + + pkg_resources = types.ModuleType('pkg_resources') + pkg_resources.declare_namespace = declare_namespace + sys.modules['pkg_resources'] = pkg_resources + + # bootstrap namespace + m = types.ModuleType(PLUGINS_PACKAGE_NAME) + m.__path__ = [] + sys.modules[PLUGINS_PACKAGE_NAME] = m diff --git a/src/mapclient/core/workflow/workflowsteps.py b/src/mapclient/core/workflow/workflowsteps.py index 36c89f2..3d46742 100644 --- a/src/mapclient/core/workflow/workflowsteps.py +++ b/src/mapclient/core/workflow/workflowsteps.py @@ -70,5 +70,5 @@ def __init__(self, manager, parent=None): def reload(self): self.clear() self.setColumnCount(1) - for step in WorkflowStepMountPoint.getPlugins(''): + for step in WorkflowStepMountPoint.get_all_plugins(''): addStep(self, step) diff --git a/src/mapclient/mountpoints/workflowstep.py b/src/mapclient/mountpoints/workflowstep.py index 8bafedf..9acaad2 100644 --- a/src/mapclient/mountpoints/workflowstep.py +++ b/src/mapclient/mountpoints/workflowstep.py @@ -328,11 +328,11 @@ def _workflow_step_relocate_configuration(self, to_location): def workflowStepFactory(step_name, location): - for step in WorkflowStepMountPoint.getPlugins(location): - if step_name == step.getName(): - return step + step = WorkflowStepMountPoint.get_plugin(step_name, location) + if step is None: + raise ValueError('Failed to find/create a step named: ' + step_name) - raise ValueError('Failed to find/create a step named: ' + step_name) + return step def removeWorkflowStep(step_module): @@ -345,7 +345,4 @@ def removeWorkflowStep(step_module): if step_module in key: del sys.modules[key] - for cls in WorkflowStepMountPoint.plugins[:]: - if cls and step_module in cls.__module__: - index = WorkflowStepMountPoint.plugins.index(cls) - WorkflowStepMountPoint.plugins.pop(index) + WorkflowStepMountPoint.remove_plugin(step_module) diff --git a/src/mapclient/tools/pluginwizard/skeletonstrings.py b/src/mapclient/tools/pluginwizard/skeletonstrings.py index 022cdf9..38b8ef1 100644 --- a/src/mapclient/tools/pluginwizard/skeletonstrings.py +++ b/src/mapclient/tools/pluginwizard/skeletonstrings.py @@ -306,7 +306,6 @@ def validate(self): NAMESPACE_INIT = """\ -__import__('pkg_resources').declare_namespace(__name__) """ diff --git a/src/mapclient/tools/renameplugin/renamedialog.py b/src/mapclient/tools/renameplugin/renamedialog.py index 425c5f3..44072d1 100644 --- a/src/mapclient/tools/renameplugin/renamedialog.py +++ b/src/mapclient/tools/renameplugin/renamedialog.py @@ -5,7 +5,7 @@ from PySide6 import QtCore, QtWidgets -from mapclient.core.managers.pluginmanager import isMapClientPluginsDir +from mapclient.core.managers.pluginmanager import is_map_client_plugins_dir from mapclient.core.utils import grep, determine_package_name, determine_step_class_name, determine_step_name, \ convert_name_to_python_package, find_file, qt_tool_wrapper from mapclient.settings.definitions import PLUGINS_PACKAGE_NAME @@ -125,7 +125,7 @@ def _chooseStepClicked(self): def _interrogateTarget(self): target = self._ui.lineEditStepLocation.text() - is_plugin_dir = isMapClientPluginsDir(target) + is_plugin_dir = is_map_client_plugins_dir(target) if is_plugin_dir: step_dir = os.path.join(target, PLUGINS_PACKAGE_NAME) files = grep(step_dir, r'(from|import) mapclient.mountpoints.workflowstep', diff --git a/src/mapclient/view/mainwindow.py b/src/mapclient/view/mainwindow.py index a68afc0..8513166 100644 --- a/src/mapclient/view/mainwindow.py +++ b/src/mapclient/view/mainwindow.py @@ -475,7 +475,7 @@ def _plugin_manager_load_plugins(self): pm.set_directories(self._plugin_manager_dlg.directories()) if pm.reloadPlugins(): - pm.load() + pm.load(initialise=False) wm = self._model.workflowManager() wm.updateAvailableSteps() self._workflowWidget.updateStepTree()