14
14
# along with this program; if not, write to the Free Software
15
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
18
17
"""bzr python plugin support.
20
19
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.
20
$BRZ_PLUGIN_PATH will be imported. The module will be imported as
21
'breezy.plugins.$BASENAME(PLUGIN)'. In the plugin's main body, it should
22
update any breezy registries it wants to extend.
25
24
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
27
BRZ_PLUGIN_PATH is also honoured for any plugins imported via
28
'import breezy.plugins.PLUGINNAME', as long as set_plugins_path has been
32
from __future__ import absolute_import
36
from bzrlib import osutils
38
from bzrlib.lazy_import import lazy_import
39
from .lazy_import import lazy_import
40
40
lazy_import(globals(), """
46
46
_format_version_tuple,
52
from bzrlib import plugins as _mod_plugins
52
from breezy.i18n import gettext
53
from breezy import plugins as _mod_plugins
55
from bzrlib.symbol_versioning import (
61
57
DEFAULT_PLUGIN_PATH = None
63
59
_plugins_disabled = False
63
# Map from plugin name, to list of string warnings about eg plugin
66
67
def are_plugins_disabled():
67
68
return _plugins_disabled
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'
80
118
def _strip_trailing_sep(path):
81
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))
84
149
def set_plugins_path(path=None):
85
150
"""Set the path for plugins to be loaded from.
93
158
_mod_plugins.__path__ = path
94
159
PluginImporter.reset()
95
160
# Set up a blacklist for disabled plugins
96
disabled_plugins = os.environ.get('BZR_DISABLE_PLUGINS', None)
161
disabled_plugins = os.environ.get('BRZ_DISABLE_PLUGINS', None)
97
162
if disabled_plugins is not None:
98
163
for name in disabled_plugins.split(os.pathsep):
99
PluginImporter.blacklist.add('bzrlib.plugins.' + name)
164
PluginImporter.blacklist.add('breezy.plugins.' + name)
100
165
# 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('@')
166
for plugin_name, plugin_path in _get_specific_plugin_paths(os.environ.get(
167
'BRZ_PLUGINS_AT', None)):
105
168
PluginImporter.specific_paths[
106
'bzrlib.plugins.%s' % plugin_name] = plugin_path
169
'breezy.plugins.%s' % plugin_name] = plugin_path
117
180
def get_core_plugin_path():
119
bzr_exe = bool(getattr(sys, 'frozen', None))
120
if bzr_exe: # expand path for bzr.exe
182
brz_exe = bool(getattr(sys, 'frozen', None))
183
if brz_exe: # expand path for brz.exe
121
184
# We need to use relative path to system-wide plugin
122
# directory because bzrlib from standalone bzr.exe
185
# directory because breezy from standalone brz.exe
123
186
# could be imported by another standalone program
124
187
# (e.g. bzr-config; or TortoiseBzr/Olive if/when they
125
188
# will become standalone exe). [bialix 20071123]
126
189
# __file__ typically is
127
# C:\Program Files\Bazaar\lib\library.zip\bzrlib\plugin.pyc
190
# C:\Program Files\Bazaar\lib\library.zip\breezy\plugin.pyc
128
191
# then plugins directory is
129
192
# C:\Program Files\Bazaar\plugins
130
193
# so relative path is ../../../plugins
131
194
core_path = osutils.abspath(osutils.pathjoin(
132
195
osutils.dirname(__file__), '../../../plugins'))
133
196
else: # don't look inside library.zip
134
# search the plugin path before the bzrlib installed dir
197
# search the plugin path before the breezy installed dir
135
198
core_path = os.path.dirname(_mod_plugins.__file__)
162
225
# Ad-Hoc default: core is not overriden by site but user can overrides both
163
226
# The rationale is that:
164
227
# - '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
228
# are supposed to be in sync with the brz installed on site.
229
# - 'core' comes before 'site' so that running brz from sources or a user
167
230
# installed version overrides the site version.
168
231
# - 'user' comes first, because... user is always right.
169
232
# - the above rules clearly defines which plugin version will be loaded if
171
234
# so that a set of plugins is disabled as once. This can be done via
172
235
# -site, -core, -user.
174
env_paths = os.environ.get('BZR_PLUGIN_PATH', '+user').split(os.pathsep)
237
env_paths = os.environ.get('BRZ_PLUGIN_PATH', '+user').split(os.pathsep)
175
238
defaults = ['+core', '+site']
177
240
# The predefined references
204
267
# Get rid of trailing slashes, since Python can't handle them when
205
268
# it tries to import modules.
206
paths = map(_strip_trailing_sep, paths)
269
paths = list(map(_strip_trailing_sep, paths))
210
273
def load_plugins(path=None):
211
"""Load bzrlib plugins.
274
"""Load breezy plugins.
213
The environment variable BZR_PLUGIN_PATH is considered a delimited
214
set of paths to look through. Each entry is searched for *.py
276
The environment variable BRZ_PLUGIN_PATH is considered a delimited
277
set of paths to look through. Each entry is searched for `*.py`
215
278
files (and whatever other extensions are used in the platform,
218
281
load_from_path() provides the underlying mechanism and is called with
219
282
the default directory list to provide the normal behaviour.
235
298
def load_from_path(dirs):
236
"""Load bzrlib plugins found in each dir in dirs.
299
"""Load breezy plugins found in each dir in dirs.
238
301
Loading a plugin means importing it into the python interpreter.
239
302
The plugin is expected to make calls to register commands when
240
303
it's loaded (or perhaps access other hooks in future.)
242
Plugins are loaded into bzrlib.plugins.NAME, and can be found there
305
Plugins are loaded into breezy.plugins.NAME, and can be found there
243
306
for future reference.
245
The python module path for bzrlib.plugins will be modified to be 'dirs'.
308
The python module path for breezy.plugins will be modified to be 'dirs'.
247
310
# Explicitly load the plugins with a specific path
248
for fullname, path in PluginImporter.specific_paths.iteritems():
249
name = fullname[len('bzrlib.plugins.'):]
311
for fullname, path in PluginImporter.specific_paths.items():
312
name = fullname[len('breezy.plugins.'):]
250
313
_load_plugin_module(name, path)
252
315
# We need to strip the trailing separators here as well as in the
253
316
# set_plugins_path function because calling code can pass anything in to
254
317
# this function, and since it sets plugins.__path__, it should set it to
255
318
# 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)
319
# run "import breezy.plugins.PLUGINNAME" after calling this function).
320
_mod_plugins.__path__ = list(map(_strip_trailing_sep, dirs))
302
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)
305
373
def _load_plugin_module(name, dir):
306
374
"""Load plugin name from dir.
308
:param name: The plugin name in the bzrlib.plugins namespace.
376
:param name: The plugin name in the breezy.plugins namespace.
309
377
:param dir: The directory the plugin is loaded from for error messages.
311
if ('bzrlib.plugins.%s' % name) in PluginImporter.blacklist:
379
if ('breezy.plugins.%s' % name) in PluginImporter.blacklist:
314
exec "import bzrlib.plugins.%s" % name in {}
382
exec("import breezy.plugins.%s" % name, {})
315
383
except KeyboardInterrupt:
317
except errors.IncompatibleAPI, e:
318
trace.warning("Unable to load plugin %r. It requested API version "
385
except errors.IncompatibleAPI as e:
387
"Unable to load plugin %r. It requested API version "
319
388
"%s of module %s but the minimum exported version is %s, and "
320
389
"the maximum is %s" %
321
390
(name, e.wanted, e.api, e.minimum, e.current))
391
record_plugin_warning(name, warning_message)
392
except Exception as e:
323
393
trace.warning("%s" % e)
324
394
if re.search('\.|-| ', name):
325
395
sanitised_name = re.sub('[-. ]', '_', name)
326
if sanitised_name.startswith('bzr_'):
327
sanitised_name = sanitised_name[len('bzr_'):]
396
if sanitised_name.startswith('brz_'):
397
sanitised_name = sanitised_name[len('brz_'):]
328
398
trace.warning("Unable to load %r in %r as a plugin because the "
329
399
"file path isn't a valid module name; try renaming "
330
400
"it to %r." % (name, dir, sanitised_name))
332
trace.warning('Unable to load plugin %r from %r' % (name, dir))
402
record_plugin_warning(
404
'Unable to load plugin %r from %r' % (name, dir))
333
405
trace.log_exception_quietly()
334
406
if 'error' in debug.debug_flags:
335
407
trace.print_exception(sys.exc_info(), sys.stderr)
347
419
if name is not None:
348
420
if name == '__init__':
349
421
# We do nothing with the __init__.py file in directories from
350
# the bzrlib.plugins module path, we may want to, one day
422
# the breezy.plugins module path, we may want to, one day
351
423
# -- vila 20100316.
352
424
continue # We don't load __init__.py in the plugins dirs
353
425
elif getattr(_mod_plugins, name, None) is not None:
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)
378
461
class PluginsHelpIndex(object):
379
462
"""A help index that returns help topics for plugins."""
425
508
result = self.module.__doc__
426
509
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)
511
from . import help_topics
512
result += help_topics._format_see_also(additional_see_also)
441
515
def get_help_topic(self):
442
"""Return the modules help topic - its __name__ after bzrlib.plugins.."""
443
return self.module.__name__[len('bzrlib.plugins.'):]
516
"""Return the module help topic: its basename."""
517
return self.module.__name__[len('breezy.plugins.'):]
446
520
class PlugIn(object):
447
"""The bzrlib representation of a plugin.
521
"""The breezy representation of a plugin.
449
523
The PlugIn object provides a way to manipulate a given plugin module.
503
577
version_info = version_info.split('.')
504
578
elif len(version_info) == 3:
505
579
version_info = tuple(version_info) + ('final', 0)
580
except TypeError as e:
507
581
# The given version_info isn't even iteratible
508
582
trace.log_exception_quietly()
509
583
version_info = (version_info,)
517
591
version_string = _format_version_tuple(version_info)
518
except (ValueError, TypeError, IndexError), e:
592
except (ValueError, TypeError, IndexError) as e:
519
593
trace.log_exception_quietly()
520
594
# try to return something usefull for bad plugins, in stead of
560
634
def load_module(self, fullname):
561
"""Load a plugin from a specific directory."""
635
"""Load a plugin from a specific directory (or file)."""
562
636
# We are called only for specific paths
563
637
plugin_path = self.specific_paths[fullname]
564
638
loading_path = None
566
639
if os.path.isdir(plugin_path):
567
640
for suffix, mode, kind in imp.get_suffixes():
568
641
if kind not in (imp.PY_SOURCE, imp.PY_COMPILED):
571
644
init_path = osutils.pathjoin(plugin_path, '__init__' + suffix)
572
645
if os.path.isfile(init_path):
573
loading_path = init_path
646
# We've got a module here and load_module needs specific
648
loading_path = plugin_path
651
kind = imp.PKG_DIRECTORY
577
654
for suffix, mode, kind in imp.get_suffixes():
581
658
if loading_path is None:
582
659
raise ImportError('%s cannot be loaded from %s'
583
660
% (fullname, plugin_path))
584
f = open(loading_path, mode)
661
if kind is imp.PKG_DIRECTORY:
664
f = open(loading_path, mode)
586
666
mod = imp.load_module(fullname, f, loading_path,
587
667
(suffix, mode, kind))
589
# The plugin can contain modules, so be ready
590
mod.__path__ = [plugin_path]
591
668
mod.__package__ = fullname
597
675
# Install a dedicated importer for plugins requiring special handling