/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: 2020-01-26 04:46:18 UTC
  • mfrom: (7463.1.3 cf-size)
  • Revision ID: breezy.the.bot@gmail.com-20200126044618-y2p8kxo82sop30bw
Add a size attribute to ContentFactory.

Merged from https://code.launchpad.net/~jelmer/brz/cf-size/+merge/378080

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