1
# Copyright (C) 2005-2010 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
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
97
def _append_new_path(paths, new_path):
98
"""Append a new path if it set and not already known."""
99
if new_path is not None and new_path not in paths:
100
paths.append(new_path)
104
def get_core_plugin_path():
106
bzr_exe = bool(getattr(sys, 'frozen', None))
107
if bzr_exe: # expand path for bzr.exe
108
# We need to use relative path to system-wide plugin
109
# directory because bzrlib from standalone bzr.exe
110
# could be imported by another standalone program
111
# (e.g. bzr-config; or TortoiseBzr/Olive if/when they
112
# will become standalone exe). [bialix 20071123]
113
# __file__ typically is
114
# C:\Program Files\Bazaar\lib\library.zip\bzrlib\plugin.pyc
115
# then plugins directory is
116
# C:\Program Files\Bazaar\plugins
117
# so relative path is ../../../plugins
118
core_path = osutils.abspath(osutils.pathjoin(
119
osutils.dirname(__file__), '../../../plugins'))
120
else: # don't look inside library.zip
121
# search the plugin path before the bzrlib installed dir
122
core_path = os.path.dirname(_mod_plugins.__file__)
126
def get_site_plugin_path():
127
"""Returns the path for the site installed plugins."""
128
if sys.platform == 'win32':
129
# We don't have (yet) a good answer for windows since that is certainly
130
# related to the way we build the installers. -- vila20090821
134
from distutils.sysconfig import get_python_lib
136
# If distutuils is not available, we just don't know where they are
139
site_path = osutils.pathjoin(get_python_lib(), 'bzrlib', 'plugins')
143
def get_user_plugin_path():
144
return osutils.pathjoin(config.config_dir(), 'plugins')
147
def get_standard_plugins_path():
148
"""Determine a plugin path suitable for general use."""
149
# Ad-Hoc default: core is not overriden by site but user can overrides both
150
# The rationale is that:
151
# - 'site' comes last, because these plugins should always be available and
152
# are supposed to be in sync with the bzr installed on site.
153
# - 'core' comes before 'site' so that running bzr from sources or a user
154
# installed version overrides the site version.
155
# - 'user' comes first, because... user is always right.
156
# - the above rules clearly defines which plugin version will be loaded if
157
# several exist. Yet, it is sometimes desirable to disable some directory
158
# so that a set of plugins is disabled as once. This can be done via
159
# -site, -core, -user.
161
env_paths = os.environ.get('BZR_PLUGIN_PATH', '+user').split(os.pathsep)
162
defaults = ['+core', '+site']
164
# The predefined references
165
refs = dict(core=get_core_plugin_path(),
166
site=get_site_plugin_path(),
167
user=get_user_plugin_path())
169
# Unset paths that should be removed
170
for k,v in refs.iteritems():
172
# defaults can never mention removing paths as that will make it
173
# impossible for the user to revoke these removals.
174
if removed in env_paths:
175
env_paths.remove(removed)
180
for p in env_paths + defaults:
181
if p.startswith('+'):
182
# Resolve references if they are known
186
# Leave them untouched otherwise, user may have paths starting
189
_append_new_path(paths, p)
191
# Get rid of trailing slashes, since Python can't handle them when
192
# it tries to import modules.
193
paths = map(_strip_trailing_sep, paths)
197
def load_plugins(path=None):
198
"""Load bzrlib plugins.
200
The environment variable BZR_PLUGIN_PATH is considered a delimited
201
set of paths to look through. Each entry is searched for *.py
202
files (and whatever other extensions are used in the platform,
205
load_from_dirs() provides the underlying mechanism and is called with
206
the default directory list to provide the normal behaviour.
208
:param path: The list of paths to search for plugins. By default,
209
path will be determined using get_standard_plugins_path.
210
if path is [], no plugins can be loaded.
214
# People can make sure plugins are loaded, they just won't be twice
218
# scan for all plugins in the path.
219
load_from_path(set_plugins_path(path))
222
def load_from_path(dirs):
223
"""Load bzrlib plugins found in each dir in dirs.
225
Loading a plugin means importing it into the python interpreter.
226
The plugin is expected to make calls to register commands when
227
it's loaded (or perhaps access other hooks in future.)
229
Plugins are loaded into bzrlib.plugins.NAME, and can be found there
230
for future reference.
232
The python module path for bzrlib.plugins will be modified to be 'dirs'.
234
# We need to strip the trailing separators here as well as in the
235
# set_plugins_path function because calling code can pass anything in to
236
# this function, and since it sets plugins.__path__, it should set it to
237
# something that will be valid for Python to use (in case people try to
238
# run "import bzrlib.plugins.PLUGINNAME" after calling this function).
239
_mod_plugins.__path__ = map(_strip_trailing_sep, dirs)
243
trace.mutter('looking for plugins in %s', d)
248
# backwards compatability: load_from_dirs was the old name
249
# This was changed in 0.15
250
load_from_dirs = load_from_path
253
def load_from_dir(d):
254
"""Load the plugins in directory d.
256
d must be in the plugins module path already.
258
# Get the list of valid python suffixes for __init__.py?
259
# this includes .py, .pyc, and .pyo (depending on if we are running -O)
260
# but it doesn't include compiled modules (.so, .dll, etc)
261
valid_suffixes = [suffix for suffix, mod_type, flags in imp.get_suffixes()
262
if flags in (imp.PY_SOURCE, imp.PY_COMPILED)]
263
package_entries = ['__init__'+suffix for suffix in valid_suffixes]
265
for f in os.listdir(d):
266
path = osutils.pathjoin(d, f)
267
if os.path.isdir(path):
268
for entry in package_entries:
269
# This directory should be a package, and thus added to
271
if os.path.isfile(osutils.pathjoin(path, entry)):
273
else: # This directory is not a package
276
for suffix_info in imp.get_suffixes():
277
if f.endswith(suffix_info[0]):
278
f = f[:-len(suffix_info[0])]
279
if suffix_info[2] == imp.C_EXTENSION and f.endswith('module'):
280
f = f[:-len('module')]
285
continue # We don't load __init__.py again in the plugin dir
286
elif getattr(_mod_plugins, f, None):
287
trace.mutter('Plugin name %s already loaded', f)
289
# trace.mutter('add plugin name %s', f)
292
for name in plugin_names:
294
exec "import bzrlib.plugins.%s" % name in {}
295
except KeyboardInterrupt:
297
except errors.IncompatibleAPI, e:
298
trace.warning("Unable to load plugin %r. It requested API version "
299
"%s of module %s but the minimum exported version is %s, and "
300
"the maximum is %s" %
301
(name, e.wanted, e.api, e.minimum, e.current))
303
trace.warning("%s" % e)
304
## import pdb; pdb.set_trace()
305
if re.search('\.|-| ', name):
306
sanitised_name = re.sub('[-. ]', '_', name)
307
if sanitised_name.startswith('bzr_'):
308
sanitised_name = sanitised_name[len('bzr_'):]
309
trace.warning("Unable to load %r in %r as a plugin because the "
310
"file path isn't a valid module name; try renaming "
311
"it to %r." % (name, d, sanitised_name))
313
trace.warning('Unable to load plugin %r from %r' % (name, d))
314
trace.log_exception_quietly()
315
if 'error' in debug.debug_flags:
316
trace.print_exception(sys.exc_info(), sys.stderr)
320
"""Return a dictionary of the plugins.
322
Each item in the dictionary is a PlugIn object.
325
for name, plugin in _mod_plugins.__dict__.items():
326
if isinstance(plugin, types.ModuleType):
327
result[name] = PlugIn(name, plugin)
331
class PluginsHelpIndex(object):
332
"""A help index that returns help topics for plugins."""
335
self.prefix = 'plugins/'
337
def get_topics(self, topic):
338
"""Search for topic in the loaded plugins.
340
This will not trigger loading of new plugins.
342
:param topic: A topic to search for.
343
:return: A list which is either empty or contains a single
344
RegisteredTopic entry.
348
if topic.startswith(self.prefix):
349
topic = topic[len(self.prefix):]
350
plugin_module_name = 'bzrlib.plugins.%s' % topic
352
module = sys.modules[plugin_module_name]
356
return [ModuleHelpTopic(module)]
359
class ModuleHelpTopic(object):
360
"""A help topic which returns the docstring for a module."""
362
def __init__(self, module):
365
:param module: The module for which help should be generated.
369
def get_help_text(self, additional_see_also=None, verbose=True):
370
"""Return a string with the help for this topic.
372
:param additional_see_also: Additional help topics to be
375
if not self.module.__doc__:
376
result = "Plugin '%s' has no docstring.\n" % self.module.__name__
378
result = self.module.__doc__
379
if result[-1] != '\n':
381
# there is code duplicated here and in bzrlib/help_topic.py's
382
# matching Topic code. This should probably be factored in
383
# to a helper function and a common base class.
384
if additional_see_also is not None:
385
see_also = sorted(set(additional_see_also))
389
result += 'See also: '
390
result += ', '.join(see_also)
394
def get_help_topic(self):
395
"""Return the modules help topic - its __name__ after bzrlib.plugins.."""
396
return self.module.__name__[len('bzrlib.plugins.'):]
399
class PlugIn(object):
400
"""The bzrlib representation of a plugin.
402
The PlugIn object provides a way to manipulate a given plugin module.
405
def __init__(self, name, module):
406
"""Construct a plugin for module."""
411
"""Get the path that this plugin was loaded from."""
412
if getattr(self.module, '__path__', None) is not None:
413
return os.path.abspath(self.module.__path__[0])
414
elif getattr(self.module, '__file__', None) is not None:
415
path = os.path.abspath(self.module.__file__)
416
if path[-4:] in ('.pyc', '.pyo'):
417
pypath = path[:-4] + '.py'
418
if os.path.isfile(pypath):
422
return repr(self.module)
425
return "<%s.%s object at %s, name=%s, module=%s>" % (
426
self.__class__.__module__, self.__class__.__name__, id(self),
427
self.name, self.module)
431
def test_suite(self):
432
"""Return the plugin's test suite."""
433
if getattr(self.module, 'test_suite', None) is not None:
434
return self.module.test_suite()
438
def load_plugin_tests(self, loader):
439
"""Return the adapted plugin's test suite.
441
:param loader: The custom loader that should be used to load additional
445
if getattr(self.module, 'load_tests', None) is not None:
446
return loader.loadTestsFromModule(self.module)
450
def version_info(self):
451
"""Return the plugin's version_tuple or None if unknown."""
452
version_info = getattr(self.module, 'version_info', None)
453
if version_info is not None:
455
if isinstance(version_info, types.StringType):
456
version_info = version_info.split('.')
457
elif len(version_info) == 3:
458
version_info = tuple(version_info) + ('final', 0)
460
# The given version_info isn't even iteratible
461
trace.log_exception_quietly()
462
version_info = (version_info,)
465
def _get__version__(self):
466
version_info = self.version_info()
467
if version_info is None or len(version_info) == 0:
470
version_string = _format_version_tuple(version_info)
471
except (ValueError, TypeError, IndexError), e:
472
trace.log_exception_quietly()
473
# try to return something usefull for bad plugins, in stead of
475
version_string = '.'.join(map(str, version_info))
476
return version_string
478
__version__ = property(_get__version__)