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