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(), """
50
from importlib import util as importlib_util
62
_MODULE_PREFIX = "breezy.plugins."
64
if __debug__ or sys.version_info > (3,):
70
def disable_plugins(state=None):
70
71
"""Disable loading plugins.
72
73
Future calls to load_plugins() will be ignored.
75
: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
78
state = breezy.get_global_state()
82
def load_plugins(path=None, state=None):
83
"""Load breezy plugins.
85
The environment variable BRZ_PLUGIN_PATH is considered a delimited
86
set of paths to look through. Each entry is searched for `*.py`
85
87
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.
90
:param path: The list of paths to search for plugins. By default,
91
it is populated from the __path__ of the breezy.plugins package.
92
:param state: The library state object that records loaded plugins.
95
state = breezy.get_global_state()
96
if getattr(state, 'plugins', None) is not None:
93
97
# 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)
101
# Calls back into extend_path() here
102
from breezy.plugins import __path__ as path
104
state.plugin_warnings = {}
105
_load_plugins(state, path)
106
state.plugins = plugins()
109
def plugin_name(module_name):
110
"""Gives unprefixed name from module_name or None."""
111
if module_name.startswith(_MODULE_PREFIX):
112
parts = module_name.split(".")
118
def extend_path(path, name):
119
"""Helper so breezy.plugins can be a sort of namespace package.
121
To be used in similar fashion to pkgutil.extend_path:
123
from breezy.plugins import extend_path
124
__path__ = extend_path(__path__, __name__)
126
Inspects the BRZ_PLUGIN* envvars, sys.path, and the filesystem to find
127
plugins. May mutate sys.modules in order to block plugin loading, and may
128
append a new meta path finder to sys.meta_path for plugins@ loading.
130
Returns a list of paths to import from, as an enhanced object that also
131
contains details of the other configuration used.
133
blocks = _env_disable_plugins()
134
_block_plugins(blocks)
136
extra_details = _env_plugins_at()
137
_install_importer_if_needed(extra_details)
139
paths = _iter_plugin_paths(_env_plugin_path(), path)
141
return _Path(name, blocks, extra_details, paths)
145
"""List type to use as __path__ but containing additional details.
147
Python 3 allows any iterable for __path__ but Python 2 is more fussy.
150
def __init__(self, package_name, blocked, extra, paths):
151
super(_Path, self).__init__(paths)
152
self.package_name = package_name
153
self.blocked_names = blocked
154
self.extra_details = extra
157
return "%s(%r, %r, %r, %s)" % (
158
self.__class__.__name__, self.package_name, self.blocked_names,
159
self.extra_details, list.__repr__(self))
162
def _expect_identifier(name, env_key, env_value):
163
"""Validate given name from envvar is usable as a Python identifier.
165
Returns the name as a native str, or None if it was invalid.
167
Per PEP 3131 this is no longer strictly correct for Python 3, but as MvL
168
didn't include a neat way to check except eval, this enforces ascii.
170
if re.match(r"^[A-Za-z_][A-Za-z0-9_]*$", name) is None:
171
trace.warning("Invalid name '%s' in %s='%s'", name, env_key, env_value)
176
def _env_disable_plugins(key='BRZ_DISABLE_PLUGINS'):
177
"""Gives list of names for plugins to disable from environ key."""
179
env = osutils.path_from_environ(key)
181
for name in env.split(os.pathsep):
182
name = _expect_identifier(name, key, env)
184
disabled_names.append(name)
185
return disabled_names
188
def _env_plugins_at(key='BRZ_PLUGINS_AT'):
189
"""Gives list of names and paths of specific plugins from environ key."""
191
env = osutils.path_from_environ(key)
193
for pair in env.split(os.pathsep):
195
name, path = pair.split('@', 1)
198
name = osutils.basename(path).split('.', 1)[0]
199
name = _expect_identifier(name, key, env)
201
plugin_details.append((name, path))
202
return plugin_details
205
def _env_plugin_path(key='BRZ_PLUGIN_PATH'):
206
"""Gives list of paths and contexts for plugins from environ key.
208
Each entry is either a specific path to load plugins from and the value
209
'path', or None and one of the three values 'user', 'core', 'site'.
212
env = osutils.path_from_environ(key)
213
defaults = {"user": not env, "core": True, "site": True}
215
# Add paths specified by user in order
216
for p in env.split(os.pathsep):
217
flag, name = p[:1], p[1:]
218
if flag in ("+", "-") and name in defaults:
219
if flag == "+" and defaults[name] is not None:
220
path_details.append((None, name))
221
defaults[name] = None
223
path_details.append((p, 'path'))
225
# Add any remaining default paths
226
for name in ('user', 'core', 'site'):
228
path_details.append((None, name))
233
def _iter_plugin_paths(paths_from_env, core_paths):
234
"""Generate paths using paths_from_env and core_paths."""
235
# GZ 2017-06-02: This is kinda horrid, should make better.
236
for path, context in paths_from_env:
237
if context == 'path':
239
elif context == 'user':
240
path = get_user_plugin_path()
131
241
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:
243
elif context == 'core':
244
for path in _get_core_plugin_paths(core_paths):
246
elif context == 'site':
247
for path in _get_site_plugin_paths(sys.path):
248
if os.path.isdir(path):
252
def _install_importer_if_needed(plugin_details):
253
"""Install a meta path finder to handle plugin_details if any."""
255
finder = _PluginsAtFinder(_MODULE_PREFIX, plugin_details)
256
# For Python 3, must insert before default PathFinder to override.
257
sys.meta_path.insert(2, finder)
260
def _load_plugins(state, paths):
261
"""Do the importing all plugins from paths."""
262
imported_names = set()
263
for name, path in _iter_possible_plugins(paths):
264
if name not in imported_names:
265
msg = _load_plugin_module(name, path)
267
state.plugin_warnings.setdefault(name, []).append(msg)
268
imported_names.add(name)
271
def _block_plugins(names):
272
"""Add names to sys.modules to block future imports."""
274
package_name = _MODULE_PREFIX + name
275
if sys.modules.get(package_name) is not None:
276
trace.mutter("Blocked plugin %s already loaded.", name)
277
sys.modules[package_name] = None
280
def _get_package_init(package_path):
281
"""Get path of __init__ file from package_path or None if not a package."""
282
init_path = osutils.pathjoin(package_path, "__init__.py")
283
if os.path.exists(init_path):
285
init_path = init_path[:-3] + COMPILED_EXT
286
if os.path.exists(init_path):
291
def _iter_possible_plugins(plugin_paths):
292
"""Generate names and paths of possible plugins from plugin_paths."""
293
# Inspect any from BRZ_PLUGINS_AT first.
294
for name, path in getattr(plugin_paths, "extra_details", ()):
296
# Then walk over files and directories in the paths from the package.
297
for path in plugin_paths:
298
if os.path.isfile(path):
299
if path.endswith(".zip"):
300
trace.mutter("Don't yet support loading plugins from zip.")
302
for name, path in _walk_modules(path):
306
def _walk_modules(path):
307
"""Generate name and path of modules and packages on path."""
308
for root, dirs, files in os.walk(path):
312
if f.endswith((".py", COMPILED_EXT)):
313
yield f.rsplit(".", 1)[0], root
317
package_dir = osutils.pathjoin(root, d)
318
fullpath = _get_package_init(package_dir)
319
if fullpath is not None:
321
# Don't descend into subdirectories
325
def describe_plugins(show_paths=False, state=None):
326
"""Generate text description of plugins.
328
Includes both those that have loaded, and those that failed to load.
330
:param show_paths: If true, include the plugin path.
331
:param state: The library state object to inspect.
332
:returns: Iterator of text lines (including newlines.)
335
state = breezy.get_global_state()
336
loaded_plugins = getattr(state, 'plugins', {})
337
plugin_warnings = set(getattr(state, 'plugin_warnings', []))
338
all_names = sorted(set(loaded_plugins.keys()).union(plugin_warnings))
339
for name in all_names:
340
if name in loaded_plugins:
341
plugin = loaded_plugins[name]
342
version = plugin.__version__
343
if version == 'unknown':
345
yield '%s %s\n' % (name, version)
346
d = plugin.module.__doc__
348
doc = d.split('\n')[0]
350
doc = '(no description)'
351
yield (" %s\n" % doc)
353
yield (" %s\n" % plugin.path())
355
yield "%s (failed to load)\n" % name
356
if name in state.plugin_warnings:
357
for line in state.plugin_warnings[name]:
358
yield " ** " + line + '\n'
362
def _get_core_plugin_paths(existing_paths):
363
"""Generate possible locations for plugins based on existing_paths."""
364
if getattr(sys, 'frozen', False):
365
# We need to use relative path to system-wide plugin
366
# directory because breezy from standalone brz.exe
367
# could be imported by another standalone program
368
# (e.g. brz-config; or TortoiseBzr/Olive if/when they
369
# will become standalone exe). [bialix 20071123]
370
# __file__ typically is
371
# C:\Program Files\Bazaar\lib\library.zip\breezy\plugin.pyc
372
# then plugins directory is
373
# C:\Program Files\Bazaar\plugins
374
# so relative path is ../../../plugins
375
yield osutils.abspath(osutils.pathjoin(
376
osutils.dirname(__file__), '../../../plugins'))
377
else: # don't look inside library.zip
378
for path in existing_paths:
382
def _get_site_plugin_paths(sys_paths):
383
"""Generate possible locations for plugins from given sys_paths."""
384
for path in sys_paths:
385
if os.path.basename(path) in ('dist-packages', 'site-packages'):
386
yield osutils.pathjoin(path, 'breezy', 'plugins')
389
def get_user_plugin_path():
390
return osutils.pathjoin(config.config_dir(), 'plugins')
393
def record_plugin_warning(warning_message):
394
trace.mutter(warning_message)
395
return warning_message
398
def _load_plugin_module(name, dir):
399
"""Load plugin by name.
401
:param name: The plugin name in the breezy.plugins namespace.
402
:param dir: The directory the plugin is loaded from for error messages.
404
if _MODULE_PREFIX + name in sys.modules:
407
__import__(_MODULE_PREFIX + name)
408
except errors.IncompatibleVersion as e:
410
"Unable to load plugin %r. It supports %s "
411
"versions %r but the current version is %s" %
412
(name, e.api.__name__, e.wanted, e.current))
413
return record_plugin_warning(warning_message)
414
except Exception as e:
415
trace.log_exception_quietly()
416
if 'error' in debug.debug_flags:
417
trace.print_exception(sys.exc_info(), sys.stderr)
418
# GZ 2017-06-02: Move this name checking up a level, no point trying
419
# to import things with bad names.
420
if re.search('\\.|-| ', name):
421
sanitised_name = re.sub('[-. ]', '_', name)
422
if sanitised_name.startswith('brz_'):
423
sanitised_name = sanitised_name[len('brz_'):]
424
trace.warning("Unable to load %r in %r as a plugin because the "
425
"file path isn't a valid module name; try renaming "
426
"it to %r." % (name, dir, sanitised_name))
428
return record_plugin_warning(
429
'Unable to load plugin %r from %r: %s' % (name, dir, e))
433
"""Return a dictionary of the plugins.
435
Each item in the dictionary is a PlugIn object.
438
for fullname in sys.modules:
439
if fullname.startswith(_MODULE_PREFIX):
440
name = fullname[len(_MODULE_PREFIX):]
441
if not "." in name and sys.modules[fullname] is not None:
442
result[name] = PlugIn(name, sys.modules[fullname])
446
def get_loaded_plugin(name):
447
"""Retrieve an already loaded plugin.
449
Returns None if there is no such plugin loaded
452
module = sys.modules[_MODULE_PREFIX + name]
457
return PlugIn(name, module)
460
def format_concise_plugin_list(state=None):
461
"""Return a string holding a concise list of plugins and their version.
464
state = breezy.get_global_state()
466
for name, a_plugin in sorted(getattr(state, 'plugins', {}).items()):
467
items.append("%s[%s]" %
468
(name, a_plugin.__version__))
469
return ', '.join(items)
472
class PluginsHelpIndex(object):
473
"""A help index that returns help topics for plugins."""
476
self.prefix = 'plugins/'
478
def get_topics(self, topic):
479
"""Search for topic in the loaded plugins.
481
This will not trigger loading of new plugins.
483
:param topic: A topic to search for.
484
:return: A list which is either empty or contains a single
485
RegisteredTopic entry.
489
if topic.startswith(self.prefix):
490
topic = topic[len(self.prefix):]
491
plugin_module_name = _MODULE_PREFIX + topic
493
module = sys.modules[plugin_module_name]
497
return [ModuleHelpTopic(module)]
500
class ModuleHelpTopic(object):
501
"""A help topic which returns the docstring for a module."""
503
def __init__(self, module):
506
:param module: The module for which help should be generated.
510
def get_help_text(self, additional_see_also=None, verbose=True):
511
"""Return a string with the help for this topic.
513
:param additional_see_also: Additional help topics to be
516
if not self.module.__doc__:
517
result = "Plugin '%s' has no docstring.\n" % self.module.__name__
519
result = self.module.__doc__
520
if result[-1] != '\n':
522
result += help_topics._format_see_also(additional_see_also)
525
def get_help_topic(self):
526
"""Return the module help topic: its basename."""
527
return self.module.__name__[len(_MODULE_PREFIX):]
530
class PlugIn(object):
531
"""The breezy representation of a plugin.
533
The PlugIn object provides a way to manipulate a given plugin module.
536
def __init__(self, name, module):
537
"""Construct a plugin for module."""
542
"""Get the path that this plugin was loaded from."""
543
if getattr(self.module, '__path__', None) is not None:
544
return os.path.abspath(self.module.__path__[0])
545
elif getattr(self.module, '__file__', None) is not None:
546
path = os.path.abspath(self.module.__file__)
547
if path[-4:] == COMPILED_EXT:
548
pypath = path[:-4] + '.py'
549
if os.path.isfile(pypath):
553
return repr(self.module)
556
return "<%s.%s name=%s, module=%s>" % (
557
self.__class__.__module__, self.__class__.__name__,
558
self.name, self.module)
560
def test_suite(self):
561
"""Return the plugin's test suite."""
562
if getattr(self.module, 'test_suite', None) is not None:
563
return self.module.test_suite()
567
def load_plugin_tests(self, loader):
568
"""Return the adapted plugin's test suite.
570
:param loader: The custom loader that should be used to load additional
573
if getattr(self.module, 'load_tests', None) is not None:
574
return loader.loadTestsFromModule(self.module)
578
def version_info(self):
579
"""Return the plugin's version_tuple or None if unknown."""
580
version_info = getattr(self.module, 'version_info', None)
581
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()
583
if isinstance(version_info, str):
584
version_info = version_info.split('.')
585
elif len(version_info) == 3:
586
version_info = tuple(version_info) + ('final', 0)
588
# The given version_info isn't even iteratible
589
trace.log_exception_quietly()
590
version_info = (version_info,)
594
def __version__(self):
595
version_info = self.version_info()
596
if version_info is None or len(version_info) == 0:
599
version_string = breezy._format_version_tuple(version_info)
600
except (ValueError, TypeError, IndexError):
601
trace.log_exception_quietly()
602
# Try to show something for the version anyway
603
version_string = '.'.join(map(str, version_info))
604
return version_string
607
class _PluginsAtFinder(object):
608
"""Meta path finder to support BRZ_PLUGINS_AT configuration."""
610
def __init__(self, prefix, names_and_paths):
612
self.names_to_path = dict((prefix + n, p) for n, p in names_and_paths)
615
return "<%s %r>" % (self.__class__.__name__, self.prefix)
617
def find_spec(self, fullname, paths, target=None):
618
"""New module spec returning find method."""
619
if fullname not in self.names_to_path:
621
path = self.names_to_path[fullname]
622
if os.path.isdir(path):
623
path = _get_package_init(path)
625
# GZ 2017-06-02: Any reason to block loading of the name from
626
# further down the path like this?
627
raise ImportError("Not loading namespace package %s as %s" % (
629
return importlib_util.spec_from_file_location(fullname, path)
631
def find_module(self, fullname, path):
632
"""Old PEP 302 import hook find_module method."""
633
if fullname not in self.names_to_path:
635
return _LegacyLoader(self.names_to_path[fullname])
638
class _LegacyLoader(object):
639
"""Source loader implementation for Python versions without importlib."""
641
def __init__(self, filepath):
642
self.filepath = filepath
645
return "<%s %r>" % (self.__class__.__name__, self.filepath)
647
def load_module(self, fullname):
648
"""Load a plugin from a specific directory (or file)."""
649
plugin_path = self.filepath
651
if os.path.isdir(plugin_path):
652
init_path = _get_package_init(plugin_path)
653
if init_path is not None:
654
loading_path = plugin_path
657
kind = imp.PKG_DIRECTORY
659
for suffix, mode, kind in imp.get_suffixes():
660
if plugin_path.endswith(suffix):
661
loading_path = plugin_path
663
if loading_path is None:
664
raise ImportError('%s cannot be loaded from %s'
665
% (fullname, plugin_path))
666
if kind is imp.PKG_DIRECTORY:
669
f = open(loading_path, mode)
671
mod = imp.load_module(fullname, f, loading_path,
672
(suffix, mode, kind))
673
mod.__package__ = fullname