1
# Copyright (C) 2004, 2005, 2007, 2008 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 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(), """
47
_format_version_tuple,
53
from bzrlib import plugins as _mod_plugins
56
from bzrlib.symbol_versioning import deprecated_function, one_three
59
DEFAULT_PLUGIN_PATH = None
62
def get_default_plugin_path():
63
"""Get the DEFAULT_PLUGIN_PATH"""
64
global DEFAULT_PLUGIN_PATH
65
if DEFAULT_PLUGIN_PATH is None:
66
DEFAULT_PLUGIN_PATH = osutils.pathjoin(config.config_dir(), 'plugins')
67
return DEFAULT_PLUGIN_PATH
70
def disable_plugins():
71
"""Disable loading plugins.
73
Future calls to load_plugins() will be ignored.
75
# TODO: jam 20060131 This should probably also disable
81
def _strip_trailing_sep(path):
82
return path.rstrip("\\/")
85
def set_plugins_path():
86
"""Set the path for plugins to be loaded from."""
87
path = os.environ.get('BZR_PLUGIN_PATH',
88
get_default_plugin_path()).split(os.pathsep)
89
bzr_exe = bool(getattr(sys, 'frozen', None))
90
if bzr_exe: # expand path for bzr.exe
91
# We need to use relative path to system-wide plugin
92
# directory because bzrlib from standalone bzr.exe
93
# could be imported by another standalone program
94
# (e.g. bzr-config; or TortoiseBzr/Olive if/when they
95
# will become standalone exe). [bialix 20071123]
96
# __file__ typically is
97
# C:\Program Files\Bazaar\lib\library.zip\bzrlib\plugin.pyc
98
# then plugins directory is
99
# C:\Program Files\Bazaar\plugins
100
# so relative path is ../../../plugins
101
path.append(osutils.abspath(osutils.pathjoin(
102
osutils.dirname(__file__), '../../../plugins')))
103
# Get rid of trailing slashes, since Python can't handle them when
104
# it tries to import modules.
105
path = map(_strip_trailing_sep, path)
106
if not bzr_exe: # don't look inside library.zip
107
# search the plugin path before the bzrlib installed dir
108
path.append(os.path.dirname(_mod_plugins.__file__))
109
# search the arch independent path if we can determine that and
110
# the plugin is found nowhere else
111
if sys.platform != 'win32':
113
from distutils.sysconfig import get_python_lib
115
# If distutuils is not available, we just won't add that path
118
archless_path = osutils.pathjoin(get_python_lib(), 'bzrlib',
120
if archless_path not in path:
121
path.append(archless_path)
122
_mod_plugins.__path__ = path
127
"""Load bzrlib plugins.
129
The environment variable BZR_PLUGIN_PATH is considered a delimited
130
set of paths to look through. Each entry is searched for *.py
131
files (and whatever other extensions are used in the platform,
134
load_from_dirs() provides the underlying mechanism and is called with
135
the default directory list to provide the normal behaviour.
139
# People can make sure plugins are loaded, they just won't be twice
143
# scan for all plugins in the path.
144
load_from_path(set_plugins_path())
147
def load_from_path(dirs):
148
"""Load bzrlib plugins found in each dir in dirs.
150
Loading a plugin means importing it into the python interpreter.
151
The plugin is expected to make calls to register commands when
152
it's loaded (or perhaps access other hooks in future.)
154
Plugins are loaded into bzrlib.plugins.NAME, and can be found there
155
for future reference.
157
The python module path for bzrlib.plugins will be modified to be 'dirs'.
159
# We need to strip the trailing separators here as well as in the
160
# set_plugins_path function because calling code can pass anything in to
161
# this function, and since it sets plugins.__path__, it should set it to
162
# something that will be valid for Python to use (in case people try to
163
# run "import bzrlib.plugins.PLUGINNAME" after calling this function).
164
_mod_plugins.__path__ = map(_strip_trailing_sep, dirs)
168
trace.mutter('looking for plugins in %s', d)
173
# backwards compatability: load_from_dirs was the old name
174
# This was changed in 0.15
175
load_from_dirs = load_from_path
178
def load_from_dir(d):
179
"""Load the plugins in directory d.
181
d must be in the plugins module path already.
183
# Get the list of valid python suffixes for __init__.py?
184
# this includes .py, .pyc, and .pyo (depending on if we are running -O)
185
# but it doesn't include compiled modules (.so, .dll, etc)
186
valid_suffixes = [suffix for suffix, mod_type, flags in imp.get_suffixes()
187
if flags in (imp.PY_SOURCE, imp.PY_COMPILED)]
188
package_entries = ['__init__'+suffix for suffix in valid_suffixes]
190
for f in os.listdir(d):
191
path = osutils.pathjoin(d, f)
192
if os.path.isdir(path):
193
for entry in package_entries:
194
# This directory should be a package, and thus added to
196
if os.path.isfile(osutils.pathjoin(path, entry)):
198
else: # This directory is not a package
201
for suffix_info in imp.get_suffixes():
202
if f.endswith(suffix_info[0]):
203
f = f[:-len(suffix_info[0])]
204
if suffix_info[2] == imp.C_EXTENSION and f.endswith('module'):
205
f = f[:-len('module')]
210
continue # We don't load __init__.py again in the plugin dir
211
elif getattr(_mod_plugins, f, None):
212
trace.mutter('Plugin name %s already loaded', f)
214
# trace.mutter('add plugin name %s', f)
217
for name in plugin_names:
219
exec "import bzrlib.plugins.%s" % name in {}
220
except KeyboardInterrupt:
222
except errors.IncompatibleAPI, e:
223
trace.warning("Unable to load plugin %r. It requested API version "
224
"%s of module %s but the minimum exported version is %s, and "
225
"the maximum is %s" %
226
(name, e.wanted, e.api, e.minimum, e.current))
228
trace.warning("%s" % e)
229
## import pdb; pdb.set_trace()
230
if re.search('\.|-| ', name):
231
sanitised_name = re.sub('[-. ]', '_', name)
232
if sanitised_name.startswith('bzr_'):
233
sanitised_name = sanitised_name[len('bzr_'):]
234
trace.warning("Unable to load %r in %r as a plugin because the "
235
"file path isn't a valid module name; try renaming "
236
"it to %r." % (name, d, sanitised_name))
238
trace.warning('Unable to load plugin %r from %r' % (name, d))
239
trace.log_exception_quietly()
240
if 'error' in debug.debug_flags:
241
trace.print_exception(sys.exc_info(), sys.stderr)
244
@deprecated_function(one_three)
245
def load_from_zip(zip_name):
246
"""Load all the plugins in a zip."""
247
valid_suffixes = ('.py', '.pyc', '.pyo') # only python modules/packages
250
index = zip_name.rindex('.zip')
253
archive = zip_name[:index+4]
254
prefix = zip_name[index+5:]
256
trace.mutter('Looking for plugins in %r', zip_name)
258
# use zipfile to get list of files/dirs inside zip
260
z = zipfile.ZipFile(archive)
261
namelist = z.namelist()
263
except zipfile.error:
268
prefix = prefix.replace('\\','/')
269
if prefix[-1] != '/':
272
namelist = [name[ix:]
274
if name.startswith(prefix)]
276
trace.mutter('Names in archive: %r', namelist)
278
for name in namelist:
279
if not name or name.endswith('/'):
282
# '/' is used to separate pathname components inside zip archives
285
head, tail = '', name
287
head, tail = name.rsplit('/',1)
289
# we don't need looking in subdirectories
292
base, suffix = osutils.splitext(tail)
293
if suffix not in valid_suffixes:
296
if base == '__init__':
307
if getattr(_mod_plugins, plugin_name, None):
308
trace.mutter('Plugin name %s already loaded', plugin_name)
312
exec "import bzrlib.plugins.%s" % plugin_name in {}
313
trace.mutter('Load plugin %s from zip %r', plugin_name, zip_name)
314
except KeyboardInterrupt:
317
## import pdb; pdb.set_trace()
318
trace.warning('Unable to load plugin %r from %r'
320
trace.log_exception_quietly()
321
if 'error' in debug.debug_flags:
322
trace.print_exception(sys.exc_info(), sys.stderr)
326
"""Return a dictionary of the plugins.
328
Each item in the dictionary is a PlugIn object.
331
for name, plugin in _mod_plugins.__dict__.items():
332
if isinstance(plugin, types.ModuleType):
333
result[name] = PlugIn(name, plugin)
337
class PluginsHelpIndex(object):
338
"""A help index that returns help topics for plugins."""
341
self.prefix = 'plugins/'
343
def get_topics(self, topic):
344
"""Search for topic in the loaded plugins.
346
This will not trigger loading of new plugins.
348
:param topic: A topic to search for.
349
:return: A list which is either empty or contains a single
350
RegisteredTopic entry.
354
if topic.startswith(self.prefix):
355
topic = topic[len(self.prefix):]
356
plugin_module_name = 'bzrlib.plugins.%s' % topic
358
module = sys.modules[plugin_module_name]
362
return [ModuleHelpTopic(module)]
365
class ModuleHelpTopic(object):
366
"""A help topic which returns the docstring for a module."""
368
def __init__(self, module):
371
:param module: The module for which help should be generated.
375
def get_help_text(self, additional_see_also=None):
376
"""Return a string with the help for this topic.
378
:param additional_see_also: Additional help topics to be
381
if not self.module.__doc__:
382
result = "Plugin '%s' has no docstring.\n" % self.module.__name__
384
result = self.module.__doc__
385
if result[-1] != '\n':
387
# there is code duplicated here and in bzrlib/help_topic.py's
388
# matching Topic code. This should probably be factored in
389
# to a helper function and a common base class.
390
if additional_see_also is not None:
391
see_also = sorted(set(additional_see_also))
395
result += 'See also: '
396
result += ', '.join(see_also)
400
def get_help_topic(self):
401
"""Return the modules help topic - its __name__ after bzrlib.plugins.."""
402
return self.module.__name__[len('bzrlib.plugins.'):]
405
class PlugIn(object):
406
"""The bzrlib representation of a plugin.
408
The PlugIn object provides a way to manipulate a given plugin module.
411
def __init__(self, name, module):
412
"""Construct a plugin for module."""
417
"""Get the path that this plugin was loaded from."""
418
if getattr(self.module, '__path__', None) is not None:
419
return os.path.abspath(self.module.__path__[0])
420
elif getattr(self.module, '__file__', None) is not None:
421
path = os.path.abspath(self.module.__file__)
422
if path[-4:] in ('.pyc', '.pyo'):
423
pypath = path[:-4] + '.py'
424
if os.path.isfile(pypath):
428
return repr(self.module)
431
return "<%s.%s object at %s, name=%s, module=%s>" % (
432
self.__class__.__module__, self.__class__.__name__, id(self),
433
self.name, self.module)
437
def test_suite(self):
438
"""Return the plugin's test suite."""
439
if getattr(self.module, 'test_suite', None) is not None:
440
return self.module.test_suite()
444
def load_plugin_tests(self, loader):
445
"""Return the adapted plugin's test suite.
447
:param loader: The custom loader that should be used to load additional
451
if getattr(self.module, 'load_tests', None) is not None:
452
return loader.loadTestsFromModule(self.module)
456
def version_info(self):
457
"""Return the plugin's version_tuple or None if unknown."""
458
version_info = getattr(self.module, 'version_info', None)
459
if version_info is not None:
461
if isinstance(version_info, types.StringType):
462
version_info = version_info.split('.')
463
elif len(version_info) == 3:
464
version_info = tuple(version_info) + ('final', 0)
466
# The given version_info isn't even iteratible
467
trace.log_exception_quietly()
468
version_info = (version_info,)
471
def _get__version__(self):
472
version_info = self.version_info()
473
if version_info is None or len(version_info) == 0:
476
version_string = _format_version_tuple(version_info)
477
except (ValueError, TypeError, IndexError), e:
478
trace.log_exception_quietly()
479
# try to return something usefull for bad plugins, in stead of
481
version_string = '.'.join(map(str, version_info))
482
return version_string
484
__version__ = property(_get__version__)