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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
18
"""bzr python plugin support
20
Any python module in $BZR_PLUGIN_PATH will be imported upon initialization of
21
bzrlib. The module will be imported as 'bzrlib.plugins.$BASENAME(PLUGIN)'.
22
In the plugin's main body, it should update any bzrlib registries it wants to
23
extend; for example, to add new commands, import bzrlib.commands and add your
24
new command to the plugin_cmds variable.
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
"""bzr python plugin support.
19
When load_plugins() is invoked, any python module in any directory in
20
$BZR_PLUGIN_PATH will be imported. The module will be imported as
21
'bzrlib.plugins.$BASENAME(PLUGIN)'. In the plugin's main body, it should
22
update any bzrlib registries it wants to extend.
24
See the plugin-api developer documentation for information about writing
27
BZR_PLUGIN_PATH is also honoured for any plugins imported via
28
'import bzrlib.plugins.PLUGINNAME', as long as set_plugins_path has been
27
# TODO: Refactor this to make it more testable. The main problem at the
28
# moment is that loading plugins affects the global process state -- for bzr
29
# in general use it's a reasonable assumption that all plugins are loaded at
30
# startup and then stay loaded, but this is less good for testing.
32
# Several specific issues:
33
# - plugins can't be unloaded and will continue to effect later tests
34
# - load_plugins does nothing if called a second time
35
# - plugin hooks can't be removed
37
# Our options are either to remove these restrictions, or work around them by
38
# loading the plugins into a different space than the one running the tests.
39
# That could be either a separate Python interpreter or perhaps a new
40
# namespace inside this interpreter.
32
from __future__ import absolute_import
37
from bzrlib import osutils
45
39
from bzrlib.lazy_import import lazy_import
46
40
lazy_import(globals(), """
50
45
from bzrlib import (
46
_format_version_tuple,
52
from bzrlib.i18n import gettext
53
from bzrlib import plugins as _mod_plugins
57
from bzrlib.trace import mutter, warning, log_exception_quietly
60
57
DEFAULT_PLUGIN_PATH = None
63
def get_default_plugin_path():
64
"""Get the DEFAULT_PLUGIN_PATH"""
65
global DEFAULT_PLUGIN_PATH
66
if DEFAULT_PLUGIN_PATH is None:
67
DEFAULT_PLUGIN_PATH = osutils.pathjoin(config.config_dir(), 'plugins')
68
return DEFAULT_PLUGIN_PATH
75
"""Return a dictionary of the plugins."""
77
for name, plugin in plugins.__dict__.items():
78
if isinstance(plugin, types.ModuleType):
59
_plugins_disabled = False
63
# Map from plugin name, to list of string warnings about eg plugin
67
def are_plugins_disabled():
68
return _plugins_disabled
83
71
def disable_plugins():
86
74
Future calls to load_plugins() will be ignored.
88
# TODO: jam 20060131 This should probably also disable
76
global _plugins_disabled
77
_plugins_disabled = True
81
def describe_plugins(show_paths=False):
82
"""Generate text description of plugins.
84
Includes both those that have loaded, and those that failed to
87
:param show_paths: If true,
88
:returns: Iterator of text lines (including newlines.)
90
from inspect import getdoc
91
loaded_plugins = plugins()
92
all_names = sorted(list(set(
93
loaded_plugins.keys() + plugin_warnings.keys())))
94
for name in all_names:
95
if name in loaded_plugins:
96
plugin = loaded_plugins[name]
97
version = plugin.__version__
98
if version == 'unknown':
100
yield '%s %s\n' % (name, version)
101
d = getdoc(plugin.module)
103
doc = d.split('\n')[0]
105
doc = '(no description)'
106
yield (" %s\n" % doc)
108
yield (" %s\n" % plugin.path())
111
yield "%s (failed to load)\n" % name
112
if name in plugin_warnings:
113
for line in plugin_warnings[name]:
114
yield " ** " + line + '\n'
118
def _strip_trailing_sep(path):
119
return path.rstrip("\\/")
122
def _get_specific_plugin_paths(paths):
123
"""Returns the plugin paths from a string describing the associations.
125
:param paths: A string describing the paths associated with the plugins.
127
:returns: A list of (plugin name, path) tuples.
129
For example, if paths is my_plugin@/test/my-test:her_plugin@/production/her,
130
[('my_plugin', '/test/my-test'), ('her_plugin', '/production/her')]
133
Note that ':' in the example above depends on the os.
138
for spec in paths.split(os.pathsep):
140
name, path = spec.split('@')
142
raise errors.BzrCommandError(gettext(
143
'"%s" is not a valid <plugin_name>@<plugin_path> description ')
145
specs.append((name, path))
149
def set_plugins_path(path=None):
150
"""Set the path for plugins to be loaded from.
152
:param path: The list of paths to search for plugins. By default,
153
path will be determined using get_standard_plugins_path.
154
if path is [], no plugins can be loaded.
157
path = get_standard_plugins_path()
158
_mod_plugins.__path__ = path
159
PluginImporter.reset()
160
# Set up a blacklist for disabled plugins
161
disabled_plugins = os.environ.get('BZR_DISABLE_PLUGINS', None)
162
if disabled_plugins is not None:
163
for name in disabled_plugins.split(os.pathsep):
164
PluginImporter.blacklist.add('bzrlib.plugins.' + name)
165
# Set up a the specific paths for plugins
166
for plugin_name, plugin_path in _get_specific_plugin_paths(os.environ.get(
167
'BZR_PLUGINS_AT', None)):
168
PluginImporter.specific_paths[
169
'bzrlib.plugins.%s' % plugin_name] = plugin_path
173
def _append_new_path(paths, new_path):
174
"""Append a new path if it set and not already known."""
175
if new_path is not None and new_path not in paths:
176
paths.append(new_path)
180
def get_core_plugin_path():
182
bzr_exe = bool(getattr(sys, 'frozen', None))
183
if bzr_exe: # expand path for bzr.exe
184
# We need to use relative path to system-wide plugin
185
# directory because bzrlib from standalone bzr.exe
186
# could be imported by another standalone program
187
# (e.g. bzr-config; or TortoiseBzr/Olive if/when they
188
# will become standalone exe). [bialix 20071123]
189
# __file__ typically is
190
# C:\Program Files\Bazaar\lib\library.zip\bzrlib\plugin.pyc
191
# then plugins directory is
192
# C:\Program Files\Bazaar\plugins
193
# so relative path is ../../../plugins
194
core_path = osutils.abspath(osutils.pathjoin(
195
osutils.dirname(__file__), '../../../plugins'))
196
else: # don't look inside library.zip
197
# search the plugin path before the bzrlib installed dir
198
core_path = os.path.dirname(_mod_plugins.__file__)
202
def get_site_plugin_path():
203
"""Returns the path for the site installed plugins."""
204
if sys.platform == 'win32':
205
# We don't have (yet) a good answer for windows since that is certainly
206
# related to the way we build the installers. -- vila20090821
210
from distutils.sysconfig import get_python_lib
212
# If distutuils is not available, we just don't know where they are
215
site_path = osutils.pathjoin(get_python_lib(), 'bzrlib', 'plugins')
219
def get_user_plugin_path():
220
return osutils.pathjoin(config.config_dir(), 'plugins')
223
def get_standard_plugins_path():
224
"""Determine a plugin path suitable for general use."""
225
# Ad-Hoc default: core is not overriden by site but user can overrides both
226
# The rationale is that:
227
# - 'site' comes last, because these plugins should always be available and
228
# are supposed to be in sync with the bzr installed on site.
229
# - 'core' comes before 'site' so that running bzr from sources or a user
230
# installed version overrides the site version.
231
# - 'user' comes first, because... user is always right.
232
# - the above rules clearly defines which plugin version will be loaded if
233
# several exist. Yet, it is sometimes desirable to disable some directory
234
# so that a set of plugins is disabled as once. This can be done via
235
# -site, -core, -user.
237
env_paths = os.environ.get('BZR_PLUGIN_PATH', '+user').split(os.pathsep)
238
defaults = ['+core', '+site']
240
# The predefined references
241
refs = dict(core=get_core_plugin_path(),
242
site=get_site_plugin_path(),
243
user=get_user_plugin_path())
245
# Unset paths that should be removed
246
for k,v in refs.iteritems():
248
# defaults can never mention removing paths as that will make it
249
# impossible for the user to revoke these removals.
250
if removed in env_paths:
251
env_paths.remove(removed)
256
for p in env_paths + defaults:
257
if p.startswith('+'):
258
# Resolve references if they are known
262
# Leave them untouched so user can still use paths starting
265
_append_new_path(paths, p)
267
# Get rid of trailing slashes, since Python can't handle them when
268
# it tries to import modules.
269
paths = map(_strip_trailing_sep, paths)
273
def load_plugins(path=None):
95
274
"""Load bzrlib plugins.
97
276
The environment variable BZR_PLUGIN_PATH is considered a delimited
98
set of paths to look through. Each entry is searched for *.py
277
set of paths to look through. Each entry is searched for `*.py`
99
278
files (and whatever other extensions are used in the platform,
102
load_from_dirs() provides the underlying mechanism and is called with
281
load_from_path() provides the underlying mechanism and is called with
103
282
the default directory list to provide the normal behaviour.
284
:param path: The list of paths to search for plugins. By default,
285
path will be determined using get_standard_plugins_path.
286
if path is [], no plugins can be loaded.
126
305
Plugins are loaded into bzrlib.plugins.NAME, and can be found there
127
306
for future reference.
308
The python module path for bzrlib.plugins will be modified to be 'dirs'.
129
# Get the list of valid python suffixes for __init__.py?
130
# this includes .py, .pyc, and .pyo (depending on if we are running -O)
131
# but it doesn't include compiled modules (.so, .dll, etc)
132
valid_suffixes = [suffix for suffix, mod_type, flags in imp.get_suffixes()
133
if flags in (imp.PY_SOURCE, imp.PY_COMPILED)]
134
package_entries = ['__init__'+suffix for suffix in valid_suffixes]
310
# Explicitly load the plugins with a specific path
311
for fullname, path in PluginImporter.specific_paths.iteritems():
312
name = fullname[len('bzrlib.plugins.'):]
313
_load_plugin_module(name, path)
315
# We need to strip the trailing separators here as well as in the
316
# set_plugins_path function because calling code can pass anything in to
317
# this function, and since it sets plugins.__path__, it should set it to
318
# something that will be valid for Python to use (in case people try to
319
# run "import bzrlib.plugins.PLUGINNAME" after calling this function).
320
_mod_plugins.__path__ = map(_strip_trailing_sep, dirs)
138
mutter('looking for plugins in %s', d)
140
if not os.path.isdir(d):
142
for f in os.listdir(d):
143
path = osutils.pathjoin(d, f)
144
if os.path.isdir(path):
145
for entry in package_entries:
146
# This directory should be a package, and thus added to
148
if os.path.isfile(osutils.pathjoin(path, entry)):
150
else: # This directory is not a package
153
for suffix_info in imp.get_suffixes():
154
if f.endswith(suffix_info[0]):
155
f = f[:-len(suffix_info[0])]
156
if suffix_info[2] == imp.C_EXTENSION and f.endswith('module'):
157
f = f[:-len('module')]
161
if getattr(plugins, f, None):
162
mutter('Plugin name %s already loaded', f)
164
# mutter('add plugin name %s', f)
167
plugin_names = list(plugin_names)
169
for name in plugin_names:
171
plugin_info = imp.find_module(name, [d])
172
# mutter('load plugin %r', plugin_info)
174
plugin = imp.load_module('bzrlib.plugins.' + name,
176
setattr(plugins, name, plugin)
178
if plugin_info[0] is not None:
179
plugin_info[0].close()
180
# mutter('loaded succesfully')
181
except KeyboardInterrupt:
184
## import pdb; pdb.set_trace()
185
warning('Unable to load plugin %r from %r' % (name, d))
186
log_exception_quietly()
189
def load_from_zips(zips):
190
"""Load bzr plugins from zip archives with zipimport.
191
It's similar to load_from_dirs but plugins searched inside archives.
196
valid_suffixes = ('.py', '.pyc', '.pyo') # only python modules/packages
198
for zip_name in zips:
199
if '.zip' not in zip_name:
202
ziobj = zipimport.zipimporter(zip_name)
203
except zipimport.ZipImportError:
206
mutter('Looking for plugins in %r', zip_name)
208
# use zipfile to get list of files/dirs inside zip
209
z = zipfile.ZipFile(ziobj.archive)
210
namelist = z.namelist()
214
prefix = ziobj.prefix.replace('\\','/')
216
namelist = [name[ix:]
218
if name.startswith(prefix)]
220
mutter('Names in archive: %r', namelist)
222
for name in namelist:
223
if not name or name.endswith('/'):
226
# '/' is used to separate pathname components inside zip archives
229
head, tail = '', name
231
head, tail = name.rsplit('/',1)
233
# we don't need looking in subdirectories
236
base, suffix = osutils.splitext(tail)
237
if suffix not in valid_suffixes:
240
if base == '__init__':
251
if getattr(plugins, plugin_name, None):
252
mutter('Plugin name %s already loaded', plugin_name)
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))
263
except KeyboardInterrupt:
266
## import pdb; pdb.set_trace()
267
warning('Unable to load plugin %r from %r'
269
log_exception_quietly()
324
trace.mutter('looking for plugins in %s', d)
329
# backwards compatability: load_from_dirs was the old name
330
# This was changed in 0.15
331
load_from_dirs = load_from_path
334
def _find_plugin_module(dir, name):
335
"""Check if there is a valid python module that can be loaded as a plugin.
337
:param dir: The directory where the search is performed.
338
:param path: An existing file path, either a python file or a package
341
:return: (name, path, description) name is the module name, path is the
342
file to load and description is the tuple returned by
345
path = osutils.pathjoin(dir, name)
346
if os.path.isdir(path):
347
# Check for a valid __init__.py file, valid suffixes depends on -O and
348
# can be .py, .pyc and .pyo
349
for suffix, mode, kind in imp.get_suffixes():
350
if kind not in (imp.PY_SOURCE, imp.PY_COMPILED):
351
# We don't recognize compiled modules (.so, .dll, etc)
353
init_path = osutils.pathjoin(path, '__init__' + suffix)
354
if os.path.isfile(init_path):
355
return name, init_path, (suffix, mode, kind)
357
for suffix, mode, kind in imp.get_suffixes():
358
if name.endswith(suffix):
359
# Clean up the module name
360
name = name[:-len(suffix)]
361
if kind == imp.C_EXTENSION and name.endswith('module'):
362
name = name[:-len('module')]
363
return name, path, (suffix, mode, kind)
364
# There is no python module here
365
return None, None, (None, None, None)
368
def record_plugin_warning(plugin_name, warning_message):
369
trace.mutter(warning_message)
370
plugin_warnings.setdefault(plugin_name, []).append(warning_message)
373
def _load_plugin_module(name, dir):
374
"""Load plugin name from dir.
376
:param name: The plugin name in the bzrlib.plugins namespace.
377
:param dir: The directory the plugin is loaded from for error messages.
379
if ('bzrlib.plugins.%s' % name) in PluginImporter.blacklist:
382
exec "import bzrlib.plugins.%s" % name in {}
383
except KeyboardInterrupt:
385
except errors.IncompatibleAPI, e:
387
"Unable to load plugin %r. It requested API version "
388
"%s of module %s but the minimum exported version is %s, and "
389
"the maximum is %s" %
390
(name, e.wanted, e.api, e.minimum, e.current))
391
record_plugin_warning(name, warning_message)
393
trace.warning("%s" % e)
394
if re.search('\.|-| ', name):
395
sanitised_name = re.sub('[-. ]', '_', name)
396
if sanitised_name.startswith('bzr_'):
397
sanitised_name = sanitised_name[len('bzr_'):]
398
trace.warning("Unable to load %r in %r as a plugin because the "
399
"file path isn't a valid module name; try renaming "
400
"it to %r." % (name, dir, sanitised_name))
402
record_plugin_warning(
404
'Unable to load plugin %r from %r' % (name, dir))
405
trace.log_exception_quietly()
406
if 'error' in debug.debug_flags:
407
trace.print_exception(sys.exc_info(), sys.stderr)
410
def load_from_dir(d):
411
"""Load the plugins in directory d.
413
d must be in the plugins module path already.
414
This function is called once for each directory in the module path.
417
for p in os.listdir(d):
418
name, path, desc = _find_plugin_module(d, p)
420
if name == '__init__':
421
# We do nothing with the __init__.py file in directories from
422
# the bzrlib.plugins module path, we may want to, one day
424
continue # We don't load __init__.py in the plugins dirs
425
elif getattr(_mod_plugins, name, None) is not None:
426
# The module has already been loaded from another directory
427
# during a previous call.
428
# FIXME: There should be a better way to report masked plugins
430
trace.mutter('Plugin name %s already loaded', name)
432
plugin_names.add(name)
434
for name in plugin_names:
435
_load_plugin_module(name, d)
439
"""Return a dictionary of the plugins.
441
Each item in the dictionary is a PlugIn object.
444
for name, plugin in _mod_plugins.__dict__.items():
445
if isinstance(plugin, types.ModuleType):
446
result[name] = PlugIn(name, plugin)
450
def format_concise_plugin_list():
451
"""Return a string holding a concise list of plugins and their version.
454
for name, a_plugin in sorted(plugins().items()):
455
items.append("%s[%s]" %
456
(name, a_plugin.__version__))
457
return ', '.join(items)
461
class PluginsHelpIndex(object):
462
"""A help index that returns help topics for plugins."""
465
self.prefix = 'plugins/'
467
def get_topics(self, topic):
468
"""Search for topic in the loaded plugins.
470
This will not trigger loading of new plugins.
472
:param topic: A topic to search for.
473
:return: A list which is either empty or contains a single
474
RegisteredTopic entry.
478
if topic.startswith(self.prefix):
479
topic = topic[len(self.prefix):]
480
plugin_module_name = 'bzrlib.plugins.%s' % topic
482
module = sys.modules[plugin_module_name]
486
return [ModuleHelpTopic(module)]
489
class ModuleHelpTopic(object):
490
"""A help topic which returns the docstring for a module."""
492
def __init__(self, module):
495
:param module: The module for which help should be generated.
499
def get_help_text(self, additional_see_also=None, verbose=True):
500
"""Return a string with the help for this topic.
502
:param additional_see_also: Additional help topics to be
505
if not self.module.__doc__:
506
result = "Plugin '%s' has no docstring.\n" % self.module.__name__
508
result = self.module.__doc__
509
if result[-1] != '\n':
511
from bzrlib import help_topics
512
result += help_topics._format_see_also(additional_see_also)
515
def get_help_topic(self):
516
"""Return the module help topic: its basename."""
517
return self.module.__name__[len('bzrlib.plugins.'):]
520
class PlugIn(object):
521
"""The bzrlib representation of a plugin.
523
The PlugIn object provides a way to manipulate a given plugin module.
526
def __init__(self, name, module):
527
"""Construct a plugin for module."""
532
"""Get the path that this plugin was loaded from."""
533
if getattr(self.module, '__path__', None) is not None:
534
return os.path.abspath(self.module.__path__[0])
535
elif getattr(self.module, '__file__', None) is not None:
536
path = os.path.abspath(self.module.__file__)
537
if path[-4:] in ('.pyc', '.pyo'):
538
pypath = path[:-4] + '.py'
539
if os.path.isfile(pypath):
543
return repr(self.module)
546
return "<%s.%s object at %s, name=%s, module=%s>" % (
547
self.__class__.__module__, self.__class__.__name__, id(self),
548
self.name, self.module)
552
def test_suite(self):
553
"""Return the plugin's test suite."""
554
if getattr(self.module, 'test_suite', None) is not None:
555
return self.module.test_suite()
559
def load_plugin_tests(self, loader):
560
"""Return the adapted plugin's test suite.
562
:param loader: The custom loader that should be used to load additional
566
if getattr(self.module, 'load_tests', None) is not None:
567
return loader.loadTestsFromModule(self.module)
571
def version_info(self):
572
"""Return the plugin's version_tuple or None if unknown."""
573
version_info = getattr(self.module, 'version_info', None)
574
if version_info is not None:
576
if isinstance(version_info, types.StringType):
577
version_info = version_info.split('.')
578
elif len(version_info) == 3:
579
version_info = tuple(version_info) + ('final', 0)
581
# The given version_info isn't even iteratible
582
trace.log_exception_quietly()
583
version_info = (version_info,)
586
def _get__version__(self):
587
version_info = self.version_info()
588
if version_info is None or len(version_info) == 0:
591
version_string = _format_version_tuple(version_info)
592
except (ValueError, TypeError, IndexError), e:
593
trace.log_exception_quietly()
594
# try to return something usefull for bad plugins, in stead of
596
version_string = '.'.join(map(str, version_info))
597
return version_string
599
__version__ = property(_get__version__)
602
class _PluginImporter(object):
603
"""An importer tailored to bzr specific needs.
605
This is a singleton that takes care of:
606
- disabled plugins specified in 'blacklist',
607
- plugins that needs to be loaded from specific directories.
614
self.blacklist = set()
615
self.specific_paths = {}
617
def find_module(self, fullname, parent_path=None):
618
"""Search a plugin module.
620
Disabled plugins raise an import error, plugins with specific paths
621
returns a specific loader.
623
:return: None if the plugin doesn't need special handling, self
626
if not fullname.startswith('bzrlib.plugins.'):
628
if fullname in self.blacklist:
629
raise ImportError('%s is disabled' % fullname)
630
if fullname in self.specific_paths:
634
def load_module(self, fullname):
635
"""Load a plugin from a specific directory (or file)."""
636
# We are called only for specific paths
637
plugin_path = self.specific_paths[fullname]
639
if os.path.isdir(plugin_path):
640
for suffix, mode, kind in imp.get_suffixes():
641
if kind not in (imp.PY_SOURCE, imp.PY_COMPILED):
642
# We don't recognize compiled modules (.so, .dll, etc)
644
init_path = osutils.pathjoin(plugin_path, '__init__' + suffix)
645
if os.path.isfile(init_path):
646
# We've got a module here and load_module needs specific
648
loading_path = plugin_path
651
kind = imp.PKG_DIRECTORY
654
for suffix, mode, kind in imp.get_suffixes():
655
if plugin_path.endswith(suffix):
656
loading_path = plugin_path
658
if loading_path is None:
659
raise ImportError('%s cannot be loaded from %s'
660
% (fullname, plugin_path))
661
if kind is imp.PKG_DIRECTORY:
664
f = open(loading_path, mode)
666
mod = imp.load_module(fullname, f, loading_path,
667
(suffix, mode, kind))
668
mod.__package__ = fullname
675
# Install a dedicated importer for plugins requiring special handling
676
PluginImporter = _PluginImporter()
677
sys.meta_path.append(PluginImporter)