~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to patches/plugins-no-plugins.patch

  • Committer: Robert Collins
  • Date: 2006-02-17 04:17:09 UTC
  • mto: (1534.1.24 integration)
  • mto: This revision was merged to the branch mainline in revision 1554.
  • Revision ID: robertc@robertcollins.net-20060217041709-7251eb701f69ca7e
Review feedback.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
*** added file 'bzrlib/plugin.py'
2
 
--- /dev/null
3
 
+++ bzrlib/plugin.py
4
 
@@ -0,0 +1,92 @@
5
 
+# Copyright (C) 2004, 2005 by Canonical Ltd
6
 
+
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.
11
 
+
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.
16
 
+
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
20
 
+
21
 
+
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.
28
 
+
29
 
+import sys, os, imp
30
 
+try:
31
 
+    set
32
 
+except NameError:
33
 
+    from sets import Set as set
34
 
+from bzrlib.trace import log_error
35
 
+
36
 
+
37
 
+def load_plugins():
38
 
+    """Find all python files which are plugins, and load them
39
 
+
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).
43
 
+    """
44
 
+    bzrpath = os.environ.get('BZR_PLUGIN_PATH', os.path.expanduser('~/.bzr/plugins'))
45
 
+
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
57
 
+        if not d:
58
 
+            continue
59
 
+        plugin_names = set()
60
 
+        if not os.path.isdir(d):
61
 
+            continue
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
67
 
+                    # the list
68
 
+                    if os.path.isfile(os.path.join(path, entry)):
69
 
+                        break
70
 
+                else: # This directory is not a package
71
 
+                    continue
72
 
+            else:
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')]
78
 
+                        break
79
 
+                else:
80
 
+                    continue
81
 
+            plugin_names.add(f)
82
 
+
83
 
+        plugin_names = list(plugin_names)
84
 
+        plugin_names.sort()
85
 
+        for name in plugin_names:
86
 
+            try:
87
 
+                plugin_info = imp.find_module(name, [d])
88
 
+                try:
89
 
+                    plugin = imp.load_module('bzrlib.plugin.' + name,
90
 
+                                             *plugin_info)
91
 
+                finally:
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))
96
 
+
97
 
 
98
 
*** modified file 'bzrlib/__init__.py'
99
 
--- bzrlib/__init__.py
100
 
+++ bzrlib/__init__.py
101
 
@@ -23,6 +23,7@@
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
106
 
 import add
107
 
 
108
 
 BZRDIR = ".bzr"
109
 
@@ -62,4 +63,4 @@
110
 
             return None
111
 
     except BzrError:
112
 
         return None
113
 
-
114
 
+
115
 
 
116
 
*** modified file 'bzrlib/commands.py'
117
 
--- bzrlib/commands.py
118
 
+++ bzrlib/commands.py
119
 
@@ -24,6 +24,24 @@
120
 
 from bzrlib.osutils import quotefn
121
 
 from bzrlib import Branch, Inventory, InventoryEntry, BZRDIR, \
122
 
      format_date
123
 
+
124
 
+
125
 
+plugin_cmds = {}
126
 
+
127
 
+
128
 
+def register_plugin_command(cmd):
129
 
+    "Utility function to help register a command"
130
 
+    global plugin_cmds
131
 
+    k = cmd.__name__
132
 
+    if k.startswith("cmd_"):
133
 
+        k_unsquished = _unsquish_command_name(k)
134
 
+    else:
135
 
+        k_unsquished = k
136
 
+    if not plugin_cmds.has_key(k_unsquished):
137
 
+        plugin_cmds[k_unsquished] = cmd
138
 
+    else:
139
 
+        log_error('Two plugins defined the same command: %r' % k)
140
 
+        log_error('Not loading the one in %r' % sys.modules[cmd.__module__])
141
 
 
142
 
 
143
 
 def _squish_command_name(cmd):
144
 
@@ -68,100 +86,34 @@
145
 
         revs = int(revstr)
146
 
     return revs
147
 
 
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"
151
 
-
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.
156
 
-
157
 
-    Inside the plugin should be a series of cmd_* function, which inherit from
158
 
-    the bzrlib.commands.Command class.
159
 
-    """
160
 
-    bzrpath = os.environ.get('BZRPLUGINPATH', '')
161
 
-
162
 
-    plugin_cmds = {}
163
 
-    if not bzrpath:
164
 
-        return plugin_cmds
165
 
-    _platform_extensions = {
166
 
-        'win32':'.pyd',
167
 
-        'cygwin':'.dll',
168
 
-        'darwin':'.dylib',
169
 
-        'linux2':'.so'
170
 
-        }
171
 
-    if _platform_extensions.has_key(sys.platform):
172
 
-        platform_extension = _platform_extensions[sys.platform]
173
 
-    else:
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'):
179
 
-                f = f[:-3]
180
 
-            elif f.endswith('.pyc') or f.endswith('.pyo'):
181
 
-                f = f[:-4]
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')]
186
 
-            else:
187
 
-                continue
188
 
-            if not plugin_names.has_key(f):
189
 
-                plugin_names[f] = True
190
 
-
191
 
-        plugin_names = plugin_names.keys()
192
 
-        plugin_names.sort()
193
 
-        try:
194
 
-            sys.path.insert(0, d)
195
 
-            for name in plugin_names:
196
 
-                try:
197
 
-                    old_module = None
198
 
-                    try:
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)
208
 
-                                else:
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))
211
 
-                    finally:
212
 
-                        if old_module:
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))
216
 
-        finally:
217
 
-            sys.path.pop(0)
218
 
-    return plugin_cmds
219
 
-
220
 
-def _get_cmd_dict(include_plugins=True):
221
 
+def _get_cmd_dict(plugins_override=True):
222
 
     d = {}
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)
231
 
+    else:
232
 
+        d2 = {}
233
 
+        d2.update(plugin_cmds)
234
 
+        d2.update(d)
235
 
+        d = d2
236
 
     return d
237
 
 
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():
243
 
         yield k,v
244
 
 
245
 
 
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.
249
 
     """
250
 
     cmd = str(cmd)                      # not unicode
251
 
 
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)
255
 
     try:
256
 
         return cmd, cmds[cmd]
257
 
     except KeyError:
258
 
@@ -1461,6 +1413,75 @@
259
 
     return argdict
260
 
 
261
 
 
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.
265
 
+
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.
270
 
+
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
275
 
+                        behavior.
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)
281
 
+
282
 
+    >>> argv, opts = _parse_master_args(['bzr', '--test'])
283
 
+    Traceback (most recent call last):
284
 
+    ...
285
 
+    BzrCommandError: Invalid master option: 'test'
286
 
+    >>> argv, opts = _parse_master_args(['bzr', '--version', 'command'])
287
 
+    >>> print argv
288
 
+    ['command']
289
 
+    >>> print opts['version']
290
 
+    True
291
 
+    >>> argv, opts = _parse_master_args(['bzr', '--profile', 'command', '--more-options'])
292
 
+    >>> print argv
293
 
+    ['command', '--more-options']
294
 
+    >>> print opts['profile']
295
 
+    True
296
 
+    >>> argv, opts = _parse_master_args(['bzr', '--no-plugins', 'command'])
297
 
+    >>> print argv
298
 
+    ['command']
299
 
+    >>> print opts['no-plugins']
300
 
+    True
301
 
+    >>> print opts['profile']
302
 
+    False
303
 
+    >>> argv, opts = _parse_master_args(['bzr', 'command', '--profile'])
304
 
+    >>> print argv
305
 
+    ['command', '--profile']
306
 
+    >>> print opts['profile']
307
 
+    False
308
 
+    """
309
 
+    master_opts = {'builtin':False,
310
 
+        'no-plugins':False,
311
 
+        'version':False,
312
 
+        'profile':False,
313
 
+        'help':False
314
 
+    }
315
 
+
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
322
 
+            break
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
331
 
 
332
 
 def run_bzr(argv):
333
 
     """Execute a command.
334
 
@@ -1470,22 +1491,21 @@
335
 
     """
336
 
     argv = [a.decode(bzrlib.user_encoding) for a in argv]
337
 
 
338
 
-    include_plugins=True
339
 
     try:
340
 
-        args, opts = parse_args(argv[1:])
341
 
-        if 'help' in opts:
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']:
347
 
             import help
348
 
             if args:
349
 
                 help.help(args[0])
350
 
             else:
351
 
                 help.help()
352
 
             return 0
353
 
-        elif 'version' in opts:
354
 
+        elif 'version' in opts or master_opts['version']:
355
 
             show_version()
356
 
             return 0
357
 
-        elif args and args[0] == 'builtin':
358
 
-            include_plugins=False
359
 
-            args = args[1:]
360
 
         cmd = str(args.pop(0))
361
 
     except IndexError:
362
 
         import help
363
 
@@ -1493,14 +1513,15 @@
364
 
         return 1
365
 
 
366
 
 
367
 
-    canonical_cmd, cmd_class = get_cmd_class(cmd,include_plugins=include_plugins)
368
 
-
369
 
-    # global option
370
 
+    plugins_override = not (master_opts['builtin'])
371
 
+    canonical_cmd, cmd_class = get_cmd_class(cmd, plugins_override=plugins_override)
372
 
+
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:
377
 
         profile = True
378
 
         del opts['profile']
379
 
-    else:
380
 
-        profile = False
381
 
 
382
 
     # check options are reasonable
383
 
     allowed = cmd_class.takes_options
384
 
 
385
 
*** modified file 'testbzr'
386
 
--- testbzr
387
 
+++ testbzr
388
 
@@ -149,6 +149,7 @@
389
 
     """Run a test involving creating a plugin to load,
390
 
     and making sure it is seen properly.
391
 
     """
392
 
+    orig_help = backtick('bzr help commands') # No plugins yet
393
 
     mkdir('plugin_test')
394
 
     f = open(os.path.join('plugin_test', 'myplug.py'), 'wb')
395
 
     f.write("""import bzrlib, bzrlib.commands
396
 
@@ -157,24 +158,36 @@
397
 
     aliases = ['mplg']
398
 
     def run(self):
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
405
 
+
406
 
+bzrlib.commands.register_plugin_command(cmd_myplug)
407
 
+bzrlib.commands.register_plugin_command(cmd_myplug_with_opt)
408
 
+bzrlib.commands.OPTIONS['aspecialoptionthatdoesntexist'] = str
409
 
 """)
410
 
     f.close()
411
 
 
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
418
 
 
419
 
 
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'
424
 
 
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"
433
 
+        return 1
434
 
 
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)
440
 
 
441
 
+bzrlib.commands.register_plugin_command(cmd_revno)
442
 
+bzrlib.commands.register_plugin_command(cmd_help)
443
 
 """)
444
 
     f.close()
445
 
 
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
451
 
-
452
 
-    assert backtick('bzr commit -m test') == "I'm sorry dave, you can't do that\n"
453
 
-
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
458
 
+
459
 
+    assert backtick('bzr revno', retcode=1) == "I'm sorry dave, you can't do that\n"
460
 
+
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):
466
 
+        pass
467
 
+
468
 
+print %r
469
 
+bzrlib.commands.register_plugin_command(cmd_noop)
470
 
+""" % print_txt)
471
 
+    f.close()
472
 
+    print_txt += '\n'
473
 
+
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)
485
 
+
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):
493
 
+        print %r
494
 
+
495
 
+bzrlib.commands.register_plugin_command(testpkgcmd)
496
 
+""" % test_str)
497
 
+    f.close()
498
 
+    test_str += '\n'
499
 
+    assert backtick('bzr testpkgcmd') == print_txt + test_str
500
 
+    runcmd('bzr --no-plugins testpkgcmd', retcode=1)
501
 
+
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
505
 
+
506
 
     shutil.rmtree('plugin_test')
507
 
 
508
 
 try:
509
 
@@ -221,6 +285,9 @@
510
 
 
511
 
     runcmd(['mkdir', TESTDIR])
512
 
     cd(TESTDIR)
513
 
+    # This means that any command that is naively run in this directory
514
 
+    # Won't affect the parent directory.
515
 
+    runcmd('bzr init')
516
 
     test_root = os.getcwd()
517
 
 
518
 
     progress("introductory commands")
519