Completed
Push — master ( 8ab379...373224 )
by Daniel
01:05
created

GwBasePattern.__init__()   D

Complexity

Conditions 8

Size

Total Lines 47

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 1 Features 0
Metric Value
cc 8
c 2
b 1
f 0
dl 0
loc 47
rs 4.3478
1
"""
2
    gw_base_pattern provides all basic classes and functions, which are needed by any kind of groundwork plugin or
3
    pattern.
4
5
    It mostly cares about the correct activation and deactivation. Including sending signals to inform
6
    other patterns or plugins about status changes of a plugin.
7
8
"""
9
import logging
10
from groundwork.patterns.exceptions import PluginAttributeMissing, PluginActivateMissing, PluginDeactivateMissing, \
11
    PluginDependencyLoop
12
13
14
class GwBasePattern(object):
15
    """
16
    Base pattern class for all plugins and patterns.
17
18
    Usage::
19
20
        from groundwork.patterns import GwBasePattern
21
22
        class MyPlugin(GwBasePattern):
23
            def __init__(self, *args, **kwargs):
24
                super().__init__(*args, **kwargs)
25
26
            def activate():
27
                self.signals.register("MySignal", "My description about signal)
28
29
            def deactivate():
30
                self.signals.unregister("MySignal")
31
32
    :param app: groundwork application object, for which the plugin shall be initialised.
33
    :type app: :class:`groundwork.App`.
34
    :param name: Unique name. Normally set by plugin.
35
    """
36
    def __init__(self, app, *args, name=None, **kwargs):
37
        #: groundwork application instance. Access it inside a plugin via ``self.app``.
38
        self.app = app
39
40
        # There must be a name for this plugin. Otherwise it is not detectable and manageable on application level
41
        if not hasattr(self, "name") and name is None:
42
            raise PluginAttributeMissing("Name not set for plugin")
43
        elif not hasattr(self, "name") and name is not None:
44
            # Set the given name, if the plugin itself does not have set one
45
            self.name = name
46
47
        # Be sure we have a set version number. Can be overwritten by the plugin itself.
48
        if not hasattr(self, "version"):
49
            self.version = "0.0.1"
50
51
        # Let's be sure active is false, even if a child class set something different
52
        if not hasattr(self, "active"):
53
            self.active = False
54
55
        # Even if this plugin has no dependencies to other plugins, we should set an empty tuple.
56
        if not hasattr(self, "needed_plugins"):
57
            #: Variable for storing dependencies to other plugins.
58
            #: Tuple must contains needed plugin names.
59
            #: needed_plugins = ("MyPlugin", "MyPlugin2)
60
            self.needed_plugins = ()
61
62
        #: A logger, especially created for this plugin. Usage inside a plugin: ``self.log.warn("WARNING!!")``.
63
        #:
64
        #: The logger name is the same as the plugin name. Therefor it is possible to configure the application logging
65
        #: to show log messages of a specif plugin only. See :ref:`plugin_logging`
66
        self.log = logging.getLogger(self.name)
67
68
        #: Instance of :class:`.SignalsPlugin`.
69
        #: Provides functions to register and manage signals and retrievers.
70
        #:
71
        #: All action takes place in the context of this plugin. For instance a ``self.signals.get()`` will return
72
        #: signals of this plugin only. To get all signals of an application, please use ``self.app.signals.get()``.
73
        self.signals = SignalsPlugin(self)
74
75
        # This is used as flag for the pluginManager to be sure, that an initiated class has called the __init__()
76
        # routine of GwPluginPatter
77
        self._plugin_base_initialised = True
78
79
        # Let's tell the pluginManager that this plugin got initialised, so that it gets tracked on app level.
80
        # This is needed if this class gets initiated by hand and the function self.app.plugins._load() was not used
81
        # for doing this job.
82
        self.app.plugins._register_initialisation(self)
83
84
    # def __getattribute__(self, name):
85
    def __getattribute__(self, name):
86
        """
87
        Catches all calls on class attributes, but cares only for activate() and deactivate().
88
89
        If the plugin gets activated or deactivated, the base class can perform extra work.
90
        So there is no need for a plugin developer to call something like super().activate() for
91
        his/her plugin. This gets done automatically.
92
        """
93
94
        attr = object.__getattribute__(self, name)
95
        if hasattr(attr, '__call__'):
96
            if hasattr(attr, "__name__"):
97
                if attr.__name__ == "activate":
98
                    def newfunc(*args, **kwargs):
99
                        self._pre_activate_injection()
100
                        result = attr(*args, **kwargs)
101
                        self._post_activate_injection()
102
                        return result
103
                    return newfunc
104
                elif attr.__name__ == "deactivate":
105
                    def newfunc(*args, **kwargs):
106
                        self._pre_deactivate_injection()
107
                        result = attr(*args, **kwargs)
108
                        self._post_deactivate_injection()
109
                        return result
110
                    return newfunc
111
112
        return attr
113
114
    def activate(self):
115
        """
116
        Must be overwritten by the plugin class itself.
117
        """
118
        self.log.warn("No activation routine in Plugin defined. Define self.activate() in plugin %s" % self.name)
119
        raise PluginActivateMissing("plugin must provide an activation routine by itself.")
120
121
    def deactivate(self):
122
        """
123
        Must be overwritten by the plugin class itself.
124
        """
125
        self.log.warn("No activation routine in Plugin defined. Define self.deactivate() in plugin %s" % self.name)
126
        raise PluginDeactivateMissing("plugin must provide an deactivation routine by itself.")
127
128
    def _pre_activate_injection(self):
129
        """
130
        Injects functions before the activation routine of child classes gets called
131
        """
132
        # Let's be sure that this plugins class is registered and available on application level under
133
        # application.plugins.classes. This allows to reuse this class for *new* plugins.
134
        if not self.app.plugins.classes.exist(self.__class__.__name__):
135
            self.app.plugins.classes.register([self.__class__])
136
137
        self._load_needed_plugins()
138
139
        self.app.signals.send("plugin_activate_pre", self)
140
141
    def _post_activate_injection(self):
142
        """
143
        Injects functions after the activation routine of child classes got called
144
        :return: None
145
        """
146
        self.active = True
147
        self.app.signals.send("plugin_activate_post", self)
148
149
    def _pre_deactivate_injection(self):
150
        """
151
        Injects functions before the deactivation routine of child classes gets called
152
        :return: None
153
        """
154
        self.app.signals.send("plugin_deactivate_pre", self)
155
156
    def _post_deactivate_injection(self):
157
        """
158
        Injects functions after the deactivation routine of child classes got called
159
        :return: None
160
        """
161
        # Lets be sure that active is really set to false.
162
        self.active = False
163
        self.app.signals.send("plugin_deactivate_post", self)
164
        # After all receivers are handled. We start to clean up signals and receivers of this plugin
165
        # Attention: This signal clean must not be called via a signal (like in other patterns),
166
        # because the call order of receivers is not clear and a signal/receiver clean up would prohibit the call
167
        # of all "later" receivers.
168
        self.signals.deactivate_plugin_signals()
169
170
    def _load_needed_plugins(self):
171
        """
172
        Checks if this plugins needs other plugins to work and tries to activate them
173
        :return: True, if all needed plugins are or got activated. Otherwise False
174
        """
175
        global plugin_recursive_store
176
        if "plugin_recursive_store" not in globals():
177
            plugin_recursive_store = []
178
179
        if self.name in plugin_recursive_store:
180
            self.log.warning("Plugin dependency loop detected: %s already checked and dependencies got activated" %
181
                             self.name)
182
            if self.app.strict:
183
                raise PluginDependencyLoop("Plugin dependency loop detected: %s already checked and dependencies "
184
                                           "got activated" % self.name)
185
            return False
186
        else:
187
            plugin_recursive_store.append(self.name)
188
189
        if not hasattr(self, "needed_plugins"):
190
            pass
191
        elif not isinstance(self.needed_plugins, tuple) or isinstance(self.needed_plugins, list):
192
            raise TypeError("needed_plugins must be a tuple or a list")
193
        elif len(self.needed_plugins) > 0:
194
            try:
195
                for needed_plugin in self.needed_plugins:
196
                    if not isinstance(needed_plugin, str):
197
                        raise TypeError("Plugin name must be a string, got %s" % type(needed_plugin).__name__)
198
                    # Check, if a plugin with this name got already activated
199
                    plugin = self.app.plugins.get(needed_plugin)
200
                    if plugin is not None and not plugin.active:
201
                        plugin.activate()
202
                    # If not, check if a plugin_class with this name is available and activate it
203
                    plugin_class = self.app.plugins.classes.get(needed_plugin)
204
                    if plugin_class is not None:
205
                        plugin_class(self.app, needed_plugin)
206
            except Exception:
207
                plugin_recursive_store.remove(self.name)
208
                return False
209
210
        plugin_recursive_store.remove(self.name)
211
        return True
212
213
214
class SignalsPlugin:
215
    """
216
    Signal and Receiver management class on plugin level.
217
    This class gets initiated once per plugin.
218
219
    Mostly delegates function calls to the :class:`groundwork.signals.SignalListApplication` instance on application
220
    level.
221
222
    :param plugin: The plugin, which wants to use signals
223
    :type plugin: GwBasePattern
224
    """
225
226
    def __init__(self, plugin):
227
        self._plugin = plugin
228
        self.__app = plugin.app
229
        self.__log = plugin.log
230
        self.__log.info("Plugin messages initialised")
231
232
    def deactivate_plugin_signals(self):
233
        receivers = self.get_receiver()
234
        for receiver in receivers.keys():
235
            self.disconnect(receiver)
236
237
        signals = self.get()
238
        for signal in signals:
239
            self.unregister(signal)
240
241
    def register(self, signal, description):
242
        """
243
        Registers a new signal.
244
        Only registered signals are allowed to be send.
245
246
        :param signal: Unique name of the signal
247
        :param description: Description of the reason or use case, why this signal is needed.
248
                            Used for documentation.
249
        """
250
        return self.__app.signals.register(signal, self._plugin, description)
251
252
    def unregister(self, signal):
253
        return self.__app.signals.unregister(signal)
254
255
    def connect(self, receiver, signal, function, description, sender=None):
256
        """
257
        Connect a receiver to a signal
258
259
        :param receiver: Name of the receiver
260
        :type receiver: str
261
        :param signal: Name of the signal. Must already be registered!
262
        :type signal: str
263
        :param function: Callable functions, which shall be executed, of signal is send.
264
        :param description: Description of the reason or use case, why this connection is needed.
265
                            Used for documentation.
266
        """
267
        return self.__app.signals.connect(receiver, signal, function, self._plugin, description, sender)
268
269
    def disconnect(self, receiver):
270
        """
271
        Disconnect a receiver from a signal.
272
        Receiver must exist, otherwise an exception is thrown.
273
274
        :param receiver: Name of the receiver
275
        """
276
        return self.__app.signals.disconnect(receiver)
277
278
    def send(self, signal, **kwargs):
279
        """
280
        Sends a signal for the given plugin.
281
282
        :param signal: Name of the signal
283
        :type signal: str
284
        """
285
        return self.__app.signals.send(signal, plugin=self._plugin, **kwargs)
286
287
    def get(self, signal=None):
288
        """
289
        Returns a single signal or a dictionary of signals for this plugin.
290
        """
291
        return self.__app.signals.get(signal, self._plugin)
292
293
    def get_receiver(self, receiver=None):
294
        """
295
        Returns a single receiver or a dictionary of receivers for this plugin.
296
        """
297
        return self.__app.signals.get_receiver(receiver, self._plugin)
298