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