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