Coverage for src/mafw/plugin_manager.py: 100%
109 statements
« prev ^ index » next coverage.py v7.15.0, created at 2026-07-26 09:13 +0000
« prev ^ index » next coverage.py v7.15.0, created at 2026-07-26 09:13 +0000
1# Copyright 2025–2026 European Union
2# Author: Bulgheroni Antonio (antonio.bulgheroni@ec.europa.eu)
3# SPDX-License-Identifier: EUPL-1.2
4"""
5Plugin management system for MAFw framework.
7This module provides the core functionality for loading and managing plugins within the MAFw framework.
8It supports loading various types of plugins including processors, standard tables, database models, and
9user interfaces.
11The plugin manager uses the pluggy library to handle plugin discovery and registration
12through entry points and hooks.
14When plugins are loaded using the :meth:`.MAFwPluginManager.load_plugins` function, the job is divided into multiple
15threads to improve performance.
17Key features:
18 - Asynchronous plugin loading with progress indication
19 - Support for both internal and external plugins
20 - Type-safe plugin handling with proper data structures
21 - Logging integration for monitoring plugin loading processes
22 - Global plugin manager singleton for consistent access
24The module defines several key components:
25 - :class:`.LoadedPlugins`: Data container for loaded plugins
26 - :class:`.MAFwPluginManager`: Main plugin manager class
27 - :func:`.get_plugin_manager`: Factory function for accessing the plugin manager
29Plugin types supported:
30 - Processors (`processors`): Classes that implement data processing logic
31 - Database Modules (`db_modules`): Model modules for database interaction
32 - User Interfaces (`ui`): UI implementations for different interfaces
34.. versionchanged:: v2.0.0
35 Complete refactoring of the plugin manager system.
37"""
39import importlib
40import itertools
41import logging
42import time
43from collections.abc import Iterable
44from concurrent.futures import Future, ThreadPoolExecutor
45from dataclasses import dataclass, field
46from typing import TYPE_CHECKING, Any, Literal, cast
48import pluggy
50from mafw import hookspecs, plugins
51from mafw.lazy_import import ProcessorClassProtocol, UserInterfaceClassProtocol
53if TYPE_CHECKING:
54 pass
56log = logging.getLogger(__name__)
59@dataclass
60class LoadedPlugins:
61 """
62 Container class for storing loaded plugins of various types.
64 This dataclass holds collections of different plugin types that have been loaded
65 by the :class:`MAFwPluginManager`. It provides organized storage for processors,
66 database modules, and user interfaces.
68 .. versionadded:: v2.0.0
69 """
71 processor_list: list[ProcessorClassProtocol] = field(default_factory=list)
72 # List[Type['Processor'] | 'LazyPlugin'] = field(default_factory=list)
73 """List of loaded processor classes."""
75 processor_dict: dict[str, ProcessorClassProtocol] = field(default_factory=dict)
76 # Dict[str, Type['Processor'] | 'LazyPlugin'] = field(default_factory=dict)
77 """Dictionary mapping processor names to their classes."""
79 db_model_modules: list[str] = field(default_factory=list)
80 """List of database model module names."""
82 ui_list: list[UserInterfaceClassProtocol] = field(default_factory=list)
83 # List[Type['UserInterfaceBase'] | 'LazyPlugin'] = field(default_factory=list)
84 """List of loaded user interface classes."""
86 ui_dict: dict[str, UserInterfaceClassProtocol] = field(default_factory=dict)
87 # Dict[str, Type['UserInterfaceBase'] | 'LazyPlugin'] = field(default_factory=dict)
88 """Dictionary mapping user interface names to their classes."""
91PluginTypes = Literal['processors', 'db_modules', 'ui']
92"""Type alias for accepted types of plugins."""
95def _as_processor_result(obj: object) -> tuple[list[ProcessorClassProtocol], dict[str, ProcessorClassProtocol]]:
96 """
97 Cast an object to the expected processor result type.
99 This helper function is used to convert the raw result from plugin loading
100 operations into the expected tuple format for processors.
102 .. versionadded:: v2.0.0
104 :param obj: The object to cast
105 :type obj: object
106 :return: A tuple containing a list of processor classes and a dictionary mapping
107 processor names to their classes
108 :rtype: tuple[list[ProcessorClassProtocol], dict[str, ProcessorClassProtocol]]
109 """
110 return cast(tuple[list[ProcessorClassProtocol], dict[str, ProcessorClassProtocol]], obj)
113def _as_ui_result(obj: object) -> tuple[list[UserInterfaceClassProtocol], dict[str, UserInterfaceClassProtocol]]:
114 """
115 Cast an object to the expected UI result type.
117 This helper function is used to convert the raw result from plugin loading
118 operations into the expected tuple format for user interfaces.
120 .. versionadded:: v2.0.0
122 :param obj: The object to cast
123 :type obj: object
124 :return: A tuple containing a list of UI classes and a dictionary mapping
125 UI names to their classes
126 :rtype: tuple[list[UserInterfaceClassProtocol], dict[str, UserInterfaceClassProtocol]]
127 """
128 return cast(tuple[list[UserInterfaceClassProtocol], dict[str, UserInterfaceClassProtocol]], obj)
131def _as_db_module_result(obj: object) -> list[str]:
132 """
133 Cast an object to the expected database module result type.
135 This helper function is used to convert the raw result from plugin loading
136 operations into the expected list format for database modules.
138 .. versionadded:: v2.0.0
140 :param obj: The object to cast
141 :type obj: object
142 :return: A list of database module names
143 :rtype: list[str]
144 """
145 return cast(list[str], obj)
148class MAFwPluginManager(pluggy.PluginManager):
149 """
150 The MAFw plugin manager.
152 The MAFwPluginManager class manages the loading and registration of plugins within the MAFw framework.
153 It supports asynchronous loading of various plugin types, including processors, database modules,
154 and user interfaces, using a thread pool executor for improved performance.
156 The class provides methods to load each type of plugin and handles delayed status messages if loading takes
157 longer than expected.
159 .. versionadded:: v2.0.0
160 """
162 max_loading_delay = 1 # sec
163 """
164 Loading delay before displaying a message.
166 If the loading of the external plugins is taking more than this value, a message is displayed to inform the user.
167 """
169 def __init__(self, project_name: str = 'mafw'):
170 super().__init__(project_name)
171 self._executor = ThreadPoolExecutor(max_workers=4)
173 def load_db_models_plugins(self) -> list[str]:
174 """
175 Load database model modules from the plugin manager.
177 This method retrieves all database model modules registered through the plugin manager's
178 :meth:`~mafw.plugins.register_db_model_modules` hook and imports them.
180 :returns: List of database model module names
181 :rtype: list[str]
182 """
183 log.debug('Starting database model plugins...')
184 db_model_module_list = list(itertools.chain(*self.hook.register_db_model_modules()))
185 for module in db_model_module_list:
186 importlib.import_module(module)
187 log.debug('Finished database model plugins')
188 return db_model_module_list
190 def load_processor_plugins(self) -> tuple[list[ProcessorClassProtocol], dict[str, ProcessorClassProtocol]]:
191 """
192 Load available processor plugins from the plugin manager.
194 This method retrieves all processor plugins registered through the plugin manager's
195 :meth:`~mafw.plugins.register_processors` hook.
196 :meth:`~mafw.plugins.register_processors` hook.
198 :returns: A tuple containing:
199 - List of available processor classes
200 - Dictionary mapping processor names to their classes
201 :rtype: tuple[list[type[processor.Processor]], dict[str, type[processor.Processor]]]
202 """
203 log.debug('Starting processor plugins...')
204 lst = list(itertools.chain(*self.hook.register_processors()))
205 dct = {}
206 for p in lst:
207 if hasattr(p, 'plugin_name'): # LazyPlugin case
208 key = p.plugin_name
209 else:
210 key = p.__name__
211 dct[key] = p
212 log.debug('Finished processor plugins')
213 return lst, dct
215 def load_user_interface_plugins(
216 self,
217 ) -> tuple[list[UserInterfaceClassProtocol], dict[str, UserInterfaceClassProtocol]]:
218 """
219 Load available user interface plugins from the plugin manager.
221 This method retrieves all user interface plugins registered through the plugin manager's
222 :meth:`~mafw.plugins.register_user_interfaces` hook.
224 :returns: A tuple containing:
225 - List of available user interface classes
226 - Dictionary mapping user interface names to their classes
227 :rtype: tuple[list[type[UserInterfaceBase]], dict[str, type[UserInterfaceBase]]]
228 """
229 log.debug('Start loading user interface plugins...')
230 lst = list(itertools.chain(*self.hook.register_user_interfaces()))
231 dct = {ui.name: ui for ui in lst}
232 log.debug('Finished loading user interface plugins')
233 return lst, dct
235 def _delayed_status_message(self, futures: list[Future[Any]]) -> None:
236 """
237 Display a warning message if plugin loading takes longer than expected.
239 This method is called after a delay to check if all plugin loading operations
240 have completed. If not, it logs a warning message to inform the user that
241 plugin loading is taking longer than expected.
243 :param futures: List of futures representing ongoing plugin loading operations
244 :type futures: list[concurrent.futures.Future]
245 """
246 time.sleep(self.max_loading_delay)
247 if not all(f.done() for f in futures):
248 log.warning('Plugin loading is taking longer than expected, please be patient.')
250 def load_plugins(self, plugins_to_load: Iterable[PluginTypes]) -> LoadedPlugins:
251 """
252 Load plugins of specified types in multiple threads.
254 This method loads plugins of the specified types using a thread pool executor
255 for improved performance. It handles different plugin types including processors,
256 standard tables, database modules, and user interfaces.
258 :param plugins_to_load: Iterable of plugin types to load
259 :type plugins_to_load: Iterable[:obj:`PluginTypes`]
260 :return: Container with loaded plugins of all requested types
261 :rtype: :obj:`LoadedPlugins`
262 """
263 plugins_to_load = list(dict.fromkeys(plugins_to_load))
264 if not plugins_to_load:
265 return LoadedPlugins()
267 lut = {
268 'processors': self.load_processor_plugins,
269 'db_modules': self.load_db_models_plugins,
270 'ui': self.load_user_interface_plugins,
271 }
273 # drop invalid plugin types
274 plugins_to_load = [p for p in plugins_to_load if p in lut]
275 if not plugins_to_load:
276 return LoadedPlugins()
278 log.debug(f'Status message will appear if loading takes > {self.max_loading_delay}s')
280 # Submit tasks to the executor
281 futures: list[Future[Any]] = []
282 for plugin_type in plugins_to_load:
283 fut = self._executor.submit(lut[plugin_type])
284 futures.append(fut)
286 # Start delayed status thread
287 status_thread = self._executor.submit(self._delayed_status_message, futures)
289 # Wait for completion
290 results = []
291 try:
292 for fut in futures:
293 results.append(fut.result()) # will re-raise exceptions
294 finally:
295 # When all tasks finished, no need for the warning message
296 if not status_thread.done():
297 status_thread.cancel()
299 # Assemble output
300 plugins_ = LoadedPlugins()
301 idx = 0
303 for plugin_type in plugins_to_load:
304 result = results[idx]
306 if plugin_type == 'processors':
307 plugins_.processor_list, plugins_.processor_dict = _as_processor_result(result)
309 elif plugin_type == 'ui':
310 plugins_.ui_list, plugins_.ui_dict = _as_ui_result(result)
312 else: # if plugin_type == 'db_modules':
313 plugins_.db_model_modules = _as_db_module_result(result)
315 idx += 1
317 return plugins_
320global_mafw_plugin_manager: dict[str, 'MAFwPluginManager'] = {}
321"""The global mafw plugin manager dictionary."""
324def get_plugin_manager(force_recreate: bool = False) -> 'MAFwPluginManager':
325 """
326 Create a new or return an existing plugin manager for a given project
328 :param force_recreate: Flag to force the creation of a new plugin manager. Defaults to False
329 :type force_recreate: bool, Optional
330 :return: The plugin manager
331 :rtype: pluggy.PluginManager
332 """
333 if 'mafw' in global_mafw_plugin_manager and force_recreate:
334 del global_mafw_plugin_manager['mafw']
336 if 'mafw' not in global_mafw_plugin_manager:
337 pm = MAFwPluginManager('mafw')
338 pm.add_hookspecs(hookspecs)
339 pm.load_setuptools_entrypoints('mafw')
340 pm.register(plugins)
341 global_mafw_plugin_manager['mafw'] = pm
343 return global_mafw_plugin_manager['mafw']