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