1
# Copyright (C) 2005-2011 Canonical Ltd
3
# This program is free software; you can redistribute it and/or modify
4
# it under the terms of the GNU General Public License as published by
5
# the Free Software Foundation; either version 2 of the License, or
6
# (at your option) any later version.
8
# This program is distributed in the hope that it will be useful,
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
# GNU General Public License for more details.
13
# You should have received a copy of the GNU General Public License
14
# along with this program; if not, write to the Free Software
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
18
"""bzr python plugin support.
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.
25
See the plugin-api developer documentation for information about writing
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
36
from bzrlib import osutils
38
from bzrlib.lazy_import import lazy_import
40
lazy_import(globals(), """
46
_format_version_tuple,
52
from bzrlib import plugins as _mod_plugins
55
from bzrlib.symbol_versioning import (
61
DEFAULT_PLUGIN_PATH = None
63
_plugins_disabled = False
67
# Map from plugin name, to list of string warnings about eg plugin
71
def are_plugins_disabled():
72
return _plugins_disabled
75
def disable_plugins():
76
"""Disable loading plugins.
78
Future calls to load_plugins() will be ignored.
80
global _plugins_disabled
81
_plugins_disabled = True
85
def describe_plugins(show_paths=False):
86
"""Generate text description of plugins.
88
Includes both those that have loaded, and those that failed to
91
:param show_paths: If true,
92
:returns: Iterator of text lines (including newlines.)
94
from inspect import getdoc
95
loaded_plugins = plugins()
96
all_names = sorted(list(set(
97
loaded_plugins.keys() + plugin_warnings.keys())))
98
for name in all_names:
99
if name in loaded_plugins:
100
plugin = loaded_plugins[name]
101
version = plugin.__version__
102
if version == 'unknown':
104
yield '%s %s\n' % (name, version)
105
d = getdoc(plugin.module)
107
doc = d.split('\n')[0]
109
doc = '(no description)'
110
yield (" %s\n" % doc)
112
yield (" %s\n" % plugin.path())
115
yield "%s (failed to load)\n" % name
116
if name in plugin_warnings:
117
for line in plugin_warnings[name]:
118
yield " ** " + line + '\n'
122
def _strip_trailing_sep(path):
123
return path.rstrip("\\/")
126
def _get_specific_plugin_paths(paths):
127
"""Returns the plugin paths from a string describing the associations.
129
:param paths: A string describing the paths associated with the plugins.
131
:returns: A list of (plugin name, path) tuples.
133
For example, if paths is my_plugin@/test/my-test:her_plugin@/production/her,
134
[('my_plugin', '/test/my-test'), ('her_plugin', '/production/her')]
137
Note that ':' in the example above depends on the os.
142
for spec in paths.split(os.pathsep):
144
name, path = spec.split('@')
146
raise errors.BzrCommandError(
147
'"%s" is not a valid <plugin_name>@<plugin_path> description '
149
specs.append((name, path))
153
def set_plugins_path(path=None):
154
"""Set the path for plugins to be loaded from.
156
:param path: The list of paths to search for plugins. By default,
157
path will be determined using get_standard_plugins_path.
158
if path is [], no plugins can be loaded.
161
path = get_standard_plugins_path()
162
_mod_plugins.__path__ = path
163
PluginImporter.reset()
164
# Set up a blacklist for disabled plugins
165
disabled_plugins = os.environ.get('BZR_DISABLE_PLUGINS', None)
166
if disabled_plugins is not None:
167
for name in disabled_plugins.split(os.pathsep):
168
PluginImporter.blacklist.add('bzrlib.plugins.' + name)
169
# Set up a the specific paths for plugins
170
for plugin_name, plugin_path in _get_specific_plugin_paths(os.environ.get(
171
'BZR_PLUGINS_AT', None)):
172
PluginImporter.specific_paths[
173
'bzrlib.plugins.%s' % plugin_name] = plugin_path
177
def _append_new_path(paths, new_path):
178
"""Append a new path if it set and not already known."""
179
if new_path is not None and new_path not in paths:
180
paths.append(new_path)
184
def get_core_plugin_path():
186
bzr_exe = bool(getattr(sys, 'frozen', None))
187
if bzr_exe: # expand path for bzr.exe
188
# We need to use relative path to system-wide plugin
189
# directory because bzrlib from standalone bzr.exe
190
# could be imported by another standalone program
191
# (e.g. bzr-config; or TortoiseBzr/Olive if/when they
192
# will become standalone exe). [bialix 20071123]
193
# __file__ typically is
194
# C:\Program Files\Bazaar\lib\library.zip\bzrlib\plugin.pyc
195
# then plugins directory is
196
# C:\Program Files\Bazaar\plugins
197
# so relative path is ../../../plugins
198
core_path = osutils.abspath(osutils.pathjoin(
199
osutils.dirname(__file__), '../../../plugins'))
200
else: # don't look inside library.zip
201
# search the plugin path before the bzrlib installed dir
202
core_path = os.path.dirname(_mod_plugins.__file__)
206
def get_site_plugin_path():
207
"""Returns the path for the site installed plugins."""
208
if sys.platform == 'win32':
209
# We don't have (yet) a good answer for windows since that is certainly
210
# related to the way we build the installers. -- vila20090821
214
from distutils.sysconfig import get_python_lib
216
# If distutuils is not available, we just don't know where they are
219
site_path = osutils.pathjoin(get_python_lib(), 'bzrlib', 'plugins')
223
def get_user_plugin_path():
224
return osutils.pathjoin(config.config_dir(), 'plugins')
227
def get_standard_plugins_path():
228
"""Determine a plugin path suitable for general use."""
229
# Ad-Hoc default: core is not overriden by site but user can overrides both
230
# The rationale is that:
231
# - 'site' comes last, because these plugins should always be available and
232
# are supposed to be in sync with the bzr installed on site.
233
# - 'core' comes before 'site' so that running bzr from sources or a user
234
# installed version overrides the site version.
235
# - 'user' comes first, because... user is always right.
236
# - the above rules clearly defines which plugin version will be loaded if
237
# several exist. Yet, it is sometimes desirable to disable some directory
238
# so that a set of plugins is disabled as once. This can be done via
239
# -site, -core, -user.
241
env_paths = os.environ.get('BZR_PLUGIN_PATH', '+user').split(os.pathsep)
242
defaults = ['+core', '+site']
244
# The predefined references
245
refs = dict(core=get_core_plugin_path(),
246
site=get_site_plugin_path(),
247
user=get_user_plugin_path())
249
# Unset paths that should be removed
250
for k,v in refs.iteritems():
252
# defaults can never mention removing paths as that will make it
253
# impossible for the user to revoke these removals.
254
if removed in env_paths:
255
env_paths.remove(removed)
260
for p in env_paths + defaults:
261
if p.startswith('+'):
262
# Resolve references if they are known
266
# Leave them untouched so user can still use paths starting
269
_append_new_path(paths, p)
271
# Get rid of trailing slashes, since Python can't handle them when
272
# it tries to import modules.
273
paths = map(_strip_trailing_sep, paths)
277
def load_plugins(path=None):
278
"""Load bzrlib plugins.
280
The environment variable BZR_PLUGIN_PATH is considered a delimited
281
set of paths to look through. Each entry is searched for *.py
282
files (and whatever other extensions are used in the platform,
285
load_from_path() provides the underlying mechanism and is called with
286
the default directory list to provide the normal behaviour.
288
:param path: The list of paths to search for plugins. By default,
289
path will be determined using get_standard_plugins_path.
290
if path is [], no plugins can be loaded.
294
# People can make sure plugins are loaded, they just won't be twice
298
# scan for all plugins in the path.
299
load_from_path(set_plugins_path(path))
302
def load_from_path(dirs):
303
"""Load bzrlib plugins found in each dir in dirs.
305
Loading a plugin means importing it into the python interpreter.
306
The plugin is expected to make calls to register commands when
307
it's loaded (or perhaps access other hooks in future.)
309
Plugins are loaded into bzrlib.plugins.NAME, and can be found there
310
for future reference.
312
The python module path for bzrlib.plugins will be modified to be 'dirs'.
314
# Explicitly load the plugins with a specific path
315
for fullname, path in PluginImporter.specific_paths.iteritems():
316
name = fullname[len('bzrlib.plugins.'):]
317
_load_plugin_module(name, path)
319
# We need to strip the trailing separators here as well as in the
320
# set_plugins_path function because calling code can pass anything in to
321
# this function, and since it sets plugins.__path__, it should set it to
322
# something that will be valid for Python to use (in case people try to
323
# run "import bzrlib.plugins.PLUGINNAME" after calling this function).
324
_mod_plugins.__path__ = map(_strip_trailing_sep, dirs)
328
trace.mutter('looking for plugins in %s', d)
333
# backwards compatability: load_from_dirs was the old name
334
# This was changed in 0.15
335
load_from_dirs = load_from_path
338
def _find_plugin_module(dir, name):
339
"""Check if there is a valid python module that can be loaded as a plugin.
341
:param dir: The directory where the search is performed.
342
:param path: An existing file path, either a python file or a package
345
:return: (name, path, description) name is the module name, path is the
346
file to load and description is the tuple returned by
349
path = osutils.pathjoin(dir, name)
350
if os.path.isdir(path):
351
# Check for a valid __init__.py file, valid suffixes depends on -O and
352
# can be .py, .pyc and .pyo
353
for suffix, mode, kind in imp.get_suffixes():
354
if kind not in (imp.PY_SOURCE, imp.PY_COMPILED):
355
# We don't recognize compiled modules (.so, .dll, etc)
357
init_path = osutils.pathjoin(path, '__init__' + suffix)
358
if os.path.isfile(init_path):
359
return name, init_path, (suffix, mode, kind)
361
for suffix, mode, kind in imp.get_suffixes():
362
if name.endswith(suffix):
363
# Clean up the module name
364
name = name[:-len(suffix)]
365
if kind == imp.C_EXTENSION and name.endswith('module'):
366
name = name[:-len('module')]
367
return name, path, (suffix, mode, kind)
368
# There is no python module here
369
return None, None, (None, None, None)
372
def record_plugin_warning(plugin_name, warning_message):
373
trace.mutter(warning_message)
374
plugin_warnings.setdefault(plugin_name, []).append(warning_message)
377
def _load_plugin_module(name, dir):
378
"""Load plugin name from dir.
380
:param name: The plugin name in the bzrlib.plugins namespace.
381
:param dir: The directory the plugin is loaded from for error messages.
383
if ('bzrlib.plugins.%s' % name) in PluginImporter.blacklist:
386
exec "import bzrlib.plugins.%s" % name in {}
387
except KeyboardInterrupt:
389
except errors.IncompatibleAPI, e:
391
"Unable to load plugin %r. It requested API version "
392
"%s of module %s but the minimum exported version is %s, and "
393
"the maximum is %s" %
394
(name, e.wanted, e.api, e.minimum, e.current))
395
record_plugin_warning(name, warning_message)
397
trace.warning("%s" % e)
398
if re.search('\.|-| ', name):
399
sanitised_name = re.sub('[-. ]', '_', name)
400
if sanitised_name.startswith('bzr_'):
401
sanitised_name = sanitised_name[len('bzr_'):]
402
trace.warning("Unable to load %r in %r as a plugin because the "
403
"file path isn't a valid module name; try renaming "
404
"it to %r." % (name, dir, sanitised_name))
406
record_plugin_warning(
408
'Unable to load plugin %r from %r' % (name, dir))
409
trace.log_exception_quietly()
410
if 'error' in debug.debug_flags:
411
trace.print_exception(sys.exc_info(), sys.stderr)
414
def load_from_dir(d):
415
"""Load the plugins in directory d.
417
d must be in the plugins module path already.
418
This function is called once for each directory in the module path.
421
for p in os.listdir(d):
422
name, path, desc = _find_plugin_module(d, p)
424
if name == '__init__':
425
# We do nothing with the __init__.py file in directories from
426
# the bzrlib.plugins module path, we may want to, one day
428
continue # We don't load __init__.py in the plugins dirs
429
elif getattr(_mod_plugins, name, None) is not None:
430
# The module has already been loaded from another directory
431
# during a previous call.
432
# FIXME: There should be a better way to report masked plugins
434
trace.mutter('Plugin name %s already loaded', name)
436
plugin_names.add(name)
438
for name in plugin_names:
439
_load_plugin_module(name, d)
443
"""Return a dictionary of the plugins.
445
Each item in the dictionary is a PlugIn object.
448
for name, plugin in _mod_plugins.__dict__.items():
449
if isinstance(plugin, types.ModuleType):
450
result[name] = PlugIn(name, plugin)
454
class PluginsHelpIndex(object):
455
"""A help index that returns help topics for plugins."""
458
self.prefix = 'plugins/'
460
def get_topics(self, topic):
461
"""Search for topic in the loaded plugins.
463
This will not trigger loading of new plugins.
465
:param topic: A topic to search for.
466
:return: A list which is either empty or contains a single
467
RegisteredTopic entry.
471
if topic.startswith(self.prefix):
472
topic = topic[len(self.prefix):]
473
plugin_module_name = 'bzrlib.plugins.%s' % topic
475
module = sys.modules[plugin_module_name]
479
return [ModuleHelpTopic(module)]
482
class ModuleHelpTopic(object):
483
"""A help topic which returns the docstring for a module."""
485
def __init__(self, module):
488
:param module: The module for which help should be generated.
492
def get_help_text(self, additional_see_also=None, verbose=True):
493
"""Return a string with the help for this topic.
495
:param additional_see_also: Additional help topics to be
498
if not self.module.__doc__:
499
result = "Plugin '%s' has no docstring.\n" % self.module.__name__
501
result = self.module.__doc__
502
if result[-1] != '\n':
504
# there is code duplicated here and in bzrlib/help_topic.py's
505
# matching Topic code. This should probably be factored in
506
# to a helper function and a common base class.
507
if additional_see_also is not None:
508
see_also = sorted(set(additional_see_also))
512
result += 'See also: '
513
result += ', '.join(see_also)
517
def get_help_topic(self):
518
"""Return the modules help topic - its __name__ after bzrlib.plugins.."""
519
return self.module.__name__[len('bzrlib.plugins.'):]
522
class PlugIn(object):
523
"""The bzrlib representation of a plugin.
525
The PlugIn object provides a way to manipulate a given plugin module.
528
def __init__(self, name, module):
529
"""Construct a plugin for module."""
534
"""Get the path that this plugin was loaded from."""
535
if getattr(self.module, '__path__', None) is not None:
536
return os.path.abspath(self.module.__path__[0])
537
elif getattr(self.module, '__file__', None) is not None:
538
path = os.path.abspath(self.module.__file__)
539
if path[-4:] in ('.pyc', '.pyo'):
540
pypath = path[:-4] + '.py'
541
if os.path.isfile(pypath):
545
return repr(self.module)
548
return "<%s.%s object at %s, name=%s, module=%s>" % (
549
self.__class__.__module__, self.__class__.__name__, id(self),
550
self.name, self.module)
554
def test_suite(self):
555
"""Return the plugin's test suite."""
556
if getattr(self.module, 'test_suite', None) is not None:
557
return self.module.test_suite()
561
def load_plugin_tests(self, loader):
562
"""Return the adapted plugin's test suite.
564
:param loader: The custom loader that should be used to load additional
568
if getattr(self.module, 'load_tests', None) is not None:
569
return loader.loadTestsFromModule(self.module)
573
def version_info(self):
574
"""Return the plugin's version_tuple or None if unknown."""
575
version_info = getattr(self.module, 'version_info', None)
576
if version_info is not None:
578
if isinstance(version_info, types.StringType):
579
version_info = version_info.split('.')
580
elif len(version_info) == 3:
581
version_info = tuple(version_info) + ('final', 0)
583
# The given version_info isn't even iteratible
584
trace.log_exception_quietly()
585
version_info = (version_info,)
588
def _get__version__(self):
589
version_info = self.version_info()
590
if version_info is None or len(version_info) == 0:
593
version_string = _format_version_tuple(version_info)
594
except (ValueError, TypeError, IndexError), e:
595
trace.log_exception_quietly()
596
# try to return something usefull for bad plugins, in stead of
598
version_string = '.'.join(map(str, version_info))
599
return version_string
601
__version__ = property(_get__version__)
604
class _PluginImporter(object):
605
"""An importer tailored to bzr specific needs.
607
This is a singleton that takes care of:
608
- disabled plugins specified in 'blacklist',
609
- plugins that needs to be loaded from specific directories.
616
self.blacklist = set()
617
self.specific_paths = {}
619
def find_module(self, fullname, parent_path=None):
620
"""Search a plugin module.
622
Disabled plugins raise an import error, plugins with specific paths
623
returns a specific loader.
625
:return: None if the plugin doesn't need special handling, self
628
if not fullname.startswith('bzrlib.plugins.'):
630
if fullname in self.blacklist:
631
raise ImportError('%s is disabled' % fullname)
632
if fullname in self.specific_paths:
636
def load_module(self, fullname):
637
"""Load a plugin from a specific directory."""
638
# We are called only for specific paths
639
plugin_path = self.specific_paths[fullname]
641
if os.path.isdir(plugin_path):
642
for suffix, mode, kind in imp.get_suffixes():
643
if kind not in (imp.PY_SOURCE, imp.PY_COMPILED):
644
# We don't recognize compiled modules (.so, .dll, etc)
646
init_path = osutils.pathjoin(plugin_path, '__init__' + suffix)
647
if os.path.isfile(init_path):
648
# We've got a module here and load_module needs specific
650
loading_path = plugin_path
653
kind = imp.PKG_DIRECTORY
656
for suffix, mode, kind in imp.get_suffixes():
657
if plugin_path.endswith(suffix):
658
loading_path = plugin_path
660
if loading_path is None:
661
raise ImportError('%s cannot be loaded from %s'
662
% (fullname, plugin_path))
663
if kind is imp.PKG_DIRECTORY:
666
f = open(loading_path, mode)
668
mod = imp.load_module(fullname, f, loading_path,
669
(suffix, mode, kind))
670
mod.__package__ = fullname
677
# Install a dedicated importer for plugins requiring special handling
678
PluginImporter = _PluginImporter()
679
sys.meta_path.append(PluginImporter)