Completed
Push — master ( 8efb92...a76a5d )
by Daniel
01:06
created

groundwork/pluginmanager.py (2 issues)

1
"""
2
The pluginmanager module cares about the management of plugin status and their changes between status.
3
4
There are two manager classes for managing plugin related objects.
5
6
 * PluginManager: Cares about initialised Plugins, which can be activated and deactivated.
7
 * PluginClassManager: Cares about plugin classes, which are used to create plugins.
8
9
A plugin class can be reused for several plugins. The only thing to care about is the naming of a plugin.
10
This plugin name must be unique inside a groundwork app and can be set during plugin initialisation/activation.
11
"""
12
from __future__ import absolute_import
13
from future.utils import raise_from
14
from pkg_resources import iter_entry_points
15
import logging
16
import inspect
17
import sys
18
19
from groundwork.patterns import GwBasePattern
20
from groundwork.exceptions import PluginNotActivatableException, PluginNotInitialisableException, \
21
    PluginRegistrationException, PluginNotDeactivatableException
22
23
24
class PluginManager:
25
    """
26
    PluginManager for searching, initialising, activating and deactivating groundwork plugins.
27
    """
28
29
    def __init__(self, app):
30
        """
31
        Initialises the plugin manager.
32
33
        Additional plugins can be registered by adding their class via the plugins argument.
34
35
        :param app: groundwork application object
36
        :param strict: If True, problems during plugin registration/initialisation or activation will throw an exception
37
        """
38
        self._log = logging.getLogger(__name__)
39
        self._app = app
40
        self._plugins = {}
41
42
        #: Instance of :class:`~groundwork.pluginmanager.PluginClassManager`.
43
        #: Handles the registration of plugin classes, which can be used to create new plugins during runtime.
44
        self.classes = PluginClassManager(self._app)
45
46
    def initialise_by_names(self, plugins=None):
47
        """
48
        Initialises given plugins, but does not activate them.
49
50
        This is needed to import and configure libraries, which are imported by used patterns, like GwFlask.
51
52
        After this action, all needed python modules are imported and configured.
53
        Also the groundwork application object is ready and contains functions and objects, which were added
54
        by patterns, like app.commands from GwCommandsPattern.
55
56
        The class of a given plugin must already be registered in the :class:`.PluginClassManager`.
57
58
        :param plugins: List of plugin names
59
        :type plugins: list of strings
60
        """
61
62
        if plugins is None:
63
            plugins = []
64
65
        self._log.debug("Plugins Initialisation started")
66
        if not isinstance(plugins, list):
67
            raise AttributeError("plugins must be a list, not %s" % type(plugins))
68
69
        self._log.debug("Plugins to initialise: %s" % ", ".join(plugins))
70
        plugin_initialised = []
71
        for plugin_name in plugins:
72
            if not isinstance(plugin_name, str):
73
                raise AttributeError("plugin name must be a str, not %s" % type(plugin_name))
74
75
            plugin_class = self.classes.get(plugin_name)
76
            self.initialise(plugin_class.clazz, plugin_name)
77
            plugin_initialised.append(plugin_name)
78
79
        self._log.info("Plugins initialised: %s" % ", ".join(plugin_initialised))
80
81
    def initialise(self, clazz, name=None):
82
        if clazz is not None and issubclass(clazz, GwBasePattern):
83
            if name is None:
84
                name = clazz.__name__
85
            try:
86
                # Plugin Initialisation
87
                plugin_instance = clazz(app=self._app, name=name)
88
                # Let's be sure the correct name was set. Even if the init_routine of
89
                # the plugin does not support the name parameter.
90
                if plugin_instance.name != name:
91
                    plugin_instance.name = name
92
            except Exception as e:
93
                self._log.warning("Plugin class %s could not be initialised" % clazz.__name__)
94
                if self._app.strict:
95
                    error = "Plugin class %s could not be initialised: %s" % (clazz.__name__, e.message)
96
                    if sys.version_info[0] < 3:
97
                        error += "Reason: %s" % e
98
                    raise_from(PluginNotInitialisableException(error), e)
99
100
            # Let's be sure, that GwBasePattern got called
101
            if not hasattr(plugin_instance, "_plugin_base_initialised") \
102
                    or plugin_instance._plugin_base_initialised is not True:
103
                self._log.error("GwBasePattern.__init__() was not called during initialisation. "
104
                                "Please add 'super(*args, **kwargs).__init__()' to the top of all involved "
105
                                "plugin/pattern init routines."
106
                                "Activate logging debug-output to see all involved classes.")
107
                for mro_class in clazz.__mro__:
108
                    self._log.debug(mro_class)
109
                raise Exception("GwBasePattern.__init()__ was not called during initialisation.")
110
            self._register_initialisation(plugin_instance)
111
            self._log.debug("Plugin %s initialised" % name)
112
            return plugin_instance
113
114
        if clazz is None:
115
            self._log.warn("Plugin class %s not found" % clazz.__name__)
116
            return None
117
118
        if not issubclass(clazz, GwBasePattern):
119
            self._log.warn("Can not load %s. Plugin is not based on groundwork.Plugin." % clazz.__name__)
120
            if self._app.strict:
121
                raise Exception("Can not load %s. Plugin is not based on groundwork.Plugin." % clazz.__name__)
122
        return None
123
124
    def _register_initialisation(self, plugin_instance):
125
        """
126
        Internal functions to perform registration actions after plugin load was successful.
127
        """
128
        self._plugins[plugin_instance.name] = plugin_instance
129
130
    def activate(self, plugins=[]):
131
        """
132
        Activates given plugins.
133
134
        This calls mainly plugin.activate() and plugins register needed resources like commands, signals or
135
        documents.
136
137
        If given plugins have not been initialised, this is also done via :func:`_load`.
138
139
        :param plugins: List of plugin names
140
        :type plugins: list of strings
141
        """
142
        self._log.debug("Plugins Activation started")
143
144
        if not isinstance(plugins, list):
145
            raise AttributeError("plugins must be a list, not %s" % type(plugins))
146
147
        self._log.debug("Plugins to activate: %s" % ", ".join(plugins))
148
149
        plugins_activated = []
150
        for plugin_name in plugins:
151
            if not isinstance(plugin_name, str):
152
                raise AttributeError("plugin name must be a str, not %s" % type(plugin_name))
153
154
            if plugin_name not in self._plugins.keys() and plugin_name in self.classes._classes.keys():
155
                self._log.debug("Initialisation needed before activation.")
156
                try:
157
                    self.initialise_by_names([plugin_name])
158
                except Exception as e:
159
                    self._log.error("Couldn't initialise plugin %s" % plugin_name)
160
                    if self._app.strict:
161
                        error = "Couldn't initialise plugin %s" % plugin_name
162
                        if sys.version_info[0] < 3:
163
                            error += "Reason: %s" % e
164
                        raise_from(Exception(error), e)
165
                    else:
166
                        continue
167 View Code Duplication
            if plugin_name in self._plugins.keys():
0 ignored issues
show
This code seems to be duplicated in your project.
Loading history...
168
                self._log.debug("Activating plugin %s" % plugin_name)
169
                if not self._plugins[plugin_name].active:
170
                    try:
171
                        self._plugins[plugin_name].activate()
172
                    except Exception as e:
173
                        raise_from(
174
                            PluginNotActivatableException("Plugin %s could not be activated: %s" % (plugin_name,
175
                                                                                                    e)), e)
176
                    else:
177
                        self._log.debug("Plugin %s activated" % plugin_name)
178
                        plugins_activated.append(plugin_name)
179
                else:
180
                    self._log.warning("Plugin %s got already activated." % plugin_name)
181
                    if self._app.strict:
182
                        raise PluginNotInitialisableException()
183
184
        self._log.info("Plugins activated: %s" % ", ".join(plugins_activated))
185
186
    def deactivate(self, plugins=[]):
187
        """
188
        Deactivates given plugins.
189
190
        A given plugin must be activated, otherwise it is ignored and no action takes place (no signals are fired,
191
        no deactivate functions are called.)
192
193
        A deactivated plugin is still loaded and initialised and can be reactivated by calling :func:`activate` again.
194
        It is also still registered in the :class:`.PluginManager` and can be requested via :func:`get`.
195
196
        :param plugins: List of plugin names
197
        :type plugins: list of strings
198
        """
199
        self._log.debug("Plugins Deactivation started")
200
201
        if not isinstance(plugins, list):
202
            raise AttributeError("plugins must be a list, not %s" % type(plugins))
203
204
        self._log.debug("Plugins to deactivate: %s" % ", ".join(plugins))
205
206
        plugins_deactivated = []
207
        for plugin_name in plugins:
208
            if not isinstance(plugin_name, str):
209
                raise AttributeError("plugin name must be a str, not %s" % type(plugin_name))
210
211 View Code Duplication
            if plugin_name not in self._plugins.keys():
0 ignored issues
show
This code seems to be duplicated in your project.
Loading history...
212
                self._log.info("Unknown activated plugin %s" % plugin_name)
213
                continue
214
            else:
215
                self._log.debug("Deactivating plugin %s" % plugin_name)
216
                if not self._plugins[plugin_name].active:
217
                    self._log.warning("Plugin %s seems to be already deactivated" % plugin_name)
218
                else:
219
                    try:
220
                        self._plugins[plugin_name].deactivate()
221
                    except Exception as e:
222
                        raise_from(
223
                            PluginNotDeactivatableException("Plugin %s could not be deactivated" % plugin_name), e)
224
                    else:
225
                        self._log.debug("Plugin %s deactivated" % plugin_name)
226
                        plugins_deactivated.append(plugin_name)
227
228
        self._log.info("Plugins deactivated: %s" % ", ".join(plugins_deactivated))
229
230
    def get(self, name=None):
231
        """
232
        Returns the plugin object with the given name.
233
        Or if a name is not given, the complete plugin dictionary is returned.
234
235
        :param name: Name of a plugin
236
        :return: None, single plugin or dictionary of plugins
237
        """
238
        if name is None:
239
            return self._plugins
240
        else:
241
            if name not in self._plugins.keys():
242
                return None
243
            else:
244
                return self._plugins[name]
245
246
    def exist(self, name):
247
        """
248
        Returns True if plugin exists.
249
        :param name: plugin name
250
        :return: boolean
251
        """
252
        if name in self._plugins.keys():
253
            return True
254
        return False
255
256
    def is_active(self, name):
257
        """
258
        Returns True if plugin exists and is active.
259
        If plugin does not exist, it returns None
260
261
        :param name: plugin name
262
        :return: boolean or None
263
        """
264
        if name in self._plugins.keys():
265
            return self._plugins["name"].active
266
        return None
267
268
269
class PluginClassManager:
270
    """
271
    Manages the plugin classes, which can be used to initialise and activate new plugins.
272
273
    Loads all plugin classes from entry_point "groundwork.plugin" automatically during own initialisation.
274
    Provides functions to register new plugin classes during runtime.
275
    """
276
277
    def __init__(self, app):
278
        self._log = logging.getLogger(__name__)
279
        self._app = app
280
        self._classes = {}
281
        self._get_plugins_by_entry_points()
282
283
    def _get_plugins_by_entry_points(self):
284
        """
285
        Registers plugin classes, which are in sys.path and have an entry_point called 'groundwork.plugin'.
286
        :return: dict of plugin classes
287
        """
288
        # Let's find and register every plugin, which is in sys.path and has defined a entry_point 'groundwork.plugin'
289
        # in it's setup.py
290
        entry_points = []
291
        classes = {}
292
        for entry_point in iter_entry_points(group='groundwork.plugin', name=None):
293
            entry_points.append(entry_point)
294
295
        for entry_point in entry_points:
296
            try:
297
                entry_point_object = entry_point.load()
298
            except Exception as e:
299
                # We should not throw an exception now, because a package/entry_point can be outdated, using an old
300
                # api from groundwork, tries to import unavailable packages, what ever...
301
                # We just do not make it available. That's all we can do.
302
                self._log.debug("Couldn't load entry_point %s. Reason: %s" % (entry_point.name, e))
303
                continue
304
305
            if not issubclass(entry_point_object, GwBasePattern):
306
                self._log.warning("entry_point  %s is not a subclass of groundworkPlugin" % entry_point.name)
307
                continue
308
            plugin_name = entry_point_object.__name__
309
310
            plugin_class = self.register_class(entry_point_object, plugin_name,
311
                                               entrypoint_name=entry_point.name,
312
                                               distribution_path=entry_point.dist.location,
313
                                               distribution_key=entry_point.dist.key,
314
                                               distribution_version=entry_point.dist.version)
315
316
            classes[plugin_name] = plugin_class
317
            # classes[plugin_name] = {
318
            #     "name": plugin_name,
319
            #     "entry_point": entry_point.name,
320
            #     "path": entry_point.dist.location,
321
            #     "class": entry_point_object,
322
            #     "distribution": {
323
            #         "key": entry_point.dist.key,
324
            #         "version": entry_point.dist.version
325
            #     },
326
            # }
327
            self._log.debug("Found plugin: %s at entry_point %s of package %s (%s)" % (plugin_name, entry_point.name,
328
                                                                                       entry_point.dist.key,
329
                                                                                       entry_point.dist.version))
330
        return classes
331
332
    def register(self, classes=[]):
333
        """
334
        Registers new plugins.
335
336
        The registration only creates a new entry for a plugin inside the _classes dictionary.
337
        It does not activate or even initialise the plugin.
338
339
        A plugin must be a class, which inherits directly or indirectly from GwBasePattern.
340
341
        :param classes: List of plugin classes
342
        :type classes: list
343
        """
344
        if not isinstance(classes, list):
345
            raise AttributeError("plugins must be a list, not %s." % type(classes))
346
347
        plugin_registered = []
348
349
        for plugin_class in classes:
350
            plugin_name = plugin_class.__name__
351
            self.register_class(plugin_class, plugin_name)
352
            self._log.debug("Plugin %s registered" % plugin_name)
353
            plugin_registered.append(plugin_name)
354
355
        self._log.info("Plugins registered: %s" % ", ".join(plugin_registered))
356
357
    def register_class(self, clazz, name=None, entrypoint_name=None, distribution_path=None,
358
                       distribution_key=None, distribution_version=None):
359
360
        if name is None:
361
            name = clazz.__name__
362
363
        if not inspect.isclass(clazz) or not issubclass(clazz, GwBasePattern):
364
            self._log.error("Given plugin is not a subclass of groundworkPlugin.")
365
            if self._app.strict:
366
                raise AttributeError("Given plugin is not a subclass of groundworkPlugin.")
367
368
        if isinstance(clazz, GwBasePattern):
369
            self._log.error("Given plugin %s is already initialised. Please provide a class not an instance.")
370
            if self._app.strict:
371
                raise Exception("Given plugin %s is already initialised. Please provide a class not an instance.")
372
373
        if name in self._classes.keys():
374
            self._log.warning("Plugin %s already registered" % name)
375
            if self._app.strict:
376
                raise PluginRegistrationException("Plugin %s already registered" % name)
377
378
        self._classes[name] = PluginClass(name, clazz, entrypoint_name, distribution_path,
379
                                          distribution_key, distribution_version)
380
381
        return self._classes[name]
382
383
        # self._classes[name] = {
384
        #     "name": plugin_name,
385
        #     "entry_point": None,
386
        #     "path": None,
387
        #     "class": plugin,
388
        #     "distribution": None,
389
        # }
390
391
    def get(self, name=None):
392
        """
393
        Returns the plugin class object with the given name.
394
        Or if a name is not given, the complete plugin dictionary is returned.
395
396
        :param name: Name of a plugin
397
        :return: None, single plugin or dictionary of plugins
398
        """
399
        if name is None:
400
            return self._classes
401
        else:
402
            if name not in self._classes.keys():
403
                return None
404
            else:
405
                return self._classes[name]
406
407
    def exist(self, name):
408
        """
409
        Returns True if plugin class exists.
410
        :param name: plugin name
411
        :return: boolean
412
        """
413
        if name in self._classes.keys():
414
            return True
415
        return False
416
417
418
class PluginClass:
419
    def __init__(self, name, clazz, entrypoint_name, distribution_path, distribution_key, distribution_version):
420
        self.name = name
421
        self.entrypoint_name = entrypoint_name
422
        self.clazz = clazz
423
        self.distribution = {
424
            "path": distribution_path,
425
            "key": distribution_key,
426
            "version": distribution_version
427
        }
428