~bzr-pqm/bzr/bzr.dev

2610.2.1 by Martin Pool
(Lukas Lalinsky) don't create a duplicate zipimporter, avoiding loading plugins twice
1
# Copyright (C) 2004, 2005, 2007 Canonical Ltd
1887.1.1 by Adeodato Simó
Do not separate paragraphs in the copyright statement with blank lines,
2
#
1393.2.1 by John Arbash Meinel
Merged in split-storage-2 branch. Need to cleanup a little bit more still.
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.
1887.1.1 by Adeodato Simó
Do not separate paragraphs in the copyright statement with blank lines,
7
#
1393.2.1 by John Arbash Meinel
Merged in split-storage-2 branch. Need to cleanup a little bit more still.
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.
1887.1.1 by Adeodato Simó
Do not separate paragraphs in the copyright statement with blank lines,
12
#
1393.2.1 by John Arbash Meinel
Merged in split-storage-2 branch. Need to cleanup a little bit more still.
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
2256.2.2 by Robert Collins
Allow 'import bzrlib.plugins.NAME' to work when the plugin NAME has not
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.
1185.16.83 by mbp at sourcefrog
- notes on testability of plugins
30
"""
31
1393.2.1 by John Arbash Meinel
Merged in split-storage-2 branch. Need to cleanup a little bit more still.
32
import os
1185.16.82 by mbp at sourcefrog
- give a quieter warning if a plugin can't be loaded
33
import sys
1996.3.17 by John Arbash Meinel
lazy_import plugin and transport/local
34
35
from bzrlib.lazy_import import lazy_import
36
lazy_import(globals(), """
37
import imp
2256.2.3 by Robert Collins
Review feedback.
38
import re
1516 by Robert Collins
* bzrlib.plugin.all_plugins has been changed from an attribute to a
39
import types
2610.2.1 by Martin Pool
(Lukas Lalinsky) don't create a duplicate zipimporter, avoiding loading plugins twice
40
import zipfile
1185.16.82 by mbp at sourcefrog
- give a quieter warning if a plugin can't be loaded
41
1996.3.17 by John Arbash Meinel
lazy_import plugin and transport/local
42
from bzrlib import (
43
    config,
44
    osutils,
45
    )
2762.2.1 by Robert Collins
* ``bzr plugins`` now lists the version number for each plugin in square
46
from bzrlib import plugins as _mod_plugins
1996.3.17 by John Arbash Meinel
lazy_import plugin and transport/local
47
""")
48
3193.7.11 by Alexander Belchenko
merge bzr.dev; update patch for 1.3
49
from bzrlib.symbol_versioning import deprecated_function, one_three
1996.3.17 by John Arbash Meinel
lazy_import plugin and transport/local
50
from bzrlib.trace import mutter, warning, log_exception_quietly
51
52
53
DEFAULT_PLUGIN_PATH = None
2256.2.2 by Robert Collins
Allow 'import bzrlib.plugins.NAME' to work when the plugin NAME has not
54
_loaded = False
1996.3.17 by John Arbash Meinel
lazy_import plugin and transport/local
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:
3193.7.1 by Alexander Belchenko
system-wide plugins for bzr.exe works even if BZR_PLUGIN_PATH env variable is set.
60
        DEFAULT_PLUGIN_PATH = osutils.pathjoin(config.config_dir(), 'plugins')
1996.3.17 by John Arbash Meinel
lazy_import plugin and transport/local
61
    return DEFAULT_PLUGIN_PATH
62
1393.2.1 by John Arbash Meinel
Merged in split-storage-2 branch. Need to cleanup a little bit more still.
63
1551.3.11 by Aaron Bentley
Merge from Robert
64
def disable_plugins():
65
    """Disable loading plugins.
66
67
    Future calls to load_plugins() will be ignored.
68
    """
69
    # TODO: jam 20060131 This should probably also disable
70
    #       load_from_dirs()
71
    global _loaded
72
    _loaded = True
73
3010.4.1 by Alexander Belchenko
bzr.exe: enable to search system-wide plugins in "plugins" subdirectory of installation directory
74
2753.1.1 by Ian Clatworthy
(Blake Winton) BZR_PLUGIN_PATH should ignore trailiing slashes
75
def _strip_trailing_sep(path):
2652.2.6 by Blake Winton
Incorporate suggestions from Alexander Belchenko
76
    return path.rstrip("\\/")
1551.3.11 by Aaron Bentley
Merge from Robert
77
3010.4.1 by Alexander Belchenko
bzr.exe: enable to search system-wide plugins in "plugins" subdirectory of installation directory
78
2256.2.2 by Robert Collins
Allow 'import bzrlib.plugins.NAME' to work when the plugin NAME has not
79
def set_plugins_path():
80
    """Set the path for plugins to be loaded from."""
81
    path = os.environ.get('BZR_PLUGIN_PATH',
2652.2.5 by Blake Winton
Get rid of CRs.
82
                          get_default_plugin_path()).split(os.pathsep)
3193.7.5 by Alexander Belchenko
deprecate bzrlib.plugins.load_from_zip feature; remove 0.91-deprecated function all_plugins()
83
    bzr_exe = bool(getattr(sys, 'frozen', None))
84
    if bzr_exe:    # expand path for bzr.exe
3193.7.1 by Alexander Belchenko
system-wide plugins for bzr.exe works even if BZR_PLUGIN_PATH env variable is set.
85
        # We need to use relative path to system-wide plugin
86
        # directory because bzrlib from standalone bzr.exe
87
        # could be imported by another standalone program
88
        # (e.g. bzr-config; or TortoiseBzr/Olive if/when they
89
        # will become standalone exe). [bialix 20071123]
90
        # __file__ typically is
91
        # C:\Program Files\Bazaar\lib\library.zip\bzrlib\plugin.pyc
92
        # then plugins directory is
93
        # C:\Program Files\Bazaar\plugins
94
        # so relative path is ../../../plugins
95
        path.append(osutils.abspath(osutils.pathjoin(
96
            osutils.dirname(__file__), '../../../plugins')))
2652.2.5 by Blake Winton
Get rid of CRs.
97
    # Get rid of trailing slashes, since Python can't handle them when
2652.2.4 by Blake Winton
Add a note explaining why I strip the slashes twice.
98
    # it tries to import modules.
2753.1.1 by Ian Clatworthy
(Blake Winton) BZR_PLUGIN_PATH should ignore trailiing slashes
99
    path = map(_strip_trailing_sep, path)
3193.7.5 by Alexander Belchenko
deprecate bzrlib.plugins.load_from_zip feature; remove 0.91-deprecated function all_plugins()
100
    if not bzr_exe:     # don't look inside library.zip
101
        # search the plugin path before the bzrlib installed dir
102
        path.append(os.path.dirname(_mod_plugins.__file__))
3232.1.1 by Toshio Kuratomi
look for plugins in arch-independent site directory
103
    # search the arch independent path if we can determine that and
104
    # the plugin is found nowhere else
105
    if sys.platform != 'win32':
106
        try:
107
            from distutils.sysconfig import get_python_lib
108
        except ImportError:
109
            # If distutuils is not available, we just won't add that path
110
            pass
111
        else:
112
            archless_path = osutils.pathjoin(get_python_lib(), 'bzrlib',
113
                    'plugins')
114
            if archless_path not in path:
115
                path.append(archless_path)
2762.2.1 by Robert Collins
* ``bzr plugins`` now lists the version number for each plugin in square
116
    _mod_plugins.__path__ = path
2256.2.2 by Robert Collins
Allow 'import bzrlib.plugins.NAME' to work when the plugin NAME has not
117
    return path
118
119
1393.2.1 by John Arbash Meinel
Merged in split-storage-2 branch. Need to cleanup a little bit more still.
120
def load_plugins():
1515 by Robert Collins
* Plugins with the same name in different directories in the bzr plugin
121
    """Load bzrlib plugins.
1393.2.1 by John Arbash Meinel
Merged in split-storage-2 branch. Need to cleanup a little bit more still.
122
123
    The environment variable BZR_PLUGIN_PATH is considered a delimited
124
    set of paths to look through. Each entry is searched for *.py
125
    files (and whatever other extensions are used in the platform,
126
    such as *.pyd).
1515 by Robert Collins
* Plugins with the same name in different directories in the bzr plugin
127
128
    load_from_dirs() provides the underlying mechanism and is called with
129
    the default directory list to provide the normal behaviour.
1393.2.1 by John Arbash Meinel
Merged in split-storage-2 branch. Need to cleanup a little bit more still.
130
    """
1515 by Robert Collins
* Plugins with the same name in different directories in the bzr plugin
131
    global _loaded
1393.2.1 by John Arbash Meinel
Merged in split-storage-2 branch. Need to cleanup a little bit more still.
132
    if _loaded:
133
        # People can make sure plugins are loaded, they just won't be twice
134
        return
135
    _loaded = True
136
2256.2.2 by Robert Collins
Allow 'import bzrlib.plugins.NAME' to work when the plugin NAME has not
137
    # scan for all plugins in the path.
138
    load_from_path(set_plugins_path())
139
140
141
def load_from_path(dirs):
1515 by Robert Collins
* Plugins with the same name in different directories in the bzr plugin
142
    """Load bzrlib plugins found in each dir in dirs.
143
144
    Loading a plugin means importing it into the python interpreter.
145
    The plugin is expected to make calls to register commands when
146
    it's loaded (or perhaps access other hooks in future.)
147
148
    Plugins are loaded into bzrlib.plugins.NAME, and can be found there
149
    for future reference.
2256.2.2 by Robert Collins
Allow 'import bzrlib.plugins.NAME' to work when the plugin NAME has not
150
151
    The python module path for bzrlib.plugins will be modified to be 'dirs'.
2652.2.5 by Blake Winton
Get rid of CRs.
152
    """
153
    # We need to strip the trailing separators here as well as in the
154
    # set_plugins_path function because calling code can pass anything in to
155
    # this function, and since it sets plugins.__path__, it should set it to
156
    # something that will be valid for Python to use (in case people try to
2652.2.4 by Blake Winton
Add a note explaining why I strip the slashes twice.
157
    # run "import bzrlib.plugins.PLUGINNAME" after calling this function).
2762.2.1 by Robert Collins
* ``bzr plugins`` now lists the version number for each plugin in square
158
    _mod_plugins.__path__ = map(_strip_trailing_sep, dirs)
2256.2.2 by Robert Collins
Allow 'import bzrlib.plugins.NAME' to work when the plugin NAME has not
159
    for d in dirs:
160
        if not d:
161
            continue
162
        mutter('looking for plugins in %s', d)
163
        if os.path.isdir(d):
164
            load_from_dir(d)
165
166
167
# backwards compatability: load_from_dirs was the old name
168
# This was changed in 0.15
169
load_from_dirs = load_from_path
170
171
172
def load_from_dir(d):
173
    """Load the plugins in directory d."""
1864.6.1 by John Arbash Meinel
Use the correct suffixes for loading plugins (bug #51810)
174
    # Get the list of valid python suffixes for __init__.py?
175
    # this includes .py, .pyc, and .pyo (depending on if we are running -O)
176
    # but it doesn't include compiled modules (.so, .dll, etc)
177
    valid_suffixes = [suffix for suffix, mod_type, flags in imp.get_suffixes()
178
                              if flags in (imp.PY_SOURCE, imp.PY_COMPILED)]
179
    package_entries = ['__init__'+suffix for suffix in valid_suffixes]
2256.2.2 by Robert Collins
Allow 'import bzrlib.plugins.NAME' to work when the plugin NAME has not
180
    plugin_names = set()
181
    for f in os.listdir(d):
182
        path = osutils.pathjoin(d, f)
183
        if os.path.isdir(path):
184
            for entry in package_entries:
185
                # This directory should be a package, and thus added to
186
                # the list
187
                if os.path.isfile(osutils.pathjoin(path, entry)):
188
                    break
189
            else: # This directory is not a package
190
                continue
191
        else:
192
            for suffix_info in imp.get_suffixes():
193
                if f.endswith(suffix_info[0]):
194
                    f = f[:-len(suffix_info[0])]
195
                    if suffix_info[2] == imp.C_EXTENSION and f.endswith('module'):
196
                        f = f[:-len('module')]
197
                    break
198
            else:
199
                continue
2762.2.1 by Robert Collins
* ``bzr plugins`` now lists the version number for each plugin in square
200
        if getattr(_mod_plugins, f, None):
2256.2.2 by Robert Collins
Allow 'import bzrlib.plugins.NAME' to work when the plugin NAME has not
201
            mutter('Plugin name %s already loaded', f)
202
        else:
203
            # mutter('add plugin name %s', f)
204
            plugin_names.add(f)
205
    
206
    for name in plugin_names:
207
        try:
2256.2.3 by Robert Collins
Review feedback.
208
            exec "import bzrlib.plugins.%s" % name in {}
2256.2.2 by Robert Collins
Allow 'import bzrlib.plugins.NAME' to work when the plugin NAME has not
209
        except KeyboardInterrupt:
210
            raise
211
        except Exception, e:
212
            ## import pdb; pdb.set_trace()
2256.2.3 by Robert Collins
Review feedback.
213
            if re.search('\.|-| ', name):
2967.4.7 by Daniel Watkins
Converted from nasty chained replaces to re.sub.
214
                sanitised_name = re.sub('[-. ]', '_', name)
3290.1.1 by James Westby
Strip "bzr_" from the start of the suggested plugin name.
215
                if sanitised_name.startswith('bzr_'):
216
                    sanitised_name = sanitised_name[len('bzr_'):]
217
                warning("Unable to load %r in %r as a plugin because the "
218
                        "file path isn't a valid module name; try renaming "
219
                        "it to %r." % (name, d, sanitised_name))
2256.2.3 by Robert Collins
Review feedback.
220
            else:
221
                warning('Unable to load plugin %r from %r' % (name, d))
2256.2.2 by Robert Collins
Allow 'import bzrlib.plugins.NAME' to work when the plugin NAME has not
222
            log_exception_quietly()
223
224
3193.7.11 by Alexander Belchenko
merge bzr.dev; update patch for 1.3
225
@deprecated_function(one_three)
2256.2.2 by Robert Collins
Allow 'import bzrlib.plugins.NAME' to work when the plugin NAME has not
226
def load_from_zip(zip_name):
227
    """Load all the plugins in a zip."""
2215.4.1 by Alexander Belchenko
Bugfix #68124: Allow plugins import from zip archives.
228
    valid_suffixes = ('.py', '.pyc', '.pyo')    # only python modules/packages
229
                                                # is allowed
2256.2.2 by Robert Collins
Allow 'import bzrlib.plugins.NAME' to work when the plugin NAME has not
230
    try:
2610.2.1 by Martin Pool
(Lukas Lalinsky) don't create a duplicate zipimporter, avoiding loading plugins twice
231
        index = zip_name.rindex('.zip')
232
    except ValueError:
2256.2.2 by Robert Collins
Allow 'import bzrlib.plugins.NAME' to work when the plugin NAME has not
233
        return
2610.2.1 by Martin Pool
(Lukas Lalinsky) don't create a duplicate zipimporter, avoiding loading plugins twice
234
    archive = zip_name[:index+4]
235
    prefix = zip_name[index+5:]
236
2256.2.2 by Robert Collins
Allow 'import bzrlib.plugins.NAME' to work when the plugin NAME has not
237
    mutter('Looking for plugins in %r', zip_name)
238
239
    # use zipfile to get list of files/dirs inside zip
2610.2.1 by Martin Pool
(Lukas Lalinsky) don't create a duplicate zipimporter, avoiding loading plugins twice
240
    try:
241
        z = zipfile.ZipFile(archive)
242
        namelist = z.namelist()
243
        z.close()
244
    except zipfile.error:
245
        # not a valid zip
246
        return
247
248
    if prefix:
249
        prefix = prefix.replace('\\','/')
250
        if prefix[-1] != '/':
251
            prefix += '/'
2256.2.2 by Robert Collins
Allow 'import bzrlib.plugins.NAME' to work when the plugin NAME has not
252
        ix = len(prefix)
253
        namelist = [name[ix:]
254
                    for name in namelist
255
                    if name.startswith(prefix)]
2610.2.1 by Martin Pool
(Lukas Lalinsky) don't create a duplicate zipimporter, avoiding loading plugins twice
256
2256.2.2 by Robert Collins
Allow 'import bzrlib.plugins.NAME' to work when the plugin NAME has not
257
    mutter('Names in archive: %r', namelist)
258
    
259
    for name in namelist:
260
        if not name or name.endswith('/'):
261
            continue
262
    
263
        # '/' is used to separate pathname components inside zip archives
264
        ix = name.rfind('/')
265
        if ix == -1:
266
            head, tail = '', name
267
        else:
268
            head, tail = name.rsplit('/',1)
269
        if '/' in head:
270
            # we don't need looking in subdirectories
271
            continue
272
    
273
        base, suffix = osutils.splitext(tail)
274
        if suffix not in valid_suffixes:
275
            continue
276
    
277
        if base == '__init__':
278
            # package
279
            plugin_name = head
280
        elif head == '':
281
            # module
282
            plugin_name = base
283
        else:
284
            continue
285
    
286
        if not plugin_name:
287
            continue
2762.2.1 by Robert Collins
* ``bzr plugins`` now lists the version number for each plugin in square
288
        if getattr(_mod_plugins, plugin_name, None):
2256.2.2 by Robert Collins
Allow 'import bzrlib.plugins.NAME' to work when the plugin NAME has not
289
            mutter('Plugin name %s already loaded', plugin_name)
290
            continue
291
    
2215.4.1 by Alexander Belchenko
Bugfix #68124: Allow plugins import from zip archives.
292
        try:
2610.2.1 by Martin Pool
(Lukas Lalinsky) don't create a duplicate zipimporter, avoiding loading plugins twice
293
            exec "import bzrlib.plugins.%s" % plugin_name in {}
2256.2.2 by Robert Collins
Allow 'import bzrlib.plugins.NAME' to work when the plugin NAME has not
294
            mutter('Load plugin %s from zip %r', plugin_name, zip_name)
295
        except KeyboardInterrupt:
296
            raise
297
        except Exception, e:
298
            ## import pdb; pdb.set_trace()
299
            warning('Unable to load plugin %r from %r'
300
                    % (name, zip_name))
301
            log_exception_quietly()
2432.1.24 by Robert Collins
Add plugins as a help index.
302
303
2762.2.1 by Robert Collins
* ``bzr plugins`` now lists the version number for each plugin in square
304
def plugins():
305
    """Return a dictionary of the plugins.
306
    
307
    Each item in the dictionary is a PlugIn object.
308
    """
309
    result = {}
310
    for name, plugin in _mod_plugins.__dict__.items():
311
        if isinstance(plugin, types.ModuleType):
312
            result[name] = PlugIn(name, plugin)
313
    return result
314
315
2432.1.24 by Robert Collins
Add plugins as a help index.
316
class PluginsHelpIndex(object):
317
    """A help index that returns help topics for plugins."""
318
319
    def __init__(self):
320
        self.prefix = 'plugins/'
321
322
    def get_topics(self, topic):
2432.1.25 by Robert Collins
Return plugin module docstrings for 'bzr help plugin'.
323
        """Search for topic in the loaded plugins.
324
325
        This will not trigger loading of new plugins.
326
327
        :param topic: A topic to search for.
328
        :return: A list which is either empty or contains a single
329
            RegisteredTopic entry.
330
        """
331
        if not topic:
332
            return []
333
        if topic.startswith(self.prefix):
334
            topic = topic[len(self.prefix):]
335
        plugin_module_name = 'bzrlib.plugins.%s' % topic
336
        try:
337
            module = sys.modules[plugin_module_name]
338
        except KeyError:
339
            return []
340
        else:
341
            return [ModuleHelpTopic(module)]
342
343
344
class ModuleHelpTopic(object):
345
    """A help topic which returns the docstring for a module."""
346
347
    def __init__(self, module):
348
        """Constructor.
349
350
        :param module: The module for which help should be generated.
351
        """
352
        self.module = module
353
354
    def get_help_text(self, additional_see_also=None):
355
        """Return a string with the help for this topic.
356
357
        :param additional_see_also: Additional help topics to be
358
            cross-referenced.
359
        """
360
        if not self.module.__doc__:
361
            result = "Plugin '%s' has no docstring.\n" % self.module.__name__
362
        else:
363
            result = self.module.__doc__
364
        if result[-1] != '\n':
365
            result += '\n'
366
        # there is code duplicated here and in bzrlib/help_topic.py's 
367
        # matching Topic code. This should probably be factored in
368
        # to a helper function and a common base class.
369
        if additional_see_also is not None:
370
            see_also = sorted(set(additional_see_also))
371
        else:
372
            see_also = None
373
        if see_also:
374
            result += 'See also: '
375
            result += ', '.join(see_also)
376
            result += '\n'
377
        return result
2432.1.29 by Robert Collins
Add get_help_topic to ModuleHelpTopic.
378
379
    def get_help_topic(self):
2432.1.30 by Robert Collins
Fix the ModuleHelpTopic get_help_topic to be tested with closer to real world data and strip the bzrlib.plugins. prefix from the name.
380
        """Return the modules help topic - its __name__ after bzrlib.plugins.."""
381
        return self.module.__name__[len('bzrlib.plugins.'):]
2762.2.1 by Robert Collins
* ``bzr plugins`` now lists the version number for each plugin in square
382
383
384
class PlugIn(object):
385
    """The bzrlib representation of a plugin.
386
387
    The PlugIn object provides a way to manipulate a given plugin module.
388
    """
389
390
    def __init__(self, name, module):
391
        """Construct a plugin for module."""
392
        self.name = name
393
        self.module = module
394
395
    def path(self):
396
        """Get the path that this plugin was loaded from."""
397
        if getattr(self.module, '__path__', None) is not None:
398
            return os.path.abspath(self.module.__path__[0])
399
        elif getattr(self.module, '__file__', None) is not None:
3193.2.1 by Alexander Belchenko
show path to plugin module as *.py instead of *.pyc if python source available
400
            path = os.path.abspath(self.module.__file__)
401
            if path[-4:] in ('.pyc', '.pyo'):
402
                pypath = path[:-4] + '.py'
403
                if os.path.isfile(pypath):
404
                    path = pypath
405
            return path
2762.2.1 by Robert Collins
* ``bzr plugins`` now lists the version number for each plugin in square
406
        else:
407
            return repr(self.module)
408
409
    def __str__(self):
410
        return "<%s.%s object at %s, name=%s, module=%s>" % (
411
            self.__class__.__module__, self.__class__.__name__, id(self),
412
            self.name, self.module)
413
414
    __repr__ = __str__
415
416
    def test_suite(self):
417
        """Return the plugin's test suite."""
418
        if getattr(self.module, 'test_suite', None) is not None:
419
            return self.module.test_suite()
420
        else:
421
            return None
422
3302.8.21 by Vincent Ladeuil
Fixed as per Robert's review.
423
    def load_plugin_tests(self, loader):
3302.8.10 by Vincent Ladeuil
Prepare bzrlib.plugin to use the new test loader.
424
        """Return the adapted plugin's test suite.
425
426
        :param loader: The custom loader that should be used to load additional
427
            tests.
428
429
        """
430
        if getattr(self.module, 'load_tests', None) is not None:
3302.8.11 by Vincent Ladeuil
Simplify plugin.load_tests.
431
            return loader.loadTestsFromModule(self.module)
3302.8.10 by Vincent Ladeuil
Prepare bzrlib.plugin to use the new test loader.
432
        else:
433
            return None
434
2762.2.1 by Robert Collins
* ``bzr plugins`` now lists the version number for each plugin in square
435
    def version_info(self):
436
        """Return the plugin's version_tuple or None if unknown."""
437
        version_info = getattr(self.module, 'version_info', None)
438
        if version_info is not None and len(version_info) == 3:
439
            version_info = tuple(version_info) + ('final', 0)
440
        return version_info
3302.8.10 by Vincent Ladeuil
Prepare bzrlib.plugin to use the new test loader.
441
2762.2.1 by Robert Collins
* ``bzr plugins`` now lists the version number for each plugin in square
442
    def _get__version__(self):
443
        version_info = self.version_info()
444
        if version_info is None:
445
            return "unknown"
446
        if version_info[3] == 'final':
447
            version_string = '%d.%d.%d' % version_info[:3]
448
        else:
449
            version_string = '%d.%d.%d%s%d' % version_info
450
        return version_string
451
452
    __version__ = property(_get__version__)