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