1
# Copyright (C) 2005-2010 Canonical Ltd
1
# Copyright (C) 2004, 2005 by Canonical Ltd
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.
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.
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., 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
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
18
# This module implements plug-in support.
19
# Any python module in $BZR_PLUGIN_PATH will be imported upon initialization
20
# of bzrlib (and then forgotten about). In the plugin's main body, it should
21
# update any bzrlib registries it wants to extend; for example, to add new
22
# commands, import bzrlib.commands and add your new command to the
23
# plugin_cmds variable.
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
27
from bzrlib.osutils import config_dir
28
DEFAULT_PLUGIN_PATH = os.path.join(config_dir(), 'plugins')
63
_plugins_disabled = False
66
def are_plugins_disabled():
67
return _plugins_disabled
70
def disable_plugins():
71
"""Disable loading plugins.
73
Future calls to load_plugins() will be ignored.
75
global _plugins_disabled
76
_plugins_disabled = True
80
def _strip_trailing_sep(path):
81
return path.rstrip("\\/")
84
def set_plugins_path(path=None):
85
"""Set the path for plugins to be loaded from.
87
:param path: The list of paths to search for plugins. By default,
88
path will be determined using get_standard_plugins_path.
89
if path is [], no plugins can be loaded.
92
path = get_standard_plugins_path()
93
_mod_plugins.__path__ = path
94
PluginImporter.reset()
95
# Set up a blacklist for disabled plugins
96
disabled_plugins = os.environ.get('BZR_DISABLE_PLUGINS', None)
97
if disabled_plugins is not None:
98
for name in disabled_plugins.split(os.pathsep):
99
PluginImporter.blacklist.add('bzrlib.plugins.' + name)
100
# Set up a the specific paths for plugins
101
specific_plugins = os.environ.get('BZR_PLUGINS_AT', None)
102
if specific_plugins is not None:
103
for spec in specific_plugins.split(os.pathsep):
104
plugin_name, plugin_path = spec.split('@')
105
PluginImporter.specific_paths[
106
'bzrlib.plugins.%s' % plugin_name] = plugin_path
110
def _append_new_path(paths, new_path):
111
"""Append a new path if it set and not already known."""
112
if new_path is not None and new_path not in paths:
113
paths.append(new_path)
117
def get_core_plugin_path():
119
bzr_exe = bool(getattr(sys, 'frozen', None))
120
if bzr_exe: # expand path for bzr.exe
121
# We need to use relative path to system-wide plugin
122
# directory because bzrlib from standalone bzr.exe
123
# could be imported by another standalone program
124
# (e.g. bzr-config; or TortoiseBzr/Olive if/when they
125
# will become standalone exe). [bialix 20071123]
126
# __file__ typically is
127
# C:\Program Files\Bazaar\lib\library.zip\bzrlib\plugin.pyc
128
# then plugins directory is
129
# C:\Program Files\Bazaar\plugins
130
# so relative path is ../../../plugins
131
core_path = osutils.abspath(osutils.pathjoin(
132
osutils.dirname(__file__), '../../../plugins'))
133
else: # don't look inside library.zip
134
# search the plugin path before the bzrlib installed dir
135
core_path = os.path.dirname(_mod_plugins.__file__)
139
def get_site_plugin_path():
140
"""Returns the path for the site installed plugins."""
141
if sys.platform == 'win32':
142
# We don't have (yet) a good answer for windows since that is certainly
143
# related to the way we build the installers. -- vila20090821
147
from distutils.sysconfig import get_python_lib
149
# If distutuils is not available, we just don't know where they are
152
site_path = osutils.pathjoin(get_python_lib(), 'bzrlib', 'plugins')
156
def get_user_plugin_path():
157
return osutils.pathjoin(config.config_dir(), 'plugins')
160
def get_standard_plugins_path():
161
"""Determine a plugin path suitable for general use."""
162
# Ad-Hoc default: core is not overriden by site but user can overrides both
163
# The rationale is that:
164
# - 'site' comes last, because these plugins should always be available and
165
# are supposed to be in sync with the bzr installed on site.
166
# - 'core' comes before 'site' so that running bzr from sources or a user
167
# installed version overrides the site version.
168
# - 'user' comes first, because... user is always right.
169
# - the above rules clearly defines which plugin version will be loaded if
170
# several exist. Yet, it is sometimes desirable to disable some directory
171
# so that a set of plugins is disabled as once. This can be done via
172
# -site, -core, -user.
174
env_paths = os.environ.get('BZR_PLUGIN_PATH', '+user').split(os.pathsep)
175
defaults = ['+core', '+site']
177
# The predefined references
178
refs = dict(core=get_core_plugin_path(),
179
site=get_site_plugin_path(),
180
user=get_user_plugin_path())
182
# Unset paths that should be removed
183
for k,v in refs.iteritems():
185
# defaults can never mention removing paths as that will make it
186
# impossible for the user to revoke these removals.
187
if removed in env_paths:
188
env_paths.remove(removed)
193
for p in env_paths + defaults:
194
if p.startswith('+'):
195
# Resolve references if they are known
199
# Leave them untouched so user can still use paths starting
202
_append_new_path(paths, p)
204
# Get rid of trailing slashes, since Python can't handle them when
205
# it tries to import modules.
206
paths = map(_strip_trailing_sep, paths)
210
def load_plugins(path=None):
211
"""Load bzrlib plugins.
36
Find all python plugins and load them.
38
Loading a plugin means importing it into the python interpreter.
39
The plugin is expected to make calls to register commands when
40
it's loaded (or perhaps access other hooks in future.)
42
A list of plugs is stored in bzrlib.plugin.all_plugins for future
213
45
The environment variable BZR_PLUGIN_PATH is considered a delimited
214
46
set of paths to look through. Each entry is searched for *.py
215
47
files (and whatever other extensions are used in the platform,
218
load_from_path() provides the underlying mechanism and is called with
219
the default directory list to provide the normal behaviour.
221
:param path: The list of paths to search for plugins. By default,
222
path will be determined using get_standard_plugins_path.
223
if path is [], no plugins can be loaded.
51
global all_plugins, _loaded
227
53
# People can make sure plugins are loaded, they just won't be twice
55
#raise BzrError("plugins already initialized")
231
# scan for all plugins in the path.
232
load_from_path(set_plugins_path(path))
235
def load_from_path(dirs):
236
"""Load bzrlib plugins found in each dir in dirs.
238
Loading a plugin means importing it into the python interpreter.
239
The plugin is expected to make calls to register commands when
240
it's loaded (or perhaps access other hooks in future.)
242
Plugins are loaded into bzrlib.plugins.NAME, and can be found there
243
for future reference.
245
The python module path for bzrlib.plugins will be modified to be 'dirs'.
247
# Explicitly load the plugins with a specific path
248
for fullname, path in PluginImporter.specific_paths.iteritems():
249
name = fullname[len('bzrlib.plugins.'):]
250
_load_plugin_module(name, path)
252
# We need to strip the trailing separators here as well as in the
253
# set_plugins_path function because calling code can pass anything in to
254
# this function, and since it sets plugins.__path__, it should set it to
255
# something that will be valid for Python to use (in case people try to
256
# run "import bzrlib.plugins.PLUGINNAME" after calling this function).
257
_mod_plugins.__path__ = map(_strip_trailing_sep, dirs)
60
from bzrlib.trace import log_error, mutter, log_exception
61
from bzrlib.errors import BzrError
62
from bzrlib import plugins
64
dirs = os.environ.get('BZR_PLUGIN_PATH', DEFAULT_PLUGIN_PATH).split(":")
65
dirs.insert(0, os.path.dirname(plugins.__file__))
67
# The problem with imp.get_suffixes() is that it doesn't include
68
# .pyo which is technically valid
69
# It also means that "testmodule.so" will show up as both test and testmodule
70
# though it is only valid as 'test'
71
# but you should be careful, because "testmodule.py" loads as testmodule.
72
suffixes = imp.get_suffixes()
73
suffixes.append(('.pyo', 'rb', imp.PY_COMPILED))
74
package_entries = ['__init__.py', '__init__.pyc', '__init__.pyo']
76
# going through them one by one allows different plugins with the same
77
# filename in different directories in the path
78
mutter('looking for plugins in %s' % d)
261
trace.mutter('looking for plugins in %s', d)
266
# backwards compatability: load_from_dirs was the old name
267
# This was changed in 0.15
268
load_from_dirs = load_from_path
271
def _find_plugin_module(dir, name):
272
"""Check if there is a valid python module that can be loaded as a plugin.
274
:param dir: The directory where the search is performed.
275
:param path: An existing file path, either a python file or a package
278
:return: (name, path, description) name is the module name, path is the
279
file to load and description is the tuple returned by
282
path = osutils.pathjoin(dir, name)
283
if os.path.isdir(path):
284
# Check for a valid __init__.py file, valid suffixes depends on -O and
285
# can be .py, .pyc and .pyo
286
for suffix, mode, kind in imp.get_suffixes():
287
if kind not in (imp.PY_SOURCE, imp.PY_COMPILED):
288
# We don't recognize compiled modules (.so, .dll, etc)
290
init_path = osutils.pathjoin(path, '__init__' + suffix)
291
if os.path.isfile(init_path):
292
return name, init_path, (suffix, mode, kind)
294
for suffix, mode, kind in imp.get_suffixes():
295
if name.endswith(suffix):
296
# Clean up the module name
297
name = name[:-len(suffix)]
298
if kind == imp.C_EXTENSION and name.endswith('module'):
299
name = name[:-len('module')]
300
return name, path, (suffix, mode, kind)
301
# There is no python module here
302
return None, None, (None, None, None)
305
def _load_plugin_module(name, dir):
306
"""Load plugin name from dir.
308
:param name: The plugin name in the bzrlib.plugins namespace.
309
:param dir: The directory the plugin is loaded from for error messages.
311
if ('bzrlib.plugins.%s' % name) in PluginImporter.blacklist:
314
exec "import bzrlib.plugins.%s" % name in {}
315
except KeyboardInterrupt:
317
except errors.IncompatibleAPI, e:
318
trace.warning("Unable to load plugin %r. It requested API version "
319
"%s of module %s but the minimum exported version is %s, and "
320
"the maximum is %s" %
321
(name, e.wanted, e.api, e.minimum, e.current))
323
trace.warning("%s" % e)
324
if re.search('\.|-| ', name):
325
sanitised_name = re.sub('[-. ]', '_', name)
326
if sanitised_name.startswith('bzr_'):
327
sanitised_name = sanitised_name[len('bzr_'):]
328
trace.warning("Unable to load %r in %r as a plugin because the "
329
"file path isn't a valid module name; try renaming "
330
"it to %r." % (name, dir, sanitised_name))
332
trace.warning('Unable to load plugin %r from %r' % (name, dir))
333
trace.log_exception_quietly()
334
if 'error' in debug.debug_flags:
335
trace.print_exception(sys.exc_info(), sys.stderr)
338
def load_from_dir(d):
339
"""Load the plugins in directory d.
341
d must be in the plugins module path already.
342
This function is called once for each directory in the module path.
345
for p in os.listdir(d):
346
name, path, desc = _find_plugin_module(d, p)
348
if name == '__init__':
349
# We do nothing with the __init__.py file in directories from
350
# the bzrlib.plugins module path, we may want to, one day
352
continue # We don't load __init__.py in the plugins dirs
353
elif getattr(_mod_plugins, name, None) is not None:
354
# The module has already been loaded from another directory
355
# during a previous call.
356
# FIXME: There should be a better way to report masked plugins
358
trace.mutter('Plugin name %s already loaded', name)
82
if not os.path.isdir(d):
84
for f in os.listdir(d):
85
path = os.path.join(d, f)
86
if os.path.isdir(path):
87
for entry in package_entries:
88
# This directory should be a package, and thus added to
90
if os.path.isfile(os.path.join(path, entry)):
92
else: # This directory is not a package
360
plugin_names.add(name)
362
for name in plugin_names:
363
_load_plugin_module(name, d)
367
"""Return a dictionary of the plugins.
369
Each item in the dictionary is a PlugIn object.
372
for name, plugin in _mod_plugins.__dict__.items():
373
if isinstance(plugin, types.ModuleType):
374
result[name] = PlugIn(name, plugin)
378
class PluginsHelpIndex(object):
379
"""A help index that returns help topics for plugins."""
382
self.prefix = 'plugins/'
384
def get_topics(self, topic):
385
"""Search for topic in the loaded plugins.
387
This will not trigger loading of new plugins.
389
:param topic: A topic to search for.
390
:return: A list which is either empty or contains a single
391
RegisteredTopic entry.
395
if topic.startswith(self.prefix):
396
topic = topic[len(self.prefix):]
397
plugin_module_name = 'bzrlib.plugins.%s' % topic
399
module = sys.modules[plugin_module_name]
403
return [ModuleHelpTopic(module)]
406
class ModuleHelpTopic(object):
407
"""A help topic which returns the docstring for a module."""
409
def __init__(self, module):
412
:param module: The module for which help should be generated.
416
def get_help_text(self, additional_see_also=None, verbose=True):
417
"""Return a string with the help for this topic.
419
:param additional_see_also: Additional help topics to be
422
if not self.module.__doc__:
423
result = "Plugin '%s' has no docstring.\n" % self.module.__name__
425
result = self.module.__doc__
426
if result[-1] != '\n':
428
# there is code duplicated here and in bzrlib/help_topic.py's
429
# matching Topic code. This should probably be factored in
430
# to a helper function and a common base class.
431
if additional_see_also is not None:
432
see_also = sorted(set(additional_see_also))
436
result += 'See also: '
437
result += ', '.join(see_also)
441
def get_help_topic(self):
442
"""Return the modules help topic - its __name__ after bzrlib.plugins.."""
443
return self.module.__name__[len('bzrlib.plugins.'):]
446
class PlugIn(object):
447
"""The bzrlib representation of a plugin.
449
The PlugIn object provides a way to manipulate a given plugin module.
452
def __init__(self, name, module):
453
"""Construct a plugin for module."""
458
"""Get the path that this plugin was loaded from."""
459
if getattr(self.module, '__path__', None) is not None:
460
return os.path.abspath(self.module.__path__[0])
461
elif getattr(self.module, '__file__', None) is not None:
462
path = os.path.abspath(self.module.__file__)
463
if path[-4:] in ('.pyc', '.pyo'):
464
pypath = path[:-4] + '.py'
465
if os.path.isfile(pypath):
469
return repr(self.module)
472
return "<%s.%s object at %s, name=%s, module=%s>" % (
473
self.__class__.__module__, self.__class__.__name__, id(self),
474
self.name, self.module)
478
def test_suite(self):
479
"""Return the plugin's test suite."""
480
if getattr(self.module, 'test_suite', None) is not None:
481
return self.module.test_suite()
485
def load_plugin_tests(self, loader):
486
"""Return the adapted plugin's test suite.
488
:param loader: The custom loader that should be used to load additional
492
if getattr(self.module, 'load_tests', None) is not None:
493
return loader.loadTestsFromModule(self.module)
497
def version_info(self):
498
"""Return the plugin's version_tuple or None if unknown."""
499
version_info = getattr(self.module, 'version_info', None)
500
if version_info is not None:
95
for suffix_info in suffixes:
96
if f.endswith(suffix_info[0]):
97
f = f[:-len(suffix_info[0])]
98
if suffix_info[2] == imp.C_EXTENSION and f.endswith('module'):
99
f = f[:-len('module')]
103
mutter('add plugin name %s' % f)
106
plugin_names = list(plugin_names)
108
for name in plugin_names:
502
if isinstance(version_info, types.StringType):
503
version_info = version_info.split('.')
504
elif len(version_info) == 3:
505
version_info = tuple(version_info) + ('final', 0)
507
# The given version_info isn't even iteratible
508
trace.log_exception_quietly()
509
version_info = (version_info,)
512
def _get__version__(self):
513
version_info = self.version_info()
514
if version_info is None or len(version_info) == 0:
517
version_string = _format_version_tuple(version_info)
518
except (ValueError, TypeError, IndexError), e:
519
trace.log_exception_quietly()
520
# try to return something usefull for bad plugins, in stead of
522
version_string = '.'.join(map(str, version_info))
523
return version_string
525
__version__ = property(_get__version__)
528
class _PluginImporter(object):
529
"""An importer tailored to bzr specific needs.
531
This is a singleton that takes care of:
532
- disabled plugins specified in 'blacklist',
533
- plugins that needs to be loaded from specific directories.
540
self.blacklist = set()
541
self.specific_paths = {}
543
def find_module(self, fullname, parent_path=None):
544
"""Search a plugin module.
546
Disabled plugins raise an import error, plugins with specific paths
547
returns a specific loader.
549
:return: None if the plugin doesn't need special handling, self
552
if not fullname.startswith('bzrlib.plugins.'):
554
if fullname in self.blacklist:
555
raise ImportError('%s is disabled' % fullname)
556
if fullname in self.specific_paths:
560
def load_module(self, fullname):
561
"""Load a plugin from a specific directory."""
562
# We are called only for specific paths
563
plugin_path = self.specific_paths[fullname]
566
if os.path.isdir(plugin_path):
567
for suffix, mode, kind in imp.get_suffixes():
568
if kind not in (imp.PY_SOURCE, imp.PY_COMPILED):
569
# We don't recognize compiled modules (.so, .dll, etc)
571
init_path = osutils.pathjoin(plugin_path, '__init__' + suffix)
572
if os.path.isfile(init_path):
573
loading_path = init_path
577
for suffix, mode, kind in imp.get_suffixes():
578
if plugin_path.endswith(suffix):
579
loading_path = plugin_path
581
if loading_path is None:
582
raise ImportError('%s cannot be loaded from %s'
583
% (fullname, plugin_path))
584
f = open(loading_path, mode)
586
mod = imp.load_module(fullname, f, loading_path,
587
(suffix, mode, kind))
589
# The plugin can contain modules, so be ready
590
mod.__path__ = [plugin_path]
591
mod.__package__ = fullname
597
# Install a dedicated importer for plugins requiring special handling
598
PluginImporter = _PluginImporter()
599
sys.meta_path.append(PluginImporter)
110
plugin_info = imp.find_module(name, [d])
111
mutter('load plugin %r' % (plugin_info,))
113
plugin = imp.load_module('bzrlib.plugin.' + name,
115
all_plugins.append(plugin)
117
if plugin_info[0] is not None:
118
plugin_info[0].close()
120
mutter('loaded succesfully')
122
log_error('Unable to load plugin %r from %r' % (name, d))