~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/plugin.py

  • Committer: Florian Dorn
  • Date: 2012-04-03 14:49:22 UTC
  • mto: This revision was merged to the branch mainline in revision 6546.
  • Revision ID: florian.dorn@boku.ac.at-20120403144922-b8y59csy8l1rzs5u
updated developer docs

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2004, 2005, 2007 Canonical Ltd
 
1
# Copyright (C) 2005-2011 Canonical Ltd
2
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
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
 
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16
16
 
17
17
 
18
18
"""bzr python plugin support.
26
26
plugins.
27
27
 
28
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 
 
29
'import bzrlib.plugins.PLUGINNAME', as long as set_plugins_path has been
30
30
called.
31
31
"""
32
32
 
33
33
import os
34
34
import sys
35
35
 
 
36
from bzrlib import osutils
 
37
 
36
38
from bzrlib.lazy_import import lazy_import
37
39
lazy_import(globals(), """
38
40
import imp
39
41
import re
40
42
import types
41
 
import zipfile
42
43
 
43
44
from bzrlib import (
 
45
    _format_version_tuple,
44
46
    config,
45
47
    debug,
46
 
    osutils,
 
48
    errors,
47
49
    trace,
48
50
    )
49
51
from bzrlib import plugins as _mod_plugins
50
52
""")
51
53
 
52
 
from bzrlib.symbol_versioning import deprecated_function, one_three
53
 
from bzrlib.trace import mutter, warning, log_exception_quietly
54
 
 
55
54
 
56
55
DEFAULT_PLUGIN_PATH = None
57
56
_loaded = False
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
 
57
_plugins_disabled = False
 
58
 
 
59
 
 
60
plugin_warnings = {}
 
61
# Map from plugin name, to list of string warnings about eg plugin
 
62
# dependencies.
 
63
 
 
64
 
 
65
def are_plugins_disabled():
 
66
    return _plugins_disabled
65
67
 
66
68
 
67
69
def disable_plugins():
69
71
 
70
72
    Future calls to load_plugins() will be ignored.
71
73
    """
72
 
    # TODO: jam 20060131 This should probably also disable
73
 
    #       load_from_dirs()
74
 
    global _loaded
75
 
    _loaded = True
 
74
    global _plugins_disabled
 
75
    _plugins_disabled = True
 
76
    load_plugins([])
 
77
 
 
78
 
 
79
def describe_plugins(show_paths=False):
 
80
    """Generate text description of plugins.
 
81
 
 
82
    Includes both those that have loaded, and those that failed to 
 
83
    load.
 
84
 
 
85
    :param show_paths: If true,
 
86
    :returns: Iterator of text lines (including newlines.)
 
87
    """
 
88
    from inspect import getdoc
 
89
    loaded_plugins = plugins()
 
90
    all_names = sorted(list(set(
 
91
        loaded_plugins.keys() + plugin_warnings.keys())))
 
92
    for name in all_names:
 
93
        if name in loaded_plugins:
 
94
            plugin = loaded_plugins[name]
 
95
            version = plugin.__version__
 
96
            if version == 'unknown':
 
97
                version = ''
 
98
            yield '%s %s\n' % (name, version)
 
99
            d = getdoc(plugin.module)
 
100
            if d:
 
101
                doc = d.split('\n')[0]
 
102
            else:
 
103
                doc = '(no description)'
 
104
            yield ("  %s\n" % doc)
 
105
            if show_paths:
 
106
                yield ("   %s\n" % plugin.path())
 
107
            del plugin
 
108
        else:
 
109
            yield "%s (failed to load)\n" % name
 
110
        if name in plugin_warnings:
 
111
            for line in plugin_warnings[name]:
 
112
                yield "  ** " + line + '\n'
 
113
        yield '\n'
76
114
 
77
115
 
78
116
def _strip_trailing_sep(path):
79
117
    return path.rstrip("\\/")
80
118
 
81
119
 
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)
 
120
def _get_specific_plugin_paths(paths):
 
121
    """Returns the plugin paths from a string describing the associations.
 
122
 
 
123
    :param paths: A string describing the paths associated with the plugins.
 
124
 
 
125
    :returns: A list of (plugin name, path) tuples.
 
126
 
 
127
    For example, if paths is my_plugin@/test/my-test:her_plugin@/production/her,
 
128
    [('my_plugin', '/test/my-test'), ('her_plugin', '/production/her')] 
 
129
    will be returned.
 
130
 
 
131
    Note that ':' in the example above depends on the os.
 
132
    """
 
133
    if not paths:
 
134
        return []
 
135
    specs = []
 
136
    for spec in paths.split(os.pathsep):
 
137
        try:
 
138
            name, path = spec.split('@')
 
139
        except ValueError:
 
140
            raise errors.BzrCommandError(
 
141
                '"%s" is not a valid <plugin_name>@<plugin_path> description '
 
142
                % spec)
 
143
        specs.append((name, path))
 
144
    return specs
 
145
 
 
146
 
 
147
def set_plugins_path(path=None):
 
148
    """Set the path for plugins to be loaded from.
 
149
 
 
150
    :param path: The list of paths to search for plugins.  By default,
 
151
        path will be determined using get_standard_plugins_path.
 
152
        if path is [], no plugins can be loaded.
 
153
    """
 
154
    if path is None:
 
155
        path = get_standard_plugins_path()
 
156
    _mod_plugins.__path__ = path
 
157
    PluginImporter.reset()
 
158
    # Set up a blacklist for disabled plugins
 
159
    disabled_plugins = os.environ.get('BZR_DISABLE_PLUGINS', None)
 
160
    if disabled_plugins is not None:
 
161
        for name in disabled_plugins.split(os.pathsep):
 
162
            PluginImporter.blacklist.add('bzrlib.plugins.' + name)
 
163
    # Set up a the specific paths for plugins
 
164
    for plugin_name, plugin_path in _get_specific_plugin_paths(os.environ.get(
 
165
            'BZR_PLUGINS_AT', None)):
 
166
            PluginImporter.specific_paths[
 
167
                'bzrlib.plugins.%s' % plugin_name] = plugin_path
 
168
    return path
 
169
 
 
170
 
 
171
def _append_new_path(paths, new_path):
 
172
    """Append a new path if it set and not already known."""
 
173
    if new_path is not None and new_path not in paths:
 
174
        paths.append(new_path)
 
175
    return paths
 
176
 
 
177
 
 
178
def get_core_plugin_path():
 
179
    core_path = None
86
180
    bzr_exe = bool(getattr(sys, 'frozen', None))
87
181
    if bzr_exe:    # expand path for bzr.exe
88
182
        # We need to use relative path to system-wide plugin
95
189
        # then plugins directory is
96
190
        # C:\Program Files\Bazaar\plugins
97
191
        # so relative path is ../../../plugins
98
 
        path.append(osutils.abspath(osutils.pathjoin(
99
 
            osutils.dirname(__file__), '../../../plugins')))
 
192
        core_path = osutils.abspath(osutils.pathjoin(
 
193
                osutils.dirname(__file__), '../../../plugins'))
 
194
    else:     # don't look inside library.zip
 
195
        # search the plugin path before the bzrlib installed dir
 
196
        core_path = os.path.dirname(_mod_plugins.__file__)
 
197
    return core_path
 
198
 
 
199
 
 
200
def get_site_plugin_path():
 
201
    """Returns the path for the site installed plugins."""
 
202
    if sys.platform == 'win32':
 
203
        # We don't have (yet) a good answer for windows since that is certainly
 
204
        # related to the way we build the installers. -- vila20090821
 
205
        return None
 
206
    site_path = None
 
207
    try:
 
208
        from distutils.sysconfig import get_python_lib
 
209
    except ImportError:
 
210
        # If distutuils is not available, we just don't know where they are
 
211
        pass
 
212
    else:
 
213
        site_path = osutils.pathjoin(get_python_lib(), 'bzrlib', 'plugins')
 
214
    return site_path
 
215
 
 
216
 
 
217
def get_user_plugin_path():
 
218
    return osutils.pathjoin(config.config_dir(), 'plugins')
 
219
 
 
220
 
 
221
def get_standard_plugins_path():
 
222
    """Determine a plugin path suitable for general use."""
 
223
    # Ad-Hoc default: core is not overriden by site but user can overrides both
 
224
    # The rationale is that:
 
225
    # - 'site' comes last, because these plugins should always be available and
 
226
    #   are supposed to be in sync with the bzr installed on site.
 
227
    # - 'core' comes before 'site' so that running bzr from sources or a user
 
228
    #   installed version overrides the site version.
 
229
    # - 'user' comes first, because... user is always right.
 
230
    # - the above rules clearly defines which plugin version will be loaded if
 
231
    #   several exist. Yet, it is sometimes desirable to disable some directory
 
232
    #   so that a set of plugins is disabled as once. This can be done via
 
233
    #   -site, -core, -user.
 
234
 
 
235
    env_paths = os.environ.get('BZR_PLUGIN_PATH', '+user').split(os.pathsep)
 
236
    defaults = ['+core', '+site']
 
237
 
 
238
    # The predefined references
 
239
    refs = dict(core=get_core_plugin_path(),
 
240
                site=get_site_plugin_path(),
 
241
                user=get_user_plugin_path())
 
242
 
 
243
    # Unset paths that should be removed
 
244
    for k,v in refs.iteritems():
 
245
        removed = '-%s' % k
 
246
        # defaults can never mention removing paths as that will make it
 
247
        # impossible for the user to revoke these removals.
 
248
        if removed in env_paths:
 
249
            env_paths.remove(removed)
 
250
            refs[k] = None
 
251
 
 
252
    # Expand references
 
253
    paths = []
 
254
    for p in env_paths + defaults:
 
255
        if p.startswith('+'):
 
256
            # Resolve references if they are known
 
257
            try:
 
258
                p = refs[p[1:]]
 
259
            except KeyError:
 
260
                # Leave them untouched so user can still use paths starting
 
261
                # with '+'
 
262
                pass
 
263
        _append_new_path(paths, p)
 
264
 
100
265
    # Get rid of trailing slashes, since Python can't handle them when
101
266
    # 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
 
def load_plugins():
 
267
    paths = map(_strip_trailing_sep, paths)
 
268
    return paths
 
269
 
 
270
 
 
271
def load_plugins(path=None):
124
272
    """Load bzrlib plugins.
125
273
 
126
274
    The environment variable BZR_PLUGIN_PATH is considered a delimited
127
 
    set of paths to look through. Each entry is searched for *.py
 
275
    set of paths to look through. Each entry is searched for `*.py`
128
276
    files (and whatever other extensions are used in the platform,
129
 
    such as *.pyd).
 
277
    such as `*.pyd`).
130
278
 
131
 
    load_from_dirs() provides the underlying mechanism and is called with
 
279
    load_from_path() provides the underlying mechanism and is called with
132
280
    the default directory list to provide the normal behaviour.
 
281
 
 
282
    :param path: The list of paths to search for plugins.  By default,
 
283
        path will be determined using get_standard_plugins_path.
 
284
        if path is [], no plugins can be loaded.
133
285
    """
134
286
    global _loaded
135
287
    if _loaded:
138
290
    _loaded = True
139
291
 
140
292
    # scan for all plugins in the path.
141
 
    load_from_path(set_plugins_path())
 
293
    load_from_path(set_plugins_path(path))
142
294
 
143
295
 
144
296
def load_from_path(dirs):
153
305
 
154
306
    The python module path for bzrlib.plugins will be modified to be 'dirs'.
155
307
    """
 
308
    # Explicitly load the plugins with a specific path
 
309
    for fullname, path in PluginImporter.specific_paths.iteritems():
 
310
        name = fullname[len('bzrlib.plugins.'):]
 
311
        _load_plugin_module(name, path)
 
312
 
156
313
    # We need to strip the trailing separators here as well as in the
157
314
    # set_plugins_path function because calling code can pass anything in to
158
315
    # this function, and since it sets plugins.__path__, it should set it to
162
319
    for d in dirs:
163
320
        if not d:
164
321
            continue
165
 
        mutter('looking for plugins in %s', d)
 
322
        trace.mutter('looking for plugins in %s', d)
166
323
        if os.path.isdir(d):
167
324
            load_from_dir(d)
168
325
 
172
329
load_from_dirs = load_from_path
173
330
 
174
331
 
 
332
def _find_plugin_module(dir, name):
 
333
    """Check if there is a valid python module that can be loaded as a plugin.
 
334
 
 
335
    :param dir: The directory where the search is performed.
 
336
    :param path: An existing file path, either a python file or a package
 
337
        directory.
 
338
 
 
339
    :return: (name, path, description) name is the module name, path is the
 
340
        file to load and description is the tuple returned by
 
341
        imp.get_suffixes().
 
342
    """
 
343
    path = osutils.pathjoin(dir, name)
 
344
    if os.path.isdir(path):
 
345
        # Check for a valid __init__.py file, valid suffixes depends on -O and
 
346
        # can be .py, .pyc and .pyo
 
347
        for suffix, mode, kind in imp.get_suffixes():
 
348
            if kind not in (imp.PY_SOURCE, imp.PY_COMPILED):
 
349
                # We don't recognize compiled modules (.so, .dll, etc)
 
350
                continue
 
351
            init_path = osutils.pathjoin(path, '__init__' + suffix)
 
352
            if os.path.isfile(init_path):
 
353
                return name, init_path, (suffix, mode, kind)
 
354
    else:
 
355
        for suffix, mode, kind in imp.get_suffixes():
 
356
            if name.endswith(suffix):
 
357
                # Clean up the module name
 
358
                name = name[:-len(suffix)]
 
359
                if kind == imp.C_EXTENSION and name.endswith('module'):
 
360
                    name = name[:-len('module')]
 
361
                return name, path, (suffix, mode, kind)
 
362
    # There is no python module here
 
363
    return None, None, (None, None, None)
 
364
 
 
365
 
 
366
def record_plugin_warning(plugin_name, warning_message):
 
367
    trace.mutter(warning_message)
 
368
    plugin_warnings.setdefault(plugin_name, []).append(warning_message)
 
369
 
 
370
 
 
371
def _load_plugin_module(name, dir):
 
372
    """Load plugin name from dir.
 
373
 
 
374
    :param name: The plugin name in the bzrlib.plugins namespace.
 
375
    :param dir: The directory the plugin is loaded from for error messages.
 
376
    """
 
377
    if ('bzrlib.plugins.%s' % name) in PluginImporter.blacklist:
 
378
        return
 
379
    try:
 
380
        exec "import bzrlib.plugins.%s" % name in {}
 
381
    except KeyboardInterrupt:
 
382
        raise
 
383
    except errors.IncompatibleAPI, e:
 
384
        warning_message = (
 
385
            "Unable to load plugin %r. It requested API version "
 
386
            "%s of module %s but the minimum exported version is %s, and "
 
387
            "the maximum is %s" %
 
388
            (name, e.wanted, e.api, e.minimum, e.current))
 
389
        record_plugin_warning(name, warning_message)
 
390
    except Exception, e:
 
391
        trace.warning("%s" % e)
 
392
        if re.search('\.|-| ', name):
 
393
            sanitised_name = re.sub('[-. ]', '_', name)
 
394
            if sanitised_name.startswith('bzr_'):
 
395
                sanitised_name = sanitised_name[len('bzr_'):]
 
396
            trace.warning("Unable to load %r in %r as a plugin because the "
 
397
                    "file path isn't a valid module name; try renaming "
 
398
                    "it to %r." % (name, dir, sanitised_name))
 
399
        else:
 
400
            record_plugin_warning(
 
401
                name,
 
402
                'Unable to load plugin %r from %r' % (name, dir))
 
403
        trace.log_exception_quietly()
 
404
        if 'error' in debug.debug_flags:
 
405
            trace.print_exception(sys.exc_info(), sys.stderr)
 
406
 
 
407
 
175
408
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]
 
409
    """Load the plugins in directory d.
 
410
 
 
411
    d must be in the plugins module path already.
 
412
    This function is called once for each directory in the module path.
 
413
    """
183
414
    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
 
415
    for p in os.listdir(d):
 
416
        name, path, desc = _find_plugin_module(d, p)
 
417
        if name is not None:
 
418
            if name == '__init__':
 
419
                # We do nothing with the __init__.py file in directories from
 
420
                # the bzrlib.plugins module path, we may want to, one day
 
421
                # -- vila 20100316.
 
422
                continue # We don't load __init__.py in the plugins dirs
 
423
            elif getattr(_mod_plugins, name, None) is not None:
 
424
                # The module has already been loaded from another directory
 
425
                # during a previous call.
 
426
                # FIXME: There should be a better way to report masked plugins
 
427
                # -- vila 20100316
 
428
                trace.mutter('Plugin name %s already loaded', name)
201
429
            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
 
    
 
430
                plugin_names.add(name)
 
431
 
209
432
    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:
224
 
                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)
 
433
        _load_plugin_module(name, d)
309
434
 
310
435
 
311
436
def plugins():
312
437
    """Return a dictionary of the plugins.
313
 
    
 
438
 
314
439
    Each item in the dictionary is a PlugIn object.
315
440
    """
316
441
    result = {}
320
445
    return result
321
446
 
322
447
 
 
448
def format_concise_plugin_list():
 
449
    """Return a string holding a concise list of plugins and their version.
 
450
    """
 
451
    items = []
 
452
    for name, a_plugin in sorted(plugins().items()):
 
453
        items.append("%s[%s]" %
 
454
            (name, a_plugin.__version__))
 
455
    return ', '.join(items)
 
456
 
 
457
 
 
458
 
323
459
class PluginsHelpIndex(object):
324
460
    """A help index that returns help topics for plugins."""
325
461
 
358
494
        """
359
495
        self.module = module
360
496
 
361
 
    def get_help_text(self, additional_see_also=None):
 
497
    def get_help_text(self, additional_see_also=None, verbose=True):
362
498
        """Return a string with the help for this topic.
363
499
 
364
500
        :param additional_see_also: Additional help topics to be
370
506
            result = self.module.__doc__
371
507
        if result[-1] != '\n':
372
508
            result += '\n'
373
 
        # there is code duplicated here and in bzrlib/help_topic.py's 
 
509
        # there is code duplicated here and in bzrlib/help_topic.py's
374
510
        # matching Topic code. This should probably be factored in
375
511
        # to a helper function and a common base class.
376
512
        if additional_see_also is not None:
442
578
    def version_info(self):
443
579
        """Return the plugin's version_tuple or None if unknown."""
444
580
        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)
 
581
        if version_info is not None:
 
582
            try:
 
583
                if isinstance(version_info, types.StringType):
 
584
                    version_info = version_info.split('.')
 
585
                elif len(version_info) == 3:
 
586
                    version_info = tuple(version_info) + ('final', 0)
 
587
            except TypeError, e:
 
588
                # The given version_info isn't even iteratible
 
589
                trace.log_exception_quietly()
 
590
                version_info = (version_info,)
447
591
        return version_info
448
592
 
449
593
    def _get__version__(self):
450
594
        version_info = self.version_info()
451
 
        if version_info is None:
 
595
        if version_info is None or len(version_info) == 0:
452
596
            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
 
597
        try:
 
598
            version_string = _format_version_tuple(version_info)
 
599
        except (ValueError, TypeError, IndexError), e:
 
600
            trace.log_exception_quietly()
 
601
            # try to return something usefull for bad plugins, in stead of
 
602
            # stack tracing.
 
603
            version_string = '.'.join(map(str, version_info))
457
604
        return version_string
458
605
 
459
606
    __version__ = property(_get__version__)
 
607
 
 
608
 
 
609
class _PluginImporter(object):
 
610
    """An importer tailored to bzr specific needs.
 
611
 
 
612
    This is a singleton that takes care of:
 
613
    - disabled plugins specified in 'blacklist',
 
614
    - plugins that needs to be loaded from specific directories.
 
615
    """
 
616
 
 
617
    def __init__(self):
 
618
        self.reset()
 
619
 
 
620
    def reset(self):
 
621
        self.blacklist = set()
 
622
        self.specific_paths = {}
 
623
 
 
624
    def find_module(self, fullname, parent_path=None):
 
625
        """Search a plugin module.
 
626
 
 
627
        Disabled plugins raise an import error, plugins with specific paths
 
628
        returns a specific loader.
 
629
 
 
630
        :return: None if the plugin doesn't need special handling, self
 
631
            otherwise.
 
632
        """
 
633
        if not fullname.startswith('bzrlib.plugins.'):
 
634
            return None
 
635
        if fullname in self.blacklist:
 
636
            raise ImportError('%s is disabled' % fullname)
 
637
        if fullname in self.specific_paths:
 
638
            return self
 
639
        return None
 
640
 
 
641
    def load_module(self, fullname):
 
642
        """Load a plugin from a specific directory."""
 
643
        # We are called only for specific paths
 
644
        plugin_path = self.specific_paths[fullname]
 
645
        loading_path = None
 
646
        if os.path.isdir(plugin_path):
 
647
            for suffix, mode, kind in imp.get_suffixes():
 
648
                if kind not in (imp.PY_SOURCE, imp.PY_COMPILED):
 
649
                    # We don't recognize compiled modules (.so, .dll, etc)
 
650
                    continue
 
651
                init_path = osutils.pathjoin(plugin_path, '__init__' + suffix)
 
652
                if os.path.isfile(init_path):
 
653
                    # We've got a module here and load_module needs specific
 
654
                    # parameters.
 
655
                    loading_path = plugin_path
 
656
                    suffix = ''
 
657
                    mode = ''
 
658
                    kind = imp.PKG_DIRECTORY
 
659
                    break
 
660
        else:
 
661
            for suffix, mode, kind in imp.get_suffixes():
 
662
                if plugin_path.endswith(suffix):
 
663
                    loading_path = plugin_path
 
664
                    break
 
665
        if loading_path is None:
 
666
            raise ImportError('%s cannot be loaded from %s'
 
667
                              % (fullname, plugin_path))
 
668
        if kind is imp.PKG_DIRECTORY:
 
669
            f = None
 
670
        else:
 
671
            f = open(loading_path, mode)
 
672
        try:
 
673
            mod = imp.load_module(fullname, f, loading_path,
 
674
                                  (suffix, mode, kind))
 
675
            mod.__package__ = fullname
 
676
            return mod
 
677
        finally:
 
678
            if f is not None:
 
679
                f.close()
 
680
 
 
681
 
 
682
# Install a dedicated importer for plugins requiring special handling
 
683
PluginImporter = _PluginImporter()
 
684
sys.meta_path.append(PluginImporter)