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, zero_ninetyone
50
from bzrlib.trace import mutter, warning, log_exception_quietly
53
DEFAULT_PLUGIN_PATH = None
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
path = [osutils.pathjoin(config.config_dir(), 'plugins')]
61
if getattr(sys, 'frozen', None): # bzr.exe
62
# We need to use relative path to system-wide plugin
63
# directory because bzrlib from standalone bzr.exe
64
# could be imported by another standalone program
65
# (e.g. bzr-config; or TortoiseBzr/Olive if/when they
66
# will become standalone exe). [bialix 20071123]
67
# __file__ typically is
68
# C:\Program Files\Bazaar\lib\library.zip\bzrlib\plugin.pyc
69
# then plugins directory is
70
# C:\Program Files\Bazaar\plugins
71
# so relative path is ../../../plugins
72
path.append(osutils.abspath(osutils.pathjoin(
73
osutils.dirname(__file__), '../../../plugins')))
74
DEFAULT_PLUGIN_PATH = os.pathsep.join(path)
75
return DEFAULT_PLUGIN_PATH
78
@deprecated_function(zero_ninetyone)
60
80
"""Return a dictionary of the plugins."""
62
for name, plugin in bzrlib.plugins.__dict__.items():
63
if isinstance(plugin, types.ModuleType):
81
return dict((name, plugin.module) for name, plugin in plugins().items())
84
def disable_plugins():
85
"""Disable loading plugins.
87
Future calls to load_plugins() will be ignored.
89
# TODO: jam 20060131 This should probably also disable
95
def _strip_trailing_sep(path):
96
return path.rstrip("\\/")
99
def set_plugins_path():
100
"""Set the path for plugins to be loaded from."""
101
path = os.environ.get('BZR_PLUGIN_PATH',
102
get_default_plugin_path()).split(os.pathsep)
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
# search the plugin path before the bzrlib installed dir
107
path.append(os.path.dirname(_mod_plugins.__file__))
108
_mod_plugins.__path__ = path
68
112
def load_plugins():
99
140
Plugins are loaded into bzrlib.plugins.NAME, and can be found there
100
141
for future reference.
143
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']
145
# We need to strip the trailing separators here as well as in the
146
# set_plugins_path function because calling code can pass anything in to
147
# this function, and since it sets plugins.__path__, it should set it to
148
# something that will be valid for Python to use (in case people try to
149
# run "import bzrlib.plugins.PLUGINNAME" after calling this function).
150
_mod_plugins.__path__ = map(_strip_trailing_sep, dirs)
113
154
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()
158
# it might be a zip: try loading from the zip.
162
# backwards compatability: load_from_dirs was the old name
163
# This was changed in 0.15
164
load_from_dirs = load_from_path
167
def load_from_dir(d):
168
"""Load the plugins in directory d."""
169
# Get the list of valid python suffixes for __init__.py?
170
# this includes .py, .pyc, and .pyo (depending on if we are running -O)
171
# but it doesn't include compiled modules (.so, .dll, etc)
172
valid_suffixes = [suffix for suffix, mod_type, flags in imp.get_suffixes()
173
if flags in (imp.PY_SOURCE, imp.PY_COMPILED)]
174
package_entries = ['__init__'+suffix for suffix in valid_suffixes]
176
for f in os.listdir(d):
177
path = osutils.pathjoin(d, f)
178
if os.path.isdir(path):
179
for entry in package_entries:
180
# This directory should be a package, and thus added to
182
if os.path.isfile(osutils.pathjoin(path, entry)):
184
else: # This directory is not a package
187
for suffix_info in imp.get_suffixes():
188
if f.endswith(suffix_info[0]):
189
f = f[:-len(suffix_info[0])]
190
if suffix_info[2] == imp.C_EXTENSION and f.endswith('module'):
191
f = f[:-len('module')]
195
if getattr(_mod_plugins, f, None):
196
mutter('Plugin name %s already loaded', f)
198
# mutter('add plugin name %s', f)
201
for name in plugin_names:
203
exec "import bzrlib.plugins.%s" % name in {}
204
except KeyboardInterrupt:
207
## import pdb; pdb.set_trace()
208
if re.search('\.|-| ', name):
209
sanitised_name = re.sub('[-. ]', '_', name)
210
warning("Unable to load %r in %r as a plugin because file path"
211
" isn't a valid module name; try renaming it to %r."
212
% (name, d, sanitised_name))
161
214
warning('Unable to load plugin %r from %r' % (name, d))
162
log_exception_quietly()
215
log_exception_quietly()
218
def load_from_zip(zip_name):
219
"""Load all the plugins in a zip."""
220
valid_suffixes = ('.py', '.pyc', '.pyo') # only python modules/packages
224
index = zip_name.rindex('.zip')
227
archive = zip_name[:index+4]
228
prefix = zip_name[index+5:]
230
mutter('Looking for plugins in %r', zip_name)
232
# use zipfile to get list of files/dirs inside zip
234
z = zipfile.ZipFile(archive)
235
namelist = z.namelist()
237
except zipfile.error:
242
prefix = prefix.replace('\\','/')
243
if prefix[-1] != '/':
246
namelist = [name[ix:]
248
if name.startswith(prefix)]
250
mutter('Names in archive: %r', namelist)
252
for name in namelist:
253
if not name or name.endswith('/'):
256
# '/' is used to separate pathname components inside zip archives
259
head, tail = '', name
261
head, tail = name.rsplit('/',1)
263
# we don't need looking in subdirectories
266
base, suffix = osutils.splitext(tail)
267
if suffix not in valid_suffixes:
270
if base == '__init__':
281
if getattr(_mod_plugins, plugin_name, None):
282
mutter('Plugin name %s already loaded', plugin_name)
286
exec "import bzrlib.plugins.%s" % plugin_name in {}
287
mutter('Load plugin %s from zip %r', plugin_name, zip_name)
288
except KeyboardInterrupt:
291
## import pdb; pdb.set_trace()
292
warning('Unable to load plugin %r from %r'
294
log_exception_quietly()
298
"""Return a dictionary of the plugins.
300
Each item in the dictionary is a PlugIn object.
303
for name, plugin in _mod_plugins.__dict__.items():
304
if isinstance(plugin, types.ModuleType):
305
result[name] = PlugIn(name, plugin)
309
class PluginsHelpIndex(object):
310
"""A help index that returns help topics for plugins."""
313
self.prefix = 'plugins/'
315
def get_topics(self, topic):
316
"""Search for topic in the loaded plugins.
318
This will not trigger loading of new plugins.
320
:param topic: A topic to search for.
321
:return: A list which is either empty or contains a single
322
RegisteredTopic entry.
326
if topic.startswith(self.prefix):
327
topic = topic[len(self.prefix):]
328
plugin_module_name = 'bzrlib.plugins.%s' % topic
330
module = sys.modules[plugin_module_name]
334
return [ModuleHelpTopic(module)]
337
class ModuleHelpTopic(object):
338
"""A help topic which returns the docstring for a module."""
340
def __init__(self, module):
343
:param module: The module for which help should be generated.
347
def get_help_text(self, additional_see_also=None):
348
"""Return a string with the help for this topic.
350
:param additional_see_also: Additional help topics to be
353
if not self.module.__doc__:
354
result = "Plugin '%s' has no docstring.\n" % self.module.__name__
356
result = self.module.__doc__
357
if result[-1] != '\n':
359
# there is code duplicated here and in bzrlib/help_topic.py's
360
# matching Topic code. This should probably be factored in
361
# to a helper function and a common base class.
362
if additional_see_also is not None:
363
see_also = sorted(set(additional_see_also))
367
result += 'See also: '
368
result += ', '.join(see_also)
372
def get_help_topic(self):
373
"""Return the modules help topic - its __name__ after bzrlib.plugins.."""
374
return self.module.__name__[len('bzrlib.plugins.'):]
377
class PlugIn(object):
378
"""The bzrlib representation of a plugin.
380
The PlugIn object provides a way to manipulate a given plugin module.
383
def __init__(self, name, module):
384
"""Construct a plugin for module."""
389
"""Get the path that this plugin was loaded from."""
390
if getattr(self.module, '__path__', None) is not None:
391
return os.path.abspath(self.module.__path__[0])
392
elif getattr(self.module, '__file__', None) is not None:
393
path = os.path.abspath(self.module.__file__)
394
if path[-4:] in ('.pyc', '.pyo'):
395
pypath = path[:-4] + '.py'
396
if os.path.isfile(pypath):
400
return repr(self.module)
403
return "<%s.%s object at %s, name=%s, module=%s>" % (
404
self.__class__.__module__, self.__class__.__name__, id(self),
405
self.name, self.module)
409
def test_suite(self):
410
"""Return the plugin's test suite."""
411
if getattr(self.module, 'test_suite', None) is not None:
412
return self.module.test_suite()
416
def version_info(self):
417
"""Return the plugin's version_tuple or None if unknown."""
418
version_info = getattr(self.module, 'version_info', None)
419
if version_info is not None and len(version_info) == 3:
420
version_info = tuple(version_info) + ('final', 0)
423
def _get__version__(self):
424
version_info = self.version_info()
425
if version_info is None:
427
if version_info[3] == 'final':
428
version_string = '%d.%d.%d' % version_info[:3]
430
version_string = '%d.%d.%d%s%d' % version_info
431
return version_string
433
__version__ = property(_get__version__)