/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar
6651.4.1 by Martin
Rewrite of the plugin module for Python 3 compat and general sanity
1
# Copyright (C) 2005-2011 Canonical Ltd, 2017 Breezy developers
1887.1.1 by Adeodato Simó
Do not separate paragraphs in the copyright statement with blank lines,
2
#
1393.2.1 by John Arbash Meinel
Merged in split-storage-2 branch. Need to cleanup a little bit more still.
3
# This program is free software; you can redistribute it and/or modify
4
# it under the terms of the GNU General Public License as published by
5
# the Free Software Foundation; either version 2 of the License, or
6
# (at your option) any later version.
1887.1.1 by Adeodato Simó
Do not separate paragraphs in the copyright statement with blank lines,
7
#
1393.2.1 by John Arbash Meinel
Merged in split-storage-2 branch. Need to cleanup a little bit more still.
8
# This program is distributed in the hope that it will be useful,
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11
# GNU General Public License for more details.
1887.1.1 by Adeodato Simó
Do not separate paragraphs in the copyright statement with blank lines,
12
#
1393.2.1 by John Arbash Meinel
Merged in split-storage-2 branch. Need to cleanup a little bit more still.
13
# You should have received a copy of the GNU General Public License
14
# along with this program; if not, write to the Free Software
4183.7.1 by Sabin Iacob
update FSF mailing address
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
1393.2.1 by John Arbash Meinel
Merged in split-storage-2 branch. Need to cleanup a little bit more still.
16
6651.4.1 by Martin
Rewrite of the plugin module for Python 3 compat and general sanity
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.
6651.4.2 by Martin
Move plugin_name logic from commands to plugin to fix test
31
- plugin_name: Gives unprefixed name of a plugin module.
3620.4.1 by Robert Collins
plugin doc strings update.
32
33
See the plugin-api developer documentation for information about writing
34
plugins.
1185.16.83 by mbp at sourcefrog
- notes on testability of plugins
35
"""
36
6379.6.7 by Jelmer Vernooij
Move importing from future until after doc string, otherwise the doc string will disappear.
37
from __future__ import absolute_import
38
1393.2.1 by John Arbash Meinel
Merged in split-storage-2 branch. Need to cleanup a little bit more still.
39
import os
6651.4.1 by Martin
Rewrite of the plugin module for Python 3 compat and general sanity
40
import re
1185.16.82 by mbp at sourcefrog
- give a quieter warning if a plugin can't be loaded
41
import sys
1996.3.17 by John Arbash Meinel
lazy_import plugin and transport/local
42
6651.4.1 by Martin
Rewrite of the plugin module for Python 3 compat and general sanity
43
import breezy
6624 by Jelmer Vernooij
Merge Python3 porting work ('py3 pokes')
44
from . import osutils
3794.1.1 by Martin Pool
Update osutils imports to fix setup.py on Windows
45
6624 by Jelmer Vernooij
Merge Python3 porting work ('py3 pokes')
46
from .lazy_import import lazy_import
1996.3.17 by John Arbash Meinel
lazy_import plugin and transport/local
47
lazy_import(globals(), """
48
import imp
6651.4.1 by Martin
Rewrite of the plugin module for Python 3 compat and general sanity
49
import importlib
50
from importlib import util as importlib_util
1185.16.82 by mbp at sourcefrog
- give a quieter warning if a plugin can't be loaded
51
6622.1.34 by Jelmer Vernooij
Rename brzlib => breezy.
52
from breezy import (
3224.5.10 by Andrew Bennetts
Replace some duplication with a different form of hackery.
53
    config,
3427.2.2 by James Westby
Just print the exception, keeping the API of log_exception_quietly the same.
54
    debug,
3766.3.2 by Robert Collins
Fix reporting of incompatible api plugin load errors, fixing bug 279451.
55
    errors,
6651.4.1 by Martin
Rewrite of the plugin module for Python 3 compat and general sanity
56
    help_topics,
3224.5.1 by Andrew Bennetts
Lots of assorted hackery to reduce the number of imports for common operations. Improves 'rocks', 'st' and 'help' times by ~50ms on my laptop.
57
    trace,
1996.3.17 by John Arbash Meinel
lazy_import plugin and transport/local
58
    )
59
""")
60
61
6651.4.1 by Martin
Rewrite of the plugin module for Python 3 compat and general sanity
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):
1551.3.11 by Aaron Bentley
Merge from Robert
71
    """Disable loading plugins.
72
73
    Future calls to load_plugins() will be ignored.
6651.4.1 by Martin
Rewrite of the plugin module for Python 3 compat and general sanity
74
75
    :param state: The library state object that records loaded plugins.
76
    """
77
    if state is None:
6759.4.2 by Jelmer Vernooij
Use get_global_state>
78
        state = breezy.get_global_state()
6651.4.1 by Martin
Rewrite of the plugin module for Python 3 compat and general sanity
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`
87
    files (and whatever other extensions are used in the platform,
88
    such as `*.pyd`).
89
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.
93
    """
94
    if state is None:
6759.4.2 by Jelmer Vernooij
Use get_global_state>
95
        state = breezy.get_global_state()
6651.4.1 by Martin
Rewrite of the plugin module for Python 3 compat and general sanity
96
    if getattr(state, 'plugins', None) is not None:
97
        # People can make sure plugins are loaded, they just won't be twice
98
        return
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
6651.4.2 by Martin
Move plugin_name logic from commands to plugin to fix test
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
6651.4.1 by Martin
Rewrite of the plugin module for Python 3 compat and general sanity
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()
241
            if os.path.isdir(path):
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):
5616.7.10 by Martin Pool
Clean up describe_plugins to sort loaded and unloaded plugins together.
326
    """Generate text description of plugins.
327
6651.4.1 by Martin
Rewrite of the plugin module for Python 3 compat and general sanity
328
    Includes both those that have loaded, and those that failed to load.
5616.7.5 by Martin Pool
Factor out describe_loaded_plugins
329
6651.4.1 by Martin
Rewrite of the plugin module for Python 3 compat and general sanity
330
    :param show_paths: If true, include the plugin path.
331
    :param state: The library state object to inspect.
5616.7.5 by Martin Pool
Factor out describe_loaded_plugins
332
    :returns: Iterator of text lines (including newlines.)
333
    """
6651.4.1 by Martin
Rewrite of the plugin module for Python 3 compat and general sanity
334
    if state is None:
6759.4.2 by Jelmer Vernooij
Use get_global_state>
335
        state = breezy.get_global_state()
6759.4.7 by Jelmer Vernooij
Fix last test.
336
    loaded_plugins = getattr(state, 'plugins', {})
6759.4.5 by Jelmer Vernooij
Fix some tests.
337
    plugin_warnings = set(getattr(state, 'plugin_warnings', []))
6759.4.7 by Jelmer Vernooij
Fix last test.
338
    all_names = sorted(set(loaded_plugins.keys()).union(plugin_warnings))
5616.7.10 by Martin Pool
Clean up describe_plugins to sort loaded and unloaded plugins together.
339
    for name in all_names:
6759.4.4 by Jelmer Vernooij
Avoid accessing global state.
340
        if name in loaded_plugins:
341
            plugin = loaded_plugins[name]
5616.7.10 by Martin Pool
Clean up describe_plugins to sort loaded and unloaded plugins together.
342
            version = plugin.__version__
343
            if version == 'unknown':
344
                version = ''
345
            yield '%s %s\n' % (name, version)
6651.4.1 by Martin
Rewrite of the plugin module for Python 3 compat and general sanity
346
            d = plugin.module.__doc__
5616.7.10 by Martin Pool
Clean up describe_plugins to sort loaded and unloaded plugins together.
347
            if d:
348
                doc = d.split('\n')[0]
349
            else:
350
                doc = '(no description)'
5616.7.11 by Martin Pool
Additional tests and fixes for refactored describe_plugins.
351
            yield ("  %s\n" % doc)
5616.7.10 by Martin Pool
Clean up describe_plugins to sort loaded and unloaded plugins together.
352
            if show_paths:
353
                yield ("   %s\n" % plugin.path())
5616.7.5 by Martin Pool
Factor out describe_loaded_plugins
354
        else:
5616.7.10 by Martin Pool
Clean up describe_plugins to sort loaded and unloaded plugins together.
355
            yield "%s (failed to load)\n" % name
6651.4.1 by Martin
Rewrite of the plugin module for Python 3 compat and general sanity
356
        if name in state.plugin_warnings:
357
            for line in state.plugin_warnings[name]:
5616.7.6 by Martin Pool
Use standard plugin list formatting in crash reports
358
                yield "  ** " + line + '\n'
359
        yield '\n'
5616.7.5 by Martin Pool
Factor out describe_loaded_plugins
360
361
6651.4.1 by Martin
Rewrite of the plugin module for Python 3 compat and general sanity
362
def _get_core_plugin_paths(existing_paths):
363
    """Generate possible locations for plugins based on existing_paths."""
364
    if getattr(sys, 'frozen', False):
4628.2.1 by Vincent Ladeuil
Start introducing accessors for plugin paths.
365
        # We need to use relative path to system-wide plugin
6622.1.35 by Jelmer Vernooij
Fix last tests.
366
        # directory because breezy from standalone brz.exe
4628.2.1 by Vincent Ladeuil
Start introducing accessors for plugin paths.
367
        # could be imported by another standalone program
6681.2.4 by Jelmer Vernooij
More renames.
368
        # (e.g. brz-config; or TortoiseBzr/Olive if/when they
4628.2.1 by Vincent Ladeuil
Start introducing accessors for plugin paths.
369
        # will become standalone exe). [bialix 20071123]
370
        # __file__ typically is
6622.1.34 by Jelmer Vernooij
Rename brzlib => breezy.
371
        # C:\Program Files\Bazaar\lib\library.zip\breezy\plugin.pyc
4628.2.1 by Vincent Ladeuil
Start introducing accessors for plugin paths.
372
        # then plugins directory is
373
        # C:\Program Files\Bazaar\plugins
374
        # so relative path is ../../../plugins
6651.4.1 by Martin
Rewrite of the plugin module for Python 3 compat and general sanity
375
        yield osutils.abspath(osutils.pathjoin(
376
            osutils.dirname(__file__), '../../../plugins'))
4628.2.1 by Vincent Ladeuil
Start introducing accessors for plugin paths.
377
    else:     # don't look inside library.zip
6651.4.1 by Martin
Rewrite of the plugin module for Python 3 compat and general sanity
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')
4628.2.1 by Vincent Ladeuil
Start introducing accessors for plugin paths.
387
388
389
def get_user_plugin_path():
390
    return osutils.pathjoin(config.config_dir(), 'plugins')
391
392
6651.4.1 by Martin
Rewrite of the plugin module for Python 3 compat and general sanity
393
def record_plugin_warning(warning_message):
5616.7.4 by Martin Pool
Also use quiet warnings for other failures to load plugins
394
    trace.mutter(warning_message)
6651.4.1 by Martin
Rewrite of the plugin module for Python 3 compat and general sanity
395
    return warning_message
5616.7.4 by Martin Pool
Also use quiet warnings for other failures to load plugins
396
397
5086.5.8 by Vincent Ladeuil
Make sure we can load from a non-standard directory name.
398
def _load_plugin_module(name, dir):
6651.4.1 by Martin
Rewrite of the plugin module for Python 3 compat and general sanity
399
    """Load plugin by name.
5086.5.10 by Vincent Ladeuil
Cleanup docs.
400
6622.1.34 by Jelmer Vernooij
Rename brzlib => breezy.
401
    :param name: The plugin name in the breezy.plugins namespace.
5086.5.10 by Vincent Ladeuil
Cleanup docs.
402
    :param dir: The directory the plugin is loaded from for error messages.
403
    """
6651.4.1 by Martin
Rewrite of the plugin module for Python 3 compat and general sanity
404
    if _MODULE_PREFIX + name in sys.modules:
5086.5.8 by Vincent Ladeuil
Make sure we can load from a non-standard directory name.
405
        return
406
    try:
6651.4.1 by Martin
Rewrite of the plugin module for Python 3 compat and general sanity
407
        __import__(_MODULE_PREFIX + name)
6672.1.2 by Jelmer Vernooij
Remove breezy.api.
408
    except errors.IncompatibleVersion as e:
5616.7.1 by Martin Pool
Record but don't show warnings about updated plugins
409
        warning_message = (
6672.1.2 by Jelmer Vernooij
Remove breezy.api.
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))
6651.4.1 by Martin
Rewrite of the plugin module for Python 3 compat and general sanity
413
        return record_plugin_warning(warning_message)
6619.3.2 by Jelmer Vernooij
Apply 2to3 except fix.
414
    except Exception as e:
6651.4.1 by Martin
Rewrite of the plugin module for Python 3 compat and general sanity
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.
6797 by Jelmer Vernooij
Merge lp:~jelmer/brz/fix-imports.
420
        if re.search('\\.|-| ', name):
5086.5.8 by Vincent Ladeuil
Make sure we can load from a non-standard directory name.
421
            sanitised_name = re.sub('[-. ]', '_', name)
6622.1.35 by Jelmer Vernooij
Fix last tests.
422
            if sanitised_name.startswith('brz_'):
423
                sanitised_name = sanitised_name[len('brz_'):]
5086.5.8 by Vincent Ladeuil
Make sure we can load from a non-standard directory name.
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:
6651.4.1 by Martin
Rewrite of the plugin module for Python 3 compat and general sanity
428
            return record_plugin_warning(
429
                'Unable to load plugin %r from %r: %s' % (name, dir, e))
2256.2.2 by Robert Collins
Allow 'import bzrlib.plugins.NAME' to work when the plugin NAME has not
430
431
2762.2.1 by Robert Collins
* ``bzr plugins`` now lists the version number for each plugin in square
432
def plugins():
433
    """Return a dictionary of the plugins.
3943.8.1 by Marius Kruger
remove all trailing whitespace from bzr source
434
2762.2.1 by Robert Collins
* ``bzr plugins`` now lists the version number for each plugin in square
435
    Each item in the dictionary is a PlugIn object.
436
    """
437
    result = {}
6651.4.1 by Martin
Rewrite of the plugin module for Python 3 compat and general sanity
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])
2762.2.1 by Robert Collins
* ``bzr plugins`` now lists the version number for each plugin in square
443
    return result
444
445
6759.4.3 by Jelmer Vernooij
Avoid accessing global state.
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
6780.1.1 by Jelmer Vernooij
Check for plugin existing in sys.modules but being None.
455
    if module is None:
456
        return None
6759.4.3 by Jelmer Vernooij
Avoid accessing global state.
457
    return PlugIn(name, module)
458
459
6677.1.5 by Martin
Make crash debug and trace modules pass on Python 3
460
def format_concise_plugin_list(state=None):
5609.23.6 by Martin Pool
Show concise list of plugins in non-apport crash; add test for this
461
    """Return a string holding a concise list of plugins and their version.
462
    """
6677.1.5 by Martin
Make crash debug and trace modules pass on Python 3
463
    if state is None:
6759.4.2 by Jelmer Vernooij
Use get_global_state>
464
        state = breezy.get_global_state()
5609.23.6 by Martin Pool
Show concise list of plugins in non-apport crash; add test for this
465
    items = []
6677.1.5 by Martin
Make crash debug and trace modules pass on Python 3
466
    for name, a_plugin in sorted(state.plugins.items()):
5609.23.6 by Martin Pool
Show concise list of plugins in non-apport crash; add test for this
467
        items.append("%s[%s]" %
468
            (name, a_plugin.__version__))
469
    return ', '.join(items)
470
471
2432.1.24 by Robert Collins
Add plugins as a help index.
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):
2432.1.25 by Robert Collins
Return plugin module docstrings for 'bzr help plugin'.
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):]
6651.4.1 by Martin
Rewrite of the plugin module for Python 3 compat and general sanity
491
        plugin_module_name = _MODULE_PREFIX + topic
2432.1.25 by Robert Collins
Return plugin module docstrings for 'bzr help plugin'.
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
3984.4.5 by Ian Clatworthy
help xxx is full help; xxx -h is concise help
510
    def get_help_text(self, additional_see_also=None, verbose=True):
2432.1.25 by Robert Collins
Return plugin module docstrings for 'bzr help plugin'.
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'
6059.3.4 by Vincent Ladeuil
Fix forgotten renaming.
522
        result += help_topics._format_see_also(additional_see_also)
2432.1.25 by Robert Collins
Return plugin module docstrings for 'bzr help plugin'.
523
        return result
2432.1.29 by Robert Collins
Add get_help_topic to ModuleHelpTopic.
524
525
    def get_help_topic(self):
6059.3.4 by Vincent Ladeuil
Fix forgotten renaming.
526
        """Return the module help topic: its basename."""
6651.4.1 by Martin
Rewrite of the plugin module for Python 3 compat and general sanity
527
        return self.module.__name__[len(_MODULE_PREFIX):]
2762.2.1 by Robert Collins
* ``bzr plugins`` now lists the version number for each plugin in square
528
529
530
class PlugIn(object):
6622.1.34 by Jelmer Vernooij
Rename brzlib => breezy.
531
    """The breezy representation of a plugin.
2762.2.1 by Robert Collins
* ``bzr plugins`` now lists the version number for each plugin in square
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
5939.3.2 by Andrew Bennetts
Take a slightly more direct approach by largely preserving BZR_DISABLE_PLUGINS/BZR_PLUGINS_AT.
541
    def path(self):
542
        """Get the path that this plugin was loaded from."""
2762.2.1 by Robert Collins
* ``bzr plugins`` now lists the version number for each plugin in square
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:
3193.2.1 by Alexander Belchenko
show path to plugin module as *.py instead of *.pyc if python source available
546
            path = os.path.abspath(self.module.__file__)
6651.4.1 by Martin
Rewrite of the plugin module for Python 3 compat and general sanity
547
            if path[-4:] == COMPILED_EXT:
3193.2.1 by Alexander Belchenko
show path to plugin module as *.py instead of *.pyc if python source available
548
                pypath = path[:-4] + '.py'
549
                if os.path.isfile(pypath):
550
                    path = pypath
551
            return path
2762.2.1 by Robert Collins
* ``bzr plugins`` now lists the version number for each plugin in square
552
        else:
5939.3.2 by Andrew Bennetts
Take a slightly more direct approach by largely preserving BZR_DISABLE_PLUGINS/BZR_PLUGINS_AT.
553
            return repr(self.module)
2762.2.1 by Robert Collins
* ``bzr plugins`` now lists the version number for each plugin in square
554
6651.4.1 by Martin
Rewrite of the plugin module for Python 3 compat and general sanity
555
    def __repr__(self):
556
        return "<%s.%s name=%s, module=%s>" % (
557
            self.__class__.__module__, self.__class__.__name__,
2762.2.1 by Robert Collins
* ``bzr plugins`` now lists the version number for each plugin in square
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
3302.8.21 by Vincent Ladeuil
Fixed as per Robert's review.
567
    def load_plugin_tests(self, loader):
3302.8.10 by Vincent Ladeuil
Prepare bzrlib.plugin to use the new test 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:
3302.8.11 by Vincent Ladeuil
Simplify plugin.load_tests.
574
            return loader.loadTestsFromModule(self.module)
3302.8.10 by Vincent Ladeuil
Prepare bzrlib.plugin to use the new test loader.
575
        else:
576
            return None
577
2762.2.1 by Robert Collins
* ``bzr plugins`` now lists the version number for each plugin in square
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)
3777.6.7 by Marius Kruger
* Can now also handle non-iteratable and string plugin versions.
581
        if version_info is not None:
582
            try:
6651.4.1 by Martin
Rewrite of the plugin module for Python 3 compat and general sanity
583
                if isinstance(version_info, str):
3777.6.7 by Marius Kruger
* Can now also handle non-iteratable and string plugin versions.
584
                    version_info = version_info.split('.')
585
                elif len(version_info) == 3:
586
                    version_info = tuple(version_info) + ('final', 0)
6651.4.1 by Martin
Rewrite of the plugin module for Python 3 compat and general sanity
587
            except TypeError:
3777.6.7 by Marius Kruger
* Can now also handle non-iteratable and string plugin versions.
588
                # The given version_info isn't even iteratible
589
                trace.log_exception_quietly()
590
                version_info = (version_info,)
2762.2.1 by Robert Collins
* ``bzr plugins`` now lists the version number for each plugin in square
591
        return version_info
3302.8.10 by Vincent Ladeuil
Prepare bzrlib.plugin to use the new test loader.
592
6651.4.1 by Martin
Rewrite of the plugin module for Python 3 compat and general sanity
593
    @property
594
    def __version__(self):
2762.2.1 by Robert Collins
* ``bzr plugins`` now lists the version number for each plugin in square
595
        version_info = self.version_info()
3777.6.1 by Marius Kruger
Try to return something usefull for plugins with bad version numbers,
596
        if version_info is None or len(version_info) == 0:
2762.2.1 by Robert Collins
* ``bzr plugins`` now lists the version number for each plugin in square
597
            return "unknown"
3777.6.1 by Marius Kruger
Try to return something usefull for plugins with bad version numbers,
598
        try:
6651.4.1 by Martin
Rewrite of the plugin module for Python 3 compat and general sanity
599
            version_string = breezy._format_version_tuple(version_info)
600
        except (ValueError, TypeError, IndexError):
3777.6.6 by Marius Kruger
catch only ValueError, TypeError, IndexError as per feedback from John
601
            trace.log_exception_quietly()
6651.4.1 by Martin
Rewrite of the plugin module for Python 3 compat and general sanity
602
            # Try to show something for the version anyway
3777.6.3 by Marius Kruger
Use bzrlib._format_version_tuple and map as per review from John.
603
            version_string = '.'.join(map(str, version_info))
2762.2.1 by Robert Collins
* ``bzr plugins`` now lists the version number for each plugin in square
604
        return version_string
605
6651.4.1 by Martin
Rewrite of the plugin module for Python 3 compat and general sanity
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)
5086.1.7 by Vincent Ladeuil
Cleaner fix for bug #411413.
646
5086.5.3 by Vincent Ladeuil
First shot at loading plugins from a specific directory.
647
    def load_module(self, fullname):
6325.1.1 by Vincent Ladeuil
Fix various typos
648
        """Load a plugin from a specific directory (or file)."""
6651.4.1 by Martin
Rewrite of the plugin module for Python 3 compat and general sanity
649
        plugin_path = self.filepath
5086.5.14 by Vincent Ladeuil
Fix bug #552922 by controlling which files can be used to load a plugin.
650
        loading_path = None
651
        if os.path.isdir(plugin_path):
6651.4.1 by Martin
Rewrite of the plugin module for Python 3 compat and general sanity
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
5086.5.14 by Vincent Ladeuil
Fix bug #552922 by controlling which files can be used to load a plugin.
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:
5086.5.3 by Vincent Ladeuil
First shot at loading plugins from a specific directory.
664
            raise ImportError('%s cannot be loaded from %s'
5086.5.14 by Vincent Ladeuil
Fix bug #552922 by controlling which files can be used to load a plugin.
665
                              % (fullname, plugin_path))
5268.6.3 by Vincent Ladeuil
BZR_PLUGINS_AT should use packages properly to handle relative imports.
666
        if kind is imp.PKG_DIRECTORY:
667
            f = None
668
        else:
669
            f = open(loading_path, mode)
5086.5.3 by Vincent Ladeuil
First shot at loading plugins from a specific directory.
670
        try:
5086.5.14 by Vincent Ladeuil
Fix bug #552922 by controlling which files can be used to load a plugin.
671
            mod = imp.load_module(fullname, f, loading_path,
672
                                  (suffix, mode, kind))
5086.5.12 by Vincent Ladeuil
Force __package__ to fix pqm failure.
673
            mod.__package__ = fullname
5086.5.3 by Vincent Ladeuil
First shot at loading plugins from a specific directory.
674
            return mod
675
        finally:
5268.6.3 by Vincent Ladeuil
BZR_PLUGINS_AT should use packages properly to handle relative imports.
676
            if f is not None:
677
                f.close()