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