~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/plugin.py

Merge in upstream.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2004, 2005, 2007 Canonical Ltd
2
 
#
 
1
# Copyright (C) 2004, 2005 by Canonical Ltd
 
2
 
3
3
# This program is free software; you can redistribute it and/or modify
4
4
# it under the terms of the GNU General Public License as published by
5
5
# the Free Software Foundation; either version 2 of the License, or
6
6
# (at your option) any later version.
7
 
#
 
7
 
8
8
# This program is distributed in the hope that it will be useful,
9
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11
11
# GNU General Public License for more details.
12
 
#
 
12
 
13
13
# You should have received a copy of the GNU General Public License
14
14
# along with this program; if not, write to the Free Software
15
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
16
16
 
17
17
 
18
 
"""bzr python plugin support.
19
 
 
20
 
When load_plugins() is invoked, any python module in any directory in
21
 
$BZR_PLUGIN_PATH will be imported.  The module will be imported as
22
 
'bzrlib.plugins.$BASENAME(PLUGIN)'.  In the plugin's main body, it should
23
 
update any bzrlib registries it wants to extend.
24
 
 
25
 
See the plugin-api developer documentation for information about writing
26
 
plugins.
27
 
 
28
 
BZR_PLUGIN_PATH is also honoured for any plugins imported via
29
 
'import bzrlib.plugins.PLUGINNAME', as long as set_plugins_path has been 
30
 
called.
 
18
"""bzr python plugin support
 
19
 
 
20
Any python module in $BZR_PLUGIN_PATH will be imported upon initialization of
 
21
bzrlib. The module will be imported as 'bzrlib.plugins.$BASENAME(PLUGIN)'.
 
22
In the plugin's main body, it should update any bzrlib registries it wants to
 
23
extend; for example, to add new commands, import bzrlib.commands and add your
 
24
new command to the plugin_cmds variable.
31
25
"""
32
26
 
 
27
# TODO: Refactor this to make it more testable.  The main problem at the
 
28
# moment is that loading plugins affects the global process state -- for bzr
 
29
# in general use it's a reasonable assumption that all plugins are loaded at
 
30
# startup and then stay loaded, but this is less good for testing.
 
31
 
32
# Several specific issues:
 
33
#  - plugins can't be unloaded and will continue to effect later tests
 
34
#  - load_plugins does nothing if called a second time
 
35
#  - plugin hooks can't be removed
 
36
#
 
37
# Our options are either to remove these restrictions, or work around them by
 
38
# loading the plugins into a different space than the one running the tests.
 
39
# That could be either a separate Python interpreter or perhaps a new
 
40
# namespace inside this interpreter.
 
41
 
 
42
import imp
33
43
import os
34
44
import sys
35
 
 
36
 
from bzrlib.lazy_import import lazy_import
37
 
lazy_import(globals(), """
38
 
import imp
39
 
import re
40
45
import types
41
 
import zipfile
42
 
 
43
 
from bzrlib import (
44
 
    config,
45
 
    debug,
46
 
    osutils,
47
 
    trace,
48
 
    )
49
 
from bzrlib import plugins as _mod_plugins
50
 
""")
51
 
 
52
 
from bzrlib.symbol_versioning import deprecated_function, one_three
53
 
from bzrlib.trace import mutter, warning, log_exception_quietly
54
 
 
55
 
 
56
 
DEFAULT_PLUGIN_PATH = None
 
46
 
 
47
import bzrlib
 
48
from bzrlib.config import config_dir
 
49
from bzrlib.trace import log_error, mutter, log_exception, warning, \
 
50
        log_exception_quietly
 
51
from bzrlib.errors import BzrError
 
52
from bzrlib import plugins
 
53
from bzrlib.osutils import pathjoin
 
54
 
 
55
DEFAULT_PLUGIN_PATH = pathjoin(config_dir(), 'plugins')
 
56
 
57
57
_loaded = False
58
58
 
59
 
def get_default_plugin_path():
60
 
    """Get the DEFAULT_PLUGIN_PATH"""
61
 
    global DEFAULT_PLUGIN_PATH
62
 
    if DEFAULT_PLUGIN_PATH is None:
63
 
        DEFAULT_PLUGIN_PATH = osutils.pathjoin(config.config_dir(), 'plugins')
64
 
    return DEFAULT_PLUGIN_PATH
 
59
 
 
60
def all_plugins():
 
61
    """Return a dictionary of the plugins."""
 
62
    result = {}
 
63
    for name, plugin in bzrlib.plugins.__dict__.items():
 
64
        if isinstance(plugin, types.ModuleType):
 
65
            result[name] = plugin
 
66
    return result
65
67
 
66
68
 
67
69
def disable_plugins():
75
77
    _loaded = True
76
78
 
77
79
 
78
 
def _strip_trailing_sep(path):
79
 
    return path.rstrip("\\/")
80
 
 
81
 
 
82
 
def set_plugins_path():
83
 
    """Set the path for plugins to be loaded from."""
84
 
    path = os.environ.get('BZR_PLUGIN_PATH',
85
 
                          get_default_plugin_path()).split(os.pathsep)
86
 
    bzr_exe = bool(getattr(sys, 'frozen', None))
87
 
    if bzr_exe:    # expand path for bzr.exe
88
 
        # We need to use relative path to system-wide plugin
89
 
        # directory because bzrlib from standalone bzr.exe
90
 
        # could be imported by another standalone program
91
 
        # (e.g. bzr-config; or TortoiseBzr/Olive if/when they
92
 
        # will become standalone exe). [bialix 20071123]
93
 
        # __file__ typically is
94
 
        # C:\Program Files\Bazaar\lib\library.zip\bzrlib\plugin.pyc
95
 
        # then plugins directory is
96
 
        # C:\Program Files\Bazaar\plugins
97
 
        # so relative path is ../../../plugins
98
 
        path.append(osutils.abspath(osutils.pathjoin(
99
 
            osutils.dirname(__file__), '../../../plugins')))
100
 
    # Get rid of trailing slashes, since Python can't handle them when
101
 
    # it tries to import modules.
102
 
    path = map(_strip_trailing_sep, path)
103
 
    if not bzr_exe:     # don't look inside library.zip
104
 
        # search the plugin path before the bzrlib installed dir
105
 
        path.append(os.path.dirname(_mod_plugins.__file__))
106
 
    # search the arch independent path if we can determine that and
107
 
    # the plugin is found nowhere else
108
 
    if sys.platform != 'win32':
109
 
        try:
110
 
            from distutils.sysconfig import get_python_lib
111
 
        except ImportError:
112
 
            # If distutuils is not available, we just won't add that path
113
 
            pass
114
 
        else:
115
 
            archless_path = osutils.pathjoin(get_python_lib(), 'bzrlib',
116
 
                    'plugins')
117
 
            if archless_path not in path:
118
 
                path.append(archless_path)
119
 
    _mod_plugins.__path__ = path
120
 
    return path
121
 
 
122
 
 
123
80
def load_plugins():
124
81
    """Load bzrlib plugins.
125
82
 
135
92
    if _loaded:
136
93
        # People can make sure plugins are loaded, they just won't be twice
137
94
        return
 
95
        #raise BzrError("plugins already initialized")
138
96
    _loaded = True
139
97
 
140
 
    # scan for all plugins in the path.
141
 
    load_from_path(set_plugins_path())
142
 
 
143
 
 
144
 
def load_from_path(dirs):
 
98
    dirs = os.environ.get('BZR_PLUGIN_PATH', DEFAULT_PLUGIN_PATH).split(os.pathsep)
 
99
    dirs.insert(0, os.path.dirname(plugins.__file__))
 
100
 
 
101
    load_from_dirs(dirs)
 
102
 
 
103
 
 
104
def load_from_dirs(dirs):
145
105
    """Load bzrlib plugins found in each dir in dirs.
146
106
 
147
107
    Loading a plugin means importing it into the python interpreter.
150
110
 
151
111
    Plugins are loaded into bzrlib.plugins.NAME, and can be found there
152
112
    for future reference.
153
 
 
154
 
    The python module path for bzrlib.plugins will be modified to be 'dirs'.
155
113
    """
156
 
    # We need to strip the trailing separators here as well as in the
157
 
    # set_plugins_path function because calling code can pass anything in to
158
 
    # this function, and since it sets plugins.__path__, it should set it to
159
 
    # something that will be valid for Python to use (in case people try to
160
 
    # run "import bzrlib.plugins.PLUGINNAME" after calling this function).
161
 
    _mod_plugins.__path__ = map(_strip_trailing_sep, dirs)
 
114
    # The problem with imp.get_suffixes() is that it doesn't include
 
115
    # .pyo which is technically valid
 
116
    # It also means that "testmodule.so" will show up as both test and testmodule
 
117
    # though it is only valid as 'test'
 
118
    # but you should be careful, because "testmodule.py" loads as testmodule.
 
119
    suffixes = imp.get_suffixes()
 
120
    suffixes.append(('.pyo', 'rb', imp.PY_COMPILED))
 
121
    package_entries = ['__init__.py', '__init__.pyc', '__init__.pyo']
162
122
    for d in dirs:
163
123
        if not d:
164
124
            continue
165
125
        mutter('looking for plugins in %s', d)
166
 
        if os.path.isdir(d):
167
 
            load_from_dir(d)
168
 
 
169
 
 
170
 
# backwards compatability: load_from_dirs was the old name
171
 
# This was changed in 0.15
172
 
load_from_dirs = load_from_path
173
 
 
174
 
 
175
 
def load_from_dir(d):
176
 
    """Load the plugins in directory d."""
177
 
    # Get the list of valid python suffixes for __init__.py?
178
 
    # this includes .py, .pyc, and .pyo (depending on if we are running -O)
179
 
    # but it doesn't include compiled modules (.so, .dll, etc)
180
 
    valid_suffixes = [suffix for suffix, mod_type, flags in imp.get_suffixes()
181
 
                              if flags in (imp.PY_SOURCE, imp.PY_COMPILED)]
182
 
    package_entries = ['__init__'+suffix for suffix in valid_suffixes]
183
 
    plugin_names = set()
184
 
    for f in os.listdir(d):
185
 
        path = osutils.pathjoin(d, f)
186
 
        if os.path.isdir(path):
187
 
            for entry in package_entries:
188
 
                # This directory should be a package, and thus added to
189
 
                # the list
190
 
                if os.path.isfile(osutils.pathjoin(path, entry)):
191
 
                    break
192
 
            else: # This directory is not a package
193
 
                continue
194
 
        else:
195
 
            for suffix_info in imp.get_suffixes():
196
 
                if f.endswith(suffix_info[0]):
197
 
                    f = f[:-len(suffix_info[0])]
198
 
                    if suffix_info[2] == imp.C_EXTENSION and f.endswith('module'):
199
 
                        f = f[:-len('module')]
200
 
                    break
201
 
            else:
202
 
                continue
203
 
        if getattr(_mod_plugins, f, None):
204
 
            mutter('Plugin name %s already loaded', f)
205
 
        else:
206
 
            # mutter('add plugin name %s', f)
207
 
            plugin_names.add(f)
208
 
    
209
 
    for name in plugin_names:
210
 
        try:
211
 
            exec "import bzrlib.plugins.%s" % name in {}
212
 
        except KeyboardInterrupt:
213
 
            raise
214
 
        except Exception, e:
215
 
            ## import pdb; pdb.set_trace()
216
 
            if re.search('\.|-| ', name):
217
 
                sanitised_name = re.sub('[-. ]', '_', name)
218
 
                if sanitised_name.startswith('bzr_'):
219
 
                    sanitised_name = sanitised_name[len('bzr_'):]
220
 
                warning("Unable to load %r in %r as a plugin because the "
221
 
                        "file path isn't a valid module name; try renaming "
222
 
                        "it to %r." % (name, d, sanitised_name))
223
 
            else:
 
126
        plugin_names = set()
 
127
        if not os.path.isdir(d):
 
128
            continue
 
129
        for f in os.listdir(d):
 
130
            path = pathjoin(d, f)
 
131
            if os.path.isdir(path):
 
132
                for entry in package_entries:
 
133
                    # This directory should be a package, and thus added to
 
134
                    # the list
 
135
                    if os.path.isfile(pathjoin(path, entry)):
 
136
                        break
 
137
                else: # This directory is not a package
 
138
                    continue
 
139
            else:
 
140
                for suffix_info in suffixes:
 
141
                    if f.endswith(suffix_info[0]):
 
142
                        f = f[:-len(suffix_info[0])]
 
143
                        if suffix_info[2] == imp.C_EXTENSION and f.endswith('module'):
 
144
                            f = f[:-len('module')]
 
145
                        break
 
146
                else:
 
147
                    continue
 
148
            if getattr(bzrlib.plugins, f, None):
 
149
                mutter('Plugin name %s already loaded', f)
 
150
            else:
 
151
                mutter('add plugin name %s', f)
 
152
                plugin_names.add(f)
 
153
 
 
154
        plugin_names = list(plugin_names)
 
155
        plugin_names.sort()
 
156
        for name in plugin_names:
 
157
            try:
 
158
                plugin_info = imp.find_module(name, [d])
 
159
                mutter('load plugin %r', plugin_info)
 
160
                try:
 
161
                    plugin = imp.load_module('bzrlib.plugins.' + name,
 
162
                                             *plugin_info)
 
163
                    setattr(bzrlib.plugins, name, plugin)
 
164
                finally:
 
165
                    if plugin_info[0] is not None:
 
166
                        plugin_info[0].close()
 
167
 
 
168
                mutter('loaded succesfully')
 
169
            except KeyboardInterrupt:
 
170
                raise
 
171
            except Exception, e:
 
172
                ## import pdb; pdb.set_trace()
224
173
                warning('Unable to load plugin %r from %r' % (name, d))
225
 
            log_exception_quietly()
226
 
            if 'error' in debug.debug_flags:
227
 
                trace.print_exception(sys.exc_info(), sys.stderr)
228
 
 
229
 
 
230
 
@deprecated_function(one_three)
231
 
def load_from_zip(zip_name):
232
 
    """Load all the plugins in a zip."""
233
 
    valid_suffixes = ('.py', '.pyc', '.pyo')    # only python modules/packages
234
 
                                                # is allowed
235
 
    try:
236
 
        index = zip_name.rindex('.zip')
237
 
    except ValueError:
238
 
        return
239
 
    archive = zip_name[:index+4]
240
 
    prefix = zip_name[index+5:]
241
 
 
242
 
    mutter('Looking for plugins in %r', zip_name)
243
 
 
244
 
    # use zipfile to get list of files/dirs inside zip
245
 
    try:
246
 
        z = zipfile.ZipFile(archive)
247
 
        namelist = z.namelist()
248
 
        z.close()
249
 
    except zipfile.error:
250
 
        # not a valid zip
251
 
        return
252
 
 
253
 
    if prefix:
254
 
        prefix = prefix.replace('\\','/')
255
 
        if prefix[-1] != '/':
256
 
            prefix += '/'
257
 
        ix = len(prefix)
258
 
        namelist = [name[ix:]
259
 
                    for name in namelist
260
 
                    if name.startswith(prefix)]
261
 
 
262
 
    mutter('Names in archive: %r', namelist)
263
 
    
264
 
    for name in namelist:
265
 
        if not name or name.endswith('/'):
266
 
            continue
267
 
    
268
 
        # '/' is used to separate pathname components inside zip archives
269
 
        ix = name.rfind('/')
270
 
        if ix == -1:
271
 
            head, tail = '', name
272
 
        else:
273
 
            head, tail = name.rsplit('/',1)
274
 
        if '/' in head:
275
 
            # we don't need looking in subdirectories
276
 
            continue
277
 
    
278
 
        base, suffix = osutils.splitext(tail)
279
 
        if suffix not in valid_suffixes:
280
 
            continue
281
 
    
282
 
        if base == '__init__':
283
 
            # package
284
 
            plugin_name = head
285
 
        elif head == '':
286
 
            # module
287
 
            plugin_name = base
288
 
        else:
289
 
            continue
290
 
    
291
 
        if not plugin_name:
292
 
            continue
293
 
        if getattr(_mod_plugins, plugin_name, None):
294
 
            mutter('Plugin name %s already loaded', plugin_name)
295
 
            continue
296
 
    
297
 
        try:
298
 
            exec "import bzrlib.plugins.%s" % plugin_name in {}
299
 
            mutter('Load plugin %s from zip %r', plugin_name, zip_name)
300
 
        except KeyboardInterrupt:
301
 
            raise
302
 
        except Exception, e:
303
 
            ## import pdb; pdb.set_trace()
304
 
            warning('Unable to load plugin %r from %r'
305
 
                    % (name, zip_name))
306
 
            log_exception_quietly()
307
 
            if 'error' in debug.debug_flags:
308
 
                trace.print_exception(sys.exc_info(), sys.stderr)
309
 
 
310
 
 
311
 
def plugins():
312
 
    """Return a dictionary of the plugins.
313
 
    
314
 
    Each item in the dictionary is a PlugIn object.
315
 
    """
316
 
    result = {}
317
 
    for name, plugin in _mod_plugins.__dict__.items():
318
 
        if isinstance(plugin, types.ModuleType):
319
 
            result[name] = PlugIn(name, plugin)
320
 
    return result
321
 
 
322
 
 
323
 
class PluginsHelpIndex(object):
324
 
    """A help index that returns help topics for plugins."""
325
 
 
326
 
    def __init__(self):
327
 
        self.prefix = 'plugins/'
328
 
 
329
 
    def get_topics(self, topic):
330
 
        """Search for topic in the loaded plugins.
331
 
 
332
 
        This will not trigger loading of new plugins.
333
 
 
334
 
        :param topic: A topic to search for.
335
 
        :return: A list which is either empty or contains a single
336
 
            RegisteredTopic entry.
337
 
        """
338
 
        if not topic:
339
 
            return []
340
 
        if topic.startswith(self.prefix):
341
 
            topic = topic[len(self.prefix):]
342
 
        plugin_module_name = 'bzrlib.plugins.%s' % topic
343
 
        try:
344
 
            module = sys.modules[plugin_module_name]
345
 
        except KeyError:
346
 
            return []
347
 
        else:
348
 
            return [ModuleHelpTopic(module)]
349
 
 
350
 
 
351
 
class ModuleHelpTopic(object):
352
 
    """A help topic which returns the docstring for a module."""
353
 
 
354
 
    def __init__(self, module):
355
 
        """Constructor.
356
 
 
357
 
        :param module: The module for which help should be generated.
358
 
        """
359
 
        self.module = module
360
 
 
361
 
    def get_help_text(self, additional_see_also=None):
362
 
        """Return a string with the help for this topic.
363
 
 
364
 
        :param additional_see_also: Additional help topics to be
365
 
            cross-referenced.
366
 
        """
367
 
        if not self.module.__doc__:
368
 
            result = "Plugin '%s' has no docstring.\n" % self.module.__name__
369
 
        else:
370
 
            result = self.module.__doc__
371
 
        if result[-1] != '\n':
372
 
            result += '\n'
373
 
        # there is code duplicated here and in bzrlib/help_topic.py's 
374
 
        # matching Topic code. This should probably be factored in
375
 
        # to a helper function and a common base class.
376
 
        if additional_see_also is not None:
377
 
            see_also = sorted(set(additional_see_also))
378
 
        else:
379
 
            see_also = None
380
 
        if see_also:
381
 
            result += 'See also: '
382
 
            result += ', '.join(see_also)
383
 
            result += '\n'
384
 
        return result
385
 
 
386
 
    def get_help_topic(self):
387
 
        """Return the modules help topic - its __name__ after bzrlib.plugins.."""
388
 
        return self.module.__name__[len('bzrlib.plugins.'):]
389
 
 
390
 
 
391
 
class PlugIn(object):
392
 
    """The bzrlib representation of a plugin.
393
 
 
394
 
    The PlugIn object provides a way to manipulate a given plugin module.
395
 
    """
396
 
 
397
 
    def __init__(self, name, module):
398
 
        """Construct a plugin for module."""
399
 
        self.name = name
400
 
        self.module = module
401
 
 
402
 
    def path(self):
403
 
        """Get the path that this plugin was loaded from."""
404
 
        if getattr(self.module, '__path__', None) is not None:
405
 
            return os.path.abspath(self.module.__path__[0])
406
 
        elif getattr(self.module, '__file__', None) is not None:
407
 
            path = os.path.abspath(self.module.__file__)
408
 
            if path[-4:] in ('.pyc', '.pyo'):
409
 
                pypath = path[:-4] + '.py'
410
 
                if os.path.isfile(pypath):
411
 
                    path = pypath
412
 
            return path
413
 
        else:
414
 
            return repr(self.module)
415
 
 
416
 
    def __str__(self):
417
 
        return "<%s.%s object at %s, name=%s, module=%s>" % (
418
 
            self.__class__.__module__, self.__class__.__name__, id(self),
419
 
            self.name, self.module)
420
 
 
421
 
    __repr__ = __str__
422
 
 
423
 
    def test_suite(self):
424
 
        """Return the plugin's test suite."""
425
 
        if getattr(self.module, 'test_suite', None) is not None:
426
 
            return self.module.test_suite()
427
 
        else:
428
 
            return None
429
 
 
430
 
    def load_plugin_tests(self, loader):
431
 
        """Return the adapted plugin's test suite.
432
 
 
433
 
        :param loader: The custom loader that should be used to load additional
434
 
            tests.
435
 
 
436
 
        """
437
 
        if getattr(self.module, 'load_tests', None) is not None:
438
 
            return loader.loadTestsFromModule(self.module)
439
 
        else:
440
 
            return None
441
 
 
442
 
    def version_info(self):
443
 
        """Return the plugin's version_tuple or None if unknown."""
444
 
        version_info = getattr(self.module, 'version_info', None)
445
 
        if version_info is not None and len(version_info) == 3:
446
 
            version_info = tuple(version_info) + ('final', 0)
447
 
        return version_info
448
 
 
449
 
    def _get__version__(self):
450
 
        version_info = self.version_info()
451
 
        if version_info is None:
452
 
            return "unknown"
453
 
        if version_info[3] == 'final':
454
 
            version_string = '%d.%d.%d' % version_info[:3]
455
 
        else:
456
 
            version_string = '%d.%d.%d%s%d' % version_info
457
 
        return version_string
458
 
 
459
 
    __version__ = property(_get__version__)
 
174
                log_exception_quietly()