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