1
# Copyright (C) 2004, 2005 by Canonical Ltd
1
# Copyright (C) 2005-2011 Canonical Ltd, 2017 Breezy developers
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., 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.
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
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.
37
from __future__ import absolute_import
48
from bzrlib.config import config_dir
49
from bzrlib.trace import log_error, mutter, log_exception, warning, \
51
from bzrlib.errors import BzrError
52
from bzrlib import plugins
53
from bzrlib.osutils import pathjoin
55
DEFAULT_PLUGIN_PATH = pathjoin(config_dir(), 'plugins')
61
"""Return a dictionary of the plugins."""
63
for name, plugin in bzrlib.plugins.__dict__.items():
64
if isinstance(plugin, types.ModuleType):
69
def disable_plugins():
46
from .lazy_import import lazy_import
47
lazy_import(globals(), """
49
from importlib import util as importlib_util
61
_MODULE_PREFIX = "breezy.plugins."
63
if __debug__ or sys.version_info > (3,):
69
def disable_plugins(state=None):
70
70
"""Disable loading plugins.
72
72
Future calls to load_plugins() will be ignored.
74
:param state: The library state object that records loaded plugins.
74
# TODO: jam 20060131 This should probably also disable
81
"""Load bzrlib plugins.
83
The environment variable BZR_PLUGIN_PATH is considered a delimited
84
set of paths to look through. Each entry is searched for *.py
77
state = breezy.get_global_state()
81
def load_plugins(path=None, state=None):
82
"""Load breezy plugins.
84
The environment variable BRZ_PLUGIN_PATH is considered a delimited
85
set of paths to look through. Each entry is searched for `*.py`
85
86
files (and whatever other extensions are used in the platform,
88
load_from_dirs() provides the underlying mechanism and is called with
89
the default directory list to provide the normal behaviour.
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.
94
state = breezy.get_global_state()
95
if getattr(state, 'plugins', None) is not None:
93
96
# People can make sure plugins are loaded, they just won't be twice
95
#raise BzrError("plugins already initialized")
98
dirs = os.environ.get('BZR_PLUGIN_PATH', DEFAULT_PLUGIN_PATH).split(os.pathsep)
99
dirs.insert(0, os.path.dirname(plugins.__file__))
104
def load_from_dirs(dirs):
105
"""Load bzrlib plugins found in each dir in dirs.
107
Loading a plugin means importing it into the python interpreter.
108
The plugin is expected to make calls to register commands when
109
it's loaded (or perhaps access other hooks in future.)
111
Plugins are loaded into bzrlib.plugins.NAME, and can be found there
112
for future reference.
114
# The problem with imp.get_suffixes() is that it doesn't include
115
# .pyo which is technically valid
116
# It also means that "testmodule.so" will show up as both test and testmodule
117
# though it is only valid as 'test'
118
# but you should be careful, because "testmodule.py" loads as testmodule.
119
suffixes = imp.get_suffixes()
120
suffixes.append(('.pyo', 'rb', imp.PY_COMPILED))
121
package_entries = ['__init__.py', '__init__.pyc', '__init__.pyo']
125
mutter('looking for plugins in %s', d)
127
if not os.path.isdir(d):
129
for f in os.listdir(d):
130
path = pathjoin(d, f)
100
# Calls back into extend_path() here
101
from breezy.plugins import __path__ as path
103
state.plugin_warnings = {}
104
_load_plugins(state, path)
105
state.plugins = plugins()
108
def plugin_name(module_name):
109
"""Gives unprefixed name from module_name or None."""
110
if module_name.startswith(_MODULE_PREFIX):
111
parts = module_name.split(".")
117
def extend_path(path, name):
118
"""Helper so breezy.plugins can be a sort of namespace package.
120
To be used in similar fashion to pkgutil.extend_path:
122
from breezy.plugins import extend_path
123
__path__ = extend_path(__path__, __name__)
125
Inspects the BRZ_PLUGIN* envvars, sys.path, and the filesystem to find
126
plugins. May mutate sys.modules in order to block plugin loading, and may
127
append a new meta path finder to sys.meta_path for plugins@ loading.
129
Returns a list of paths to import from, as an enhanced object that also
130
contains details of the other configuration used.
132
blocks = _env_disable_plugins()
133
_block_plugins(blocks)
135
extra_details = _env_plugins_at()
136
_install_importer_if_needed(extra_details)
138
paths = _iter_plugin_paths(_env_plugin_path(), path)
140
return _Path(name, blocks, extra_details, paths)
144
"""List type to use as __path__ but containing additional details.
146
Python 3 allows any iterable for __path__ but Python 2 is more fussy.
149
def __init__(self, package_name, blocked, extra, paths):
150
super(_Path, self).__init__(paths)
151
self.package_name = package_name
152
self.blocked_names = blocked
153
self.extra_details = extra
156
return "%s(%r, %r, %r, %s)" % (
157
self.__class__.__name__, self.package_name, self.blocked_names,
158
self.extra_details, list.__repr__(self))
161
def _expect_identifier(name, env_key, env_value):
162
"""Validate given name from envvar is usable as a Python identifier.
164
Returns the name as a native str, or None if it was invalid.
166
Per PEP 3131 this is no longer strictly correct for Python 3, but as MvL
167
didn't include a neat way to check except eval, this enforces ascii.
169
if re.match(r"^[A-Za-z_][A-Za-z0-9_]*$", name) is None:
170
trace.warning("Invalid name '%s' in %s='%s'", name, env_key, env_value)
175
def _env_disable_plugins(key='BRZ_DISABLE_PLUGINS'):
176
"""Gives list of names for plugins to disable from environ key."""
178
env = osutils.path_from_environ(key)
180
for name in env.split(os.pathsep):
181
name = _expect_identifier(name, key, env)
183
disabled_names.append(name)
184
return disabled_names
187
def _env_plugins_at(key='BRZ_PLUGINS_AT'):
188
"""Gives list of names and paths of specific plugins from environ key."""
190
env = osutils.path_from_environ(key)
192
for pair in env.split(os.pathsep):
194
name, path = pair.split('@', 1)
197
name = osutils.basename(path).split('.', 1)[0]
198
name = _expect_identifier(name, key, env)
200
plugin_details.append((name, path))
201
return plugin_details
204
def _env_plugin_path(key='BRZ_PLUGIN_PATH'):
205
"""Gives list of paths and contexts for plugins from environ key.
207
Each entry is either a specific path to load plugins from and the value
208
'path', or None and one of the three values 'user', 'core', 'site'.
211
env = osutils.path_from_environ(key)
212
defaults = {"user": not env, "core": True, "site": True}
214
# Add paths specified by user in order
215
for p in env.split(os.pathsep):
216
flag, name = p[:1], p[1:]
217
if flag in ("+", "-") and name in defaults:
218
if flag == "+" and defaults[name] is not None:
219
path_details.append((None, name))
220
defaults[name] = None
222
path_details.append((p, 'path'))
224
# Add any remaining default paths
225
for name in ('user', 'core', 'site'):
227
path_details.append((None, name))
232
def _iter_plugin_paths(paths_from_env, core_paths):
233
"""Generate paths using paths_from_env and core_paths."""
234
# GZ 2017-06-02: This is kinda horrid, should make better.
235
for path, context in paths_from_env:
236
if context == 'path':
238
elif context == 'user':
239
path = get_user_plugin_path()
131
240
if os.path.isdir(path):
132
for entry in package_entries:
133
# This directory should be a package, and thus added to
135
if os.path.isfile(pathjoin(path, entry)):
137
else: # This directory is not a package
140
for suffix_info in suffixes:
141
if f.endswith(suffix_info[0]):
142
f = f[:-len(suffix_info[0])]
143
if suffix_info[2] == imp.C_EXTENSION and f.endswith('module'):
144
f = f[:-len('module')]
148
if getattr(bzrlib.plugins, f, None):
149
mutter('Plugin name %s already loaded', f)
151
mutter('add plugin name %s', f)
154
plugin_names = list(plugin_names)
156
for name in plugin_names:
242
elif context == 'core':
243
for path in _get_core_plugin_paths(core_paths):
245
elif context == 'site':
246
for path in _get_site_plugin_paths(sys.path):
247
if os.path.isdir(path):
251
def _install_importer_if_needed(plugin_details):
252
"""Install a meta path finder to handle plugin_details if any."""
254
finder = _PluginsAtFinder(_MODULE_PREFIX, plugin_details)
255
# For Python 3, must insert before default PathFinder to override.
256
sys.meta_path.insert(2, finder)
259
def _load_plugins(state, paths):
260
"""Do the importing all plugins from paths."""
261
imported_names = set()
262
for name, path in _iter_possible_plugins(paths):
263
if name not in imported_names:
264
msg = _load_plugin_module(name, path)
266
state.plugin_warnings.setdefault(name, []).append(msg)
267
imported_names.add(name)
270
def _block_plugins(names):
271
"""Add names to sys.modules to block future imports."""
273
package_name = _MODULE_PREFIX + name
274
if sys.modules.get(package_name) is not None:
275
trace.mutter("Blocked plugin %s already loaded.", name)
276
sys.modules[package_name] = None
279
def _get_package_init(package_path):
280
"""Get path of __init__ file from package_path or None if not a package."""
281
init_path = osutils.pathjoin(package_path, "__init__.py")
282
if os.path.exists(init_path):
284
init_path = init_path[:-3] + COMPILED_EXT
285
if os.path.exists(init_path):
290
def _iter_possible_plugins(plugin_paths):
291
"""Generate names and paths of possible plugins from plugin_paths."""
292
# Inspect any from BRZ_PLUGINS_AT first.
293
for name, path in getattr(plugin_paths, "extra_details", ()):
295
# Then walk over files and directories in the paths from the package.
296
for path in plugin_paths:
297
if os.path.isfile(path):
298
if path.endswith(".zip"):
299
trace.mutter("Don't yet support loading plugins from zip.")
301
for name, path in _walk_modules(path):
305
def _walk_modules(path):
306
"""Generate name and path of modules and packages on path."""
307
for root, dirs, files in os.walk(path):
311
if f.endswith((".py", COMPILED_EXT)):
312
yield f.rsplit(".", 1)[0], root
316
package_dir = osutils.pathjoin(root, d)
317
fullpath = _get_package_init(package_dir)
318
if fullpath is not None:
320
# Don't descend into subdirectories
324
def describe_plugins(show_paths=False, state=None):
325
"""Generate text description of plugins.
327
Includes both those that have loaded, and those that failed to load.
329
:param show_paths: If true, include the plugin path.
330
:param state: The library state object to inspect.
331
:returns: Iterator of text lines (including newlines.)
334
state = breezy.get_global_state()
335
loaded_plugins = getattr(state, 'plugins', {})
336
plugin_warnings = set(getattr(state, 'plugin_warnings', []))
337
all_names = sorted(set(loaded_plugins.keys()).union(plugin_warnings))
338
for name in all_names:
339
if name in loaded_plugins:
340
plugin = loaded_plugins[name]
341
version = plugin.__version__
342
if version == 'unknown':
344
yield '%s %s\n' % (name, version)
345
d = plugin.module.__doc__
347
doc = d.split('\n')[0]
349
doc = '(no description)'
350
yield (" %s\n" % doc)
352
yield (" %s\n" % plugin.path())
354
yield "%s (failed to load)\n" % name
355
if name in state.plugin_warnings:
356
for line in state.plugin_warnings[name]:
357
yield " ** " + line + '\n'
361
def _get_core_plugin_paths(existing_paths):
362
"""Generate possible locations for plugins based on existing_paths."""
363
if getattr(sys, 'frozen', False):
364
# We need to use relative path to system-wide plugin
365
# directory because breezy from standalone brz.exe
366
# could be imported by another standalone program
367
# (e.g. brz-config; or TortoiseBzr/Olive if/when they
368
# will become standalone exe). [bialix 20071123]
369
# __file__ typically is
370
# C:\Program Files\Bazaar\lib\library.zip\breezy\plugin.pyc
371
# then plugins directory is
372
# C:\Program Files\Bazaar\plugins
373
# so relative path is ../../../plugins
374
yield osutils.abspath(osutils.pathjoin(
375
osutils.dirname(__file__), '../../../plugins'))
376
else: # don't look inside library.zip
377
for path in existing_paths:
381
def _get_site_plugin_paths(sys_paths):
382
"""Generate possible locations for plugins from given sys_paths."""
383
for path in sys_paths:
384
if os.path.basename(path) in ('dist-packages', 'site-packages'):
385
yield osutils.pathjoin(path, 'breezy', 'plugins')
388
def get_user_plugin_path():
389
return osutils.pathjoin(config.config_dir(), 'plugins')
392
def record_plugin_warning(warning_message):
393
trace.mutter(warning_message)
394
return warning_message
397
def _load_plugin_module(name, dir):
398
"""Load plugin by name.
400
:param name: The plugin name in the breezy.plugins namespace.
401
:param dir: The directory the plugin is loaded from for error messages.
403
if _MODULE_PREFIX + name in sys.modules:
406
__import__(_MODULE_PREFIX + name)
407
except errors.IncompatibleVersion as e:
409
"Unable to load plugin %r. It supports %s "
410
"versions %r but the current version is %s" %
411
(name, e.api.__name__, e.wanted, e.current))
412
return record_plugin_warning(warning_message)
413
except Exception as e:
414
trace.log_exception_quietly()
415
if 'error' in debug.debug_flags:
416
trace.print_exception(sys.exc_info(), sys.stderr)
417
# GZ 2017-06-02: Move this name checking up a level, no point trying
418
# to import things with bad names.
419
if re.search('\\.|-| ', name):
420
sanitised_name = re.sub('[-. ]', '_', name)
421
if sanitised_name.startswith('brz_'):
422
sanitised_name = sanitised_name[len('brz_'):]
423
trace.warning("Unable to load %r in %r as a plugin because the "
424
"file path isn't a valid module name; try renaming "
425
"it to %r." % (name, dir, sanitised_name))
427
return record_plugin_warning(
428
'Unable to load plugin %r from %r: %s' % (name, dir, e))
432
"""Return a dictionary of the plugins.
434
Each item in the dictionary is a PlugIn object.
437
for fullname in sys.modules:
438
if fullname.startswith(_MODULE_PREFIX):
439
name = fullname[len(_MODULE_PREFIX):]
440
if "." not in name and sys.modules[fullname] is not None:
441
result[name] = PlugIn(name, sys.modules[fullname])
445
def get_loaded_plugin(name):
446
"""Retrieve an already loaded plugin.
448
Returns None if there is no such plugin loaded
451
module = sys.modules[_MODULE_PREFIX + name]
456
return PlugIn(name, module)
459
def format_concise_plugin_list(state=None):
460
"""Return a string holding a concise list of plugins and their version.
463
state = breezy.get_global_state()
465
for name, a_plugin in sorted(getattr(state, 'plugins', {}).items()):
466
items.append("%s[%s]" %
467
(name, a_plugin.__version__))
468
return ', '.join(items)
471
class PluginsHelpIndex(object):
472
"""A help index that returns help topics for plugins."""
475
self.prefix = 'plugins/'
477
def get_topics(self, topic):
478
"""Search for topic in the loaded plugins.
480
This will not trigger loading of new plugins.
482
:param topic: A topic to search for.
483
:return: A list which is either empty or contains a single
484
RegisteredTopic entry.
488
if topic.startswith(self.prefix):
489
topic = topic[len(self.prefix):]
490
plugin_module_name = _MODULE_PREFIX + topic
492
module = sys.modules[plugin_module_name]
496
return [ModuleHelpTopic(module)]
499
class ModuleHelpTopic(object):
500
"""A help topic which returns the docstring for a module."""
502
def __init__(self, module):
505
:param module: The module for which help should be generated.
509
def get_help_text(self, additional_see_also=None, verbose=True):
510
"""Return a string with the help for this topic.
512
:param additional_see_also: Additional help topics to be
515
if not self.module.__doc__:
516
result = "Plugin '%s' has no docstring.\n" % self.module.__name__
518
result = self.module.__doc__
519
if result[-1] != '\n':
521
result += help_topics._format_see_also(additional_see_also)
524
def get_help_topic(self):
525
"""Return the module help topic: its basename."""
526
return self.module.__name__[len(_MODULE_PREFIX):]
529
class PlugIn(object):
530
"""The breezy representation of a plugin.
532
The PlugIn object provides a way to manipulate a given plugin module.
535
def __init__(self, name, module):
536
"""Construct a plugin for module."""
541
"""Get the path that this plugin was loaded from."""
542
if getattr(self.module, '__path__', None) is not None:
543
return os.path.abspath(self.module.__path__[0])
544
elif getattr(self.module, '__file__', None) is not None:
545
path = os.path.abspath(self.module.__file__)
546
if path[-4:] == COMPILED_EXT:
547
pypath = path[:-4] + '.py'
548
if os.path.isfile(pypath):
552
return repr(self.module)
555
return "<%s.%s name=%s, module=%s>" % (
556
self.__class__.__module__, self.__class__.__name__,
557
self.name, self.module)
559
def test_suite(self):
560
"""Return the plugin's test suite."""
561
if getattr(self.module, 'test_suite', None) is not None:
562
return self.module.test_suite()
566
def load_plugin_tests(self, loader):
567
"""Return the adapted plugin's test suite.
569
:param loader: The custom loader that should be used to load additional
572
if getattr(self.module, 'load_tests', None) is not None:
573
return loader.loadTestsFromModule(self.module)
577
def version_info(self):
578
"""Return the plugin's version_tuple or None if unknown."""
579
version_info = getattr(self.module, 'version_info', None)
580
if version_info is not None:
158
plugin_info = imp.find_module(name, [d])
159
mutter('load plugin %r', plugin_info)
161
plugin = imp.load_module('bzrlib.plugins.' + name,
163
setattr(bzrlib.plugins, name, plugin)
165
if plugin_info[0] is not None:
166
plugin_info[0].close()
168
mutter('loaded succesfully')
169
except KeyboardInterrupt:
172
## import pdb; pdb.set_trace()
173
warning('Unable to load plugin %r from %r' % (name, d))
174
log_exception_quietly()
582
if isinstance(version_info, str):
583
version_info = version_info.split('.')
584
elif len(version_info) == 3:
585
version_info = tuple(version_info) + ('final', 0)
587
# The given version_info isn't even iteratible
588
trace.log_exception_quietly()
589
version_info = (version_info,)
593
def __version__(self):
594
version_info = self.version_info()
595
if version_info is None or len(version_info) == 0:
598
version_string = breezy._format_version_tuple(version_info)
599
except (ValueError, TypeError, IndexError):
600
trace.log_exception_quietly()
601
# Try to show something for the version anyway
602
version_string = '.'.join(map(str, version_info))
603
return version_string
606
class _PluginsAtFinder(object):
607
"""Meta path finder to support BRZ_PLUGINS_AT configuration."""
609
def __init__(self, prefix, names_and_paths):
611
self.names_to_path = dict((prefix + n, p) for n, p in names_and_paths)
614
return "<%s %r>" % (self.__class__.__name__, self.prefix)
616
def find_spec(self, fullname, paths, target=None):
617
"""New module spec returning find method."""
618
if fullname not in self.names_to_path:
620
path = self.names_to_path[fullname]
621
if os.path.isdir(path):
622
path = _get_package_init(path)
624
# GZ 2017-06-02: Any reason to block loading of the name from
625
# further down the path like this?
626
raise ImportError("Not loading namespace package %s as %s" % (
628
return importlib_util.spec_from_file_location(fullname, path)
630
def find_module(self, fullname, path):
631
"""Old PEP 302 import hook find_module method."""
632
if fullname not in self.names_to_path:
634
return _LegacyLoader(self.names_to_path[fullname])
637
class _LegacyLoader(object):
638
"""Source loader implementation for Python versions without importlib."""
640
def __init__(self, filepath):
641
self.filepath = filepath
644
return "<%s %r>" % (self.__class__.__name__, self.filepath)
646
def load_module(self, fullname):
647
"""Load a plugin from a specific directory (or file)."""
648
plugin_path = self.filepath
650
if os.path.isdir(plugin_path):
651
init_path = _get_package_init(plugin_path)
652
if init_path is not None:
653
loading_path = plugin_path
656
kind = imp.PKG_DIRECTORY
658
for suffix, mode, kind in imp.get_suffixes():
659
if plugin_path.endswith(suffix):
660
loading_path = plugin_path
662
if loading_path is None:
663
raise ImportError('%s cannot be loaded from %s'
664
% (fullname, plugin_path))
665
if kind is imp.PKG_DIRECTORY:
668
f = open(loading_path, mode)
670
mod = imp.load_module(fullname, f, loading_path,
671
(suffix, mode, kind))
672
mod.__package__ = fullname