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() |