1
*** added file 'bzrlib/plugin.py'
5
+# Copyright (C) 2004, 2005 by Canonical Ltd
7
+# This program is free software; you can redistribute it and/or modify
8
+# it under the terms of the GNU General Public License as published by
9
+# the Free Software Foundation; either version 2 of the License, or
10
+# (at your option) any later version.
12
+# This program is distributed in the hope that it will be useful,
13
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
14
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
+# GNU General Public License for more details.
17
+# You should have received a copy of the GNU General Public License
18
+# along with this program; if not, write to the Free Software
19
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
22
+# This module implements plug-in support.
23
+# Any python module in $BZR_PLUGIN_PATH will be imported upon initialization
24
+# of bzrlib (and then forgotten about). In the plugin's main body, it should
25
+# update any bzrlib registries it wants to extend; for example, to add new
26
+# commands, import bzrlib.commands and add your new command to the
27
+# plugin_cmds variable.
33
+ from sets import Set as set
34
+from bzrlib.trace import log_error
38
+ """Find all python files which are plugins, and load them
40
+ The environment variable BZR_PLUGIN_PATH is considered a delimited set of
41
+ paths to look through. Each entry is searched for *.py files (and whatever
42
+ other extensions are used in the platform, such as *.pyd).
44
+ bzrpath = os.environ.get('BZR_PLUGIN_PATH', os.path.expanduser('~/.bzr/plugins'))
46
+ # The problem with imp.get_suffixes() is that it doesn't include
47
+ # .pyo which is technically valid
48
+ # It also means that "testmodule.so" will show up as both test and testmodule
49
+ # though it is only valid as 'test'
50
+ # but you should be careful, because "testmodule.py" loads as testmodule.
51
+ suffixes = imp.get_suffixes()
52
+ suffixes.append(('.pyo', 'rb', imp.PY_COMPILED))
53
+ package_entries = ['__init__.py', '__init__.pyc', '__init__.pyo']
54
+ for d in bzrpath.split(os.pathsep):
55
+ # going trough them one by one allows different plugins with the same
56
+ # filename in different directories in the path
59
+ plugin_names = set()
60
+ if not os.path.isdir(d):
62
+ for f in os.listdir(d):
63
+ path = os.path.join(d, f)
64
+ if os.path.isdir(path):
65
+ for entry in package_entries:
66
+ # This directory should be a package, and thus added to
68
+ if os.path.isfile(os.path.join(path, entry)):
70
+ else: # This directory is not a package
73
+ for suffix_info in suffixes:
74
+ if f.endswith(suffix_info[0]):
75
+ f = f[:-len(suffix_info[0])]
76
+ if suffix_info[2] == imp.C_EXTENSION and f.endswith('module'):
77
+ f = f[:-len('module')]
83
+ plugin_names = list(plugin_names)
85
+ for name in plugin_names:
87
+ plugin_info = imp.find_module(name, [d])
89
+ plugin = imp.load_module('bzrlib.plugin.' + name,
92
+ if plugin_info[0] is not None:
93
+ plugin_info[0].close()
94
+ except Exception, e:
95
+ log_error('Unable to load plugin: %r from %r\n%s' % (name, d, e))
98
*** modified file 'bzrlib/__init__.py'
99
--- bzrlib/__init__.py
100
+++ bzrlib/__init__.py
102
from diff import compare_trees
103
from trace import mutter, warning, open_tracefile
104
from log import show_log
105
+from plugin import load_plugins
116
*** modified file 'bzrlib/commands.py'
117
--- bzrlib/commands.py
118
+++ bzrlib/commands.py
120
from bzrlib.osutils import quotefn
121
from bzrlib import Branch, Inventory, InventoryEntry, BZRDIR, \
128
+def register_plugin_command(cmd):
129
+ "Utility function to help register a command"
132
+ if k.startswith("cmd_"):
133
+ k_unsquished = _unsquish_command_name(k)
136
+ if not plugin_cmds.has_key(k_unsquished):
137
+ plugin_cmds[k_unsquished] = cmd
139
+ log_error('Two plugins defined the same command: %r' % k)
140
+ log_error('Not loading the one in %r' % sys.modules[cmd.__module__])
143
def _squish_command_name(cmd):
148
-def _find_plugins():
149
- """Find all python files which are plugins, and load their commands
150
- to add to the list of "all commands"
152
- The environment variable BZRPATH is considered a delimited set of
153
- paths to look through. Each entry is searched for *.py files.
154
- If a directory is found, it is also searched, but they are
155
- not searched recursively. This allows you to revctl the plugins.
157
- Inside the plugin should be a series of cmd_* function, which inherit from
158
- the bzrlib.commands.Command class.
160
- bzrpath = os.environ.get('BZRPLUGINPATH', '')
165
- _platform_extensions = {
171
- if _platform_extensions.has_key(sys.platform):
172
- platform_extension = _platform_extensions[sys.platform]
174
- platform_extension = None
175
- for d in bzrpath.split(os.pathsep):
176
- plugin_names = {} # This should really be a set rather than a dict
177
- for f in os.listdir(d):
178
- if f.endswith('.py'):
180
- elif f.endswith('.pyc') or f.endswith('.pyo'):
182
- elif platform_extension and f.endswith(platform_extension):
183
- f = f[:-len(platform_extension)]
184
- if f.endswidth('module'):
185
- f = f[:-len('module')]
188
- if not plugin_names.has_key(f):
189
- plugin_names[f] = True
191
- plugin_names = plugin_names.keys()
192
- plugin_names.sort()
194
- sys.path.insert(0, d)
195
- for name in plugin_names:
199
- if sys.modules.has_key(name):
200
- old_module = sys.modules[name]
201
- del sys.modules[name]
202
- plugin = __import__(name, locals())
203
- for k in dir(plugin):
204
- if k.startswith('cmd_'):
205
- k_unsquished = _unsquish_command_name(k)
206
- if not plugin_cmds.has_key(k_unsquished):
207
- plugin_cmds[k_unsquished] = getattr(plugin, k)
209
- log_error('Two plugins defined the same command: %r' % k)
210
- log_error('Not loading the one in %r in dir %r' % (name, d))
213
- sys.modules[name] = old_module
214
- except ImportError, e:
215
- log_error('Unable to load plugin: %r from %r\n%s' % (name, d, e))
220
-def _get_cmd_dict(include_plugins=True):
221
+def _get_cmd_dict(plugins_override=True):
223
for k, v in globals().iteritems():
224
if k.startswith("cmd_"):
225
d[_unsquish_command_name(k)] = v
226
- if include_plugins:
227
- d.update(_find_plugins())
228
+ # If we didn't load plugins, the plugin_cmds dict will be empty
229
+ if plugins_override:
230
+ d.update(plugin_cmds)
233
+ d2.update(plugin_cmds)
238
-def get_all_cmds(include_plugins=True):
239
+def get_all_cmds(plugins_override=True):
240
"""Return canonical name and class for all registered commands."""
241
- for k, v in _get_cmd_dict(include_plugins=include_plugins).iteritems():
242
+ for k, v in _get_cmd_dict(plugins_override=plugins_override).iteritems():
246
-def get_cmd_class(cmd,include_plugins=True):
247
+def get_cmd_class(cmd, plugins_override=True):
248
"""Return the canonical name and command class for a command.
250
cmd = str(cmd) # not unicode
252
# first look up this command under the specified name
253
- cmds = _get_cmd_dict(include_plugins=include_plugins)
254
+ cmds = _get_cmd_dict(plugins_override=plugins_override)
256
return cmd, cmds[cmd]
258
@@ -1461,6 +1413,75 @@
262
+def _parse_master_args(argv):
263
+ """Parse the arguments that always go with the original command.
264
+ These are things like bzr --no-plugins, etc.
266
+ There are now 2 types of option flags. Ones that come *before* the command,
267
+ and ones that come *after* the command.
268
+ Ones coming *before* the command are applied against all possible commands.
269
+ And are generally applied before plugins are loaded.
271
+ The current list are:
272
+ --builtin Allow plugins to load, but don't let them override builtin commands,
273
+ they will still be allowed if they do not override a builtin.
274
+ --no-plugins Don't load any plugins. This lets you get back to official source
276
+ --profile Enable the hotspot profile before running the command.
277
+ For backwards compatibility, this is also a non-master option.
278
+ --version Spit out the version of bzr that is running and exit.
279
+ This is also a non-master option.
280
+ --help Run help and exit, also a non-master option (I think that should stay, though)
282
+ >>> argv, opts = _parse_master_args(['bzr', '--test'])
283
+ Traceback (most recent call last):
285
+ BzrCommandError: Invalid master option: 'test'
286
+ >>> argv, opts = _parse_master_args(['bzr', '--version', 'command'])
289
+ >>> print opts['version']
291
+ >>> argv, opts = _parse_master_args(['bzr', '--profile', 'command', '--more-options'])
293
+ ['command', '--more-options']
294
+ >>> print opts['profile']
296
+ >>> argv, opts = _parse_master_args(['bzr', '--no-plugins', 'command'])
299
+ >>> print opts['no-plugins']
301
+ >>> print opts['profile']
303
+ >>> argv, opts = _parse_master_args(['bzr', 'command', '--profile'])
305
+ ['command', '--profile']
306
+ >>> print opts['profile']
309
+ master_opts = {'builtin':False,
310
+ 'no-plugins':False,
316
+ # This is the point where we could hook into argv[0] to determine
317
+ # what front-end is supposed to be run
318
+ # For now, we are just ignoring it.
319
+ cmd_name = argv.pop(0)
320
+ for arg in argv[:]:
321
+ if arg[:2] != '--': # at the first non-option, we return the rest
323
+ arg = arg[2:] # Remove '--'
324
+ if arg not in master_opts:
325
+ # We could say that this is not an error, that we should
326
+ # just let it be handled by the main section instead
327
+ raise BzrCommandError('Invalid master option: %r' % arg)
328
+ argv.pop(0) # We are consuming this entry
329
+ master_opts[arg] = True
330
+ return argv, master_opts
333
"""Execute a command.
334
@@ -1470,22 +1491,21 @@
336
argv = [a.decode(bzrlib.user_encoding) for a in argv]
338
- include_plugins=True
340
- args, opts = parse_args(argv[1:])
342
+ argv, master_opts = _parse_master_args(argv)
343
+ if not master_opts['no-plugins']:
344
+ bzrlib.load_plugins()
345
+ args, opts = parse_args(argv)
346
+ if 'help' in opts or master_opts['help']:
353
- elif 'version' in opts:
354
+ elif 'version' in opts or master_opts['version']:
357
- elif args and args[0] == 'builtin':
358
- include_plugins=False
360
cmd = str(args.pop(0))
363
@@ -1493,14 +1513,15 @@
367
- canonical_cmd, cmd_class = get_cmd_class(cmd,include_plugins=include_plugins)
370
+ plugins_override = not (master_opts['builtin'])
371
+ canonical_cmd, cmd_class = get_cmd_class(cmd, plugins_override=plugins_override)
373
+ profile = master_opts['profile']
374
+ # For backwards compatibility, I would rather stick with --profile being a
375
+ # master/global option
376
if 'profile' in opts:
382
# check options are reasonable
383
allowed = cmd_class.takes_options
385
*** modified file 'testbzr'
389
"""Run a test involving creating a plugin to load,
390
and making sure it is seen properly.
392
+ orig_help = backtick('bzr help commands') # No plugins yet
394
f = open(os.path.join('plugin_test', 'myplug.py'), 'wb')
395
f.write("""import bzrlib, bzrlib.commands
396
@@ -157,24 +158,36 @@
399
print 'Hello from my plugin'
400
+class cmd_myplug_with_opt(bzrlib.commands.Command):
401
+ '''A simple plugin that requires a special option'''
402
+ takes_options = ['aspecialoptionthatdoesntexist']
403
+ def run(self, aspecialoptionthatdoesntexist=None):
404
+ print 'Found: %s' % aspecialoptionthatdoesntexist
406
+bzrlib.commands.register_plugin_command(cmd_myplug)
407
+bzrlib.commands.register_plugin_command(cmd_myplug_with_opt)
408
+bzrlib.commands.OPTIONS['aspecialoptionthatdoesntexist'] = str
412
- os.environ['BZRPLUGINPATH'] = os.path.abspath('plugin_test')
413
- help = backtick('bzr help commands')
414
+ os.environ['BZR_PLUGIN_PATH'] = os.path.abspath('plugin_test')
415
+ help = backtick('bzr help commands') #Help with user-visible plugins
416
assert help.find('myplug') != -1
417
assert help.find('Just a simple test plugin.') != -1
420
assert backtick('bzr myplug') == 'Hello from my plugin\n'
421
assert backtick('bzr mplg') == 'Hello from my plugin\n'
422
+ assert backtick('bzr myplug-with-opt') == 'Found: None\n'
423
+ assert backtick('bzr myplug-with-opt --aspecialoptionthatdoesntexist=2') == 'Found: 2\n'
425
f = open(os.path.join('plugin_test', 'override.py'), 'wb')
426
f.write("""import bzrlib, bzrlib.commands
427
-class cmd_commit(bzrlib.commands.cmd_commit):
428
- '''Commit changes into a new revision.'''
429
+class cmd_revno(bzrlib.commands.cmd_revno):
430
+ '''Show current revision number.'''
431
def run(self, *args, **kwargs):
432
print "I'm sorry dave, you can't do that"
435
class cmd_help(bzrlib.commands.cmd_help):
436
'''Show help on a command or other topic.'''
437
@@ -182,16 +195,67 @@
438
print "You have been overridden"
439
bzrlib.commands.cmd_help.run(self, *args, **kwargs)
441
+bzrlib.commands.register_plugin_command(cmd_revno)
442
+bzrlib.commands.register_plugin_command(cmd_help)
446
- newhelp = backtick('bzr help commands')
447
+ newhelp = backtick('bzr help commands') # Help with no new commands,
448
assert newhelp.startswith('You have been overridden\n')
449
# We added a line, but the rest should work
450
assert newhelp[25:] == help
452
- assert backtick('bzr commit -m test') == "I'm sorry dave, you can't do that\n"
454
+ # Make sure we can get back to the original command
455
+ # Not overridden, and no extra commands present
456
+ assert backtick('bzr --builtin help commands') == help
457
+ assert backtick('bzr --no-plugins help commands') == orig_help
459
+ assert backtick('bzr revno', retcode=1) == "I'm sorry dave, you can't do that\n"
461
+ print_txt = '** Loading noop plugin'
462
+ f = open(os.path.join('plugin_test', 'loading.py'), 'wb')
463
+ f.write("""import bzrlib, bzrlib.commands
464
+class cmd_noop(bzrlib.commands.Command):
465
+ def run(self, *args, **kwargs):
469
+bzrlib.commands.register_plugin_command(cmd_noop)
474
+ # Check that --builtin still loads the plugin, and enables it as
475
+ # an extra command, but not as an override
476
+ # and that --no-plugins doesn't load the command at all
477
+ assert backtick('bzr noop') == print_txt
478
+ assert backtick('bzr --builtin help')[:len(print_txt)] == print_txt
479
+ assert backtick('bzr --no-plugins help')[:len(print_txt)] != print_txt
480
+ runcmd('bzr revno', retcode=1)
481
+ runcmd('bzr --builtin revno', retcode=0)
482
+ runcmd('bzr --no-plugins revno', retcode=0)
483
+ runcmd('bzr --builtin noop', retcode=0)
484
+ runcmd('bzr --no-plugins noop', retcode=1)
486
+ # Check that packages can also be loaded
487
+ test_str = 'packages work'
488
+ os.mkdir(os.path.join('plugin_test', 'testpkg'))
489
+ f = open(os.path.join('plugin_test', 'testpkg', '__init__.py'), 'wb')
490
+ f.write("""import bzrlib, bzrlib.commands
491
+class testpkgcmd(bzrlib.commands.Command):
492
+ def run(self, *args, **kwargs):
495
+bzrlib.commands.register_plugin_command(testpkgcmd)
499
+ assert backtick('bzr testpkgcmd') == print_txt + test_str
500
+ runcmd('bzr --no-plugins testpkgcmd', retcode=1)
502
+ # Make sure that setting BZR_PLUGIN_PATH to empty is the same as using --no-plugins
503
+ os.environ['BZR_PLUGIN_PATH'] = ''
504
+ assert backtick('bzr help commands') == orig_help
506
shutil.rmtree('plugin_test')
511
runcmd(['mkdir', TESTDIR])
513
+ # This means that any command that is naively run in this directory
514
+ # Won't affect the parent directory.
516
test_root = os.getcwd()
518
progress("introductory commands")