14
14
# along with this program; if not, write to the Free Software
15
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
"""Breezy plugin support.
19
Which plugins to load can be configured by setting these environment variables:
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.
25
The interfaces this module exports include:
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.
31
- plugin_name: Gives unprefixed name of a plugin module.
18
"""bzr python plugin support.
20
When load_plugins() is invoked, any python module in any directory in
21
$BZR_PLUGIN_PATH will be imported. The module will be imported as
22
'bzrlib.plugins.$BASENAME(PLUGIN)'. In the plugin's main body, it should
23
update any bzrlib registries it wants to extend.
33
25
See the plugin-api developer documentation for information about writing
28
BZR_PLUGIN_PATH is also honoured for any plugins imported via
29
'import bzrlib.plugins.PLUGINNAME', as long as set_plugins_path has been
37
from __future__ import absolute_import
46
from .lazy_import import lazy_import
36
from bzrlib import osutils
38
from bzrlib.lazy_import import lazy_import
47
40
lazy_import(globals(), """
49
from importlib import util as importlib_util
46
_format_version_tuple,
52
from bzrlib import plugins as _mod_plugins
55
from bzrlib.symbol_versioning import (
64
_MODULE_PREFIX = "breezy.plugins."
66
if __debug__ or sys.version_info > (3,):
72
def disable_plugins(state=None):
61
DEFAULT_PLUGIN_PATH = None
63
_plugins_disabled = False
66
def are_plugins_disabled():
67
return _plugins_disabled
70
def disable_plugins():
73
71
"""Disable loading plugins.
75
73
Future calls to load_plugins() will be ignored.
77
:param state: The library state object that records loaded plugins.
80
state = breezy.get_global_state()
84
def load_plugins(path=None, state=None):
85
"""Load breezy plugins.
87
The environment variable BRZ_PLUGIN_PATH is considered a delimited
88
set of paths to look through. Each entry is searched for `*.py`
89
files (and whatever other extensions are used in the platform,
75
global _plugins_disabled
76
_plugins_disabled = True
80
def _strip_trailing_sep(path):
81
return path.rstrip("\\/")
84
def _get_specific_plugin_paths(paths):
85
"""Returns the plugin paths from a string describing the associations.
87
:param paths: A string describing the paths associated with the plugins.
89
:returns: A list of (plugin name, path) tuples.
91
For example, if paths is my_plugin@/test/my-test:her_plugin@/production/her,
92
[('my_plugin', '/test/my-test'), ('her_plugin', '/production/her')]
95
Note that ':' in the example above depends on the os.
100
for spec in paths.split(os.pathsep):
102
name, path = spec.split('@')
104
raise errors.BzrCommandError(
105
'"%s" is not a valid <plugin_name>@<plugin_path> description '
107
specs.append((name, path))
111
def set_plugins_path(path=None):
112
"""Set the path for plugins to be loaded from.
92
114
:param path: The list of paths to search for plugins. By default,
93
it is populated from the __path__ of the breezy.plugins package.
94
:param state: The library state object that records loaded plugins.
115
path will be determined using get_standard_plugins_path.
116
if path is [], no plugins can be loaded.
97
state = breezy.get_global_state()
98
if getattr(state, 'plugins', None) is not None:
99
# People can make sure plugins are loaded, they just won't be twice
103
# Calls back into extend_path() here
104
from breezy.plugins import __path__ as path
106
state.plugin_warnings = {}
107
_load_plugins_from_path(state, path)
108
if (None, 'entrypoints') in _env_plugin_path():
109
_load_plugins_from_entrypoints(state)
110
state.plugins = plugins()
113
def _load_plugins_from_entrypoints(state):
117
# No pkg_resources, no entrypoints.
120
for ep in pkg_resources.iter_entry_points('breezy.plugin'):
121
fullname = _MODULE_PREFIX + ep.name
122
if fullname in sys.modules:
124
sys.modules[fullname] = ep.load()
127
def plugin_name(module_name):
128
"""Gives unprefixed name from module_name or None."""
129
if module_name.startswith(_MODULE_PREFIX):
130
parts = module_name.split(".")
136
def extend_path(path, name):
137
"""Helper so breezy.plugins can be a sort of namespace package.
139
To be used in similar fashion to pkgutil.extend_path:
141
from breezy.plugins import extend_path
142
__path__ = extend_path(__path__, __name__)
144
Inspects the BRZ_PLUGIN* envvars, sys.path, and the filesystem to find
145
plugins. May mutate sys.modules in order to block plugin loading, and may
146
append a new meta path finder to sys.meta_path for plugins@ loading.
148
Returns a list of paths to import from, as an enhanced object that also
149
contains details of the other configuration used.
151
blocks = _env_disable_plugins()
152
_block_plugins(blocks)
154
extra_details = _env_plugins_at()
155
_install_importer_if_needed(extra_details)
157
paths = _iter_plugin_paths(_env_plugin_path(), path)
159
return _Path(name, blocks, extra_details, paths)
163
"""List type to use as __path__ but containing additional details.
165
Python 3 allows any iterable for __path__ but Python 2 is more fussy.
168
def __init__(self, package_name, blocked, extra, paths):
169
super(_Path, self).__init__(paths)
170
self.package_name = package_name
171
self.blocked_names = blocked
172
self.extra_details = extra
175
return "%s(%r, %r, %r, %s)" % (
176
self.__class__.__name__, self.package_name, self.blocked_names,
177
self.extra_details, list.__repr__(self))
180
def _expect_identifier(name, env_key, env_value):
181
"""Validate given name from envvar is usable as a Python identifier.
183
Returns the name as a native str, or None if it was invalid.
185
Per PEP 3131 this is no longer strictly correct for Python 3, but as MvL
186
didn't include a neat way to check except eval, this enforces ascii.
188
if re.match(r"^[A-Za-z_][A-Za-z0-9_]*$", name) is None:
189
trace.warning("Invalid name '%s' in %s='%s'", name, env_key, env_value)
194
def _env_disable_plugins(key='BRZ_DISABLE_PLUGINS'):
195
"""Gives list of names for plugins to disable from environ key."""
197
env = osutils.path_from_environ(key)
199
for name in env.split(os.pathsep):
200
name = _expect_identifier(name, key, env)
202
disabled_names.append(name)
203
return disabled_names
206
def _env_plugins_at(key='BRZ_PLUGINS_AT'):
207
"""Gives list of names and paths of specific plugins from environ key."""
209
env = osutils.path_from_environ(key)
211
for pair in env.split(os.pathsep):
213
name, path = pair.split('@', 1)
216
name = osutils.basename(path).split('.', 1)[0]
217
name = _expect_identifier(name, key, env)
219
plugin_details.append((name, path))
220
return plugin_details
223
def _env_plugin_path(key='BRZ_PLUGIN_PATH'):
224
"""Gives list of paths and contexts for plugins from environ key.
226
Each entry is either a specific path to load plugins from and the value
227
'path', or None and one of the values 'user', 'core', 'entrypoints', 'site'.
230
env = osutils.path_from_environ(key)
235
'entrypoints': False,
238
# Add paths specified by user in order
239
for p in env.split(os.pathsep):
240
flag, name = p[:1], p[1:]
241
if flag in ("+", "-") and name in defaults:
242
if flag == "+" and defaults[name] is not None:
243
path_details.append((None, name))
244
defaults[name] = None
246
path_details.append((p, 'path'))
248
# Add any remaining default paths
249
for name in ('user', 'core', 'entrypoints', 'site'):
251
path_details.append((None, name))
256
def _iter_plugin_paths(paths_from_env, core_paths):
257
"""Generate paths using paths_from_env and core_paths."""
258
# GZ 2017-06-02: This is kinda horrid, should make better.
259
for path, context in paths_from_env:
260
if context == 'path':
262
elif context == 'user':
263
path = get_user_plugin_path()
264
if os.path.isdir(path):
266
elif context == 'core':
267
for path in _get_core_plugin_paths(core_paths):
269
elif context == 'site':
270
for path in _get_site_plugin_paths(sys.path):
271
if os.path.isdir(path):
275
def _install_importer_if_needed(plugin_details):
276
"""Install a meta path finder to handle plugin_details if any."""
278
finder = _PluginsAtFinder(_MODULE_PREFIX, plugin_details)
279
# For Python 3, must insert before default PathFinder to override.
280
sys.meta_path.insert(2, finder)
283
def _load_plugins_from_path(state, paths):
284
"""Do the importing all plugins from paths."""
285
imported_names = set()
286
for name, path in _iter_possible_plugins(paths):
287
if name not in imported_names:
288
msg = _load_plugin_module(name, path)
290
state.plugin_warnings.setdefault(name, []).append(msg)
291
imported_names.add(name)
294
def _block_plugins(names):
295
"""Add names to sys.modules to block future imports."""
297
package_name = _MODULE_PREFIX + name
298
if sys.modules.get(package_name) is not None:
299
trace.mutter("Blocked plugin %s already loaded.", name)
300
sys.modules[package_name] = None
303
def _get_package_init(package_path):
304
"""Get path of __init__ file from package_path or None if not a package."""
305
init_path = osutils.pathjoin(package_path, "__init__.py")
306
if os.path.exists(init_path):
308
init_path = init_path[:-3] + COMPILED_EXT
309
if os.path.exists(init_path):
314
def _iter_possible_plugins(plugin_paths):
315
"""Generate names and paths of possible plugins from plugin_paths."""
316
# Inspect any from BRZ_PLUGINS_AT first.
317
for name, path in getattr(plugin_paths, "extra_details", ()):
319
# Then walk over files and directories in the paths from the package.
320
for path in plugin_paths:
321
if os.path.isfile(path):
322
if path.endswith(".zip"):
323
trace.mutter("Don't yet support loading plugins from zip.")
325
for name, path in _walk_modules(path):
329
def _walk_modules(path):
330
"""Generate name and path of modules and packages on path."""
331
for root, dirs, files in os.walk(path):
335
if f.endswith((".py", COMPILED_EXT)):
336
yield f.rsplit(".", 1)[0], root
340
package_dir = osutils.pathjoin(root, d)
341
fullpath = _get_package_init(package_dir)
342
if fullpath is not None:
344
# Don't descend into subdirectories
348
def describe_plugins(show_paths=False, state=None):
349
"""Generate text description of plugins.
351
Includes both those that have loaded, and those that failed to load.
353
:param show_paths: If true, include the plugin path.
354
:param state: The library state object to inspect.
355
:returns: Iterator of text lines (including newlines.)
358
state = breezy.get_global_state()
359
loaded_plugins = getattr(state, 'plugins', {})
360
plugin_warnings = set(getattr(state, 'plugin_warnings', []))
361
all_names = sorted(set(loaded_plugins.keys()).union(plugin_warnings))
362
for name in all_names:
363
if name in loaded_plugins:
364
plugin = loaded_plugins[name]
365
version = plugin.__version__
366
if version == 'unknown':
368
yield '%s %s\n' % (name, version)
369
d = plugin.module.__doc__
371
doc = d.split('\n')[0]
373
doc = '(no description)'
374
yield (" %s\n" % doc)
376
yield (" %s\n" % plugin.path())
378
yield "%s (failed to load)\n" % name
379
if name in state.plugin_warnings:
380
for line in state.plugin_warnings[name]:
381
yield " ** " + line + '\n'
385
def _get_core_plugin_paths(existing_paths):
386
"""Generate possible locations for plugins based on existing_paths."""
387
if getattr(sys, 'frozen', False):
119
path = get_standard_plugins_path()
120
_mod_plugins.__path__ = path
121
PluginImporter.reset()
122
# Set up a blacklist for disabled plugins
123
disabled_plugins = os.environ.get('BZR_DISABLE_PLUGINS', None)
124
if disabled_plugins is not None:
125
for name in disabled_plugins.split(os.pathsep):
126
PluginImporter.blacklist.add('bzrlib.plugins.' + name)
127
# Set up a the specific paths for plugins
128
for plugin_name, plugin_path in _get_specific_plugin_paths(os.environ.get(
129
'BZR_PLUGINS_AT', None)):
130
PluginImporter.specific_paths[
131
'bzrlib.plugins.%s' % plugin_name] = plugin_path
135
def _append_new_path(paths, new_path):
136
"""Append a new path if it set and not already known."""
137
if new_path is not None and new_path not in paths:
138
paths.append(new_path)
142
def get_core_plugin_path():
144
bzr_exe = bool(getattr(sys, 'frozen', None))
145
if bzr_exe: # expand path for bzr.exe
388
146
# We need to use relative path to system-wide plugin
389
# directory because breezy from standalone brz.exe
147
# directory because bzrlib from standalone bzr.exe
390
148
# could be imported by another standalone program
391
# (e.g. brz-config; or TortoiseBzr/Olive if/when they
149
# (e.g. bzr-config; or TortoiseBzr/Olive if/when they
392
150
# will become standalone exe). [bialix 20071123]
393
151
# __file__ typically is
394
# C:\Program Files\Bazaar\lib\library.zip\breezy\plugin.pyc
152
# C:\Program Files\Bazaar\lib\library.zip\bzrlib\plugin.pyc
395
153
# then plugins directory is
396
154
# C:\Program Files\Bazaar\plugins
397
155
# so relative path is ../../../plugins
398
yield osutils.abspath(osutils.pathjoin(
399
osutils.dirname(__file__), '../../../plugins'))
156
core_path = osutils.abspath(osutils.pathjoin(
157
osutils.dirname(__file__), '../../../plugins'))
400
158
else: # don't look inside library.zip
401
for path in existing_paths:
405
def _get_site_plugin_paths(sys_paths):
406
"""Generate possible locations for plugins from given sys_paths."""
407
for path in sys_paths:
408
if os.path.basename(path) in ('dist-packages', 'site-packages'):
409
yield osutils.pathjoin(path, 'breezy', 'plugins')
159
# search the plugin path before the bzrlib installed dir
160
core_path = os.path.dirname(_mod_plugins.__file__)
164
def get_site_plugin_path():
165
"""Returns the path for the site installed plugins."""
166
if sys.platform == 'win32':
167
# We don't have (yet) a good answer for windows since that is certainly
168
# related to the way we build the installers. -- vila20090821
172
from distutils.sysconfig import get_python_lib
174
# If distutuils is not available, we just don't know where they are
177
site_path = osutils.pathjoin(get_python_lib(), 'bzrlib', 'plugins')
412
181
def get_user_plugin_path():
413
return osutils.pathjoin(bedding.config_dir(), 'plugins')
416
def record_plugin_warning(warning_message):
417
trace.mutter(warning_message)
418
return warning_message
182
return osutils.pathjoin(config.config_dir(), 'plugins')
185
def get_standard_plugins_path():
186
"""Determine a plugin path suitable for general use."""
187
# Ad-Hoc default: core is not overriden by site but user can overrides both
188
# The rationale is that:
189
# - 'site' comes last, because these plugins should always be available and
190
# are supposed to be in sync with the bzr installed on site.
191
# - 'core' comes before 'site' so that running bzr from sources or a user
192
# installed version overrides the site version.
193
# - 'user' comes first, because... user is always right.
194
# - the above rules clearly defines which plugin version will be loaded if
195
# several exist. Yet, it is sometimes desirable to disable some directory
196
# so that a set of plugins is disabled as once. This can be done via
197
# -site, -core, -user.
199
env_paths = os.environ.get('BZR_PLUGIN_PATH', '+user').split(os.pathsep)
200
defaults = ['+core', '+site']
202
# The predefined references
203
refs = dict(core=get_core_plugin_path(),
204
site=get_site_plugin_path(),
205
user=get_user_plugin_path())
207
# Unset paths that should be removed
208
for k,v in refs.iteritems():
210
# defaults can never mention removing paths as that will make it
211
# impossible for the user to revoke these removals.
212
if removed in env_paths:
213
env_paths.remove(removed)
218
for p in env_paths + defaults:
219
if p.startswith('+'):
220
# Resolve references if they are known
224
# Leave them untouched so user can still use paths starting
227
_append_new_path(paths, p)
229
# Get rid of trailing slashes, since Python can't handle them when
230
# it tries to import modules.
231
paths = map(_strip_trailing_sep, paths)
235
def load_plugins(path=None):
236
"""Load bzrlib plugins.
238
The environment variable BZR_PLUGIN_PATH is considered a delimited
239
set of paths to look through. Each entry is searched for *.py
240
files (and whatever other extensions are used in the platform,
243
load_from_path() provides the underlying mechanism and is called with
244
the default directory list to provide the normal behaviour.
246
:param path: The list of paths to search for plugins. By default,
247
path will be determined using get_standard_plugins_path.
248
if path is [], no plugins can be loaded.
252
# People can make sure plugins are loaded, they just won't be twice
256
# scan for all plugins in the path.
257
load_from_path(set_plugins_path(path))
260
def load_from_path(dirs):
261
"""Load bzrlib plugins found in each dir in dirs.
263
Loading a plugin means importing it into the python interpreter.
264
The plugin is expected to make calls to register commands when
265
it's loaded (or perhaps access other hooks in future.)
267
Plugins are loaded into bzrlib.plugins.NAME, and can be found there
268
for future reference.
270
The python module path for bzrlib.plugins will be modified to be 'dirs'.
272
# Explicitly load the plugins with a specific path
273
for fullname, path in PluginImporter.specific_paths.iteritems():
274
name = fullname[len('bzrlib.plugins.'):]
275
_load_plugin_module(name, path)
277
# We need to strip the trailing separators here as well as in the
278
# set_plugins_path function because calling code can pass anything in to
279
# this function, and since it sets plugins.__path__, it should set it to
280
# something that will be valid for Python to use (in case people try to
281
# run "import bzrlib.plugins.PLUGINNAME" after calling this function).
282
_mod_plugins.__path__ = map(_strip_trailing_sep, dirs)
286
trace.mutter('looking for plugins in %s', d)
291
# backwards compatability: load_from_dirs was the old name
292
# This was changed in 0.15
293
load_from_dirs = load_from_path
296
def _find_plugin_module(dir, name):
297
"""Check if there is a valid python module that can be loaded as a plugin.
299
:param dir: The directory where the search is performed.
300
:param path: An existing file path, either a python file or a package
303
:return: (name, path, description) name is the module name, path is the
304
file to load and description is the tuple returned by
307
path = osutils.pathjoin(dir, name)
308
if os.path.isdir(path):
309
# Check for a valid __init__.py file, valid suffixes depends on -O and
310
# can be .py, .pyc and .pyo
311
for suffix, mode, kind in imp.get_suffixes():
312
if kind not in (imp.PY_SOURCE, imp.PY_COMPILED):
313
# We don't recognize compiled modules (.so, .dll, etc)
315
init_path = osutils.pathjoin(path, '__init__' + suffix)
316
if os.path.isfile(init_path):
317
return name, init_path, (suffix, mode, kind)
319
for suffix, mode, kind in imp.get_suffixes():
320
if name.endswith(suffix):
321
# Clean up the module name
322
name = name[:-len(suffix)]
323
if kind == imp.C_EXTENSION and name.endswith('module'):
324
name = name[:-len('module')]
325
return name, path, (suffix, mode, kind)
326
# There is no python module here
327
return None, None, (None, None, None)
421
330
def _load_plugin_module(name, dir):
422
"""Load plugin by name.
331
"""Load plugin name from dir.
424
:param name: The plugin name in the breezy.plugins namespace.
333
:param name: The plugin name in the bzrlib.plugins namespace.
425
334
:param dir: The directory the plugin is loaded from for error messages.
427
if _MODULE_PREFIX + name in sys.modules:
336
if ('bzrlib.plugins.%s' % name) in PluginImporter.blacklist:
430
__import__(_MODULE_PREFIX + name)
431
except errors.IncompatibleVersion as e:
433
"Unable to load plugin %r. It supports %s "
434
"versions %r but the current version is %s" %
435
(name, e.api.__name__, e.wanted, e.current))
436
return record_plugin_warning(warning_message)
437
except Exception as e:
339
exec "import bzrlib.plugins.%s" % name in {}
340
except KeyboardInterrupt:
342
except errors.IncompatibleAPI, e:
343
trace.warning("Unable to load plugin %r. It requested API version "
344
"%s of module %s but the minimum exported version is %s, and "
345
"the maximum is %s" %
346
(name, e.wanted, e.api, e.minimum, e.current))
348
trace.warning("%s" % e)
349
if re.search('\.|-| ', name):
350
sanitised_name = re.sub('[-. ]', '_', name)
351
if sanitised_name.startswith('bzr_'):
352
sanitised_name = sanitised_name[len('bzr_'):]
353
trace.warning("Unable to load %r in %r as a plugin because the "
354
"file path isn't a valid module name; try renaming "
355
"it to %r." % (name, dir, sanitised_name))
357
trace.warning('Unable to load plugin %r from %r' % (name, dir))
438
358
trace.log_exception_quietly()
439
359
if 'error' in debug.debug_flags:
440
360
trace.print_exception(sys.exc_info(), sys.stderr)
441
# GZ 2017-06-02: Move this name checking up a level, no point trying
442
# to import things with bad names.
443
if re.search('\\.|-| ', name):
444
sanitised_name = re.sub('[-. ]', '_', name)
445
if sanitised_name.startswith('brz_'):
446
sanitised_name = sanitised_name[len('brz_'):]
447
trace.warning("Unable to load %r in %r as a plugin because the "
448
"file path isn't a valid module name; try renaming "
449
"it to %r." % (name, dir, sanitised_name))
451
return record_plugin_warning(
452
'Unable to load plugin %r from %r: %s' % (name, dir, e))
363
def load_from_dir(d):
364
"""Load the plugins in directory d.
366
d must be in the plugins module path already.
367
This function is called once for each directory in the module path.
370
for p in os.listdir(d):
371
name, path, desc = _find_plugin_module(d, p)
373
if name == '__init__':
374
# We do nothing with the __init__.py file in directories from
375
# the bzrlib.plugins module path, we may want to, one day
377
continue # We don't load __init__.py in the plugins dirs
378
elif getattr(_mod_plugins, name, None) is not None:
379
# The module has already been loaded from another directory
380
# during a previous call.
381
# FIXME: There should be a better way to report masked plugins
383
trace.mutter('Plugin name %s already loaded', name)
385
plugin_names.add(name)
387
for name in plugin_names:
388
_load_plugin_module(name, d)