plugin_manager.py 11.7 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
mathieui's avatar
mathieui committed
11
import logging
12
from gettext import gettext as _
13
from sys import version_info
14

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__)

mathieui's avatar
mathieui committed
22 23
load_path = []

24 25 26 27 28
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')
29
plugins_dir = os.path.expanduser(plugins_dir)
30

mathieui's avatar
mathieui committed
31 32 33 34 35 36 37
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)
38

39
try:
40
    os.makedirs(plugins_dir)
41 42
except OSError:
    pass
mathieui's avatar
mathieui committed
43 44
else:
    load_path.append(plugins_dir)
45

46
try:
47
    os.makedirs(plugins_conf_dir)
48 49
except OSError:
    pass
mathieui's avatar
mathieui committed
50

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

mathieui's avatar
mathieui committed
53 54 55 56 57 58 59 60 61
if os.path.exists(default_plugin_path):
    load_path.append(default_plugin_path)

try:
    import poezio_plugins
except:
    pass
else:
    if poezio_plugins.__path__:
62
        load_path.append(list(poezio_plugins.__path__)[0])
mathieui's avatar
mathieui committed
63

64 65 66 67
if version_info[1] >= 3: # 3.3 & >
    from importlib import machinery
    finder = machinery.PathFinder()

68
class PluginManager(object):
mathieui's avatar
mathieui committed
69 70 71 72 73
    """
    Plugin Manager
    Contains all the references to the plugins
    And keeps track of everything the plugin has done through the API.
    """
74 75
    def __init__(self, core):
        self.core = core
76 77 78 79
        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
80
        self.tab_commands = {} #module name -> dict of tab types; tab type -> commands loaded by the module
mathieui's avatar
mathieui committed
81
        self.keys = {} # module name → dict of keys/handlers loaded for the module
82
        self.tab_keys = {} #module name → dict of tab types; tab type → list of keybinds (tuples)
83 84
        self.roster_elements = {}
        self.plugin_api = PluginAPI(core, self)
85

mathieui's avatar
mathieui committed
86 87 88 89 90 91 92
    def disable_plugins(self):
        for plugin in set(self.plugins.keys()):
            try:
                self.unload(plugin)
            except:
                pass

93
    def load(self, name, notify=True):
mathieui's avatar
mathieui committed
94 95 96
        """
        Load a plugin.
        """
97
        if name in self.plugins:
98
            self.unload(name)
99 100

        try:
101 102 103 104 105 106
            module = None
            if version_info[1] < 3: # < 3.3
                if name in self.modules:
                    imp.acquire_lock()
                    module = imp.reload(self.modules[name])
                else:
mathieui's avatar
mathieui committed
107
                    file, filename, info = imp.find_module(name, load_path)
108 109 110
                    imp.acquire_lock()
                    module = imp.load_module(name, file, filename, info)
            else: # 3.3 & >
111
                loader = finder.find_module(name, load_path)
112
                if not loader:
louiz’'s avatar
louiz’ committed
113
                    self.core.information('Could not find plugin')
114 115 116
                    return
                module = loader.load_module()

117
        except Exception as e:
118
            import traceback
119 120
            log.debug("Could not load plugin %s: \n%s", name, traceback.format_exc())
            self.core.information("Could not load plugin %s: %s" % (name, e), 'Error')
121
        finally:
122
            if version_info[1] < 3 and imp.lock_held():
123
                imp.release_lock()
124 125
            if not module:
                return
126 127 128

        self.modules[name] = module
        self.commands[name] = {}
mathieui's avatar
mathieui committed
129
        self.keys[name] = {}
130
        self.tab_keys[name] = {}
131
        self.tab_commands[name] = {}
132
        self.event_handlers[name] = []
133
        self.plugins[name] = module.Plugin(self.plugin_api, self.core, plugins_conf_dir)
134 135
        if notify:
            self.core.information('Plugin %s loaded' % name, 'Info')
136

137
    def unload(self, name, notify=True):
138 139
        if name in self.plugins:
            try:
140 141
                for command in self.commands[name].keys():
                    del self.core.commands[command]
mathieui's avatar
mathieui committed
142 143
                for key in self.keys[name].keys():
                    del self.core.key_func[key]
144
                for tab in list(self.tab_commands[name].keys()):
145
                    for command in self.tab_commands[name][tab][:]:
146 147
                        self.del_tab_command(name, getattr(tabs, tab), command[0])
                    del self.tab_commands[name][tab]
148
                for tab in list(self.tab_keys[name].keys()):
149
                    for key in self.tab_keys[name][tab][:]:
150 151
                        self.del_tab_key(name, getattr(tabs, tab), key[0])
                    del self.tab_keys[name][tab]
152
                for event_name, handler in self.event_handlers[name][:]:
153
                    self.del_event_handler(name, event_name, handler)
154

155 156
                self.plugins[name].unload()
                del self.plugins[name]
157
                del self.commands[name]
mathieui's avatar
mathieui committed
158
                del self.keys[name]
159
                del self.tab_commands[name]
160
                del self.event_handlers[name]
161 162
                if notify:
                    self.core.information('Plugin %s unloaded' % name, 'Info')
163
            except Exception as e:
164
                import traceback
165 166
                log.debug("Could not unload plugin: \n%s", traceback.format_exc())
                self.core.information("Could not unload plugin: %s" % e, 'Error')
167

168
    def add_command(self, module_name, name, handler, help, completion=None, short='', usage=''):
mathieui's avatar
mathieui committed
169 170 171 172 173 174 175
        """
        Add a global command.
        """
        if name in self.core.commands:
            raise Exception(_("Command '%s' already exists") % (name,))

        commands = self.commands[module_name]
176 177
        commands[name] = core.Command(handler, help, completion, short, usage)
        self.core.commands[name] = commands[name]
mathieui's avatar
mathieui committed
178

179
    def del_command(self, module_name, name):
mathieui's avatar
mathieui committed
180 181 182
        """
        Remove a global command added through add_command.
        """
183 184 185 186 187
        if name in self.commands[module_name]:
            del self.commands[module_name][name]
            if name in self.core.commands:
                del self.core.commands[name]

188
    def add_tab_command(self, module_name, tab_type, name, handler, help, completion=None, short='', usage=''):
mathieui's avatar
mathieui committed
189 190 191
        """
        Add a command only for a type of Tab.
        """
192 193
        commands = self.tab_commands[module_name]
        t = tab_type.__name__
mathieui's avatar
mathieui committed
194 195
        if name in tab_type.plugin_commands:
            return
196 197 198
        if not t in commands:
            commands[t] = []
        commands[t].append((name, handler, help, completion))
199
        tab_type.plugin_commands[name] = core.Command(handler, help, completion, short, usage)
200 201
        for tab in self.core.tabs:
            if isinstance(tab, tab_type):
mathieui's avatar
mathieui committed
202
                tab.update_commands()
203 204

    def del_tab_command(self, module_name, tab_type, name):
mathieui's avatar
mathieui committed
205 206 207
        """
        Remove a command added through add_tab_command.
        """
208 209 210 211 212 213 214 215 216 217 218 219
        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]

220
    def add_tab_key(self, module_name, tab_type, key, handler):
mathieui's avatar
mathieui committed
221 222 223
        """
        Associate a key binding to a handler only for a type of Tab.
        """
224 225 226 227 228 229 230 231 232 233 234 235 236
        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
237 238 239
        """
        Remove a key binding added through add_tab_key.
        """
240 241 242 243 244 245 246 247 248 249 250 251
        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
252
    def add_key(self, module_name, key, handler):
mathieui's avatar
mathieui committed
253 254 255 256
        """
        Associate a global key binding to a handler, except if it
        already exists.
        """
mathieui's avatar
mathieui committed
257 258 259 260 261 262 263
        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
264 265 266
        """
        Remove a global key binding added by a plugin.
        """
mathieui's avatar
mathieui committed
267 268 269 270 271
        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]

272
    def add_event_handler(self, module_name, event_name, handler, position=0):
mathieui's avatar
mathieui committed
273 274 275 276
        """
        Add an event handler. If event_name isn’t in the event list, assume
        it is a sleekxmpp event.
        """
277 278
        eh = self.event_handlers[module_name]
        eh.append((event_name, handler))
279 280 281 282
        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)
283 284

    def del_event_handler(self, module_name, event_name, handler):
mathieui's avatar
mathieui committed
285 286 287
        """
        Remove an event handler if it exists.
        """
288 289 290 291
        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)
292 293
        eh = self.event_handlers[module_name]
        eh = list(filter(lambda e : e != (event_name, handler), eh))
294 295 296 297 298 299 300

    def completion_load(self, the_input):
        """
        completion function that completes the name of the plugins, from
        all .py files in plugins_dir
        """
        try:
mathieui's avatar
mathieui committed
301 302 303 304 305 306 307
            names = set()
            for path in load_path:
                try:
                    add = set(os.listdir(path))
                    names |= add
                except:
                    pass
308 309 310
        except OSError as e:
            self.core.information(_('Completion failed: %s' % e), 'Error')
            return
mathieui's avatar
mathieui committed
311 312 313
        plugins_files = [name[:-3] for name in names if name.endswith('.py')
                and name != '__init__.py' and not name.startswith('.')]
        plugins_files.sort()
314
        return the_input.new_completion(plugins_files, 1, '', quotify=False)
315 316 317 318 319

    def completion_unload(self, the_input):
        """
        completion function that completes the name of the plugins that are loaded
        """
320
        return the_input.new_completion(sorted(self.plugins.keys()), 1, '', quotify=False)
321 322 323

    def on_plugins_dir_change(self, new_value):
        global plugins_dir
324 325 326
        if plugins_dir in load_path:
            load_path.remove(plugins_dir)
        load_path.insert(0, new_value)
327
        plugins_dir = new_value
mathieui's avatar
mathieui committed
328 329 330 331

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