/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to bzrlib/plugin.py

  • Committer: Robert Collins
  • Date: 2007-08-29 01:16:53 UTC
  • mto: This revision was merged to the branch mainline in revision 2764.
  • Revision ID: robertc@robertcollins.net-20070829011653-lb22o8no67g4y7yp
 * ``bzr plugins`` now lists the version number for each plugin in square
   brackets after the path. (Robert Collins, #125421)
 * ``bzrlib.plugin.all_plugins`` has been deprecated in favour of
   ``bzrlib.plugin.plugins()`` which returns PlugIn objects that provide
   useful functionality for determining the path of a plugin, its tests, and
   its version information. (Robert Collins)

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2004, 2005, 2007 Canonical Ltd
 
2
#
 
3
# This program is free software; you can redistribute it and/or modify
 
4
# it under the terms of the GNU General Public License as published by
 
5
# the Free Software Foundation; either version 2 of the License, or
 
6
# (at your option) any later version.
 
7
#
 
8
# This program is distributed in the hope that it will be useful,
 
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
11
# GNU General Public License for more details.
 
12
#
 
13
# You should have received a copy of the GNU General Public License
 
14
# along with this program; if not, write to the Free Software
 
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
16
 
 
17
 
 
18
"""bzr python plugin support.
 
19
 
 
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
 
25
variable.
 
26
 
 
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 
 
29
called.
 
30
"""
 
31
 
 
32
import os
 
33
import sys
 
34
 
 
35
from bzrlib.lazy_import import lazy_import
 
36
lazy_import(globals(), """
 
37
import imp
 
38
import re
 
39
import types
 
40
import zipfile
 
41
 
 
42
from bzrlib import (
 
43
    config,
 
44
    osutils,
 
45
    )
 
46
from bzrlib import plugins as _mod_plugins
 
47
""")
 
48
 
 
49
from bzrlib.symbol_versioning import deprecated_function, zero_ninetyone
 
50
from bzrlib.trace import mutter, warning, log_exception_quietly
 
51
 
 
52
 
 
53
DEFAULT_PLUGIN_PATH = None
 
54
_loaded = False
 
55
 
 
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
 
62
 
 
63
 
 
64
@deprecated_function(zero_ninetyone)
 
65
def all_plugins():
 
66
    """Return a dictionary of the plugins."""
 
67
    result = {}
 
68
    for name, plugin in plugins().items():
 
69
        result[name] = plugin.module
 
70
    return result
 
71
 
 
72
 
 
73
def disable_plugins():
 
74
    """Disable loading plugins.
 
75
 
 
76
    Future calls to load_plugins() will be ignored.
 
77
    """
 
78
    # TODO: jam 20060131 This should probably also disable
 
79
    #       load_from_dirs()
 
80
    global _loaded
 
81
    _loaded = True
 
82
 
 
83
def _strip_trailing_sep(path):
 
84
    return path.rstrip("\\/")
 
85
 
 
86
def set_plugins_path():
 
87
    """Set the path for plugins to be loaded from."""
 
88
    path = os.environ.get('BZR_PLUGIN_PATH',
 
89
                          get_default_plugin_path()).split(os.pathsep)
 
90
    # Get rid of trailing slashes, since Python can't handle them when
 
91
    # it tries to import modules.
 
92
    path = map(_strip_trailing_sep, path)
 
93
    # search the plugin path before the bzrlib installed dir
 
94
    path.append(os.path.dirname(_mod_plugins.__file__))
 
95
    _mod_plugins.__path__ = path
 
96
    return path
 
97
 
 
98
 
 
99
def load_plugins():
 
100
    """Load bzrlib plugins.
 
101
 
 
102
    The environment variable BZR_PLUGIN_PATH is considered a delimited
 
103
    set of paths to look through. Each entry is searched for *.py
 
104
    files (and whatever other extensions are used in the platform,
 
105
    such as *.pyd).
 
106
 
 
107
    load_from_dirs() provides the underlying mechanism and is called with
 
108
    the default directory list to provide the normal behaviour.
 
109
    """
 
110
    global _loaded
 
111
    if _loaded:
 
112
        # People can make sure plugins are loaded, they just won't be twice
 
113
        return
 
114
    _loaded = True
 
115
 
 
116
    # scan for all plugins in the path.
 
117
    load_from_path(set_plugins_path())
 
118
 
 
119
 
 
120
def load_from_path(dirs):
 
121
    """Load bzrlib plugins found in each dir in dirs.
 
122
 
 
123
    Loading a plugin means importing it into the python interpreter.
 
124
    The plugin is expected to make calls to register commands when
 
125
    it's loaded (or perhaps access other hooks in future.)
 
126
 
 
127
    Plugins are loaded into bzrlib.plugins.NAME, and can be found there
 
128
    for future reference.
 
129
 
 
130
    The python module path for bzrlib.plugins will be modified to be 'dirs'.
 
131
    """
 
132
    # We need to strip the trailing separators here as well as in the
 
133
    # set_plugins_path function because calling code can pass anything in to
 
134
    # this function, and since it sets plugins.__path__, it should set it to
 
135
    # something that will be valid for Python to use (in case people try to
 
136
    # run "import bzrlib.plugins.PLUGINNAME" after calling this function).
 
137
    _mod_plugins.__path__ = map(_strip_trailing_sep, dirs)
 
138
    for d in dirs:
 
139
        if not d:
 
140
            continue
 
141
        mutter('looking for plugins in %s', d)
 
142
        if os.path.isdir(d):
 
143
            load_from_dir(d)
 
144
        else:
 
145
            # it might be a zip: try loading from the zip.
 
146
            load_from_zip(d)
 
147
            continue
 
148
 
 
149
 
 
150
# backwards compatability: load_from_dirs was the old name
 
151
# This was changed in 0.15
 
152
load_from_dirs = load_from_path
 
153
 
 
154
 
 
155
def load_from_dir(d):
 
156
    """Load the plugins in directory d."""
 
157
    # Get the list of valid python suffixes for __init__.py?
 
158
    # this includes .py, .pyc, and .pyo (depending on if we are running -O)
 
159
    # but it doesn't include compiled modules (.so, .dll, etc)
 
160
    valid_suffixes = [suffix for suffix, mod_type, flags in imp.get_suffixes()
 
161
                              if flags in (imp.PY_SOURCE, imp.PY_COMPILED)]
 
162
    package_entries = ['__init__'+suffix for suffix in valid_suffixes]
 
163
    plugin_names = set()
 
164
    for f in os.listdir(d):
 
165
        path = osutils.pathjoin(d, f)
 
166
        if os.path.isdir(path):
 
167
            for entry in package_entries:
 
168
                # This directory should be a package, and thus added to
 
169
                # the list
 
170
                if os.path.isfile(osutils.pathjoin(path, entry)):
 
171
                    break
 
172
            else: # This directory is not a package
 
173
                continue
 
174
        else:
 
175
            for suffix_info in imp.get_suffixes():
 
176
                if f.endswith(suffix_info[0]):
 
177
                    f = f[:-len(suffix_info[0])]
 
178
                    if suffix_info[2] == imp.C_EXTENSION and f.endswith('module'):
 
179
                        f = f[:-len('module')]
 
180
                    break
 
181
            else:
 
182
                continue
 
183
        if getattr(_mod_plugins, f, None):
 
184
            mutter('Plugin name %s already loaded', f)
 
185
        else:
 
186
            # mutter('add plugin name %s', f)
 
187
            plugin_names.add(f)
 
188
    
 
189
    for name in plugin_names:
 
190
        try:
 
191
            exec "import bzrlib.plugins.%s" % name in {}
 
192
        except KeyboardInterrupt:
 
193
            raise
 
194
        except Exception, e:
 
195
            ## import pdb; pdb.set_trace()
 
196
            if re.search('\.|-| ', name):
 
197
                warning('Unable to load plugin %r from %r: '
 
198
                    'It is not a valid python module name.' % (name, d))
 
199
            else:
 
200
                warning('Unable to load plugin %r from %r' % (name, d))
 
201
            log_exception_quietly()
 
202
 
 
203
 
 
204
def load_from_zip(zip_name):
 
205
    """Load all the plugins in a zip."""
 
206
    valid_suffixes = ('.py', '.pyc', '.pyo')    # only python modules/packages
 
207
                                                # is allowed
 
208
 
 
209
    try:
 
210
        index = zip_name.rindex('.zip')
 
211
    except ValueError:
 
212
        return
 
213
    archive = zip_name[:index+4]
 
214
    prefix = zip_name[index+5:]
 
215
 
 
216
    mutter('Looking for plugins in %r', zip_name)
 
217
 
 
218
    # use zipfile to get list of files/dirs inside zip
 
219
    try:
 
220
        z = zipfile.ZipFile(archive)
 
221
        namelist = z.namelist()
 
222
        z.close()
 
223
    except zipfile.error:
 
224
        # not a valid zip
 
225
        return
 
226
 
 
227
    if prefix:
 
228
        prefix = prefix.replace('\\','/')
 
229
        if prefix[-1] != '/':
 
230
            prefix += '/'
 
231
        ix = len(prefix)
 
232
        namelist = [name[ix:]
 
233
                    for name in namelist
 
234
                    if name.startswith(prefix)]
 
235
 
 
236
    mutter('Names in archive: %r', namelist)
 
237
    
 
238
    for name in namelist:
 
239
        if not name or name.endswith('/'):
 
240
            continue
 
241
    
 
242
        # '/' is used to separate pathname components inside zip archives
 
243
        ix = name.rfind('/')
 
244
        if ix == -1:
 
245
            head, tail = '', name
 
246
        else:
 
247
            head, tail = name.rsplit('/',1)
 
248
        if '/' in head:
 
249
            # we don't need looking in subdirectories
 
250
            continue
 
251
    
 
252
        base, suffix = osutils.splitext(tail)
 
253
        if suffix not in valid_suffixes:
 
254
            continue
 
255
    
 
256
        if base == '__init__':
 
257
            # package
 
258
            plugin_name = head
 
259
        elif head == '':
 
260
            # module
 
261
            plugin_name = base
 
262
        else:
 
263
            continue
 
264
    
 
265
        if not plugin_name:
 
266
            continue
 
267
        if getattr(_mod_plugins, plugin_name, None):
 
268
            mutter('Plugin name %s already loaded', plugin_name)
 
269
            continue
 
270
    
 
271
        try:
 
272
            exec "import bzrlib.plugins.%s" % plugin_name in {}
 
273
            mutter('Load plugin %s from zip %r', plugin_name, zip_name)
 
274
        except KeyboardInterrupt:
 
275
            raise
 
276
        except Exception, e:
 
277
            ## import pdb; pdb.set_trace()
 
278
            warning('Unable to load plugin %r from %r'
 
279
                    % (name, zip_name))
 
280
            log_exception_quietly()
 
281
 
 
282
 
 
283
def plugins():
 
284
    """Return a dictionary of the plugins.
 
285
    
 
286
    Each item in the dictionary is a PlugIn object.
 
287
    """
 
288
    result = {}
 
289
    for name, plugin in _mod_plugins.__dict__.items():
 
290
        if isinstance(plugin, types.ModuleType):
 
291
            result[name] = PlugIn(name, plugin)
 
292
    return result
 
293
 
 
294
 
 
295
class PluginsHelpIndex(object):
 
296
    """A help index that returns help topics for plugins."""
 
297
 
 
298
    def __init__(self):
 
299
        self.prefix = 'plugins/'
 
300
 
 
301
    def get_topics(self, topic):
 
302
        """Search for topic in the loaded plugins.
 
303
 
 
304
        This will not trigger loading of new plugins.
 
305
 
 
306
        :param topic: A topic to search for.
 
307
        :return: A list which is either empty or contains a single
 
308
            RegisteredTopic entry.
 
309
        """
 
310
        if not topic:
 
311
            return []
 
312
        if topic.startswith(self.prefix):
 
313
            topic = topic[len(self.prefix):]
 
314
        plugin_module_name = 'bzrlib.plugins.%s' % topic
 
315
        try:
 
316
            module = sys.modules[plugin_module_name]
 
317
        except KeyError:
 
318
            return []
 
319
        else:
 
320
            return [ModuleHelpTopic(module)]
 
321
 
 
322
 
 
323
class ModuleHelpTopic(object):
 
324
    """A help topic which returns the docstring for a module."""
 
325
 
 
326
    def __init__(self, module):
 
327
        """Constructor.
 
328
 
 
329
        :param module: The module for which help should be generated.
 
330
        """
 
331
        self.module = module
 
332
 
 
333
    def get_help_text(self, additional_see_also=None):
 
334
        """Return a string with the help for this topic.
 
335
 
 
336
        :param additional_see_also: Additional help topics to be
 
337
            cross-referenced.
 
338
        """
 
339
        if not self.module.__doc__:
 
340
            result = "Plugin '%s' has no docstring.\n" % self.module.__name__
 
341
        else:
 
342
            result = self.module.__doc__
 
343
        if result[-1] != '\n':
 
344
            result += '\n'
 
345
        # there is code duplicated here and in bzrlib/help_topic.py's 
 
346
        # matching Topic code. This should probably be factored in
 
347
        # to a helper function and a common base class.
 
348
        if additional_see_also is not None:
 
349
            see_also = sorted(set(additional_see_also))
 
350
        else:
 
351
            see_also = None
 
352
        if see_also:
 
353
            result += 'See also: '
 
354
            result += ', '.join(see_also)
 
355
            result += '\n'
 
356
        return result
 
357
 
 
358
    def get_help_topic(self):
 
359
        """Return the modules help topic - its __name__ after bzrlib.plugins.."""
 
360
        return self.module.__name__[len('bzrlib.plugins.'):]
 
361
 
 
362
 
 
363
class PlugIn(object):
 
364
    """The bzrlib representation of a plugin.
 
365
 
 
366
    The PlugIn object provides a way to manipulate a given plugin module.
 
367
    """
 
368
 
 
369
    def __init__(self, name, module):
 
370
        """Construct a plugin for module."""
 
371
        self.name = name
 
372
        self.module = module
 
373
 
 
374
    def path(self):
 
375
        """Get the path that this plugin was loaded from."""
 
376
        if getattr(self.module, '__path__', None) is not None:
 
377
            return os.path.abspath(self.module.__path__[0])
 
378
        elif getattr(self.module, '__file__', None) is not None:
 
379
            return os.path.abspath(self.module.__file__)
 
380
        else:
 
381
            return repr(self.module)
 
382
 
 
383
    def __str__(self):
 
384
        return "<%s.%s object at %s, name=%s, module=%s>" % (
 
385
            self.__class__.__module__, self.__class__.__name__, id(self),
 
386
            self.name, self.module)
 
387
 
 
388
    __repr__ = __str__
 
389
 
 
390
    def test_suite(self):
 
391
        """Return the plugin's test suite."""
 
392
        if getattr(self.module, 'test_suite', None) is not None:
 
393
            return self.module.test_suite()
 
394
        else:
 
395
            return None
 
396
 
 
397
    def version_info(self):
 
398
        """Return the plugin's version_tuple or None if unknown."""
 
399
        version_info = getattr(self.module, 'version_info', None)
 
400
        if version_info is not None and len(version_info) == 3:
 
401
            version_info = tuple(version_info) + ('final', 0)
 
402
        return version_info
 
403
    
 
404
    def _get__version__(self):
 
405
        version_info = self.version_info()
 
406
        if version_info is None:
 
407
            return "unknown"
 
408
        if version_info[3] == 'final':
 
409
            version_string = '%d.%d.%d' % version_info[:3]
 
410
        else:
 
411
            version_string = '%d.%d.%d%s%d' % version_info
 
412
        return version_string
 
413
 
 
414
    __version__ = property(_get__version__)