/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 breezy/plugin.py

  • Committer: Breezy landing bot
  • Author(s): Jelmer Vernooij
  • Date: 2018-08-20 22:50:42 UTC
  • mfrom: (7067.3.1 fix-spurious)
  • Revision ID: breezy.the.bot@gmail.com-20180820225042-oqt9y92xn3az7drc
Fix a spuriously failing test.

Merged from https://code.launchpad.net/~jelmer/brz/fix-spurious/+merge/353457

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2004, 2005 by Canonical Ltd
2
 
 
 
1
# Copyright (C) 2005-2011 Canonical Ltd, 2017 Breezy developers
 
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
5
5
# the Free Software Foundation; either version 2 of the License, or
6
6
# (at your option) any later version.
7
 
 
 
7
#
8
8
# This program is distributed in the hope that it will be useful,
9
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11
11
# GNU General Public License for more details.
12
 
 
 
12
#
13
13
# You should have received a copy of the GNU General Public License
14
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
 
Any python module in $BZR_PLUGIN_PATH will be imported upon initialization of
21
 
bzrlib. The module will be imported as 'bzrlib.plugins.$BASENAME(PLUGIN)'.
22
 
In the plugin's main body, it should update any bzrlib registries it wants to
23
 
extend; for example, to add new commands, import bzrlib.commands and add your
24
 
new command to the plugin_cmds variable.
 
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 
16
 
 
17
"""Breezy plugin support.
 
18
 
 
19
Which plugins to load can be configured by setting these environment variables:
 
20
 
 
21
- BRZ_PLUGIN_PATH: Paths to look for plugins in.
 
22
- BRZ_DISABLE_PLUGINS: Plugin names to block from being loaded.
 
23
- BRZ_PLUGINS_AT: Name and paths for plugins to load from specific locations.
 
24
 
 
25
The interfaces this module exports include:
 
26
 
 
27
- disable_plugins: Load no plugins and stop future automatic loading.
 
28
- load_plugins: Load all plugins that can be found in configuration.
 
29
- describe_plugins: Generate text for each loaded (or failed) plugin.
 
30
- extend_path: Mechanism by which the plugins package path is set.
 
31
- plugin_name: Gives unprefixed name of a plugin module.
 
32
 
 
33
See the plugin-api developer documentation for information about writing
 
34
plugins.
25
35
"""
26
36
 
27
 
# TODO: Refactor this to make it more testable.  The main problem at the
28
 
# moment is that loading plugins affects the global process state -- for bzr
29
 
# in general use it's a reasonable assumption that all plugins are loaded at
30
 
# startup and then stay loaded, but this is less good for testing.
31
 
32
 
# Several specific issues:
33
 
#  - plugins can't be unloaded and will continue to effect later tests
34
 
#  - load_plugins does nothing if called a second time
35
 
#  - plugin hooks can't be removed
36
 
#
37
 
# Our options are either to remove these restrictions, or work around them by
38
 
# loading the plugins into a different space than the one running the tests.
39
 
# That could be either a separate Python interpreter or perhaps a new
40
 
# namespace inside this interpreter.
 
37
from __future__ import absolute_import
41
38
 
42
 
import imp
43
39
import os
 
40
import re
44
41
import sys
45
 
import types
46
 
 
47
 
import bzrlib
48
 
from bzrlib.config import config_dir
49
 
from bzrlib.trace import log_error, mutter, log_exception, warning, \
50
 
        log_exception_quietly
51
 
from bzrlib.errors import BzrError
52
 
from bzrlib import plugins
53
 
from bzrlib.osutils import pathjoin
54
 
 
55
 
DEFAULT_PLUGIN_PATH = pathjoin(config_dir(), 'plugins')
56
 
 
57
 
_loaded = False
58
 
 
59
 
 
60
 
def all_plugins():
61
 
    """Return a dictionary of the plugins."""
62
 
    result = {}
63
 
    for name, plugin in bzrlib.plugins.__dict__.items():
64
 
        if isinstance(plugin, types.ModuleType):
65
 
            result[name] = plugin
66
 
    return result
67
 
 
68
 
 
69
 
def disable_plugins():
 
42
 
 
43
import breezy
 
44
from . import osutils
 
45
 
 
46
from .lazy_import import lazy_import
 
47
lazy_import(globals(), """
 
48
import imp
 
49
import importlib
 
50
from importlib import util as importlib_util
 
51
 
 
52
from breezy import (
 
53
    config,
 
54
    debug,
 
55
    errors,
 
56
    help_topics,
 
57
    trace,
 
58
    )
 
59
""")
 
60
 
 
61
 
 
62
_MODULE_PREFIX = "breezy.plugins."
 
63
 
 
64
if __debug__ or sys.version_info > (3,):
 
65
    COMPILED_EXT = ".pyc"
 
66
else:
 
67
    COMPILED_EXT = ".pyo"
 
68
 
 
69
 
 
70
def disable_plugins(state=None):
70
71
    """Disable loading plugins.
71
72
 
72
73
    Future calls to load_plugins() will be ignored.
 
74
 
 
75
    :param state: The library state object that records loaded plugins.
73
76
    """
74
 
    # TODO: jam 20060131 This should probably also disable
75
 
    #       load_from_dirs()
76
 
    global _loaded
77
 
    _loaded = True
78
 
 
79
 
 
80
 
def load_plugins():
81
 
    """Load bzrlib plugins.
82
 
 
83
 
    The environment variable BZR_PLUGIN_PATH is considered a delimited
84
 
    set of paths to look through. Each entry is searched for *.py
 
77
    if state is None:
 
78
        state = breezy.get_global_state()
 
79
    state.plugins = {}
 
80
 
 
81
 
 
82
def load_plugins(path=None, state=None):
 
83
    """Load breezy plugins.
 
84
 
 
85
    The environment variable BRZ_PLUGIN_PATH is considered a delimited
 
86
    set of paths to look through. Each entry is searched for `*.py`
85
87
    files (and whatever other extensions are used in the platform,
86
 
    such as *.pyd).
 
88
    such as `*.pyd`).
87
89
 
88
 
    load_from_dirs() provides the underlying mechanism and is called with
89
 
    the default directory list to provide the normal behaviour.
 
90
    :param path: The list of paths to search for plugins.  By default,
 
91
        it is populated from the __path__ of the breezy.plugins package.
 
92
    :param state: The library state object that records loaded plugins.
90
93
    """
91
 
    global _loaded
92
 
    if _loaded:
 
94
    if state is None:
 
95
        state = breezy.get_global_state()
 
96
    if getattr(state, 'plugins', None) is not None:
93
97
        # People can make sure plugins are loaded, they just won't be twice
94
98
        return
95
 
        #raise BzrError("plugins already initialized")
96
 
    _loaded = True
97
 
 
98
 
    dirs = os.environ.get('BZR_PLUGIN_PATH', DEFAULT_PLUGIN_PATH).split(os.pathsep)
99
 
    dirs.insert(0, os.path.dirname(plugins.__file__))
100
 
 
101
 
    load_from_dirs(dirs)
102
 
 
103
 
 
104
 
def load_from_dirs(dirs):
105
 
    """Load bzrlib plugins found in each dir in dirs.
106
 
 
107
 
    Loading a plugin means importing it into the python interpreter.
108
 
    The plugin is expected to make calls to register commands when
109
 
    it's loaded (or perhaps access other hooks in future.)
110
 
 
111
 
    Plugins are loaded into bzrlib.plugins.NAME, and can be found there
112
 
    for future reference.
113
 
    """
114
 
    # The problem with imp.get_suffixes() is that it doesn't include
115
 
    # .pyo which is technically valid
116
 
    # It also means that "testmodule.so" will show up as both test and testmodule
117
 
    # though it is only valid as 'test'
118
 
    # but you should be careful, because "testmodule.py" loads as testmodule.
119
 
    suffixes = imp.get_suffixes()
120
 
    suffixes.append(('.pyo', 'rb', imp.PY_COMPILED))
121
 
    package_entries = ['__init__.py', '__init__.pyc', '__init__.pyo']
122
 
    for d in dirs:
123
 
        if not d:
124
 
            continue
125
 
        mutter('looking for plugins in %s', d)
126
 
        plugin_names = set()
127
 
        if not os.path.isdir(d):
128
 
            continue
129
 
        for f in os.listdir(d):
130
 
            path = pathjoin(d, f)
 
99
 
 
100
    if path is None:
 
101
        # Calls back into extend_path() here
 
102
        from breezy.plugins import __path__ as path
 
103
 
 
104
    state.plugin_warnings = {}
 
105
    _load_plugins(state, path)
 
106
    state.plugins = plugins()
 
107
 
 
108
 
 
109
def plugin_name(module_name):
 
110
    """Gives unprefixed name from module_name or None."""
 
111
    if module_name.startswith(_MODULE_PREFIX):
 
112
        parts = module_name.split(".")
 
113
        if len(parts) > 2:
 
114
            return parts[2]
 
115
    return None
 
116
 
 
117
 
 
118
def extend_path(path, name):
 
119
    """Helper so breezy.plugins can be a sort of namespace package.
 
120
 
 
121
    To be used in similar fashion to pkgutil.extend_path:
 
122
 
 
123
        from breezy.plugins import extend_path
 
124
        __path__ = extend_path(__path__, __name__)
 
125
 
 
126
    Inspects the BRZ_PLUGIN* envvars, sys.path, and the filesystem to find
 
127
    plugins. May mutate sys.modules in order to block plugin loading, and may
 
128
    append a new meta path finder to sys.meta_path for plugins@ loading.
 
129
 
 
130
    Returns a list of paths to import from, as an enhanced object that also
 
131
    contains details of the other configuration used.
 
132
    """
 
133
    blocks = _env_disable_plugins()
 
134
    _block_plugins(blocks)
 
135
 
 
136
    extra_details = _env_plugins_at()
 
137
    _install_importer_if_needed(extra_details)
 
138
 
 
139
    paths = _iter_plugin_paths(_env_plugin_path(), path)
 
140
 
 
141
    return _Path(name, blocks, extra_details, paths)
 
142
 
 
143
 
 
144
class _Path(list):
 
145
    """List type to use as __path__ but containing additional details.
 
146
 
 
147
    Python 3 allows any iterable for __path__ but Python 2 is more fussy.
 
148
    """
 
149
 
 
150
    def __init__(self, package_name, blocked, extra, paths):
 
151
        super(_Path, self).__init__(paths)
 
152
        self.package_name = package_name
 
153
        self.blocked_names = blocked
 
154
        self.extra_details = extra
 
155
 
 
156
    def __repr__(self):
 
157
        return "%s(%r, %r, %r, %s)" % (
 
158
            self.__class__.__name__, self.package_name, self.blocked_names,
 
159
            self.extra_details, list.__repr__(self))
 
160
 
 
161
 
 
162
def _expect_identifier(name, env_key, env_value):
 
163
    """Validate given name from envvar is usable as a Python identifier.
 
164
 
 
165
    Returns the name as a native str, or None if it was invalid.
 
166
 
 
167
    Per PEP 3131 this is no longer strictly correct for Python 3, but as MvL
 
168
    didn't include a neat way to check except eval, this enforces ascii.
 
169
    """
 
170
    if re.match(r"^[A-Za-z_][A-Za-z0-9_]*$", name) is None:
 
171
        trace.warning("Invalid name '%s' in %s='%s'", name, env_key, env_value)
 
172
        return None
 
173
    return str(name)
 
174
 
 
175
 
 
176
def _env_disable_plugins(key='BRZ_DISABLE_PLUGINS'):
 
177
    """Gives list of names for plugins to disable from environ key."""
 
178
    disabled_names = []
 
179
    env = osutils.path_from_environ(key)
 
180
    if env:
 
181
        for name in env.split(os.pathsep):
 
182
            name = _expect_identifier(name, key, env)
 
183
            if name is not None:
 
184
                disabled_names.append(name)
 
185
    return disabled_names
 
186
 
 
187
 
 
188
def _env_plugins_at(key='BRZ_PLUGINS_AT'):
 
189
    """Gives list of names and paths of specific plugins from environ key."""
 
190
    plugin_details = []
 
191
    env = osutils.path_from_environ(key)
 
192
    if env:
 
193
        for pair in env.split(os.pathsep):
 
194
            if '@' in pair:
 
195
                name, path = pair.split('@', 1)
 
196
            else:
 
197
                path = pair
 
198
                name = osutils.basename(path).split('.', 1)[0]
 
199
            name = _expect_identifier(name, key, env)
 
200
            if name is not None:
 
201
                plugin_details.append((name, path))
 
202
    return plugin_details
 
203
 
 
204
 
 
205
def _env_plugin_path(key='BRZ_PLUGIN_PATH'):
 
206
    """Gives list of paths and contexts for plugins from environ key.
 
207
 
 
208
    Each entry is either a specific path to load plugins from and the value
 
209
    'path', or None and one of the three values 'user', 'core', 'site'.
 
210
    """
 
211
    path_details = []
 
212
    env = osutils.path_from_environ(key)
 
213
    defaults = {"user": not env, "core": True, "site": True}
 
214
    if env:
 
215
        # Add paths specified by user in order
 
216
        for p in env.split(os.pathsep):
 
217
            flag, name = p[:1], p[1:]
 
218
            if flag in ("+", "-") and name in defaults:
 
219
                if flag == "+" and defaults[name] is not None:
 
220
                    path_details.append((None, name))
 
221
                defaults[name] = None
 
222
            else:
 
223
                path_details.append((p, 'path'))
 
224
 
 
225
    # Add any remaining default paths
 
226
    for name in ('user', 'core', 'site'):
 
227
        if defaults[name]:
 
228
            path_details.append((None, name))
 
229
 
 
230
    return path_details
 
231
 
 
232
 
 
233
def _iter_plugin_paths(paths_from_env, core_paths):
 
234
    """Generate paths using paths_from_env and core_paths."""
 
235
    # GZ 2017-06-02: This is kinda horrid, should make better.
 
236
    for path, context in paths_from_env:
 
237
        if context == 'path':
 
238
            yield path
 
239
        elif context == 'user':
 
240
            path = get_user_plugin_path()
131
241
            if os.path.isdir(path):
132
 
                for entry in package_entries:
133
 
                    # This directory should be a package, and thus added to
134
 
                    # the list
135
 
                    if os.path.isfile(pathjoin(path, entry)):
136
 
                        break
137
 
                else: # This directory is not a package
138
 
                    continue
139
 
            else:
140
 
                for suffix_info in suffixes:
141
 
                    if f.endswith(suffix_info[0]):
142
 
                        f = f[:-len(suffix_info[0])]
143
 
                        if suffix_info[2] == imp.C_EXTENSION and f.endswith('module'):
144
 
                            f = f[:-len('module')]
145
 
                        break
146
 
                else:
147
 
                    continue
148
 
            if getattr(bzrlib.plugins, f, None):
149
 
                mutter('Plugin name %s already loaded', f)
150
 
            else:
151
 
                mutter('add plugin name %s', f)
152
 
                plugin_names.add(f)
153
 
 
154
 
        plugin_names = list(plugin_names)
155
 
        plugin_names.sort()
156
 
        for name in plugin_names:
 
242
                yield path
 
243
        elif context == 'core':
 
244
            for path in _get_core_plugin_paths(core_paths):
 
245
                yield path
 
246
        elif context == 'site':
 
247
            for path in _get_site_plugin_paths(sys.path):
 
248
                if os.path.isdir(path):
 
249
                    yield path
 
250
 
 
251
 
 
252
def _install_importer_if_needed(plugin_details):
 
253
    """Install a meta path finder to handle plugin_details if any."""
 
254
    if plugin_details:
 
255
        finder = _PluginsAtFinder(_MODULE_PREFIX, plugin_details)
 
256
        # For Python 3, must insert before default PathFinder to override.
 
257
        sys.meta_path.insert(2, finder)
 
258
 
 
259
 
 
260
def _load_plugins(state, paths):
 
261
    """Do the importing all plugins from paths."""
 
262
    imported_names = set()
 
263
    for name, path in _iter_possible_plugins(paths):
 
264
        if name not in imported_names:
 
265
            msg = _load_plugin_module(name, path)
 
266
            if msg is not None:
 
267
                state.plugin_warnings.setdefault(name, []).append(msg)
 
268
            imported_names.add(name)
 
269
 
 
270
 
 
271
def _block_plugins(names):
 
272
    """Add names to sys.modules to block future imports."""
 
273
    for name in names:
 
274
        package_name = _MODULE_PREFIX + name
 
275
        if sys.modules.get(package_name) is not None:
 
276
            trace.mutter("Blocked plugin %s already loaded.", name)
 
277
        sys.modules[package_name] = None
 
278
 
 
279
 
 
280
def _get_package_init(package_path):
 
281
    """Get path of __init__ file from package_path or None if not a package."""
 
282
    init_path = osutils.pathjoin(package_path, "__init__.py")
 
283
    if os.path.exists(init_path):
 
284
        return init_path
 
285
    init_path = init_path[:-3] + COMPILED_EXT
 
286
    if os.path.exists(init_path):
 
287
        return init_path
 
288
    return None
 
289
 
 
290
 
 
291
def _iter_possible_plugins(plugin_paths):
 
292
    """Generate names and paths of possible plugins from plugin_paths."""
 
293
    # Inspect any from BRZ_PLUGINS_AT first.
 
294
    for name, path in getattr(plugin_paths, "extra_details", ()):
 
295
        yield name, path
 
296
    # Then walk over files and directories in the paths from the package.
 
297
    for path in plugin_paths:
 
298
        if os.path.isfile(path):
 
299
            if path.endswith(".zip"):
 
300
                trace.mutter("Don't yet support loading plugins from zip.")
 
301
        else:
 
302
            for name, path in _walk_modules(path):
 
303
                yield name, path
 
304
 
 
305
 
 
306
def _walk_modules(path):
 
307
    """Generate name and path of modules and packages on path."""
 
308
    for root, dirs, files in os.walk(path):
 
309
        files.sort()
 
310
        for f in files:
 
311
            if f[:2] != "__":
 
312
                if f.endswith((".py", COMPILED_EXT)):
 
313
                    yield f.rsplit(".", 1)[0], root
 
314
        dirs.sort()
 
315
        for d in dirs:
 
316
            if d[:2] != "__":
 
317
                package_dir = osutils.pathjoin(root, d)
 
318
                fullpath = _get_package_init(package_dir)
 
319
                if fullpath is not None:
 
320
                    yield d, package_dir
 
321
        # Don't descend into subdirectories
 
322
        del dirs[:]
 
323
 
 
324
 
 
325
def describe_plugins(show_paths=False, state=None):
 
326
    """Generate text description of plugins.
 
327
 
 
328
    Includes both those that have loaded, and those that failed to load.
 
329
 
 
330
    :param show_paths: If true, include the plugin path.
 
331
    :param state: The library state object to inspect.
 
332
    :returns: Iterator of text lines (including newlines.)
 
333
    """
 
334
    if state is None:
 
335
        state = breezy.get_global_state()
 
336
    loaded_plugins = getattr(state, 'plugins', {})
 
337
    plugin_warnings = set(getattr(state, 'plugin_warnings', []))
 
338
    all_names = sorted(set(loaded_plugins.keys()).union(plugin_warnings))
 
339
    for name in all_names:
 
340
        if name in loaded_plugins:
 
341
            plugin = loaded_plugins[name]
 
342
            version = plugin.__version__
 
343
            if version == 'unknown':
 
344
                version = ''
 
345
            yield '%s %s\n' % (name, version)
 
346
            d = plugin.module.__doc__
 
347
            if d:
 
348
                doc = d.split('\n')[0]
 
349
            else:
 
350
                doc = '(no description)'
 
351
            yield ("  %s\n" % doc)
 
352
            if show_paths:
 
353
                yield ("   %s\n" % plugin.path())
 
354
        else:
 
355
            yield "%s (failed to load)\n" % name
 
356
        if name in state.plugin_warnings:
 
357
            for line in state.plugin_warnings[name]:
 
358
                yield "  ** " + line + '\n'
 
359
        yield '\n'
 
360
 
 
361
 
 
362
def _get_core_plugin_paths(existing_paths):
 
363
    """Generate possible locations for plugins based on existing_paths."""
 
364
    if getattr(sys, 'frozen', False):
 
365
        # We need to use relative path to system-wide plugin
 
366
        # directory because breezy from standalone brz.exe
 
367
        # could be imported by another standalone program
 
368
        # (e.g. brz-config; or TortoiseBzr/Olive if/when they
 
369
        # will become standalone exe). [bialix 20071123]
 
370
        # __file__ typically is
 
371
        # C:\Program Files\Bazaar\lib\library.zip\breezy\plugin.pyc
 
372
        # then plugins directory is
 
373
        # C:\Program Files\Bazaar\plugins
 
374
        # so relative path is ../../../plugins
 
375
        yield osutils.abspath(osutils.pathjoin(
 
376
            osutils.dirname(__file__), '../../../plugins'))
 
377
    else:     # don't look inside library.zip
 
378
        for path in existing_paths:
 
379
            yield path
 
380
 
 
381
 
 
382
def _get_site_plugin_paths(sys_paths):
 
383
    """Generate possible locations for plugins from given sys_paths."""
 
384
    for path in sys_paths:
 
385
        if os.path.basename(path) in ('dist-packages', 'site-packages'):
 
386
            yield osutils.pathjoin(path, 'breezy', 'plugins')
 
387
 
 
388
 
 
389
def get_user_plugin_path():
 
390
    return osutils.pathjoin(config.config_dir(), 'plugins')
 
391
 
 
392
 
 
393
def record_plugin_warning(warning_message):
 
394
    trace.mutter(warning_message)
 
395
    return warning_message
 
396
 
 
397
 
 
398
def _load_plugin_module(name, dir):
 
399
    """Load plugin by name.
 
400
 
 
401
    :param name: The plugin name in the breezy.plugins namespace.
 
402
    :param dir: The directory the plugin is loaded from for error messages.
 
403
    """
 
404
    if _MODULE_PREFIX + name in sys.modules:
 
405
        return
 
406
    try:
 
407
        __import__(_MODULE_PREFIX + name)
 
408
    except errors.IncompatibleVersion as e:
 
409
        warning_message = (
 
410
            "Unable to load plugin %r. It supports %s "
 
411
            "versions %r but the current version is %s" %
 
412
            (name, e.api.__name__, e.wanted, e.current))
 
413
        return record_plugin_warning(warning_message)
 
414
    except Exception as e:
 
415
        trace.log_exception_quietly()
 
416
        if 'error' in debug.debug_flags:
 
417
            trace.print_exception(sys.exc_info(), sys.stderr)
 
418
        # GZ 2017-06-02: Move this name checking up a level, no point trying
 
419
        # to import things with bad names.
 
420
        if re.search('\\.|-| ', name):
 
421
            sanitised_name = re.sub('[-. ]', '_', name)
 
422
            if sanitised_name.startswith('brz_'):
 
423
                sanitised_name = sanitised_name[len('brz_'):]
 
424
            trace.warning("Unable to load %r in %r as a plugin because the "
 
425
                    "file path isn't a valid module name; try renaming "
 
426
                    "it to %r." % (name, dir, sanitised_name))
 
427
        else:
 
428
            return record_plugin_warning(
 
429
                'Unable to load plugin %r from %r: %s' % (name, dir, e))
 
430
 
 
431
 
 
432
def plugins():
 
433
    """Return a dictionary of the plugins.
 
434
 
 
435
    Each item in the dictionary is a PlugIn object.
 
436
    """
 
437
    result = {}
 
438
    for fullname in sys.modules:
 
439
        if fullname.startswith(_MODULE_PREFIX):
 
440
            name = fullname[len(_MODULE_PREFIX):]
 
441
            if not "." in name and sys.modules[fullname] is not None:
 
442
                result[name] = PlugIn(name, sys.modules[fullname])
 
443
    return result
 
444
 
 
445
 
 
446
def get_loaded_plugin(name):
 
447
    """Retrieve an already loaded plugin.
 
448
 
 
449
    Returns None if there is no such plugin loaded
 
450
    """
 
451
    try:
 
452
        module = sys.modules[_MODULE_PREFIX + name]
 
453
    except KeyError:
 
454
        return None
 
455
    if module is None:
 
456
        return None
 
457
    return PlugIn(name, module)
 
458
 
 
459
 
 
460
def format_concise_plugin_list(state=None):
 
461
    """Return a string holding a concise list of plugins and their version.
 
462
    """
 
463
    if state is None:
 
464
        state = breezy.get_global_state()
 
465
    items = []
 
466
    for name, a_plugin in sorted(getattr(state, 'plugins', {}).items()):
 
467
        items.append("%s[%s]" %
 
468
            (name, a_plugin.__version__))
 
469
    return ', '.join(items)
 
470
 
 
471
 
 
472
class PluginsHelpIndex(object):
 
473
    """A help index that returns help topics for plugins."""
 
474
 
 
475
    def __init__(self):
 
476
        self.prefix = 'plugins/'
 
477
 
 
478
    def get_topics(self, topic):
 
479
        """Search for topic in the loaded plugins.
 
480
 
 
481
        This will not trigger loading of new plugins.
 
482
 
 
483
        :param topic: A topic to search for.
 
484
        :return: A list which is either empty or contains a single
 
485
            RegisteredTopic entry.
 
486
        """
 
487
        if not topic:
 
488
            return []
 
489
        if topic.startswith(self.prefix):
 
490
            topic = topic[len(self.prefix):]
 
491
        plugin_module_name = _MODULE_PREFIX + topic
 
492
        try:
 
493
            module = sys.modules[plugin_module_name]
 
494
        except KeyError:
 
495
            return []
 
496
        else:
 
497
            return [ModuleHelpTopic(module)]
 
498
 
 
499
 
 
500
class ModuleHelpTopic(object):
 
501
    """A help topic which returns the docstring for a module."""
 
502
 
 
503
    def __init__(self, module):
 
504
        """Constructor.
 
505
 
 
506
        :param module: The module for which help should be generated.
 
507
        """
 
508
        self.module = module
 
509
 
 
510
    def get_help_text(self, additional_see_also=None, verbose=True):
 
511
        """Return a string with the help for this topic.
 
512
 
 
513
        :param additional_see_also: Additional help topics to be
 
514
            cross-referenced.
 
515
        """
 
516
        if not self.module.__doc__:
 
517
            result = "Plugin '%s' has no docstring.\n" % self.module.__name__
 
518
        else:
 
519
            result = self.module.__doc__
 
520
        if result[-1] != '\n':
 
521
            result += '\n'
 
522
        result += help_topics._format_see_also(additional_see_also)
 
523
        return result
 
524
 
 
525
    def get_help_topic(self):
 
526
        """Return the module help topic: its basename."""
 
527
        return self.module.__name__[len(_MODULE_PREFIX):]
 
528
 
 
529
 
 
530
class PlugIn(object):
 
531
    """The breezy representation of a plugin.
 
532
 
 
533
    The PlugIn object provides a way to manipulate a given plugin module.
 
534
    """
 
535
 
 
536
    def __init__(self, name, module):
 
537
        """Construct a plugin for module."""
 
538
        self.name = name
 
539
        self.module = module
 
540
 
 
541
    def path(self):
 
542
        """Get the path that this plugin was loaded from."""
 
543
        if getattr(self.module, '__path__', None) is not None:
 
544
            return os.path.abspath(self.module.__path__[0])
 
545
        elif getattr(self.module, '__file__', None) is not None:
 
546
            path = os.path.abspath(self.module.__file__)
 
547
            if path[-4:] == COMPILED_EXT:
 
548
                pypath = path[:-4] + '.py'
 
549
                if os.path.isfile(pypath):
 
550
                    path = pypath
 
551
            return path
 
552
        else:
 
553
            return repr(self.module)
 
554
 
 
555
    def __repr__(self):
 
556
        return "<%s.%s name=%s, module=%s>" % (
 
557
            self.__class__.__module__, self.__class__.__name__,
 
558
            self.name, self.module)
 
559
 
 
560
    def test_suite(self):
 
561
        """Return the plugin's test suite."""
 
562
        if getattr(self.module, 'test_suite', None) is not None:
 
563
            return self.module.test_suite()
 
564
        else:
 
565
            return None
 
566
 
 
567
    def load_plugin_tests(self, loader):
 
568
        """Return the adapted plugin's test suite.
 
569
 
 
570
        :param loader: The custom loader that should be used to load additional
 
571
            tests.
 
572
        """
 
573
        if getattr(self.module, 'load_tests', None) is not None:
 
574
            return loader.loadTestsFromModule(self.module)
 
575
        else:
 
576
            return None
 
577
 
 
578
    def version_info(self):
 
579
        """Return the plugin's version_tuple or None if unknown."""
 
580
        version_info = getattr(self.module, 'version_info', None)
 
581
        if version_info is not None:
157
582
            try:
158
 
                plugin_info = imp.find_module(name, [d])
159
 
                mutter('load plugin %r', plugin_info)
160
 
                try:
161
 
                    plugin = imp.load_module('bzrlib.plugins.' + name,
162
 
                                             *plugin_info)
163
 
                    setattr(bzrlib.plugins, name, plugin)
164
 
                finally:
165
 
                    if plugin_info[0] is not None:
166
 
                        plugin_info[0].close()
167
 
 
168
 
                mutter('loaded succesfully')
169
 
            except KeyboardInterrupt:
170
 
                raise
171
 
            except Exception, e:
172
 
                ## import pdb; pdb.set_trace()
173
 
                warning('Unable to load plugin %r from %r' % (name, d))
174
 
                log_exception_quietly()
 
583
                if isinstance(version_info, str):
 
584
                    version_info = version_info.split('.')
 
585
                elif len(version_info) == 3:
 
586
                    version_info = tuple(version_info) + ('final', 0)
 
587
            except TypeError:
 
588
                # The given version_info isn't even iteratible
 
589
                trace.log_exception_quietly()
 
590
                version_info = (version_info,)
 
591
        return version_info
 
592
 
 
593
    @property
 
594
    def __version__(self):
 
595
        version_info = self.version_info()
 
596
        if version_info is None or len(version_info) == 0:
 
597
            return "unknown"
 
598
        try:
 
599
            version_string = breezy._format_version_tuple(version_info)
 
600
        except (ValueError, TypeError, IndexError):
 
601
            trace.log_exception_quietly()
 
602
            # Try to show something for the version anyway
 
603
            version_string = '.'.join(map(str, version_info))
 
604
        return version_string
 
605
 
 
606
 
 
607
class _PluginsAtFinder(object):
 
608
    """Meta path finder to support BRZ_PLUGINS_AT configuration."""
 
609
 
 
610
    def __init__(self, prefix, names_and_paths):
 
611
        self.prefix = prefix
 
612
        self.names_to_path = dict((prefix + n, p) for n, p in names_and_paths)
 
613
 
 
614
    def __repr__(self):
 
615
        return "<%s %r>" % (self.__class__.__name__, self.prefix)
 
616
 
 
617
    def find_spec(self, fullname, paths, target=None):
 
618
        """New module spec returning find method."""
 
619
        if fullname not in self.names_to_path:
 
620
            return None
 
621
        path = self.names_to_path[fullname]
 
622
        if os.path.isdir(path):
 
623
            path = _get_package_init(path)
 
624
            if path is None:
 
625
                # GZ 2017-06-02: Any reason to block loading of the name from
 
626
                # further down the path like this?
 
627
                raise ImportError("Not loading namespace package %s as %s" % (
 
628
                    path, fullname))
 
629
        return importlib_util.spec_from_file_location(fullname, path)
 
630
 
 
631
    def find_module(self, fullname, path):
 
632
        """Old PEP 302 import hook find_module method."""
 
633
        if fullname not in self.names_to_path:
 
634
            return None
 
635
        return _LegacyLoader(self.names_to_path[fullname])
 
636
 
 
637
 
 
638
class _LegacyLoader(object):
 
639
    """Source loader implementation for Python versions without importlib."""
 
640
 
 
641
    def __init__(self, filepath):
 
642
        self.filepath = filepath
 
643
 
 
644
    def __repr__(self):
 
645
        return "<%s %r>" % (self.__class__.__name__, self.filepath)
 
646
 
 
647
    def load_module(self, fullname):
 
648
        """Load a plugin from a specific directory (or file)."""
 
649
        plugin_path = self.filepath
 
650
        loading_path = None
 
651
        if os.path.isdir(plugin_path):
 
652
            init_path = _get_package_init(plugin_path)
 
653
            if init_path is not None:
 
654
                loading_path = plugin_path
 
655
                suffix = ''
 
656
                mode = ''
 
657
                kind = imp.PKG_DIRECTORY
 
658
        else:
 
659
            for suffix, mode, kind in imp.get_suffixes():
 
660
                if plugin_path.endswith(suffix):
 
661
                    loading_path = plugin_path
 
662
                    break
 
663
        if loading_path is None:
 
664
            raise ImportError('%s cannot be loaded from %s'
 
665
                              % (fullname, plugin_path))
 
666
        if kind is imp.PKG_DIRECTORY:
 
667
            f = None
 
668
        else:
 
669
            f = open(loading_path, mode)
 
670
        try:
 
671
            mod = imp.load_module(fullname, f, loading_path,
 
672
                                  (suffix, mode, kind))
 
673
            mod.__package__ = fullname
 
674
            return mod
 
675
        finally:
 
676
            if f is not None:
 
677
                f.close()