1# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX
2# All rights reserved.
3#
4# This software is provided without warranty under the terms of the BSD
5# license included in LICENSE.txt and may be redistributed only
6# under the conditions described in the aforementioned license.  The license
7# is also available online at http://www.enthought.com/licenses/BSD.txt
8# Thanks for using Enthought open source!
9""" A simple plugin manager implementation. """
10
11
12from fnmatch import fnmatch
13import logging
14
15from traits.api import Event, HasTraits, Instance, List, Str, provides
16
17from .i_application import IApplication
18from .i_plugin import IPlugin
19from .i_plugin_manager import IPluginManager
20from .plugin_event import PluginEvent
21
22
23
24logger = logging.getLogger(__name__)
25
26
27@provides(IPluginManager)
28class PluginManager(HasTraits):
29    """ A simple plugin manager implementation.
30
31    This implementation manages an explicit collection of plugin instances,
32    e.g::
33
34        plugin_manager = PluginManager(
35             plugins = [
36                 MyPlugin(),
37                 YourPlugin()
38             ]
39        )
40
41    Plugins can be added and removed after construction time via the methods
42    'add_plugin' and 'remove_plugin'.
43
44    """
45
46    #### 'IPluginManager' protocol #############################################
47
48    # Fired when a plugin has been added to the manager.
49    plugin_added = Event(PluginEvent)
50
51    # Fired when a plugin has been removed from the manager.
52    plugin_removed = Event(PluginEvent)
53
54    #### 'PluginManager' protocol ##############################################
55
56    # The application that the plugin manager is part of.
57    application = Instance(IApplication)
58    def _application_changed(self, trait_name, old, new):
59        """ Static trait change handler. """
60
61        self._update_plugin_application([], self._plugins)
62
63        return
64
65    # An optional list of the Ids of the plugins that are to be excluded by
66    # the manager.
67    #
68    # Each item in the list is actually an 'fnmatch' expression.
69    exclude = List(Str)
70
71    # An optional list of the Ids of the plugins that are to be included by
72    # the manager (i.e. *only* plugins with Ids in this list will be added to
73    # the manager).
74    #
75    # Each item in the list is actually an 'fnmatch' expression.
76    include = List(Str)
77
78    #### 'object' protocol #####################################################
79
80    def __init__(self, plugins=None, **traits):
81        """ Constructor.
82
83        We allow the caller to specify an initial list of plugins, but the
84        list itself is not part of the public API. To add and remove plugins
85        after construction, use the 'add_plugin' and 'remove_plugin' methods
86        respectively. The manager is also iterable, so to iterate over the
87        plugins use 'for plugin in plugin_manager'.
88
89        """
90
91        super(PluginManager, self).__init__(**traits)
92
93        if plugins is not None:
94            self._plugins = plugins
95
96        return
97
98    def __iter__(self):
99        """ Return an iterator over the manager's plugins. """
100
101        plugins = [
102            plugin for plugin in self._plugins
103
104            if self._include_plugin(plugin.id)
105        ]
106
107        return iter(plugins)
108
109    #### 'IPluginManager' protocol #############################################
110
111    def add_plugin(self, plugin):
112        """ Add a plugin to the manager. """
113
114        self._plugins.append(plugin)
115        self.plugin_added = PluginEvent(plugin=plugin)
116
117        return
118
119    def get_plugin(self, plugin_id):
120        """ Return the plugin with the specified Id. """
121
122        for plugin in self._plugins:
123            if plugin_id == plugin.id:
124                if not self._include_plugin(plugin.id):
125                    plugin = None
126
127                break
128
129        else:
130            plugin = None
131
132        return plugin
133
134    def remove_plugin(self, plugin):
135        """ Remove a plugin from the manager. """
136
137        self._plugins.remove(plugin)
138        self.plugin_removed = PluginEvent(plugin=plugin)
139
140        return
141
142    def start(self):
143        """ Start the plugin manager. """
144
145        for plugin in self._plugins:
146            self.start_plugin(plugin)
147
148        return
149
150    def start_plugin(self, plugin=None, plugin_id=None):
151        """ Start the specified plugin. """
152
153        plugin = plugin or self.get_plugin(plugin_id)
154        if plugin is not None:
155            logger.debug('plugin %s starting', plugin.id)
156            plugin.activator.start_plugin(plugin)
157            logger.debug('plugin %s started', plugin.id)
158
159        else:
160            raise SystemError('no such plugin %s' % plugin_id)
161
162        return
163
164    def stop(self):
165        """ Stop the plugin manager. """
166
167        # We stop the plugins in the reverse order that they were started.
168        stop_order = self._plugins[:]
169        stop_order.reverse()
170
171        for plugin in stop_order:
172            self.stop_plugin(plugin)
173
174        return
175
176    def stop_plugin(self, plugin=None, plugin_id=None):
177        """ Stop the specified plugin. """
178
179        plugin = plugin or self.get_plugin(plugin_id)
180        if plugin is not None:
181            logger.debug('plugin %s stopping', plugin.id)
182            plugin.activator.stop_plugin(plugin)
183            logger.debug('plugin %s stopped', plugin.id)
184
185        else:
186            raise SystemError('no such plugin %s' % plugin_id)
187
188        return
189
190    #### Protected 'PluginManager' #############################################
191
192    # The plugins that the manager manages!
193    _plugins = List(IPlugin)
194    def __plugins_changed(self, trait_name, old, new):
195        """ Static trait change handler. """
196
197        self._update_plugin_application(old, new)
198
199        return
200
201    def __plugins_items_changed(self, trait_name, old, new):
202        """ Static trait change handler. """
203
204        self._update_plugin_application(new.removed, new.added)
205
206        return
207
208    def _include_plugin(self, plugin_id):
209        """ Return True if the plugin should be included.
210
211        This is just shorthand for:-
212
213        if self._is_included(plugin_id) and not self._is_excluded(plugin_id):
214            ...
215
216        """
217
218        return self._is_included(plugin_id) and not self._is_excluded(plugin_id)
219
220    #### Private protocol ######################################################
221
222    def _is_excluded(self, plugin_id):
223        """ Return True if the plugin Id is excluded.
224
225        If no 'exclude' patterns are specified then this method returns False
226        for all plugin Ids.
227
228        """
229
230        if len(self.exclude) == 0:
231            return False
232
233        for pattern in self.exclude:
234            if fnmatch(plugin_id, pattern):
235                return True
236
237        return False
238
239    def _is_included(self, plugin_id):
240        """ Return True if the plugin Id is included.
241
242        If no 'include' patterns are specified then this method returns True
243        for all plugin Ids.
244
245        """
246
247        if len(self.include) == 0:
248            return True
249
250        for pattern in self.include:
251            if fnmatch(plugin_id, pattern):
252                return True
253
254        return False
255
256    def _update_plugin_application(self, removed, added):
257        """ Update the 'application' trait of plugins added/removed. """
258
259        for plugin in removed:
260            plugin.application = None
261
262        for plugin in added:
263            plugin.application = self.application
264
265        return
266
267#### EOF ######################################################################
268