1
# Copyright (C) 2004, 2005 by Canonical Ltd
1
# Copyright (C) 2004, 2005, 2007 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
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.
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; for example, to add new
24
commands, import bzrlib.commands and add your new command to the plugin_cmds
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.
35
from bzrlib.lazy_import import lazy_import
36
lazy_import(globals(), """
48
from bzrlib.config import config_dir
49
from bzrlib.trace import log_error, mutter, log_exception, warning, \
51
from bzrlib.errors import BzrError
52
from bzrlib import plugins
54
DEFAULT_PLUGIN_PATH = os.path.join(config_dir(), 'plugins')
46
from bzrlib import plugins as _mod_plugins
49
from bzrlib.symbol_versioning import deprecated_function, one_three
50
from bzrlib.trace import mutter, warning, log_exception_quietly
53
DEFAULT_PLUGIN_PATH = None
60
"""Return a dictionary of the plugins."""
62
for name, plugin in bzrlib.plugins.__dict__.items():
63
if isinstance(plugin, types.ModuleType):
56
def get_default_plugin_path():
57
"""Get the DEFAULT_PLUGIN_PATH"""
58
global DEFAULT_PLUGIN_PATH
59
if DEFAULT_PLUGIN_PATH is None:
60
DEFAULT_PLUGIN_PATH = osutils.pathjoin(config.config_dir(), 'plugins')
61
return DEFAULT_PLUGIN_PATH
64
def disable_plugins():
65
"""Disable loading plugins.
67
Future calls to load_plugins() will be ignored.
69
# TODO: jam 20060131 This should probably also disable
75
def _strip_trailing_sep(path):
76
return path.rstrip("\\/")
79
def set_plugins_path():
80
"""Set the path for plugins to be loaded from."""
81
path = os.environ.get('BZR_PLUGIN_PATH',
82
get_default_plugin_path()).split(os.pathsep)
83
bzr_exe = bool(getattr(sys, 'frozen', None))
84
if bzr_exe: # expand path for bzr.exe
85
# We need to use relative path to system-wide plugin
86
# directory because bzrlib from standalone bzr.exe
87
# could be imported by another standalone program
88
# (e.g. bzr-config; or TortoiseBzr/Olive if/when they
89
# will become standalone exe). [bialix 20071123]
90
# __file__ typically is
91
# C:\Program Files\Bazaar\lib\library.zip\bzrlib\plugin.pyc
92
# then plugins directory is
93
# C:\Program Files\Bazaar\plugins
94
# so relative path is ../../../plugins
95
path.append(osutils.abspath(osutils.pathjoin(
96
osutils.dirname(__file__), '../../../plugins')))
97
# Get rid of trailing slashes, since Python can't handle them when
98
# it tries to import modules.
99
path = map(_strip_trailing_sep, path)
100
if not bzr_exe: # don't look inside library.zip
101
# search the plugin path before the bzrlib installed dir
102
path.append(os.path.dirname(_mod_plugins.__file__))
103
# search the arch independent path if we can determine that and
104
# the plugin is found nowhere else
105
if sys.platform != 'win32':
107
from distutils.sysconfig import get_python_lib
109
# If distutuils is not available, we just won't add that path
112
archless_path = osutils.pathjoin(get_python_lib(), 'bzrlib',
114
if archless_path not in path:
115
path.append(archless_path)
116
_mod_plugins.__path__ = path
68
120
def load_plugins():
99
148
Plugins are loaded into bzrlib.plugins.NAME, and can be found there
100
149
for future reference.
151
The python module path for bzrlib.plugins will be modified to be 'dirs'.
102
# The problem with imp.get_suffixes() is that it doesn't include
103
# .pyo which is technically valid
104
# It also means that "testmodule.so" will show up as both test and testmodule
105
# though it is only valid as 'test'
106
# but you should be careful, because "testmodule.py" loads as testmodule.
107
suffixes = imp.get_suffixes()
108
suffixes.append(('.pyo', 'rb', imp.PY_COMPILED))
109
package_entries = ['__init__.py', '__init__.pyc', '__init__.pyo']
153
# We need to strip the trailing separators here as well as in the
154
# set_plugins_path function because calling code can pass anything in to
155
# this function, and since it sets plugins.__path__, it should set it to
156
# something that will be valid for Python to use (in case people try to
157
# run "import bzrlib.plugins.PLUGINNAME" after calling this function).
158
_mod_plugins.__path__ = map(_strip_trailing_sep, dirs)
113
162
mutter('looking for plugins in %s', d)
115
if not os.path.isdir(d):
117
for f in os.listdir(d):
118
path = os.path.join(d, f)
119
if os.path.isdir(path):
120
for entry in package_entries:
121
# This directory should be a package, and thus added to
123
if os.path.isfile(os.path.join(path, entry)):
125
else: # This directory is not a package
128
for suffix_info in suffixes:
129
if f.endswith(suffix_info[0]):
130
f = f[:-len(suffix_info[0])]
131
if suffix_info[2] == imp.C_EXTENSION and f.endswith('module'):
132
f = f[:-len('module')]
136
if getattr(bzrlib.plugins, f, None):
137
mutter('Plugin name %s already loaded', f)
139
mutter('add plugin name %s', f)
142
plugin_names = list(plugin_names)
144
for name in plugin_names:
146
plugin_info = imp.find_module(name, [d])
147
mutter('load plugin %r', plugin_info)
149
plugin = imp.load_module('bzrlib.plugins.' + name,
151
setattr(bzrlib.plugins, name, plugin)
153
if plugin_info[0] is not None:
154
plugin_info[0].close()
156
mutter('loaded succesfully')
157
except KeyboardInterrupt:
160
## import pdb; pdb.set_trace()
167
# backwards compatability: load_from_dirs was the old name
168
# This was changed in 0.15
169
load_from_dirs = load_from_path
172
def load_from_dir(d):
173
"""Load the plugins in directory d."""
174
# Get the list of valid python suffixes for __init__.py?
175
# this includes .py, .pyc, and .pyo (depending on if we are running -O)
176
# but it doesn't include compiled modules (.so, .dll, etc)
177
valid_suffixes = [suffix for suffix, mod_type, flags in imp.get_suffixes()
178
if flags in (imp.PY_SOURCE, imp.PY_COMPILED)]
179
package_entries = ['__init__'+suffix for suffix in valid_suffixes]
181
for f in os.listdir(d):
182
path = osutils.pathjoin(d, f)
183
if os.path.isdir(path):
184
for entry in package_entries:
185
# This directory should be a package, and thus added to
187
if os.path.isfile(osutils.pathjoin(path, entry)):
189
else: # This directory is not a package
192
for suffix_info in imp.get_suffixes():
193
if f.endswith(suffix_info[0]):
194
f = f[:-len(suffix_info[0])]
195
if suffix_info[2] == imp.C_EXTENSION and f.endswith('module'):
196
f = f[:-len('module')]
200
if getattr(_mod_plugins, f, None):
201
mutter('Plugin name %s already loaded', f)
203
# mutter('add plugin name %s', f)
206
for name in plugin_names:
208
exec "import bzrlib.plugins.%s" % name in {}
209
except KeyboardInterrupt:
212
## import pdb; pdb.set_trace()
213
if re.search('\.|-| ', name):
214
sanitised_name = re.sub('[-. ]', '_', name)
215
if sanitised_name.startswith('bzr_'):
216
sanitised_name = sanitised_name[len('bzr_'):]
217
warning("Unable to load %r in %r as a plugin because the "
218
"file path isn't a valid module name; try renaming "
219
"it to %r." % (name, d, sanitised_name))
161
221
warning('Unable to load plugin %r from %r' % (name, d))
162
log_exception_quietly()
222
log_exception_quietly()
225
@deprecated_function(one_three)
226
def load_from_zip(zip_name):
227
"""Load all the plugins in a zip."""
228
valid_suffixes = ('.py', '.pyc', '.pyo') # only python modules/packages
231
index = zip_name.rindex('.zip')
234
archive = zip_name[:index+4]
235
prefix = zip_name[index+5:]
237
mutter('Looking for plugins in %r', zip_name)
239
# use zipfile to get list of files/dirs inside zip
241
z = zipfile.ZipFile(archive)
242
namelist = z.namelist()
244
except zipfile.error:
249
prefix = prefix.replace('\\','/')
250
if prefix[-1] != '/':
253
namelist = [name[ix:]
255
if name.startswith(prefix)]
257
mutter('Names in archive: %r', namelist)
259
for name in namelist:
260
if not name or name.endswith('/'):
263
# '/' is used to separate pathname components inside zip archives
266
head, tail = '', name
268
head, tail = name.rsplit('/',1)
270
# we don't need looking in subdirectories
273
base, suffix = osutils.splitext(tail)
274
if suffix not in valid_suffixes:
277
if base == '__init__':
288
if getattr(_mod_plugins, plugin_name, None):
289
mutter('Plugin name %s already loaded', plugin_name)
293
exec "import bzrlib.plugins.%s" % plugin_name in {}
294
mutter('Load plugin %s from zip %r', plugin_name, zip_name)
295
except KeyboardInterrupt:
298
## import pdb; pdb.set_trace()
299
warning('Unable to load plugin %r from %r'
301
log_exception_quietly()
305
"""Return a dictionary of the plugins.
307
Each item in the dictionary is a PlugIn object.
310
for name, plugin in _mod_plugins.__dict__.items():
311
if isinstance(plugin, types.ModuleType):
312
result[name] = PlugIn(name, plugin)
316
class PluginsHelpIndex(object):
317
"""A help index that returns help topics for plugins."""
320
self.prefix = 'plugins/'
322
def get_topics(self, topic):
323
"""Search for topic in the loaded plugins.
325
This will not trigger loading of new plugins.
327
:param topic: A topic to search for.
328
:return: A list which is either empty or contains a single
329
RegisteredTopic entry.
333
if topic.startswith(self.prefix):
334
topic = topic[len(self.prefix):]
335
plugin_module_name = 'bzrlib.plugins.%s' % topic
337
module = sys.modules[plugin_module_name]
341
return [ModuleHelpTopic(module)]
344
class ModuleHelpTopic(object):
345
"""A help topic which returns the docstring for a module."""
347
def __init__(self, module):
350
:param module: The module for which help should be generated.
354
def get_help_text(self, additional_see_also=None):
355
"""Return a string with the help for this topic.
357
:param additional_see_also: Additional help topics to be
360
if not self.module.__doc__:
361
result = "Plugin '%s' has no docstring.\n" % self.module.__name__
363
result = self.module.__doc__
364
if result[-1] != '\n':
366
# there is code duplicated here and in bzrlib/help_topic.py's
367
# matching Topic code. This should probably be factored in
368
# to a helper function and a common base class.
369
if additional_see_also is not None:
370
see_also = sorted(set(additional_see_also))
374
result += 'See also: '
375
result += ', '.join(see_also)
379
def get_help_topic(self):
380
"""Return the modules help topic - its __name__ after bzrlib.plugins.."""
381
return self.module.__name__[len('bzrlib.plugins.'):]
384
class PlugIn(object):
385
"""The bzrlib representation of a plugin.
387
The PlugIn object provides a way to manipulate a given plugin module.
390
def __init__(self, name, module):
391
"""Construct a plugin for module."""
396
"""Get the path that this plugin was loaded from."""
397
if getattr(self.module, '__path__', None) is not None:
398
return os.path.abspath(self.module.__path__[0])
399
elif getattr(self.module, '__file__', None) is not None:
400
path = os.path.abspath(self.module.__file__)
401
if path[-4:] in ('.pyc', '.pyo'):
402
pypath = path[:-4] + '.py'
403
if os.path.isfile(pypath):
407
return repr(self.module)
410
return "<%s.%s object at %s, name=%s, module=%s>" % (
411
self.__class__.__module__, self.__class__.__name__, id(self),
412
self.name, self.module)
416
def test_suite(self):
417
"""Return the plugin's test suite."""
418
if getattr(self.module, 'test_suite', None) is not None:
419
return self.module.test_suite()
423
def load_plugin_tests(self, loader):
424
"""Return the adapted plugin's test suite.
426
:param loader: The custom loader that should be used to load additional
430
if getattr(self.module, 'load_tests', None) is not None:
431
return loader.loadTestsFromModule(self.module)
435
def version_info(self):
436
"""Return the plugin's version_tuple or None if unknown."""
437
version_info = getattr(self.module, 'version_info', None)
438
if version_info is not None and len(version_info) == 3:
439
version_info = tuple(version_info) + ('final', 0)
442
def _get__version__(self):
443
version_info = self.version_info()
444
if version_info is None:
446
if version_info[3] == 'final':
447
version_string = '%d.%d.%d' % version_info[:3]
449
version_string = '%d.%d.%d%s%d' % version_info
450
return version_string
452
__version__ = property(_get__version__)