plugins.py 8.2 KB
Newer Older
1
2
3
4
# -*- coding: utf-8 -*-
#
#   plugins.py — Plugin loader
#
Arno Töll's avatar
Arno Töll committed
5
#   This file is part of debexpo - https://alioth.debian.org/projects/debexpo/
6
#
Jonny Lamb's avatar
Jonny Lamb committed
7
#   Copyright © 2008 Jonny Lamb <jonny@debian.org>
8
#   Copyright © 2010 Jan Dittberner <jandd@debian.org>
Clément Schreiner's avatar
Clément Schreiner committed
9
#   Copyright © 2012 Clément Schreiner <clement@mux.me>
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#
#   Permission is hereby granted, free of charge, to any person
#   obtaining a copy of this software and associated documentation
#   files (the "Software"), to deal in the Software without
#   restriction, including without limitation the rights to use,
#   copy, modify, merge, publish, distribute, sublicense, and/or sell
#   copies of the Software, and to permit persons to whom the
#   Software is furnished to do so, subject to the following
#   conditions:
#
#   The above copyright notice and this permission notice shall be
#   included in all copies or substantial portions of the Software.
#
#   THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
#   EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
#   OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
#   NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
#   HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
#   WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
#   FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
#   OTHER DEALINGS IN THE SOFTWARE.

"""
Holds the plugin loader.
"""

__author__ = 'Jonny Lamb'
Clément Schreiner's avatar
Clément Schreiner committed
37
38
39
40
41
__copyright__ = ', '.join([
    'Copyright © 2008 Jonny Lamb',
    'Copyright © 2010 Jan Dittberner',
    'Copyright © 2012 Clément Schreiner',
    ])
42
43
44
45
46
__license__ = 'MIT'

import logging
import os
import shutil
47
import sys
48
import traceback
49
from collections import namedtuple
50

51
import pylons
52

Clément Schreiner's avatar
Clément Schreiner committed
53
from debexpo.model import meta
Clément Schreiner's avatar
Clément Schreiner committed
54

Clément Schreiner's avatar
Clément Schreiner committed
55

56
57
log = logging.getLogger(__name__)

58
59
60

PluginModule = namedtuple('PluginModule', ['name', 'stage', 'plugin', 'models'])

Clément Schreiner's avatar
Clément Schreiner committed
61

62
class Plugins(object):
Clément Schreiner's avatar
Clément Schreiner committed
63
64
65
    """
    Plugin loader.
    """
66

Clément Schreiner's avatar
Clément Schreiner committed
67
    # FIXME: remove changes and changes_file arguments
68
    # (once they're accessible from the git storage backend, that is)
69
    def __init__(self, type, package_version, changes=None, changes_file=None, **kw):
70
71
72
73
        """
        Class constructor. Sets class attributes and then runs the plugins.

        ``type``
74
            Type of plugins to import (for example 'qa'.)
75

Clément Schreiner's avatar
Clément Schreiner committed
76
77
78
        ``package_version``
            PackageVersion instance of the package we load plugins for

79
        ``changes``
Clément Schreiner's avatar
Clément Schreiner committed
80
            Changes class for the package to test. Needed for the importer
81
82
83

        ``changes_file``
            Name of the changes file.
84

85
86
        ``kw``
            Extra information for plugins to have.
87
        """
88
        self.config = pylons.config
89
90
91
        self.type = type.replace('-', '_')
        self.changes = changes
        self.changes_file = changes_file
92

93
        self.result_objects = []
94
        self.tempdir = None
95
        self.kw = kw
96

97
98
        self.package_version = package_version

99
        self.modules = self.import_plugins(self.type)
100

101
102
        # the plugin instances
        self.plugins = {}
103

104
105
    @classmethod
    def _import_plugin(cls, name):
106
107
108
109
110
111
112
113
114
115
        """
        Imports a module and returns it.

        This method was stolen from:
            http://docs.python.org/lib/built-in-funcs.html

        ``name``
            Module to import.
        """
        try:
Christoph Haas's avatar
Christoph Haas committed
116
            log.debug('Trying to import plugin: %s', name)
117
118
119
120
            mod = __import__(name)
            components = name.split('.')
            for comp in components[1:]:
                mod = getattr(mod, comp)
Christoph Haas's avatar
Christoph Haas committed
121
            log.debug('Import succeeded.')
122
            return mod
Christoph Haas's avatar
Christoph Haas committed
123
        except ImportError, e:
124
            if str(e).startswith('No module named'):
125
126
127
128
129
130
131
                # Not fatal: the plugin module was not found at this location
                # (might be okay because plugins are looked for in several locations)
                log.debug('Import failed - module not found: %s', e)
            else:
                # The module was found but failed to import for other reasons.
                # It's worth looking into why importing failed.
                log.warn('Import of module "%s" failed with error: %s', name, e)
132
133
            return None

134
    @classmethod
135
    def _what_plugins(cls, stage):
136
        """
137
        List of the plugins that must be run.
138
        """
139

140
141
142
        config = pylons.config

        key = 'debexpo.plugins.' + stage
143
        log.debug("Getting plugins with key (repr): %s", repr(key))
144
        plugins = config.get(key)
145

146
147
148
149
150
151
152
        return plugins.split(' ')

    @classmethod
    def import_plugins(cls, stage):
        """
        Import all plugins for given stage and return them as a list.
        """
153

154
155
156
157
158
159
        modules = {}
        config = pylons.config

        plugins = cls._what_plugins(stage)
        log.debug("Importing these plugins: %s", plugins)

160
        for plugin_name in plugins:
161
            if config.get('debexpo.plugindir') != '':
162
                # First try in the user-defined plugindir
163
                sys.path.append(config['debexpo.plugindir'])
164
                module = cls._import_plugin(plugin_name)
165
                if module is not None:
166
                    log.debug('Found module in debexpo.plugin_dir')
167

168
169
                else:
                    # Try in debexpo.plugins
170
                    name = 'debexpo.plugins.%s' % plugin_name
171
                    module = cls._import_plugin(name)
172

173
174
175
176
177
178
179
180
181
182
                if module is None or not hasattr(module, 'plugin'):
                    log.debug('No plugin found in module %s' % plugin_name)
                    continue

                models = getattr(module, 'models', [])

                modules[plugin_name] = PluginModule(name=plugin_name,
                                               plugin=getattr(module, 'plugin'),
                                               models=models,
                                               stage=stage)
183
184

        return modules
185

186
187
188
189
    def load_plugins(self):
        """
        Instantiate all plugins and register them in the 'plugins'
        dictionary attribute.
Clément Schreiner's avatar
Clément Schreiner committed
190
        e.g.: self.plugins['native'] -> <NativePlugin 42>
191
        """
192
193
194
195
196
        for module in self.modules.itervalues():
            try:
                self.plugins[module.name] = module.plugin(module.name,
                                                          self.package_version,
                                                          models=module.models,
197
198
                                                          changes=self.changes,
                                                          kw=self.kw)
199
200
201
            except Exception:
                log.debug('Something went wrong while loading the plugin {}:'
                         '{}'.format(module.name, traceback.format_exc()))
202

Clément Schreiner's avatar
Clément Schreiner committed
203

204
205
    def run_plugins(self):
        """
Clément Schreiner's avatar
Clément Schreiner committed
206
        Run all imported plugins.
207
208
        """

Clément Schreiner's avatar
Clément Schreiner committed
209
210
        if len(self) == 0:
            log.debug('No plugin loaded.')
211
212

        # Run each plugin.
Clément Schreiner's avatar
Clément Schreiner committed
213
        for name, plugin in self.iteritems():
214
215
            log.debug('Running plugin: %s' % name)
            try:
216
                plugin.run()
217
218
            except Exception:
                log.debug("Something wrong happened while running the plugin '%s': %s"
Clément Schreiner's avatar
Clément Schreiner committed
219
                          % (name, traceback.format_exc()))
220
            else:
221
                plugin.save()
222
223
224

    def load_results(self):
        """
225
        Calls ``load`` method on all loaded plugin instances.
226
227
        """

228
229
        for name, plugin in self.iteritems():
            plugin.load()
Clément Schreiner's avatar
Clément Schreiner committed
230

231

Clément Schreiner's avatar
Clément Schreiner committed
232
233
234
235
236
237
    #
    # magic methods from accessing the ``plugins`` attribute
    #

    # FIXME: do this properly with collections.abc? (not sure)

Clément Schreiner's avatar
Clément Schreiner committed
238
    def __getitem__(self, key):
Clément Schreiner's avatar
Clément Schreiner committed
239
240
241
242
        """
        Returns a plugin instance for the given plugin name.
        plugins['native'] -> NativePlugin instance
        """
Clément Schreiner's avatar
Clément Schreiner committed
243
244
245
246
        return self.plugins[key]

    def get(self, key, default):
        return self.plugins.get(key, default)
Clément Schreiner's avatar
Clément Schreiner committed
247

248
    def iteritems(self):
Clément Schreiner's avatar
Clément Schreiner committed
249
        """Iter the items in the ``plugins`` dictionary attribute."""
250
251
252
        return self.plugins.iteritems()

    def __len__(self):
Clément Schreiner's avatar
Clément Schreiner committed
253
        """Number of plugins that have been loaded."""
254
        return len(self.plugins)
Clément Schreiner's avatar
Clément Schreiner committed
255
256

    def __iter__(self):
Clément Schreiner's avatar
Clément Schreiner committed
257
258
259
        """ Iter over plugin instances' names """
        for name in self.plugins:
            yield name
260
261
262

    def __contains__(self, key):
        return key in self.plugins