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