~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/plugin.py

  • Committer: Canonical.com Patch Queue Manager
  • Date: 2010-03-25 00:02:51 UTC
  • mfrom: (5106.1.1 version-bump)
  • Revision ID: pqm@pqm.ubuntu.com-20100325000251-bwsv5c5d3l9x3lnn
(Jelmer) Bump API version for 2.2.0.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005-2011 Canonical Ltd
 
1
# Copyright (C) 2005-2010 Canonical Ltd
2
2
#
3
3
# This program is free software; you can redistribute it and/or modify
4
4
# it under the terms of the GNU General Public License as published by
14
14
# along with this program; if not, write to the Free Software
15
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16
16
 
 
17
 
17
18
"""bzr python plugin support.
18
19
 
19
20
When load_plugins() is invoked, any python module in any directory in
29
30
called.
30
31
"""
31
32
 
32
 
from __future__ import absolute_import
33
 
 
34
33
import os
35
34
import sys
36
35
 
37
36
from bzrlib import osutils
38
37
 
39
38
from bzrlib.lazy_import import lazy_import
 
39
 
40
40
lazy_import(globals(), """
41
41
import imp
42
42
import re
49
49
    errors,
50
50
    trace,
51
51
    )
52
 
from bzrlib.i18n import gettext
53
52
from bzrlib import plugins as _mod_plugins
54
53
""")
55
54
 
 
55
from bzrlib.symbol_versioning import (
 
56
    deprecated_function,
 
57
    deprecated_in,
 
58
    )
 
59
 
56
60
 
57
61
DEFAULT_PLUGIN_PATH = None
58
62
_loaded = False
59
63
_plugins_disabled = False
60
64
 
61
65
 
62
 
plugin_warnings = {}
63
 
# Map from plugin name, to list of string warnings about eg plugin
64
 
# dependencies.
65
 
 
66
 
 
67
66
def are_plugins_disabled():
68
67
    return _plugins_disabled
69
68
 
78
77
    load_plugins([])
79
78
 
80
79
 
81
 
def describe_plugins(show_paths=False):
82
 
    """Generate text description of plugins.
83
 
 
84
 
    Includes both those that have loaded, and those that failed to 
85
 
    load.
86
 
 
87
 
    :param show_paths: If true,
88
 
    :returns: Iterator of text lines (including newlines.)
89
 
    """
90
 
    from inspect import getdoc
91
 
    loaded_plugins = plugins()
92
 
    all_names = sorted(list(set(
93
 
        loaded_plugins.keys() + plugin_warnings.keys())))
94
 
    for name in all_names:
95
 
        if name in loaded_plugins:
96
 
            plugin = loaded_plugins[name]
97
 
            version = plugin.__version__
98
 
            if version == 'unknown':
99
 
                version = ''
100
 
            yield '%s %s\n' % (name, version)
101
 
            d = getdoc(plugin.module)
102
 
            if d:
103
 
                doc = d.split('\n')[0]
104
 
            else:
105
 
                doc = '(no description)'
106
 
            yield ("  %s\n" % doc)
107
 
            if show_paths:
108
 
                yield ("   %s\n" % plugin.path())
109
 
            del plugin
110
 
        else:
111
 
            yield "%s (failed to load)\n" % name
112
 
        if name in plugin_warnings:
113
 
            for line in plugin_warnings[name]:
114
 
                yield "  ** " + line + '\n'
115
 
        yield '\n'
116
 
 
117
 
 
118
80
def _strip_trailing_sep(path):
119
81
    return path.rstrip("\\/")
120
82
 
121
83
 
122
 
def _get_specific_plugin_paths(paths):
123
 
    """Returns the plugin paths from a string describing the associations.
124
 
 
125
 
    :param paths: A string describing the paths associated with the plugins.
126
 
 
127
 
    :returns: A list of (plugin name, path) tuples.
128
 
 
129
 
    For example, if paths is my_plugin@/test/my-test:her_plugin@/production/her,
130
 
    [('my_plugin', '/test/my-test'), ('her_plugin', '/production/her')] 
131
 
    will be returned.
132
 
 
133
 
    Note that ':' in the example above depends on the os.
134
 
    """
135
 
    if not paths:
136
 
        return []
137
 
    specs = []
138
 
    for spec in paths.split(os.pathsep):
139
 
        try:
140
 
            name, path = spec.split('@')
141
 
        except ValueError:
142
 
            raise errors.BzrCommandError(gettext(
143
 
                '"%s" is not a valid <plugin_name>@<plugin_path> description ')
144
 
                % spec)
145
 
        specs.append((name, path))
146
 
    return specs
147
 
 
148
 
 
149
84
def set_plugins_path(path=None):
150
85
    """Set the path for plugins to be loaded from.
151
86
 
163
98
        for name in disabled_plugins.split(os.pathsep):
164
99
            PluginImporter.blacklist.add('bzrlib.plugins.' + name)
165
100
    # Set up a the specific paths for plugins
166
 
    for plugin_name, plugin_path in _get_specific_plugin_paths(os.environ.get(
167
 
            'BZR_PLUGINS_AT', None)):
 
101
    specific_plugins = os.environ.get('BZR_PLUGINS_AT', None)
 
102
    if specific_plugins is not None:
 
103
        for spec in specific_plugins.split(os.pathsep):
 
104
            plugin_name, plugin_path = spec.split('@')
168
105
            PluginImporter.specific_paths[
169
106
                'bzrlib.plugins.%s' % plugin_name] = plugin_path
170
107
    return path
274
211
    """Load bzrlib plugins.
275
212
 
276
213
    The environment variable BZR_PLUGIN_PATH is considered a delimited
277
 
    set of paths to look through. Each entry is searched for `*.py`
 
214
    set of paths to look through. Each entry is searched for *.py
278
215
    files (and whatever other extensions are used in the platform,
279
 
    such as `*.pyd`).
 
216
    such as *.pyd).
280
217
 
281
218
    load_from_path() provides the underlying mechanism and is called with
282
219
    the default directory list to provide the normal behaviour.
365
302
    return None, None, (None, None, None)
366
303
 
367
304
 
368
 
def record_plugin_warning(plugin_name, warning_message):
369
 
    trace.mutter(warning_message)
370
 
    plugin_warnings.setdefault(plugin_name, []).append(warning_message)
371
 
 
372
 
 
373
305
def _load_plugin_module(name, dir):
374
 
    """Load plugin name from dir.
 
306
    """Load plugine name from dir.
375
307
 
376
308
    :param name: The plugin name in the bzrlib.plugins namespace.
377
309
    :param dir: The directory the plugin is loaded from for error messages.
383
315
    except KeyboardInterrupt:
384
316
        raise
385
317
    except errors.IncompatibleAPI, e:
386
 
        warning_message = (
387
 
            "Unable to load plugin %r. It requested API version "
 
318
        trace.warning("Unable to load plugin %r. It requested API version "
388
319
            "%s of module %s but the minimum exported version is %s, and "
389
320
            "the maximum is %s" %
390
321
            (name, e.wanted, e.api, e.minimum, e.current))
391
 
        record_plugin_warning(name, warning_message)
392
322
    except Exception, e:
393
323
        trace.warning("%s" % e)
394
324
        if re.search('\.|-| ', name):
399
329
                    "file path isn't a valid module name; try renaming "
400
330
                    "it to %r." % (name, dir, sanitised_name))
401
331
        else:
402
 
            record_plugin_warning(
403
 
                name,
404
 
                'Unable to load plugin %r from %r' % (name, dir))
 
332
            trace.warning('Unable to load plugin %r from %r' % (name, dir))
405
333
        trace.log_exception_quietly()
406
334
        if 'error' in debug.debug_flags:
407
335
            trace.print_exception(sys.exc_info(), sys.stderr)
447
375
    return result
448
376
 
449
377
 
450
 
def format_concise_plugin_list():
451
 
    """Return a string holding a concise list of plugins and their version.
452
 
    """
453
 
    items = []
454
 
    for name, a_plugin in sorted(plugins().items()):
455
 
        items.append("%s[%s]" %
456
 
            (name, a_plugin.__version__))
457
 
    return ', '.join(items)
458
 
 
459
 
 
460
 
 
461
378
class PluginsHelpIndex(object):
462
379
    """A help index that returns help topics for plugins."""
463
380
 
508
425
            result = self.module.__doc__
509
426
        if result[-1] != '\n':
510
427
            result += '\n'
511
 
        from bzrlib import help_topics
512
 
        result += help_topics._format_see_also(additional_see_also)
 
428
        # there is code duplicated here and in bzrlib/help_topic.py's
 
429
        # matching Topic code. This should probably be factored in
 
430
        # to a helper function and a common base class.
 
431
        if additional_see_also is not None:
 
432
            see_also = sorted(set(additional_see_also))
 
433
        else:
 
434
            see_also = None
 
435
        if see_also:
 
436
            result += 'See also: '
 
437
            result += ', '.join(see_also)
 
438
            result += '\n'
513
439
        return result
514
440
 
515
441
    def get_help_topic(self):
516
 
        """Return the module help topic: its basename."""
 
442
        """Return the modules help topic - its __name__ after bzrlib.plugins.."""
517
443
        return self.module.__name__[len('bzrlib.plugins.'):]
518
444
 
519
445
 
632
558
        return None
633
559
 
634
560
    def load_module(self, fullname):
635
 
        """Load a plugin from a specific directory (or file)."""
 
561
        """Load a plugin from a specific directory."""
636
562
        # We are called only for specific paths
637
 
        plugin_path = self.specific_paths[fullname]
638
 
        loading_path = None
639
 
        if os.path.isdir(plugin_path):
640
 
            for suffix, mode, kind in imp.get_suffixes():
641
 
                if kind not in (imp.PY_SOURCE, imp.PY_COMPILED):
642
 
                    # We don't recognize compiled modules (.so, .dll, etc)
643
 
                    continue
644
 
                init_path = osutils.pathjoin(plugin_path, '__init__' + suffix)
645
 
                if os.path.isfile(init_path):
646
 
                    # We've got a module here and load_module needs specific
647
 
                    # parameters.
648
 
                    loading_path = plugin_path
649
 
                    suffix = ''
650
 
                    mode = ''
651
 
                    kind = imp.PKG_DIRECTORY
652
 
                    break
653
 
        else:
654
 
            for suffix, mode, kind in imp.get_suffixes():
655
 
                if plugin_path.endswith(suffix):
656
 
                    loading_path = plugin_path
657
 
                    break
658
 
        if loading_path is None:
 
563
        plugin_dir = self.specific_paths[fullname]
 
564
        candidate = None
 
565
        maybe_package = False
 
566
        for p in os.listdir(plugin_dir):
 
567
            if os.path.isdir(osutils.pathjoin(plugin_dir, p)):
 
568
                # We're searching for files only and don't want submodules to
 
569
                # be recognized as plugins (they are submodules inside the
 
570
                # plugin).
 
571
                continue
 
572
            name, path, (
 
573
                suffix, mode, kind) = _find_plugin_module(plugin_dir, p)
 
574
            if name is not None:
 
575
                candidate = (name, path, suffix, mode, kind)
 
576
                if kind == imp.PY_SOURCE:
 
577
                    # We favour imp.PY_SOURCE (which will use the compiled
 
578
                    # version if available) over imp.PY_COMPILED (which is used
 
579
                    # only if the source is not available)
 
580
                    break
 
581
        if candidate is None:
659
582
            raise ImportError('%s cannot be loaded from %s'
660
 
                              % (fullname, plugin_path))
661
 
        if kind is imp.PKG_DIRECTORY:
662
 
            f = None
663
 
        else:
664
 
            f = open(loading_path, mode)
 
583
                              % (fullname, plugin_dir))
 
584
        f = open(path, mode)
665
585
        try:
666
 
            mod = imp.load_module(fullname, f, loading_path,
667
 
                                  (suffix, mode, kind))
 
586
            mod = imp.load_module(fullname, f, path, (suffix, mode, kind))
 
587
            # The plugin can contain modules, so be ready
 
588
            mod.__path__ = [plugin_dir]
668
589
            mod.__package__ = fullname
669
590
            return mod
670
591
        finally:
671
 
            if f is not None:
672
 
                f.close()
 
592
            f.close()
673
593
 
674
594
 
675
595
# Install a dedicated importer for plugins requiring special handling