1
# Copyright (C) 2005-2011 Canonical Ltd, 2017 Breezy developers
1
# Copyright (C) 2004, 2005 by Canonical Ltd
3
3
# This program is free software; you can redistribute it and/or modify
4
4
# it under the terms of the GNU General Public License as published by
5
5
# the Free Software Foundation; either version 2 of the License, or
6
6
# (at your option) any later version.
8
8
# This program is distributed in the hope that it will be useful,
9
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
11
# GNU General Public License for more details.
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
Any python module in $BZR_PLUGIN_PATH will be imported upon initialization of
21
bzrlib. The module will be imported as 'bzrlib.plugins.$BASENAME(PLUGIN)'.
22
In the plugin's main body, it should update any bzrlib registries it wants to
23
extend; for example, to add new commands, import bzrlib.commands and add your
24
new command to the plugin_cmds variable.
37
from __future__ import absolute_import
27
# TODO: Refactor this to make it more testable. The main problem at the
28
# moment is that loading plugins affects the global process state -- for bzr
29
# in general use it's a reasonable assumption that all plugins are loaded at
30
# startup and then stay loaded, but this is less good for testing.
32
# Several specific issues:
33
# - plugins can't be unloaded and will continue to effect later tests
34
# - load_plugins does nothing if called a second time
35
# - plugin hooks can't be removed
37
# Our options are either to remove these restrictions, or work around them by
38
# loading the plugins into a different space than the one running the tests.
39
# That could be either a separate Python interpreter or perhaps a new
40
# namespace inside this interpreter.
46
from .lazy_import import lazy_import
47
lazy_import(globals(), """
49
from importlib import util as importlib_util
64
_MODULE_PREFIX = "breezy.plugins."
66
if __debug__ or sys.version_info > (3,):
72
def disable_plugins(state=None):
73
"""Disable loading plugins.
75
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`
47
from bzrlib.config import config_dir
48
from bzrlib.trace import log_error, mutter, log_exception, warning, \
50
from bzrlib.errors import BzrError
51
from bzrlib import plugins
53
DEFAULT_PLUGIN_PATH = os.path.join(config_dir(), 'plugins')
60
"""Find all python plugins and load them.
62
Loading a plugin means importing it into the python interpreter.
63
The plugin is expected to make calls to register commands when
64
it's loaded (or perhaps access other hooks in future.)
66
A list of plugs is stored in bzrlib.plugin.all_plugins for future
69
The environment variable BZR_PLUGIN_PATH is considered a delimited
70
set of paths to look through. Each entry is searched for *.py
89
71
files (and whatever other extensions are used in the platform,
92
: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.
97
state = breezy.get_global_state()
98
if getattr(state, 'plugins', None) is not None:
75
global all_plugins, _loaded
99
77
# 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()
79
#raise BzrError("plugins already initialized")
82
dirs = os.environ.get('BZR_PLUGIN_PATH', DEFAULT_PLUGIN_PATH).split(":")
83
dirs.insert(0, os.path.dirname(plugins.__file__))
85
# The problem with imp.get_suffixes() is that it doesn't include
86
# .pyo which is technically valid
87
# It also means that "testmodule.so" will show up as both test and testmodule
88
# though it is only valid as 'test'
89
# but you should be careful, because "testmodule.py" loads as testmodule.
90
suffixes = imp.get_suffixes()
91
suffixes.append(('.pyo', 'rb', imp.PY_COMPILED))
92
package_entries = ['__init__.py', '__init__.pyc', '__init__.pyo']
94
# going through them one by one allows different plugins with the same
95
# filename in different directories in the path
96
mutter('looking for plugins in %s', d)
100
if not os.path.isdir(d):
102
for f in os.listdir(d):
103
path = os.path.join(d, f)
264
104
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]
105
for entry in package_entries:
106
# This directory should be a package, and thus added to
108
if os.path.isfile(os.path.join(path, entry)):
110
else: # This directory is not a package
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):
388
# We need to use relative path to system-wide plugin
389
# directory because breezy from standalone brz.exe
390
# could be imported by another standalone program
391
# (e.g. brz-config; or TortoiseBzr/Olive if/when they
392
# will become standalone exe). [bialix 20071123]
393
# __file__ typically is
394
# C:\Program Files\Bazaar\lib\library.zip\breezy\plugin.pyc
395
# then plugins directory is
396
# C:\Program Files\Bazaar\plugins
397
# so relative path is ../../../plugins
398
yield osutils.abspath(osutils.pathjoin(
399
osutils.dirname(__file__), '../../../plugins'))
400
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')
412
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
421
def _load_plugin_module(name, dir):
422
"""Load plugin by name.
424
:param name: The plugin name in the breezy.plugins namespace.
425
:param dir: The directory the plugin is loaded from for error messages.
427
if _MODULE_PREFIX + name in sys.modules:
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:
438
trace.log_exception_quietly()
439
if 'error' in debug.debug_flags:
440
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))
456
"""Return a dictionary of the plugins.
458
Each item in the dictionary is a PlugIn object.
461
for fullname in sys.modules:
462
if fullname.startswith(_MODULE_PREFIX):
463
name = fullname[len(_MODULE_PREFIX):]
464
if "." not in name and sys.modules[fullname] is not None:
465
result[name] = PlugIn(name, sys.modules[fullname])
469
def get_loaded_plugin(name):
470
"""Retrieve an already loaded plugin.
472
Returns None if there is no such plugin loaded
475
module = sys.modules[_MODULE_PREFIX + name]
480
return PlugIn(name, module)
483
def format_concise_plugin_list(state=None):
484
"""Return a string holding a concise list of plugins and their version.
487
state = breezy.get_global_state()
489
for name, a_plugin in sorted(getattr(state, 'plugins', {}).items()):
490
items.append("%s[%s]" %
491
(name, a_plugin.__version__))
492
return ', '.join(items)
495
class PluginsHelpIndex(object):
496
"""A help index that returns help topics for plugins."""
499
self.prefix = 'plugins/'
501
def get_topics(self, topic):
502
"""Search for topic in the loaded plugins.
504
This will not trigger loading of new plugins.
506
:param topic: A topic to search for.
507
:return: A list which is either empty or contains a single
508
RegisteredTopic entry.
512
if topic.startswith(self.prefix):
513
topic = topic[len(self.prefix):]
514
plugin_module_name = _MODULE_PREFIX + topic
516
module = sys.modules[plugin_module_name]
520
return [ModuleHelpTopic(module)]
523
class ModuleHelpTopic(object):
524
"""A help topic which returns the docstring for a module."""
526
def __init__(self, module):
529
:param module: The module for which help should be generated.
533
def get_help_text(self, additional_see_also=None, verbose=True):
534
"""Return a string with the help for this topic.
536
:param additional_see_also: Additional help topics to be
539
if not self.module.__doc__:
540
result = "Plugin '%s' has no docstring.\n" % self.module.__name__
542
result = self.module.__doc__
543
if result[-1] != '\n':
545
result += help_topics._format_see_also(additional_see_also)
548
def get_help_topic(self):
549
"""Return the module help topic: its basename."""
550
return self.module.__name__[len(_MODULE_PREFIX):]
553
class PlugIn(object):
554
"""The breezy representation of a plugin.
556
The PlugIn object provides a way to manipulate a given plugin module.
559
def __init__(self, name, module):
560
"""Construct a plugin for module."""
565
"""Get the path that this plugin was loaded from."""
566
if getattr(self.module, '__path__', None) is not None:
567
return os.path.abspath(self.module.__path__[0])
568
elif getattr(self.module, '__file__', None) is not None:
569
path = os.path.abspath(self.module.__file__)
570
if path[-4:] == COMPILED_EXT:
571
pypath = path[:-4] + '.py'
572
if os.path.isfile(pypath):
576
return repr(self.module)
579
return "<%s.%s name=%s, module=%s>" % (
580
self.__class__.__module__, self.__class__.__name__,
581
self.name, self.module)
583
def test_suite(self):
584
"""Return the plugin's test suite."""
585
if getattr(self.module, 'test_suite', None) is not None:
586
return self.module.test_suite()
590
def load_plugin_tests(self, loader):
591
"""Return the adapted plugin's test suite.
593
:param loader: The custom loader that should be used to load additional
596
if getattr(self.module, 'load_tests', None) is not None:
597
return loader.loadTestsFromModule(self.module)
601
def version_info(self):
602
"""Return the plugin's version_tuple or None if unknown."""
603
version_info = getattr(self.module, 'version_info', None)
604
if version_info is not None:
113
for suffix_info in suffixes:
114
if f.endswith(suffix_info[0]):
115
f = f[:-len(suffix_info[0])]
116
if suffix_info[2] == imp.C_EXTENSION and f.endswith('module'):
117
f = f[:-len('module')]
121
mutter('add plugin name %s', f)
124
plugin_names = list(plugin_names)
126
for name in plugin_names:
606
if isinstance(version_info, str):
607
version_info = version_info.split('.')
608
elif len(version_info) == 3:
609
version_info = tuple(version_info) + ('final', 0)
611
# The given version_info isn't even iteratible
612
trace.log_exception_quietly()
613
version_info = (version_info,)
617
def __version__(self):
618
version_info = self.version_info()
619
if version_info is None or len(version_info) == 0:
622
version_string = breezy._format_version_tuple(version_info)
623
except (ValueError, TypeError, IndexError):
624
trace.log_exception_quietly()
625
# Try to show something for the version anyway
626
version_string = '.'.join(map(str, version_info))
627
return version_string
630
class _PluginsAtFinder(object):
631
"""Meta path finder to support BRZ_PLUGINS_AT configuration."""
633
def __init__(self, prefix, names_and_paths):
635
self.names_to_path = dict((prefix + n, p) for n, p in names_and_paths)
638
return "<%s %r>" % (self.__class__.__name__, self.prefix)
640
def find_spec(self, fullname, paths, target=None):
641
"""New module spec returning find method."""
642
if fullname not in self.names_to_path:
644
path = self.names_to_path[fullname]
645
if os.path.isdir(path):
646
path = _get_package_init(path)
648
# GZ 2017-06-02: Any reason to block loading of the name from
649
# further down the path like this?
650
raise ImportError("Not loading namespace package %s as %s" % (
652
return importlib_util.spec_from_file_location(fullname, path)
654
def find_module(self, fullname, path):
655
"""Old PEP 302 import hook find_module method."""
656
if fullname not in self.names_to_path:
658
return _LegacyLoader(self.names_to_path[fullname])
661
class _LegacyLoader(object):
662
"""Source loader implementation for Python versions without importlib."""
664
def __init__(self, filepath):
665
self.filepath = filepath
668
return "<%s %r>" % (self.__class__.__name__, self.filepath)
670
def load_module(self, fullname):
671
"""Load a plugin from a specific directory (or file)."""
672
plugin_path = self.filepath
674
if os.path.isdir(plugin_path):
675
init_path = _get_package_init(plugin_path)
676
if init_path is not None:
677
loading_path = plugin_path
680
kind = imp.PKG_DIRECTORY
682
for suffix, mode, kind in imp.get_suffixes():
683
if plugin_path.endswith(suffix):
684
loading_path = plugin_path
686
if loading_path is None:
687
raise ImportError('%s cannot be loaded from %s'
688
% (fullname, plugin_path))
689
if kind is imp.PKG_DIRECTORY:
692
f = open(loading_path, mode)
694
mod = imp.load_module(fullname, f, loading_path,
695
(suffix, mode, kind))
696
mod.__package__ = fullname
128
plugin_info = imp.find_module(name, [d])
129
mutter('load plugin %r', plugin_info)
131
plugin = imp.load_module('bzrlib.plugins.' + name,
133
all_plugins.append(plugin)
134
setattr(bzrlib.plugins, name, plugin)
136
if plugin_info[0] is not None:
137
plugin_info[0].close()
139
mutter('loaded succesfully')
140
except KeyboardInterrupt:
143
## import pdb; pdb.set_trace()
144
warning('Unable to load plugin %r from %r' % (name, d))
145
log_exception_quietly()