plugin_manager.py 10.9 KB
Newer Older
mathieui's avatar
mathieui committed
1
2
3
4
5
6
7
"""
Plugin manager module.
Define the PluginManager class, the one that glues all the plugins and
the API together. Defines also a bunch of variables related to the
plugin env.
"""

8
import imp
9
import os
mathieui's avatar
mathieui committed
10
from os import path
11
import sys
mathieui's avatar
mathieui committed
12
import logging
13
14
from gettext import gettext as _

15
import core
mathieui's avatar
mathieui committed
16
import tabs
17
from plugin import PluginAPI
mathieui's avatar
mathieui committed
18
19
from config import config

mathieui's avatar
mathieui committed
20
21
log = logging.getLogger(__name__)

22
23
24
25
26
plugins_dir = config.get('plugins_dir', '')
plugins_dir = plugins_dir or\
    os.path.join(os.environ.get('XDG_DATA_HOME') or\
                     os.path.join(os.environ.get('HOME'), '.local', 'share'),
                 'poezio', 'plugins')
27
plugins_dir = os.path.expanduser(plugins_dir)
28

mathieui's avatar
mathieui committed
29
30
31
32
33
34
35
plugins_conf_dir = config.get('plugins_conf_dir', '')
if not plugins_conf_dir:
    config_home = os.environ.get('XDG_CONFIG_HOME')
    if not config_home:
        config_home = os.path.join(os.environ.get('HOME'), '.config')
    plugins_conf_dir = os.path.join(config_home, 'poezio', 'plugins')
plugins_conf_dir = os.path.expanduser(plugins_conf_dir)
36

37
38
39
40
41
try:
    os.makedirs(plugins_dir)
except OSError:
    pass

42
43
44
45
try:
    os.makedirs(plugins_conf_dir)
except OSError:
    pass
mathieui's avatar
mathieui committed
46

mathieui's avatar
mathieui committed
47
default_plugin_path = path.join(path.dirname(path.dirname(__file__)), 'plugins')
48

mathieui's avatar
mathieui committed
49
sys.path.append(default_plugin_path)
50
sys.path.append(plugins_dir)
51

52
class PluginManager(object):
mathieui's avatar
mathieui committed
53
54
55
56
57
    """
    Plugin Manager
    Contains all the references to the plugins
    And keeps track of everything the plugin has done through the API.
    """
58
59
    def __init__(self, core):
        self.core = core
60
61
62
63
        self.modules = {} # module name -> module object
        self.plugins = {} # module name -> plugin object
        self.commands = {} # module name -> dict of commands loaded for the module
        self.event_handlers = {} # module name -> list of event_name/handler pairs loaded for the module
64
        self.tab_commands = {} #module name -> dict of tab types; tab type -> commands loaded by the module
mathieui's avatar
mathieui committed
65
        self.keys = {} # module name → dict of keys/handlers loaded for the module
66
        self.tab_keys = {} #module name → dict of tab types; tab type → list of keybinds (tuples)
67
68
        self.roster_elements = {}
        self.plugin_api = PluginAPI(core, self)
69

mathieui's avatar
mathieui committed
70
71
72
73
74
75
76
    def disable_plugins(self):
        for plugin in set(self.plugins.keys()):
            try:
                self.unload(plugin)
            except:
                pass

77
    def load(self, name, notify=True):
mathieui's avatar
mathieui committed
78
79
80
        """
        Load a plugin.
        """
81
        if name in self.plugins:
82
            self.unload(name)
83
84

        try:
85
86
87
88
89
90
91
92
93
            if name in self.modules:
                imp.acquire_lock()
                module = imp.reload(self.modules[name])
                imp.release_lock()
            else:
                file, filename, info = imp.find_module(name, [plugins_dir, default_plugin_path])
                imp.acquire_lock()
                module = imp.load_module(name, file, filename, info)
                imp.release_lock()
94
        except Exception as e:
95
            import traceback
mathieui's avatar
mathieui committed
96
97
            log.debug("Could not load plugin: \n%s", traceback.format_exc())
            self.core.information("Could not load plugin: %s" % e, 'Error')
98
            return
99
100
101
        finally:
            if imp.lock_held():
                imp.release_lock()
102
103
104

        self.modules[name] = module
        self.commands[name] = {}
mathieui's avatar
mathieui committed
105
        self.keys[name] = {}
106
        self.tab_keys[name] = {}
107
        self.tab_commands[name] = {}
108
        self.event_handlers[name] = []
109
        self.plugins[name] = module.Plugin(self.plugin_api, self.core, plugins_conf_dir)
110
111
        if notify:
            self.core.information('Plugin %s loaded' % name, 'Info')
112

113
    def unload(self, name, notify=True):
114
115
        if name in self.plugins:
            try:
116
117
                for command in self.commands[name].keys():
                    del self.core.commands[command]
mathieui's avatar
mathieui committed
118
119
                for key in self.keys[name].keys():
                    del self.core.key_func[key]
120
                for tab in list(self.tab_commands[name].keys()):
121
                    for command in self.tab_commands[name][tab][:]:
122
123
                        self.del_tab_command(name, getattr(tabs, tab), command[0])
                    del self.tab_commands[name][tab]
124
                for tab in list(self.tab_keys[name].keys()):
125
                    for key in self.tab_keys[name][tab][:]:
126
127
                        self.del_tab_key(name, getattr(tabs, tab), key[0])
                    del self.tab_keys[name][tab]
128
                for event_name, handler in self.event_handlers[name][:]:
129
                    self.del_event_handler(name, event_name, handler)
130

131
132
                self.plugins[name].unload()
                del self.plugins[name]
133
                del self.commands[name]
mathieui's avatar
mathieui committed
134
                del self.keys[name]
135
                del self.tab_commands[name]
136
                del self.event_handlers[name]
137
138
                if notify:
                    self.core.information('Plugin %s unloaded' % name, 'Info')
139
            except Exception as e:
140
                import traceback
141
142
                log.debug("Could not unload plugin: \n%s", traceback.format_exc())
                self.core.information("Could not unload plugin: %s" % e, 'Error')
143

144
    def add_command(self, module_name, name, handler, help, completion=None, short='', usage=''):
mathieui's avatar
mathieui committed
145
146
147
148
149
150
151
        """
        Add a global command.
        """
        if name in self.core.commands:
            raise Exception(_("Command '%s' already exists") % (name,))

        commands = self.commands[module_name]
152
153
        commands[name] = core.Command(handler, help, completion, short, usage)
        self.core.commands[name] = commands[name]
mathieui's avatar
mathieui committed
154

155
    def del_command(self, module_name, name):
mathieui's avatar
mathieui committed
156
157
158
        """
        Remove a global command added through add_command.
        """
159
160
161
162
163
        if name in self.commands[module_name]:
            del self.commands[module_name][name]
            if name in self.core.commands:
                del self.core.commands[name]

164
    def add_tab_command(self, module_name, tab_type, name, handler, help, completion=None, short='', usage=''):
mathieui's avatar
mathieui committed
165
166
167
        """
        Add a command only for a type of Tab.
        """
168
169
        commands = self.tab_commands[module_name]
        t = tab_type.__name__
mathieui's avatar
mathieui committed
170
171
        if name in tab_type.plugin_commands:
            return
172
173
174
        if not t in commands:
            commands[t] = []
        commands[t].append((name, handler, help, completion))
175
        tab_type.plugin_commands[name] = core.Command(handler, help, completion, short, usage)
176
177
        for tab in self.core.tabs:
            if isinstance(tab, tab_type):
mathieui's avatar
mathieui committed
178
                tab.update_commands()
179
180

    def del_tab_command(self, module_name, tab_type, name):
mathieui's avatar
mathieui committed
181
182
183
        """
        Remove a command added through add_tab_command.
        """
184
185
186
187
188
189
190
191
192
193
194
195
        commands = self.tab_commands[module_name]
        t = tab_type.__name__
        if not t in commands:
            return
        for command in commands[t]:
            if command[0] == name:
                commands[t].remove(command)
                del tab_type.plugin_commands[name]
                for tab in self.core.tabs:
                    if isinstance(tab, tab_type) and name in tab.commands:
                        del tab.commands[name]

196
    def add_tab_key(self, module_name, tab_type, key, handler):
mathieui's avatar
mathieui committed
197
198
199
        """
        Associate a key binding to a handler only for a type of Tab.
        """
200
201
202
203
204
205
206
207
208
209
210
211
212
        keys = self.tab_keys[module_name]
        t = tab_type.__name__
        if key in tab_type.plugin_keys:
            return
        if not t in keys:
            keys[t] = []
        keys[t].append((key, handler))
        tab_type.plugin_keys[key] = handler
        for tab in self.core.tabs:
            if isinstance(tab, tab_type):
                tab.update_keys()

    def del_tab_key(self, module_name, tab_type, key):
mathieui's avatar
mathieui committed
213
214
215
        """
        Remove a key binding added through add_tab_key.
        """
216
217
218
219
220
221
222
223
224
225
226
227
        keys = self.tab_keys[module_name]
        t = tab_type.__name__
        if not t in keys:
            return
        for _key in keys[t]:
            if _key[0] == key:
                keys[t].remove(_key)
                del tab_type.plugin_keys[key]
                for tab in self.core.tabs:
                    if isinstance(tab, tab_type) and key in tab.key_func:
                        del tab.key_func[key]

mathieui's avatar
mathieui committed
228
    def add_key(self, module_name, key, handler):
mathieui's avatar
mathieui committed
229
230
231
232
        """
        Associate a global key binding to a handler, except if it
        already exists.
        """
mathieui's avatar
mathieui committed
233
234
235
236
237
238
239
        if key in self.core.key_func:
            raise Exception(_("Key '%s' already exists") % (key,))
        keys = self.keys[module_name]
        keys[key] = handler
        self.core.key_func[key] = handler

    def del_key(self, module_name, key):
mathieui's avatar
mathieui committed
240
241
242
        """
        Remove a global key binding added by a plugin.
        """
mathieui's avatar
mathieui committed
243
244
245
246
247
        if key in self.keys[module_name]:
            del self.keys[module_name][key]
            if key in self.core.key_func:
                del self.core.commands[key]

248
    def add_event_handler(self, module_name, event_name, handler, position=0):
mathieui's avatar
mathieui committed
249
250
251
252
        """
        Add an event handler. If event_name isn’t in the event list, assume
        it is a sleekxmpp event.
        """
253
254
        eh = self.event_handlers[module_name]
        eh.append((event_name, handler))
255
256
257
258
        if event_name in self.core.events.events:
            self.core.events.add_event_handler(event_name, handler, position)
        else:
            self.core.xmpp.add_event_handler(event_name, handler)
259
260

    def del_event_handler(self, module_name, event_name, handler):
mathieui's avatar
mathieui committed
261
262
263
        """
        Remove an event handler if it exists.
        """
264
265
266
267
        if event_name in self.core.events.events:
            self.core.events.del_event_handler(None, handler)
        else:
            self.core.xmpp.del_event_handler(event_name, handler)
268
269
        eh = self.event_handlers[module_name]
        eh = list(filter(lambda e : e != (event_name, handler), eh))
270
271
272
273
274
275
276

    def completion_load(self, the_input):
        """
        completion function that completes the name of the plugins, from
        all .py files in plugins_dir
        """
        try:
277
278
279
280
281
            try:
                names = set(os.listdir(default_plugin_path))
            except:
                names = set()
            names |= set(os.listdir(plugins_dir))
282
283
284
285
        except OSError as e:
            self.core.information(_('Completion failed: %s' % e), 'Error')
            return
        plugins_files = [name[:-3] for name in names if name.endswith('.py')]
286
        return the_input.auto_completion(plugins_files, '', quotify=False)
287
288
289
290
291

    def completion_unload(self, the_input):
        """
        completion function that completes the name of the plugins that are loaded
        """
292
        return the_input.auto_completion(list(self.plugins.keys()), '', quotify=False)
293
294
295
296
297
298
299

    def on_plugins_dir_change(self, new_value):
        global plugins_dir
        if plugins_dir in sys.path:
            sys.path.remove(plugins_dir)
        sys.path.append(new_value)
        plugins_dir = new_value
mathieui's avatar
mathieui committed
300
301
302
303

    def on_plugins_conf_dir_change(self, new_value):
        global plugins_conf_dir
        plugins_conf_dir = new_value