/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: 2019-01-24 02:11:03 UTC
  • mfrom: (7240.6.2 release-notes)
  • Revision ID: breezy.the.bot@gmail.com-20190124021103-hlav1a0ga9gv4stf
Add some release notes ahead of beta1.

Merged from https://code.launchpad.net/~jelmer/brz/release-notes/+merge/361717

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