From 5ec525b4c82d4ecb9e6fa878bc5c793920018690 Mon Sep 17 00:00:00 2001 From: Hugh Sorby Date: Fri, 3 Jul 2026 20:29:29 +1200 Subject: [PATCH 1/6] Try and import plugins slightly differently to handle the loss of pkg_resources. --- src/mapclient/core/managers/pluginmanager.py | 37 ++++++------ src/mapclient/core/pluginframework.py | 58 ++++++++++++++++--- src/mapclient/core/workflow/workflowsteps.py | 2 +- src/mapclient/mountpoints/workflowstep.py | 8 +-- .../tools/renameplugin/renamedialog.py | 4 +- src/mapclient/view/mainwindow.py | 2 +- 6 files changed, 77 insertions(+), 34 deletions(-) diff --git a/src/mapclient/core/managers/pluginmanager.py b/src/mapclient/core/managers/pluginmanager.py index 109dd83b..9d983a71 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 @@ -218,7 +220,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 +239,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 = [] @@ -314,6 +311,8 @@ def load(self): traceback.print_exception(exc_type, exc_value, exc_traceback, file=redirect_output) logger.warning(''.join(redirect_output.messages)) + if initialise: + WorkflowStepMountPoint.initialise_plugin_map() # installed = [pkg.key for pkg in pkg_resources.working_set] # print(installed) @@ -444,20 +443,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 47e04b75..dc36201d 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,35 @@ 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) + if index: + return self._plugins[index](*args, **kwargs) + + return None # Plugin mount points are defined below. @@ -143,3 +163,27 @@ 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. + try: + import pkg_resources + except ModuleNotFoundError: + 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 36c89f26..3d467422 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 8bafedff..1649c939 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): diff --git a/src/mapclient/tools/renameplugin/renamedialog.py b/src/mapclient/tools/renameplugin/renamedialog.py index 425c5f3a..44072d12 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 a68afc06..8513166b 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() From 023d6dd91133daaf42612e86da3812eb852612b7 Mon Sep 17 00:00:00 2001 From: Hugh Sorby Date: Tue, 7 Jul 2026 12:05:55 +1200 Subject: [PATCH 2/6] Update the way a plugin is removed. --- src/mapclient/application.py | 1 + src/mapclient/core/pluginframework.py | 9 +++++++++ src/mapclient/mountpoints/workflowstep.py | 5 +---- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/mapclient/application.py b/src/mapclient/application.py index 2746f764..ce4f09ee 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/pluginframework.py b/src/mapclient/core/pluginframework.py index dc36201d..64bdfd7e 100644 --- a/src/mapclient/core/pluginframework.py +++ b/src/mapclient/core/pluginframework.py @@ -111,6 +111,15 @@ def get_plugin(self, name, *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. # For running in both python 2.x and python 3.x we must follow the example found diff --git a/src/mapclient/mountpoints/workflowstep.py b/src/mapclient/mountpoints/workflowstep.py index 1649c939..9acaad2c 100644 --- a/src/mapclient/mountpoints/workflowstep.py +++ b/src/mapclient/mountpoints/workflowstep.py @@ -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) From 9fda2a0a7018ea3e82558fa2b88a0f47b34051ab Mon Sep 17 00:00:00 2001 From: Hugh Sorby Date: Tue, 7 Jul 2026 13:52:05 +1200 Subject: [PATCH 3/6] Stop adding pkg_resources use in skeleton generated plugins. --- src/mapclient/core/managers/pluginmanager.py | 7 ------- src/mapclient/tools/pluginwizard/skeletonstrings.py | 1 - 2 files changed, 8 deletions(-) diff --git a/src/mapclient/core/managers/pluginmanager.py b/src/mapclient/core/managers/pluginmanager.py index 9d983a71..39b68af1 100644 --- a/src/mapclient/core/managers/pluginmanager.py +++ b/src/mapclient/core/managers/pluginmanager.py @@ -194,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) @@ -251,10 +250,6 @@ def load(self, initialise=True): 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: @@ -313,8 +308,6 @@ def load(self, initialise=True): if initialise: WorkflowStepMountPoint.initialise_plugin_map() - # installed = [pkg.key for pkg in pkg_resources.working_set] - # print(installed) def get_plugin_error_names(self): return self._plugin_error_names diff --git a/src/mapclient/tools/pluginwizard/skeletonstrings.py b/src/mapclient/tools/pluginwizard/skeletonstrings.py index 022cdf99..38b8ef14 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__) """ From da3d9a5229ebf58cf9fbbbf190ff5e0bf84bc3ad Mon Sep 17 00:00:00 2001 From: Hugh Sorby Date: Tue, 7 Jul 2026 17:07:46 +1200 Subject: [PATCH 4/6] Deal with PyInstaller mocked pkg_resources. --- src/mapclient/core/pluginframework.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/mapclient/core/pluginframework.py b/src/mapclient/core/pluginframework.py index 64bdfd7e..6b5d7bfe 100644 --- a/src/mapclient/core/pluginframework.py +++ b/src/mapclient/core/pluginframework.py @@ -182,9 +182,15 @@ def discover_plugins(plugin_directories): # 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 From dfe5245e017c45787e86870136268e3f4071a727 Mon Sep 17 00:00:00 2001 From: Hugh Sorby Date: Tue, 7 Jul 2026 17:12:00 +1200 Subject: [PATCH 5/6] Allow get_plugin to get any valid index (including zero). --- src/mapclient/core/pluginframework.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mapclient/core/pluginframework.py b/src/mapclient/core/pluginframework.py index 6b5d7bfe..79876196 100644 --- a/src/mapclient/core/pluginframework.py +++ b/src/mapclient/core/pluginframework.py @@ -106,7 +106,7 @@ def get_all_plugins(self, *args, **kwargs): def get_plugin(self, name, *args, **kwargs): index = self._map.get(name) - if index: + if indexj >= 0: return self._plugins[index](*args, **kwargs) return None From 7fb9f35ad61f599d5a7a20a93706e6be83e99ef7 Mon Sep 17 00:00:00 2001 From: Hugh Sorby Date: Tue, 7 Jul 2026 17:15:43 +1200 Subject: [PATCH 6/6] Make the get_plugin return an invalid index if not found. --- src/mapclient/core/pluginframework.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mapclient/core/pluginframework.py b/src/mapclient/core/pluginframework.py index 79876196..2c68c9f2 100644 --- a/src/mapclient/core/pluginframework.py +++ b/src/mapclient/core/pluginframework.py @@ -105,7 +105,7 @@ def get_all_plugins(self, *args, **kwargs): return self.initialise_plugin_map() def get_plugin(self, name, *args, **kwargs): - index = self._map.get(name) + index = self._map.get(name, -1) if indexj >= 0: return self._plugins[index](*args, **kwargs)