13
13
# You should have received a copy of the GNU General Public License
14
14
# along with this program; if not, write to the Free Software
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.
33
See the plugin-api developer documentation for information about writing
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
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; for example, to add new
24
commands, import bzrlib.commands and add your new command to the plugin_cmds
27
BZR_PLUGIN_PATH is also honoured for any plugins imported via
28
'import bzrlib.plugins.PLUGINNAME', as long as set_plugins_path has been
44
from .lazy_import import lazy_import
35
from bzrlib.lazy_import import lazy_import
45
36
lazy_import(globals(), """
47
from importlib import util as importlib_util
62
_MODULE_PREFIX = "breezy.plugins."
67
def disable_plugins(state=None):
49
from bzrlib.trace import mutter, warning, log_exception_quietly
52
DEFAULT_PLUGIN_PATH = None
55
def get_default_plugin_path():
56
"""Get the DEFAULT_PLUGIN_PATH"""
57
global DEFAULT_PLUGIN_PATH
58
if DEFAULT_PLUGIN_PATH is None:
59
DEFAULT_PLUGIN_PATH = osutils.pathjoin(config.config_dir(), 'plugins')
60
return DEFAULT_PLUGIN_PATH
64
"""Return a dictionary of the plugins."""
66
for name, plugin in plugins.__dict__.items():
67
if isinstance(plugin, types.ModuleType):
72
def disable_plugins():
68
73
"""Disable loading plugins.
70
75
Future calls to load_plugins() will be ignored.
72
:param state: The library state object that records loaded plugins.
75
state = breezy.get_global_state()
79
def load_plugins(path=None, state=None, warn_load_problems=True):
80
"""Load breezy plugins.
82
The environment variable BRZ_PLUGIN_PATH is considered a delimited
83
set of paths to look through. Each entry is searched for `*.py`
77
# TODO: jam 20060131 This should probably also disable
83
def set_plugins_path():
84
"""Set the path for plugins to be loaded from."""
85
path = os.environ.get('BZR_PLUGIN_PATH',
86
get_default_plugin_path()).split(os.pathsep)
87
# search the plugin path before the bzrlib installed dir
88
path.append(os.path.dirname(plugins.__file__))
89
plugins.__path__ = path
94
"""Load bzrlib plugins.
96
The environment variable BZR_PLUGIN_PATH is considered a delimited
97
set of paths to look through. Each entry is searched for *.py
84
98
files (and whatever other extensions are used in the platform,
87
:param path: The list of paths to search for plugins. By default,
88
it is populated from the __path__ of the breezy.plugins package.
89
:param state: The library state object that records loaded plugins.
101
load_from_dirs() provides the underlying mechanism and is called with
102
the default directory list to provide the normal behaviour.
92
state = breezy.get_global_state()
93
if getattr(state, 'plugins', None) is not None:
94
106
# People can make sure plugins are loaded, they just won't be twice
98
# Calls back into extend_path() here
99
from breezy.plugins import __path__ as path
101
state.plugin_warnings = {}
102
_load_plugins_from_path(state, path)
103
if (None, 'entrypoints') in _env_plugin_path():
104
_load_plugins_from_entrypoints(state)
105
state.plugins = plugins()
106
if warn_load_problems:
107
for plugin, errors in state.plugin_warnings.items():
109
trace.warning('%s', error)
112
def _load_plugins_from_entrypoints(state):
116
# No pkg_resources, no entrypoints.
119
for ep in pkg_resources.iter_entry_points('breezy.plugin'):
120
fullname = _MODULE_PREFIX + ep.name
121
if fullname in sys.modules:
123
sys.modules[fullname] = ep.load()
126
def plugin_name(module_name):
127
"""Gives unprefixed name from module_name or None."""
128
if module_name.startswith(_MODULE_PREFIX):
129
parts = module_name.split(".")
135
def extend_path(path, name):
136
"""Helper so breezy.plugins can be a sort of namespace package.
138
To be used in similar fashion to pkgutil.extend_path:
140
from breezy.plugins import extend_path
141
__path__ = extend_path(__path__, __name__)
143
Inspects the BRZ_PLUGIN* envvars, sys.path, and the filesystem to find
144
plugins. May mutate sys.modules in order to block plugin loading, and may
145
append a new meta path finder to sys.meta_path for plugins@ loading.
147
Returns a list of paths to import from, as an enhanced object that also
148
contains details of the other configuration used.
150
blocks = _env_disable_plugins()
151
_block_plugins(blocks)
153
extra_details = _env_plugins_at()
154
_install_importer_if_needed(extra_details)
156
paths = _iter_plugin_paths(_env_plugin_path(), path)
158
return _Path(name, blocks, extra_details, paths)
162
"""List type to use as __path__ but containing additional details.
164
Python 3 allows any iterable for __path__ but Python 2 is more fussy.
167
def __init__(self, package_name, blocked, extra, paths):
168
super(_Path, self).__init__(paths)
169
self.package_name = package_name
170
self.blocked_names = blocked
171
self.extra_details = extra
174
return "%s(%r, %r, %r, %s)" % (
175
self.__class__.__name__, self.package_name, self.blocked_names,
176
self.extra_details, list.__repr__(self))
179
def _expect_identifier(name, env_key, env_value):
180
"""Validate given name from envvar is usable as a Python identifier.
182
Returns the name as a native str, or None if it was invalid.
184
Per PEP 3131 this is no longer strictly correct for Python 3, but as MvL
185
didn't include a neat way to check except eval, this enforces ascii.
187
if re.match(r"^[A-Za-z_][A-Za-z0-9_]*$", name) is None:
188
trace.warning("Invalid name '%s' in %s='%s'", name, env_key, env_value)
193
def _env_disable_plugins(key='BRZ_DISABLE_PLUGINS'):
194
"""Gives list of names for plugins to disable from environ key."""
196
env = os.environ.get(key)
198
for name in env.split(os.pathsep):
199
name = _expect_identifier(name, key, env)
201
disabled_names.append(name)
202
return disabled_names
205
def _env_plugins_at(key='BRZ_PLUGINS_AT'):
206
"""Gives list of names and paths of specific plugins from environ key."""
208
env = os.environ.get(key)
210
for pair in env.split(os.pathsep):
212
name, path = pair.split('@', 1)
215
name = osutils.basename(path).split('.', 1)[0]
216
name = _expect_identifier(name, key, env)
218
plugin_details.append((name, path))
219
return plugin_details
222
def _env_plugin_path(key='BRZ_PLUGIN_PATH'):
223
"""Gives list of paths and contexts for plugins from environ key.
225
Each entry is either a specific path to load plugins from and the value
226
'path', or None and one of the values 'user', 'core', 'entrypoints', 'site'.
229
env = os.environ.get(key)
234
'entrypoints': False,
237
# Add paths specified by user in order
238
for p in env.split(os.pathsep):
239
flag, name = p[:1], p[1:]
240
if flag in ("+", "-") and name in defaults:
241
if flag == "+" and defaults[name] is not None:
242
path_details.append((None, name))
243
defaults[name] = None
245
path_details.append((p, 'path'))
247
# Add any remaining default paths
248
for name in ('user', 'core', 'entrypoints', 'site'):
250
path_details.append((None, name))
255
def _iter_plugin_paths(paths_from_env, core_paths):
256
"""Generate paths using paths_from_env and core_paths."""
257
# GZ 2017-06-02: This is kinda horrid, should make better.
258
for path, context in paths_from_env:
259
if context == 'path':
261
elif context == 'user':
262
path = get_user_plugin_path()
263
if os.path.isdir(path):
265
elif context == 'core':
266
for path in _get_core_plugin_paths(core_paths):
268
elif context == 'site':
269
for path in _get_site_plugin_paths(sys.path):
270
if os.path.isdir(path):
274
def _install_importer_if_needed(plugin_details):
275
"""Install a meta path finder to handle plugin_details if any."""
277
finder = _PluginsAtFinder(_MODULE_PREFIX, plugin_details)
278
# For Python 3, must insert before default PathFinder to override.
279
sys.meta_path.insert(2, finder)
282
def _load_plugins_from_path(state, paths):
283
"""Do the importing all plugins from paths."""
284
imported_names = set()
285
for name, path in _iter_possible_plugins(paths):
286
if name not in imported_names:
287
msg = _load_plugin_module(name, path)
289
state.plugin_warnings.setdefault(name, []).append(msg)
290
imported_names.add(name)
293
def _block_plugins(names):
294
"""Add names to sys.modules to block future imports."""
296
package_name = _MODULE_PREFIX + name
297
if sys.modules.get(package_name) is not None:
298
trace.mutter("Blocked plugin %s already loaded.", name)
299
sys.modules[package_name] = None
302
def _get_package_init(package_path):
303
"""Get path of __init__ file from package_path or None if not a package."""
304
init_path = osutils.pathjoin(package_path, "__init__.py")
305
if os.path.exists(init_path):
307
init_path = init_path[:-3] + COMPILED_EXT
308
if os.path.exists(init_path):
313
def _iter_possible_plugins(plugin_paths):
314
"""Generate names and paths of possible plugins from plugin_paths."""
315
# Inspect any from BRZ_PLUGINS_AT first.
316
for name, path in getattr(plugin_paths, "extra_details", ()):
318
# Then walk over files and directories in the paths from the package.
319
for path in plugin_paths:
320
if os.path.isfile(path):
321
if path.endswith(".zip"):
322
trace.mutter("Don't yet support loading plugins from zip.")
324
for name, path in _walk_modules(path):
328
def _walk_modules(path):
329
"""Generate name and path of modules and packages on path."""
330
for root, dirs, files in os.walk(path):
334
if f.endswith((".py", COMPILED_EXT)):
335
yield f.rsplit(".", 1)[0], root
339
package_dir = osutils.pathjoin(root, d)
340
fullpath = _get_package_init(package_dir)
341
if fullpath is not None:
343
# Don't descend into subdirectories
347
def describe_plugins(show_paths=False, state=None):
348
"""Generate text description of plugins.
350
Includes both those that have loaded, and those that failed to load.
352
:param show_paths: If true, include the plugin path.
353
:param state: The library state object to inspect.
354
:returns: Iterator of text lines (including newlines.)
357
state = breezy.get_global_state()
358
loaded_plugins = getattr(state, 'plugins', {})
359
plugin_warnings = set(getattr(state, 'plugin_warnings', []))
360
all_names = sorted(set(loaded_plugins.keys()).union(plugin_warnings))
361
for name in all_names:
362
if name in loaded_plugins:
363
plugin = loaded_plugins[name]
364
version = plugin.__version__
365
if version == 'unknown':
367
yield '%s %s\n' % (name, version)
368
d = plugin.module.__doc__
370
doc = d.split('\n')[0]
372
doc = '(no description)'
373
yield (" %s\n" % doc)
375
yield (" %s\n" % plugin.path())
377
yield "%s (failed to load)\n" % name
378
if name in state.plugin_warnings:
379
for line in state.plugin_warnings[name]:
380
yield " ** " + line + '\n'
384
def _get_core_plugin_paths(existing_paths):
385
"""Generate possible locations for plugins based on existing_paths."""
386
if getattr(sys, 'frozen', False):
387
# We need to use relative path to system-wide plugin
388
# directory because breezy from standalone brz.exe
389
# could be imported by another standalone program
390
# (e.g. brz-config; or TortoiseBzr/Olive if/when they
391
# will become standalone exe). [bialix 20071123]
392
# __file__ typically is
393
# C:\Program Files\Bazaar\lib\library.zip\breezy\plugin.pyc
394
# then plugins directory is
395
# C:\Program Files\Bazaar\plugins
396
# so relative path is ../../../plugins
397
yield osutils.abspath(osutils.pathjoin(
398
osutils.dirname(__file__), '../../../plugins'))
399
else: # don't look inside library.zip
400
for path in existing_paths:
404
def _get_site_plugin_paths(sys_paths):
405
"""Generate possible locations for plugins from given sys_paths."""
406
for path in sys_paths:
407
if os.path.basename(path) in ('dist-packages', 'site-packages'):
408
yield osutils.pathjoin(path, 'breezy', 'plugins')
411
def get_user_plugin_path():
412
return osutils.pathjoin(bedding.config_dir(), 'plugins')
415
def record_plugin_warning(warning_message):
416
trace.mutter(warning_message)
417
return warning_message
420
def _load_plugin_module(name, dir):
421
"""Load plugin by name.
423
:param name: The plugin name in the breezy.plugins namespace.
424
:param dir: The directory the plugin is loaded from for error messages.
426
if _MODULE_PREFIX + name in sys.modules:
429
__import__(_MODULE_PREFIX + name)
430
except errors.IncompatibleVersion as e:
432
"Unable to load plugin %r. It supports %s "
433
"versions %r but the current version is %s" %
434
(name, e.api.__name__, e.wanted, e.current))
435
return record_plugin_warning(warning_message)
436
except Exception as e:
437
trace.log_exception_quietly()
438
if 'error' in debug.debug_flags:
439
trace.print_exception(sys.exc_info(), sys.stderr)
440
# GZ 2017-06-02: Move this name checking up a level, no point trying
441
# to import things with bad names.
442
if re.search('\\.|-| ', name):
443
sanitised_name = re.sub('[-. ]', '_', name)
444
if sanitised_name.startswith('brz_'):
445
sanitised_name = sanitised_name[len('brz_'):]
446
trace.warning("Unable to load %r in %r as a plugin because the "
447
"file path isn't a valid module name; try renaming "
448
"it to %r." % (name, dir, sanitised_name))
450
return record_plugin_warning(
451
'Unable to load plugin %r from %r: %s' % (name, dir, e))
455
"""Return a dictionary of the plugins.
457
Each item in the dictionary is a PlugIn object.
460
for fullname in sys.modules:
461
if fullname.startswith(_MODULE_PREFIX):
462
name = fullname[len(_MODULE_PREFIX):]
463
if "." not in name and sys.modules[fullname] is not None:
464
result[name] = PlugIn(name, sys.modules[fullname])
468
def get_loaded_plugin(name):
469
"""Retrieve an already loaded plugin.
471
Returns None if there is no such plugin loaded
474
module = sys.modules[_MODULE_PREFIX + name]
479
return PlugIn(name, module)
482
def format_concise_plugin_list(state=None):
483
"""Return a string holding a concise list of plugins and their version.
486
state = breezy.get_global_state()
488
for name, a_plugin in sorted(getattr(state, 'plugins', {}).items()):
489
items.append("%s[%s]" %
490
(name, a_plugin.__version__))
491
return ', '.join(items)
494
class PluginsHelpIndex(object):
495
"""A help index that returns help topics for plugins."""
498
self.prefix = 'plugins/'
500
def get_topics(self, topic):
501
"""Search for topic in the loaded plugins.
503
This will not trigger loading of new plugins.
505
:param topic: A topic to search for.
506
:return: A list which is either empty or contains a single
507
RegisteredTopic entry.
511
if topic.startswith(self.prefix):
512
topic = topic[len(self.prefix):]
513
plugin_module_name = _MODULE_PREFIX + topic
515
module = sys.modules[plugin_module_name]
519
return [ModuleHelpTopic(module)]
522
class ModuleHelpTopic(object):
523
"""A help topic which returns the docstring for a module."""
525
def __init__(self, module):
528
:param module: The module for which help should be generated.
532
def get_help_text(self, additional_see_also=None, verbose=True):
533
"""Return a string with the help for this topic.
535
:param additional_see_also: Additional help topics to be
538
if not self.module.__doc__:
539
result = "Plugin '%s' has no docstring.\n" % self.module.__name__
541
result = self.module.__doc__
542
if result[-1] != '\n':
544
result += help_topics._format_see_also(additional_see_also)
547
def get_help_topic(self):
548
"""Return the module help topic: its basename."""
549
return self.module.__name__[len(_MODULE_PREFIX):]
552
class PlugIn(object):
553
"""The breezy representation of a plugin.
555
The PlugIn object provides a way to manipulate a given plugin module.
558
def __init__(self, name, module):
559
"""Construct a plugin for module."""
564
"""Get the path that this plugin was loaded from."""
565
if getattr(self.module, '__path__', None) is not None:
566
return os.path.abspath(self.module.__path__[0])
567
elif getattr(self.module, '__file__', None) is not None:
568
path = os.path.abspath(self.module.__file__)
569
if path[-4:] == COMPILED_EXT:
570
pypath = path[:-4] + '.py'
571
if os.path.isfile(pypath):
575
return repr(self.module)
578
return "<%s.%s name=%s, module=%s>" % (
579
self.__class__.__module__, self.__class__.__name__,
580
self.name, self.module)
582
def test_suite(self):
583
"""Return the plugin's test suite."""
584
if getattr(self.module, 'test_suite', None) is not None:
585
return self.module.test_suite()
589
def load_plugin_tests(self, loader):
590
"""Return the adapted plugin's test suite.
592
:param loader: The custom loader that should be used to load additional
595
if getattr(self.module, 'load_tests', None) is not None:
596
return loader.loadTestsFromModule(self.module)
600
def version_info(self):
601
"""Return the plugin's version_tuple or None if unknown."""
602
version_info = getattr(self.module, 'version_info', None)
603
if version_info is not None:
605
if isinstance(version_info, str):
606
version_info = version_info.split('.')
607
elif len(version_info) == 3:
608
version_info = tuple(version_info) + ('final', 0)
610
# The given version_info isn't even iteratible
611
trace.log_exception_quietly()
612
version_info = (version_info,)
616
def __version__(self):
617
version_info = self.version_info()
618
if version_info is None or len(version_info) == 0:
621
version_string = breezy._format_version_tuple(version_info)
622
except (ValueError, TypeError, IndexError):
623
trace.log_exception_quietly()
624
# Try to show something for the version anyway
625
version_string = '.'.join(map(str, version_info))
626
return version_string
629
class _PluginsAtFinder(object):
630
"""Meta path finder to support BRZ_PLUGINS_AT configuration."""
632
def __init__(self, prefix, names_and_paths):
634
self.names_to_path = dict((prefix + n, p) for n, p in names_and_paths)
637
return "<%s %r>" % (self.__class__.__name__, self.prefix)
639
def find_spec(self, fullname, paths, target=None):
640
"""New module spec returning find method."""
641
if fullname not in self.names_to_path:
643
path = self.names_to_path[fullname]
110
# scan for all plugins in the path.
111
load_from_path(set_plugins_path())
114
def load_from_path(dirs):
115
"""Load bzrlib plugins found in each dir in dirs.
117
Loading a plugin means importing it into the python interpreter.
118
The plugin is expected to make calls to register commands when
119
it's loaded (or perhaps access other hooks in future.)
121
Plugins are loaded into bzrlib.plugins.NAME, and can be found there
122
for future reference.
124
The python module path for bzrlib.plugins will be modified to be 'dirs'.
126
plugins.__path__ = dirs
130
mutter('looking for plugins in %s', d)
134
# it might be a zip: try loading from the zip.
139
# backwards compatability: load_from_dirs was the old name
140
# This was changed in 0.15
141
load_from_dirs = load_from_path
144
def load_from_dir(d):
145
"""Load the plugins in directory d."""
146
# Get the list of valid python suffixes for __init__.py?
147
# this includes .py, .pyc, and .pyo (depending on if we are running -O)
148
# but it doesn't include compiled modules (.so, .dll, etc)
149
valid_suffixes = [suffix for suffix, mod_type, flags in imp.get_suffixes()
150
if flags in (imp.PY_SOURCE, imp.PY_COMPILED)]
151
package_entries = ['__init__'+suffix for suffix in valid_suffixes]
153
for f in os.listdir(d):
154
path = osutils.pathjoin(d, f)
644
155
if os.path.isdir(path):
645
path = _get_package_init(path)
647
# GZ 2017-06-02: Any reason to block loading of the name from
648
# further down the path like this?
649
raise ImportError("Not loading namespace package %s as %s" % (
651
return importlib_util.spec_from_file_location(fullname, path)
653
def find_module(self, fullname, path):
654
"""Old PEP 302 import hook find_module method."""
655
if fullname not in self.names_to_path:
657
return _LegacyLoader(self.names_to_path[fullname])
660
class _LegacyLoader(object):
661
"""Source loader implementation for Python versions without importlib."""
663
def __init__(self, filepath):
664
self.filepath = filepath
667
return "<%s %r>" % (self.__class__.__name__, self.filepath)
669
def load_module(self, fullname):
670
"""Load a plugin from a specific directory (or file)."""
671
plugin_path = self.filepath
673
if os.path.isdir(plugin_path):
674
init_path = _get_package_init(plugin_path)
675
if init_path is not None:
676
loading_path = plugin_path
679
kind = imp.PKG_DIRECTORY
681
for suffix, mode, kind in imp.get_suffixes():
682
if plugin_path.endswith(suffix):
683
loading_path = plugin_path
685
if loading_path is None:
686
raise ImportError('%s cannot be loaded from %s'
687
% (fullname, plugin_path))
688
if kind is imp.PKG_DIRECTORY:
691
f = open(loading_path, mode)
693
mod = imp.load_module(fullname, f, loading_path,
694
(suffix, mode, kind))
695
mod.__package__ = fullname
156
for entry in package_entries:
157
# This directory should be a package, and thus added to
159
if os.path.isfile(osutils.pathjoin(path, entry)):
161
else: # This directory is not a package
164
for suffix_info in imp.get_suffixes():
165
if f.endswith(suffix_info[0]):
166
f = f[:-len(suffix_info[0])]
167
if suffix_info[2] == imp.C_EXTENSION and f.endswith('module'):
168
f = f[:-len('module')]
172
if getattr(plugins, f, None):
173
mutter('Plugin name %s already loaded', f)
175
# mutter('add plugin name %s', f)
178
for name in plugin_names:
180
exec "import bzrlib.plugins.%s" % name in {}
181
except KeyboardInterrupt:
184
## import pdb; pdb.set_trace()
185
if re.search('\.|-| ', name):
186
warning('Unable to load plugin %r from %r: '
187
'It is not a valid python module name.' % (name, d))
189
warning('Unable to load plugin %r from %r' % (name, d))
190
log_exception_quietly()
193
def load_from_zip(zip_name):
194
"""Load all the plugins in a zip."""
195
valid_suffixes = ('.py', '.pyc', '.pyo') # only python modules/packages
197
if '.zip' not in zip_name:
200
ziobj = zipimport.zipimporter(zip_name)
201
except zipimport.ZipImportError:
204
mutter('Looking for plugins in %r', zip_name)
208
# use zipfile to get list of files/dirs inside zip
209
z = zipfile.ZipFile(ziobj.archive)
210
namelist = z.namelist()
214
prefix = ziobj.prefix.replace('\\','/')
216
namelist = [name[ix:]
218
if name.startswith(prefix)]
220
mutter('Names in archive: %r', namelist)
222
for name in namelist:
223
if not name or name.endswith('/'):
226
# '/' is used to separate pathname components inside zip archives
229
head, tail = '', name
231
head, tail = name.rsplit('/',1)
233
# we don't need looking in subdirectories
236
base, suffix = osutils.splitext(tail)
237
if suffix not in valid_suffixes:
240
if base == '__init__':
251
if getattr(plugins, plugin_name, None):
252
mutter('Plugin name %s already loaded', plugin_name)
256
plugin = ziobj.load_module(plugin_name)
257
setattr(plugins, plugin_name, plugin)
258
mutter('Load plugin %s from zip %r', plugin_name, zip_name)
259
except zipimport.ZipImportError, e:
260
mutter('Unable to load plugin %r from %r: %s',
261
plugin_name, zip_name, str(e))
263
except KeyboardInterrupt:
266
## import pdb; pdb.set_trace()
267
warning('Unable to load plugin %r from %r'
269
log_exception_quietly()