/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

More work on roundtrip push support.

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
 
        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
76
 
 
77
 
 
78
 
@deprecated_function(zero_ninetyone)
79
 
def all_plugins():
80
 
    """Return a dictionary of the plugins."""
81
 
    return dict((name, plugin.module) for name, plugin in plugins().items())
82
 
 
83
 
 
84
 
def disable_plugins():
85
 
    """Disable loading plugins.
86
 
 
87
 
    Future calls to load_plugins() will be ignored.
88
 
    """
89
 
    # TODO: jam 20060131 This should probably also disable
90
 
    #       load_from_dirs()
91
 
    global _loaded
92
 
    _loaded = True
93
 
 
94
 
 
95
 
def _strip_trailing_sep(path):
96
 
    return path.rstrip("\\/")
97
 
 
98
 
 
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
109
 
    return path
110
 
 
111
 
 
112
 
def load_plugins():
113
 
    """Load bzrlib plugins.
114
 
 
115
 
    The environment variable BZR_PLUGIN_PATH is considered a delimited
116
 
    set of paths to look through. Each entry is searched for *.py
117
 
    files (and whatever other extensions are used in the platform,
118
 
    such as *.pyd).
119
 
 
120
 
    load_from_dirs() provides the underlying mechanism and is called with
121
 
    the default directory list to provide the normal behaviour.
122
 
    """
123
 
    global _loaded
124
 
    if _loaded:
125
 
        # People can make sure plugins are loaded, they just won't be twice
126
 
        return
127
 
    _loaded = True
128
 
 
129
 
    # scan for all plugins in the path.
130
 
    load_from_path(set_plugins_path())
131
 
 
132
 
 
133
 
def load_from_path(dirs):
134
 
    """Load bzrlib plugins found in each dir in dirs.
135
 
 
136
 
    Loading a plugin means importing it into the python interpreter.
137
 
    The plugin is expected to make calls to register commands when
138
 
    it's loaded (or perhaps access other hooks in future.)
139
 
 
140
 
    Plugins are loaded into bzrlib.plugins.NAME, and can be found there
141
 
    for future reference.
142
 
 
143
 
    The python module path for bzrlib.plugins will be modified to be 'dirs'.
144
 
    """
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)
151
 
    for d in dirs:
152
 
        if not d:
153
 
            continue
154
 
        mutter('looking for plugins in %s', d)
155
 
        if os.path.isdir(d):
156
 
            load_from_dir(d)
157
 
        else:
158
 
            # it might be a zip: try loading from the zip.
159
 
            load_from_zip(d)
160
 
 
161
 
 
162
 
# backwards compatability: load_from_dirs was the old name
163
 
# This was changed in 0.15
164
 
load_from_dirs = load_from_path
165
 
 
166
 
 
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]
175
 
    plugin_names = set()
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
181
 
                # the list
182
 
                if os.path.isfile(osutils.pathjoin(path, entry)):
183
 
                    break
184
 
            else: # This directory is not a package
185
 
                continue
186
 
        else:
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')]
192
 
                    break
193
 
            else:
194
 
                continue
195
 
        if getattr(_mod_plugins, f, None):
196
 
            mutter('Plugin name %s already loaded', f)
197
 
        else:
198
 
            # mutter('add plugin name %s', f)
199
 
            plugin_names.add(f)
200
 
    
201
 
    for name in plugin_names:
202
 
        try:
203
 
            exec "import bzrlib.plugins.%s" % name in {}
204
 
        except KeyboardInterrupt:
205
 
            raise
206
 
        except Exception, e:
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))
213
 
            else:
214
 
                warning('Unable to load plugin %r from %r' % (name, d))
215
 
            log_exception_quietly()
216
 
 
217
 
 
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
221
 
                                                # is allowed
222
 
 
223
 
    try:
224
 
        index = zip_name.rindex('.zip')
225
 
    except ValueError:
226
 
        return
227
 
    archive = zip_name[:index+4]
228
 
    prefix = zip_name[index+5:]
229
 
 
230
 
    mutter('Looking for plugins in %r', zip_name)
231
 
 
232
 
    # use zipfile to get list of files/dirs inside zip
233
 
    try:
234
 
        z = zipfile.ZipFile(archive)
235
 
        namelist = z.namelist()
236
 
        z.close()
237
 
    except zipfile.error:
238
 
        # not a valid zip
239
 
        return
240
 
 
241
 
    if prefix:
242
 
        prefix = prefix.replace('\\','/')
243
 
        if prefix[-1] != '/':
244
 
            prefix += '/'
245
 
        ix = len(prefix)
246
 
        namelist = [name[ix:]
247
 
                    for name in namelist
248
 
                    if name.startswith(prefix)]
249
 
 
250
 
    mutter('Names in archive: %r', namelist)
251
 
    
252
 
    for name in namelist:
253
 
        if not name or name.endswith('/'):
254
 
            continue
255
 
    
256
 
        # '/' is used to separate pathname components inside zip archives
257
 
        ix = name.rfind('/')
258
 
        if ix == -1:
259
 
            head, tail = '', name
260
 
        else:
261
 
            head, tail = name.rsplit('/',1)
262
 
        if '/' in head:
263
 
            # we don't need looking in subdirectories
264
 
            continue
265
 
    
266
 
        base, suffix = osutils.splitext(tail)
267
 
        if suffix not in valid_suffixes:
268
 
            continue
269
 
    
270
 
        if base == '__init__':
271
 
            # package
272
 
            plugin_name = head
273
 
        elif head == '':
274
 
            # module
275
 
            plugin_name = base
276
 
        else:
277
 
            continue
278
 
    
279
 
        if not plugin_name:
280
 
            continue
281
 
        if getattr(_mod_plugins, plugin_name, None):
282
 
            mutter('Plugin name %s already loaded', plugin_name)
283
 
            continue
284
 
    
285
 
        try:
286
 
            exec "import bzrlib.plugins.%s" % plugin_name in {}
287
 
            mutter('Load plugin %s from zip %r', plugin_name, zip_name)
288
 
        except KeyboardInterrupt:
289
 
            raise
290
 
        except Exception, e:
291
 
            ## import pdb; pdb.set_trace()
292
 
            warning('Unable to load plugin %r from %r'
293
 
                    % (name, zip_name))
294
 
            log_exception_quietly()
295
 
 
296
 
 
297
 
def plugins():
298
 
    """Return a dictionary of the plugins.
299
 
    
300
 
    Each item in the dictionary is a PlugIn object.
301
 
    """
302
 
    result = {}
303
 
    for name, plugin in _mod_plugins.__dict__.items():
304
 
        if isinstance(plugin, types.ModuleType):
305
 
            result[name] = PlugIn(name, plugin)
306
 
    return result
307
 
 
308
 
 
309
 
class PluginsHelpIndex(object):
310
 
    """A help index that returns help topics for plugins."""
311
 
 
312
 
    def __init__(self):
313
 
        self.prefix = 'plugins/'
314
 
 
315
 
    def get_topics(self, topic):
316
 
        """Search for topic in the loaded plugins.
317
 
 
318
 
        This will not trigger loading of new plugins.
319
 
 
320
 
        :param topic: A topic to search for.
321
 
        :return: A list which is either empty or contains a single
322
 
            RegisteredTopic entry.
323
 
        """
324
 
        if not topic:
325
 
            return []
326
 
        if topic.startswith(self.prefix):
327
 
            topic = topic[len(self.prefix):]
328
 
        plugin_module_name = 'bzrlib.plugins.%s' % topic
329
 
        try:
330
 
            module = sys.modules[plugin_module_name]
331
 
        except KeyError:
332
 
            return []
333
 
        else:
334
 
            return [ModuleHelpTopic(module)]
335
 
 
336
 
 
337
 
class ModuleHelpTopic(object):
338
 
    """A help topic which returns the docstring for a module."""
339
 
 
340
 
    def __init__(self, module):
341
 
        """Constructor.
342
 
 
343
 
        :param module: The module for which help should be generated.
344
 
        """
345
 
        self.module = module
346
 
 
347
 
    def get_help_text(self, additional_see_also=None):
348
 
        """Return a string with the help for this topic.
349
 
 
350
 
        :param additional_see_also: Additional help topics to be
351
 
            cross-referenced.
352
 
        """
353
 
        if not self.module.__doc__:
354
 
            result = "Plugin '%s' has no docstring.\n" % self.module.__name__
355
 
        else:
356
 
            result = self.module.__doc__
357
 
        if result[-1] != '\n':
358
 
            result += '\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))
364
 
        else:
365
 
            see_also = None
366
 
        if see_also:
367
 
            result += 'See also: '
368
 
            result += ', '.join(see_also)
369
 
            result += '\n'
370
 
        return result
371
 
 
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.'):]
375
 
 
376
 
 
377
 
class PlugIn(object):
378
 
    """The bzrlib representation of a plugin.
379
 
 
380
 
    The PlugIn object provides a way to manipulate a given plugin module.
381
 
    """
382
 
 
383
 
    def __init__(self, name, module):
384
 
        """Construct a plugin for module."""
385
 
        self.name = name
386
 
        self.module = module
387
 
 
388
 
    def path(self):
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
 
            return os.path.abspath(self.module.__file__)
394
 
        else:
395
 
            return repr(self.module)
396
 
 
397
 
    def __str__(self):
398
 
        return "<%s.%s object at %s, name=%s, module=%s>" % (
399
 
            self.__class__.__module__, self.__class__.__name__, id(self),
400
 
            self.name, self.module)
401
 
 
402
 
    __repr__ = __str__
403
 
 
404
 
    def test_suite(self):
405
 
        """Return the plugin's test suite."""
406
 
        if getattr(self.module, 'test_suite', None) is not None:
407
 
            return self.module.test_suite()
408
 
        else:
409
 
            return None
410
 
 
411
 
    def version_info(self):
412
 
        """Return the plugin's version_tuple or None if unknown."""
413
 
        version_info = getattr(self.module, 'version_info', None)
414
 
        if version_info is not None and len(version_info) == 3:
415
 
            version_info = tuple(version_info) + ('final', 0)
416
 
        return version_info
417
 
    
418
 
    def _get__version__(self):
419
 
        version_info = self.version_info()
420
 
        if version_info is None:
421
 
            return "unknown"
422
 
        if version_info[3] == 'final':
423
 
            version_string = '%d.%d.%d' % version_info[:3]
424
 
        else:
425
 
            version_string = '%d.%d.%d%s%d' % version_info
426
 
        return version_string
427
 
 
428
 
    __version__ = property(_get__version__)