~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

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

  • Committer: Martin Pool
  • Date: 2005-07-13 00:30:30 UTC
  • Revision ID: mbp@sourcefrog.net-20050713003030-2e89871a9ce24c7b
- typo in testsweet

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