~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/plugin.py

  • Committer: Jelmer Vernooij
  • Date: 2009-01-28 18:42:55 UTC
  • mto: This revision was merged to the branch mainline in revision 3968.
  • Revision ID: jelmer@samba.org-20090128184255-bdmklkvm83ltk191
Update NEWS

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2004, 2005 Canonical Ltd
 
1
# Copyright (C) 2004, 2005, 2007, 2008 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
20
20
When load_plugins() is invoked, any python module in any directory in
21
21
$BZR_PLUGIN_PATH will be imported.  The module will be imported as
22
22
'bzrlib.plugins.$BASENAME(PLUGIN)'.  In the plugin's main body, it should
23
 
update any bzrlib registries it wants to extend; for example, to add new
24
 
commands, import bzrlib.commands and add your new command to the plugin_cmds
25
 
variable.
 
23
update any bzrlib registries it wants to extend.
 
24
 
 
25
See the plugin-api developer documentation for information about writing
 
26
plugins.
26
27
 
27
28
BZR_PLUGIN_PATH is also honoured for any plugins imported via
28
29
'import bzrlib.plugins.PLUGINNAME', as long as set_plugins_path has been 
32
33
import os
33
34
import sys
34
35
 
 
36
from bzrlib import osutils
 
37
 
35
38
from bzrlib.lazy_import import lazy_import
 
39
 
36
40
lazy_import(globals(), """
37
41
import imp
38
42
import re
39
43
import types
40
 
import zipimport
 
44
import zipfile
41
45
 
42
46
from bzrlib import (
 
47
    _format_version_tuple,
43
48
    config,
44
 
    osutils,
45
 
    plugins,
 
49
    debug,
 
50
    errors,
 
51
    trace,
46
52
    )
 
53
from bzrlib import plugins as _mod_plugins
47
54
""")
48
55
 
49
 
from bzrlib.trace import mutter, warning, log_exception_quietly
 
56
from bzrlib.symbol_versioning import deprecated_function, one_three
50
57
 
51
58
 
52
59
DEFAULT_PLUGIN_PATH = None
60
67
    return DEFAULT_PLUGIN_PATH
61
68
 
62
69
 
63
 
def all_plugins():
64
 
    """Return a dictionary of the plugins."""
65
 
    result = {}
66
 
    for name, plugin in plugins.__dict__.items():
67
 
        if isinstance(plugin, types.ModuleType):
68
 
            result[name] = plugin
69
 
    return result
70
 
 
71
 
 
72
70
def disable_plugins():
73
71
    """Disable loading plugins.
74
72
 
75
73
    Future calls to load_plugins() will be ignored.
76
74
    """
77
 
    # TODO: jam 20060131 This should probably also disable
78
 
    #       load_from_dirs()
79
 
    global _loaded
80
 
    _loaded = True
81
 
 
82
 
 
83
 
def set_plugins_path():
84
 
    """Set the path for plugins to be loaded from."""
 
75
    load_plugins([])
 
76
 
 
77
 
 
78
def _strip_trailing_sep(path):
 
79
    return path.rstrip("\\/")
 
80
 
 
81
 
 
82
def set_plugins_path(path=None):
 
83
    """Set the path for plugins to be loaded from.
 
84
 
 
85
    :param path: The list of paths to search for plugins.  By default,
 
86
        path will be determined using get_standard_plugins_path.
 
87
        if path is [], no plugins can be loaded.
 
88
    """
 
89
    if path is None:
 
90
        path = get_standard_plugins_path()
 
91
    _mod_plugins.__path__ = path
 
92
    return path
 
93
 
 
94
 
 
95
def get_standard_plugins_path():
 
96
    """Determine a plugin path suitable for general use."""
85
97
    path = os.environ.get('BZR_PLUGIN_PATH',
86
98
                          get_default_plugin_path()).split(os.pathsep)
87
 
    # search the bzrlib installed dir before anything else.
88
 
    path.insert(0, os.path.dirname(plugins.__file__))
89
 
    plugins.__path__ = path
 
99
    # Get rid of trailing slashes, since Python can't handle them when
 
100
    # it tries to import modules.
 
101
    path = map(_strip_trailing_sep, path)
 
102
    bzr_exe = bool(getattr(sys, 'frozen', None))
 
103
    if bzr_exe:    # expand path for bzr.exe
 
104
        # We need to use relative path to system-wide plugin
 
105
        # directory because bzrlib from standalone bzr.exe
 
106
        # could be imported by another standalone program
 
107
        # (e.g. bzr-config; or TortoiseBzr/Olive if/when they
 
108
        # will become standalone exe). [bialix 20071123]
 
109
        # __file__ typically is
 
110
        # C:\Program Files\Bazaar\lib\library.zip\bzrlib\plugin.pyc
 
111
        # then plugins directory is
 
112
        # C:\Program Files\Bazaar\plugins
 
113
        # so relative path is ../../../plugins
 
114
        path.append(osutils.abspath(osutils.pathjoin(
 
115
            osutils.dirname(__file__), '../../../plugins')))
 
116
    if not bzr_exe:     # don't look inside library.zip
 
117
        # search the plugin path before the bzrlib installed dir
 
118
        path.append(os.path.dirname(_mod_plugins.__file__))
 
119
    # search the arch independent path if we can determine that and
 
120
    # the plugin is found nowhere else
 
121
    if sys.platform != 'win32':
 
122
        try:
 
123
            from distutils.sysconfig import get_python_lib
 
124
        except ImportError:
 
125
            # If distutuils is not available, we just won't add that path
 
126
            pass
 
127
        else:
 
128
            archless_path = osutils.pathjoin(get_python_lib(), 'bzrlib',
 
129
                    'plugins')
 
130
            if archless_path not in path:
 
131
                path.append(archless_path)
90
132
    return path
91
133
 
92
134
 
93
 
def load_plugins():
 
135
def load_plugins(path=None):
94
136
    """Load bzrlib plugins.
95
137
 
96
138
    The environment variable BZR_PLUGIN_PATH is considered a delimited
100
142
 
101
143
    load_from_dirs() provides the underlying mechanism and is called with
102
144
    the default directory list to provide the normal behaviour.
 
145
 
 
146
    :param path: The list of paths to search for plugins.  By default,
 
147
        path will be determined using get_standard_plugins_path.
 
148
        if path is [], no plugins can be loaded.
103
149
    """
104
150
    global _loaded
105
151
    if _loaded:
108
154
    _loaded = True
109
155
 
110
156
    # scan for all plugins in the path.
111
 
    load_from_path(set_plugins_path())
 
157
    load_from_path(set_plugins_path(path))
112
158
 
113
159
 
114
160
def load_from_path(dirs):
123
169
 
124
170
    The python module path for bzrlib.plugins will be modified to be 'dirs'.
125
171
    """
126
 
    plugins.__path__ = dirs
 
172
    # We need to strip the trailing separators here as well as in the
 
173
    # set_plugins_path function because calling code can pass anything in to
 
174
    # this function, and since it sets plugins.__path__, it should set it to
 
175
    # something that will be valid for Python to use (in case people try to
 
176
    # run "import bzrlib.plugins.PLUGINNAME" after calling this function).
 
177
    _mod_plugins.__path__ = map(_strip_trailing_sep, dirs)
127
178
    for d in dirs:
128
179
        if not d:
129
180
            continue
130
 
        mutter('looking for plugins in %s', d)
 
181
        trace.mutter('looking for plugins in %s', d)
131
182
        if os.path.isdir(d):
132
183
            load_from_dir(d)
133
 
        else:
134
 
            # it might be a zip: try loading from the zip.
135
 
            load_from_zip(d)
136
 
            continue
137
184
 
138
185
 
139
186
# backwards compatability: load_from_dirs was the old name
142
189
 
143
190
 
144
191
def load_from_dir(d):
145
 
    """Load the plugins in directory d."""
 
192
    """Load the plugins in directory d.
 
193
    
 
194
    d must be in the plugins module path already.
 
195
    """
146
196
    # Get the list of valid python suffixes for __init__.py?
147
197
    # this includes .py, .pyc, and .pyo (depending on if we are running -O)
148
198
    # but it doesn't include compiled modules (.so, .dll, etc)
169
219
                    break
170
220
            else:
171
221
                continue
172
 
        if getattr(plugins, f, None):
173
 
            mutter('Plugin name %s already loaded', f)
 
222
        if f == '__init__':
 
223
            continue # We don't load __init__.py again in the plugin dir
 
224
        elif getattr(_mod_plugins, f, None):
 
225
            trace.mutter('Plugin name %s already loaded', f)
174
226
        else:
175
 
            # mutter('add plugin name %s', f)
 
227
            # trace.mutter('add plugin name %s', f)
176
228
            plugin_names.add(f)
177
229
    
178
230
    for name in plugin_names:
180
232
            exec "import bzrlib.plugins.%s" % name in {}
181
233
        except KeyboardInterrupt:
182
234
            raise
 
235
        except errors.IncompatibleAPI, e:
 
236
            trace.warning("Unable to load plugin %r. It requested API version "
 
237
                "%s of module %s but the minimum exported version is %s, and "
 
238
                "the maximum is %s" %
 
239
                (name, e.wanted, e.api, e.minimum, e.current))
183
240
        except Exception, e:
 
241
            trace.warning("%s" % e)
184
242
            ## import pdb; pdb.set_trace()
185
243
            if re.search('\.|-| ', name):
186
 
                warning('Unable to load plugin %r from %r: '
187
 
                    'It is not a valid python module name.' % (name, d))
 
244
                sanitised_name = re.sub('[-. ]', '_', name)
 
245
                if sanitised_name.startswith('bzr_'):
 
246
                    sanitised_name = sanitised_name[len('bzr_'):]
 
247
                trace.warning("Unable to load %r in %r as a plugin because the "
 
248
                        "file path isn't a valid module name; try renaming "
 
249
                        "it to %r." % (name, d, sanitised_name))
188
250
            else:
189
 
                warning('Unable to load plugin %r from %r' % (name, d))
190
 
            log_exception_quietly()
191
 
 
192
 
 
 
251
                trace.warning('Unable to load plugin %r from %r' % (name, d))
 
252
            trace.log_exception_quietly()
 
253
            if 'error' in debug.debug_flags:
 
254
                trace.print_exception(sys.exc_info(), sys.stderr)
 
255
 
 
256
 
 
257
@deprecated_function(one_three)
193
258
def load_from_zip(zip_name):
194
259
    """Load all the plugins in a zip."""
195
260
    valid_suffixes = ('.py', '.pyc', '.pyo')    # only python modules/packages
196
261
                                                # is allowed
197
 
    if '.zip' not in zip_name:
 
262
    try:
 
263
        index = zip_name.rindex('.zip')
 
264
    except ValueError:
198
265
        return
 
266
    archive = zip_name[:index+4]
 
267
    prefix = zip_name[index+5:]
 
268
 
 
269
    trace.mutter('Looking for plugins in %r', zip_name)
 
270
 
 
271
    # use zipfile to get list of files/dirs inside zip
199
272
    try:
200
 
        ziobj = zipimport.zipimporter(zip_name)
201
 
    except zipimport.ZipImportError:
 
273
        z = zipfile.ZipFile(archive)
 
274
        namelist = z.namelist()
 
275
        z.close()
 
276
    except zipfile.error:
202
277
        # not a valid zip
203
278
        return
204
 
    mutter('Looking for plugins in %r', zip_name)
205
 
    
206
 
    import zipfile
207
279
 
208
 
    # use zipfile to get list of files/dirs inside zip
209
 
    z = zipfile.ZipFile(ziobj.archive)
210
 
    namelist = z.namelist()
211
 
    z.close()
212
 
    
213
 
    if ziobj.prefix:
214
 
        prefix = ziobj.prefix.replace('\\','/')
 
280
    if prefix:
 
281
        prefix = prefix.replace('\\','/')
 
282
        if prefix[-1] != '/':
 
283
            prefix += '/'
215
284
        ix = len(prefix)
216
285
        namelist = [name[ix:]
217
286
                    for name in namelist
218
287
                    if name.startswith(prefix)]
219
 
    
220
 
    mutter('Names in archive: %r', namelist)
 
288
 
 
289
    trace.mutter('Names in archive: %r', namelist)
221
290
    
222
291
    for name in namelist:
223
292
        if not name or name.endswith('/'):
248
317
    
249
318
        if not plugin_name:
250
319
            continue
251
 
        if getattr(plugins, plugin_name, None):
252
 
            mutter('Plugin name %s already loaded', plugin_name)
 
320
        if getattr(_mod_plugins, plugin_name, None):
 
321
            trace.mutter('Plugin name %s already loaded', plugin_name)
253
322
            continue
254
323
    
255
324
        try:
256
 
            plugin = ziobj.load_module(plugin_name)
257
 
            setattr(plugins, plugin_name, plugin)
258
 
            mutter('Load plugin %s from zip %r', plugin_name, zip_name)
259
 
        except zipimport.ZipImportError, e:
260
 
            mutter('Unable to load plugin %r from %r: %s',
261
 
                   plugin_name, zip_name, str(e))
262
 
            continue
 
325
            exec "import bzrlib.plugins.%s" % plugin_name in {}
 
326
            trace.mutter('Load plugin %s from zip %r', plugin_name, zip_name)
263
327
        except KeyboardInterrupt:
264
328
            raise
265
329
        except Exception, e:
266
330
            ## import pdb; pdb.set_trace()
267
 
            warning('Unable to load plugin %r from %r'
 
331
            trace.warning('Unable to load plugin %r from %r'
268
332
                    % (name, zip_name))
269
 
            log_exception_quietly()
 
333
            trace.log_exception_quietly()
 
334
            if 'error' in debug.debug_flags:
 
335
                trace.print_exception(sys.exc_info(), sys.stderr)
 
336
 
 
337
 
 
338
def plugins():
 
339
    """Return a dictionary of the plugins.
 
340
    
 
341
    Each item in the dictionary is a PlugIn object.
 
342
    """
 
343
    result = {}
 
344
    for name, plugin in _mod_plugins.__dict__.items():
 
345
        if isinstance(plugin, types.ModuleType):
 
346
            result[name] = PlugIn(name, plugin)
 
347
    return result
 
348
 
 
349
 
 
350
class PluginsHelpIndex(object):
 
351
    """A help index that returns help topics for plugins."""
 
352
 
 
353
    def __init__(self):
 
354
        self.prefix = 'plugins/'
 
355
 
 
356
    def get_topics(self, topic):
 
357
        """Search for topic in the loaded plugins.
 
358
 
 
359
        This will not trigger loading of new plugins.
 
360
 
 
361
        :param topic: A topic to search for.
 
362
        :return: A list which is either empty or contains a single
 
363
            RegisteredTopic entry.
 
364
        """
 
365
        if not topic:
 
366
            return []
 
367
        if topic.startswith(self.prefix):
 
368
            topic = topic[len(self.prefix):]
 
369
        plugin_module_name = 'bzrlib.plugins.%s' % topic
 
370
        try:
 
371
            module = sys.modules[plugin_module_name]
 
372
        except KeyError:
 
373
            return []
 
374
        else:
 
375
            return [ModuleHelpTopic(module)]
 
376
 
 
377
 
 
378
class ModuleHelpTopic(object):
 
379
    """A help topic which returns the docstring for a module."""
 
380
 
 
381
    def __init__(self, module):
 
382
        """Constructor.
 
383
 
 
384
        :param module: The module for which help should be generated.
 
385
        """
 
386
        self.module = module
 
387
 
 
388
    def get_help_text(self, additional_see_also=None):
 
389
        """Return a string with the help for this topic.
 
390
 
 
391
        :param additional_see_also: Additional help topics to be
 
392
            cross-referenced.
 
393
        """
 
394
        if not self.module.__doc__:
 
395
            result = "Plugin '%s' has no docstring.\n" % self.module.__name__
 
396
        else:
 
397
            result = self.module.__doc__
 
398
        if result[-1] != '\n':
 
399
            result += '\n'
 
400
        # there is code duplicated here and in bzrlib/help_topic.py's 
 
401
        # matching Topic code. This should probably be factored in
 
402
        # to a helper function and a common base class.
 
403
        if additional_see_also is not None:
 
404
            see_also = sorted(set(additional_see_also))
 
405
        else:
 
406
            see_also = None
 
407
        if see_also:
 
408
            result += 'See also: '
 
409
            result += ', '.join(see_also)
 
410
            result += '\n'
 
411
        return result
 
412
 
 
413
    def get_help_topic(self):
 
414
        """Return the modules help topic - its __name__ after bzrlib.plugins.."""
 
415
        return self.module.__name__[len('bzrlib.plugins.'):]
 
416
 
 
417
 
 
418
class PlugIn(object):
 
419
    """The bzrlib representation of a plugin.
 
420
 
 
421
    The PlugIn object provides a way to manipulate a given plugin module.
 
422
    """
 
423
 
 
424
    def __init__(self, name, module):
 
425
        """Construct a plugin for module."""
 
426
        self.name = name
 
427
        self.module = module
 
428
 
 
429
    def path(self):
 
430
        """Get the path that this plugin was loaded from."""
 
431
        if getattr(self.module, '__path__', None) is not None:
 
432
            return os.path.abspath(self.module.__path__[0])
 
433
        elif getattr(self.module, '__file__', None) is not None:
 
434
            path = os.path.abspath(self.module.__file__)
 
435
            if path[-4:] in ('.pyc', '.pyo'):
 
436
                pypath = path[:-4] + '.py'
 
437
                if os.path.isfile(pypath):
 
438
                    path = pypath
 
439
            return path
 
440
        else:
 
441
            return repr(self.module)
 
442
 
 
443
    def __str__(self):
 
444
        return "<%s.%s object at %s, name=%s, module=%s>" % (
 
445
            self.__class__.__module__, self.__class__.__name__, id(self),
 
446
            self.name, self.module)
 
447
 
 
448
    __repr__ = __str__
 
449
 
 
450
    def test_suite(self):
 
451
        """Return the plugin's test suite."""
 
452
        if getattr(self.module, 'test_suite', None) is not None:
 
453
            return self.module.test_suite()
 
454
        else:
 
455
            return None
 
456
 
 
457
    def load_plugin_tests(self, loader):
 
458
        """Return the adapted plugin's test suite.
 
459
 
 
460
        :param loader: The custom loader that should be used to load additional
 
461
            tests.
 
462
 
 
463
        """
 
464
        if getattr(self.module, 'load_tests', None) is not None:
 
465
            return loader.loadTestsFromModule(self.module)
 
466
        else:
 
467
            return None
 
468
 
 
469
    def version_info(self):
 
470
        """Return the plugin's version_tuple or None if unknown."""
 
471
        version_info = getattr(self.module, 'version_info', None)
 
472
        if version_info is not None:
 
473
            try:
 
474
                if isinstance(version_info, types.StringType):
 
475
                    version_info = version_info.split('.')
 
476
                elif len(version_info) == 3:
 
477
                    version_info = tuple(version_info) + ('final', 0)
 
478
            except TypeError, e:
 
479
                # The given version_info isn't even iteratible
 
480
                trace.log_exception_quietly()
 
481
                version_info = (version_info,)
 
482
        return version_info
 
483
 
 
484
    def _get__version__(self):
 
485
        version_info = self.version_info()
 
486
        if version_info is None or len(version_info) == 0:
 
487
            return "unknown"
 
488
        try:
 
489
            version_string = _format_version_tuple(version_info)
 
490
        except (ValueError, TypeError, IndexError), e:
 
491
            trace.log_exception_quietly()
 
492
            # try to return something usefull for bad plugins, in stead of
 
493
            # stack tracing.
 
494
            version_string = '.'.join(map(str, version_info))
 
495
        return version_string
 
496
 
 
497
    __version__ = property(_get__version__)