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.
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():
44
from .lazy_import import lazy_import
45
lazy_import(globals(), """
47
from importlib import util as importlib_util
62
_MODULE_PREFIX = "breezy.plugins."
67
def disable_plugins(state=None):
70
68
"""Disable loading plugins.
72
70
Future calls to load_plugins() will be ignored.
72
: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
75
state = breezy.get_global_state()
79
def load_plugins(path=None, state=None):
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`
85
84
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.
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.
92
state = breezy.get_global_state()
93
if getattr(state, 'plugins', None) is not None:
93
94
# 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)
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()
108
def _load_plugins_from_entrypoints(state):
112
# No pkg_resources, no entrypoints.
115
for ep in pkg_resources.iter_entry_points('breezy.plugin'):
116
fullname = _MODULE_PREFIX + ep.name
117
if fullname in sys.modules:
119
sys.modules[fullname] = ep.load()
122
def plugin_name(module_name):
123
"""Gives unprefixed name from module_name or None."""
124
if module_name.startswith(_MODULE_PREFIX):
125
parts = module_name.split(".")
131
def extend_path(path, name):
132
"""Helper so breezy.plugins can be a sort of namespace package.
134
To be used in similar fashion to pkgutil.extend_path:
136
from breezy.plugins import extend_path
137
__path__ = extend_path(__path__, __name__)
139
Inspects the BRZ_PLUGIN* envvars, sys.path, and the filesystem to find
140
plugins. May mutate sys.modules in order to block plugin loading, and may
141
append a new meta path finder to sys.meta_path for plugins@ loading.
143
Returns a list of paths to import from, as an enhanced object that also
144
contains details of the other configuration used.
146
blocks = _env_disable_plugins()
147
_block_plugins(blocks)
149
extra_details = _env_plugins_at()
150
_install_importer_if_needed(extra_details)
152
paths = _iter_plugin_paths(_env_plugin_path(), path)
154
return _Path(name, blocks, extra_details, paths)
158
"""List type to use as __path__ but containing additional details.
160
Python 3 allows any iterable for __path__ but Python 2 is more fussy.
163
def __init__(self, package_name, blocked, extra, paths):
164
super(_Path, self).__init__(paths)
165
self.package_name = package_name
166
self.blocked_names = blocked
167
self.extra_details = extra
170
return "%s(%r, %r, %r, %s)" % (
171
self.__class__.__name__, self.package_name, self.blocked_names,
172
self.extra_details, list.__repr__(self))
175
def _expect_identifier(name, env_key, env_value):
176
"""Validate given name from envvar is usable as a Python identifier.
178
Returns the name as a native str, or None if it was invalid.
180
Per PEP 3131 this is no longer strictly correct for Python 3, but as MvL
181
didn't include a neat way to check except eval, this enforces ascii.
183
if re.match(r"^[A-Za-z_][A-Za-z0-9_]*$", name) is None:
184
trace.warning("Invalid name '%s' in %s='%s'", name, env_key, env_value)
189
def _env_disable_plugins(key='BRZ_DISABLE_PLUGINS'):
190
"""Gives list of names for plugins to disable from environ key."""
192
env = osutils.path_from_environ(key)
194
for name in env.split(os.pathsep):
195
name = _expect_identifier(name, key, env)
197
disabled_names.append(name)
198
return disabled_names
201
def _env_plugins_at(key='BRZ_PLUGINS_AT'):
202
"""Gives list of names and paths of specific plugins from environ key."""
204
env = osutils.path_from_environ(key)
206
for pair in env.split(os.pathsep):
208
name, path = pair.split('@', 1)
211
name = osutils.basename(path).split('.', 1)[0]
212
name = _expect_identifier(name, key, env)
214
plugin_details.append((name, path))
215
return plugin_details
218
def _env_plugin_path(key='BRZ_PLUGIN_PATH'):
219
"""Gives list of paths and contexts for plugins from environ key.
221
Each entry is either a specific path to load plugins from and the value
222
'path', or None and one of the values 'user', 'core', 'entrypoints', 'site'.
225
env = osutils.path_from_environ(key)
230
'entrypoints': False,
233
# Add paths specified by user in order
234
for p in env.split(os.pathsep):
235
flag, name = p[:1], p[1:]
236
if flag in ("+", "-") and name in defaults:
237
if flag == "+" and defaults[name] is not None:
238
path_details.append((None, name))
239
defaults[name] = None
241
path_details.append((p, 'path'))
243
# Add any remaining default paths
244
for name in ('user', 'core', 'entrypoints', 'site'):
246
path_details.append((None, name))
251
def _iter_plugin_paths(paths_from_env, core_paths):
252
"""Generate paths using paths_from_env and core_paths."""
253
# GZ 2017-06-02: This is kinda horrid, should make better.
254
for path, context in paths_from_env:
255
if context == 'path':
257
elif context == 'user':
258
path = get_user_plugin_path()
131
259
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:
261
elif context == 'core':
262
for path in _get_core_plugin_paths(core_paths):
264
elif context == 'site':
265
for path in _get_site_plugin_paths(sys.path):
266
if os.path.isdir(path):
270
def _install_importer_if_needed(plugin_details):
271
"""Install a meta path finder to handle plugin_details if any."""
273
finder = _PluginsAtFinder(_MODULE_PREFIX, plugin_details)
274
# For Python 3, must insert before default PathFinder to override.
275
sys.meta_path.insert(2, finder)
278
def _load_plugins_from_path(state, paths):
279
"""Do the importing all plugins from paths."""
280
imported_names = set()
281
for name, path in _iter_possible_plugins(paths):
282
if name not in imported_names:
283
msg = _load_plugin_module(name, path)
285
state.plugin_warnings.setdefault(name, []).append(msg)
286
imported_names.add(name)
289
def _block_plugins(names):
290
"""Add names to sys.modules to block future imports."""
292
package_name = _MODULE_PREFIX + name
293
if sys.modules.get(package_name) is not None:
294
trace.mutter("Blocked plugin %s already loaded.", name)
295
sys.modules[package_name] = None
298
def _get_package_init(package_path):
299
"""Get path of __init__ file from package_path or None if not a package."""
300
init_path = osutils.pathjoin(package_path, "__init__.py")
301
if os.path.exists(init_path):
303
init_path = init_path[:-3] + COMPILED_EXT
304
if os.path.exists(init_path):
309
def _iter_possible_plugins(plugin_paths):
310
"""Generate names and paths of possible plugins from plugin_paths."""
311
# Inspect any from BRZ_PLUGINS_AT first.
312
for name, path in getattr(plugin_paths, "extra_details", ()):
314
# Then walk over files and directories in the paths from the package.
315
for path in plugin_paths:
316
if os.path.isfile(path):
317
if path.endswith(".zip"):
318
trace.mutter("Don't yet support loading plugins from zip.")
320
for name, path in _walk_modules(path):
324
def _walk_modules(path):
325
"""Generate name and path of modules and packages on path."""
326
for root, dirs, files in os.walk(path):
330
if f.endswith((".py", COMPILED_EXT)):
331
yield f.rsplit(".", 1)[0], root
335
package_dir = osutils.pathjoin(root, d)
336
fullpath = _get_package_init(package_dir)
337
if fullpath is not None:
339
# Don't descend into subdirectories
343
def describe_plugins(show_paths=False, state=None):
344
"""Generate text description of plugins.
346
Includes both those that have loaded, and those that failed to load.
348
:param show_paths: If true, include the plugin path.
349
:param state: The library state object to inspect.
350
:returns: Iterator of text lines (including newlines.)
353
state = breezy.get_global_state()
354
loaded_plugins = getattr(state, 'plugins', {})
355
plugin_warnings = set(getattr(state, 'plugin_warnings', []))
356
all_names = sorted(set(loaded_plugins.keys()).union(plugin_warnings))
357
for name in all_names:
358
if name in loaded_plugins:
359
plugin = loaded_plugins[name]
360
version = plugin.__version__
361
if version == 'unknown':
363
yield '%s %s\n' % (name, version)
364
d = plugin.module.__doc__
366
doc = d.split('\n')[0]
368
doc = '(no description)'
369
yield (" %s\n" % doc)
371
yield (" %s\n" % plugin.path())
373
yield "%s (failed to load)\n" % name
374
if name in state.plugin_warnings:
375
for line in state.plugin_warnings[name]:
376
yield " ** " + line + '\n'
380
def _get_core_plugin_paths(existing_paths):
381
"""Generate possible locations for plugins based on existing_paths."""
382
if getattr(sys, 'frozen', False):
383
# We need to use relative path to system-wide plugin
384
# directory because breezy from standalone brz.exe
385
# could be imported by another standalone program
386
# (e.g. brz-config; or TortoiseBzr/Olive if/when they
387
# will become standalone exe). [bialix 20071123]
388
# __file__ typically is
389
# C:\Program Files\Bazaar\lib\library.zip\breezy\plugin.pyc
390
# then plugins directory is
391
# C:\Program Files\Bazaar\plugins
392
# so relative path is ../../../plugins
393
yield osutils.abspath(osutils.pathjoin(
394
osutils.dirname(__file__), '../../../plugins'))
395
else: # don't look inside library.zip
396
for path in existing_paths:
400
def _get_site_plugin_paths(sys_paths):
401
"""Generate possible locations for plugins from given sys_paths."""
402
for path in sys_paths:
403
if os.path.basename(path) in ('dist-packages', 'site-packages'):
404
yield osutils.pathjoin(path, 'breezy', 'plugins')
407
def get_user_plugin_path():
408
return osutils.pathjoin(bedding.config_dir(), 'plugins')
411
def record_plugin_warning(warning_message):
412
trace.mutter(warning_message)
413
return warning_message
416
def _load_plugin_module(name, dir):
417
"""Load plugin by name.
419
:param name: The plugin name in the breezy.plugins namespace.
420
:param dir: The directory the plugin is loaded from for error messages.
422
if _MODULE_PREFIX + name in sys.modules:
425
__import__(_MODULE_PREFIX + name)
426
except errors.IncompatibleVersion as e:
428
"Unable to load plugin %r. It supports %s "
429
"versions %r but the current version is %s" %
430
(name, e.api.__name__, e.wanted, e.current))
431
return record_plugin_warning(warning_message)
432
except Exception as e:
433
trace.log_exception_quietly()
434
if 'error' in debug.debug_flags:
435
trace.print_exception(sys.exc_info(), sys.stderr)
436
# GZ 2017-06-02: Move this name checking up a level, no point trying
437
# to import things with bad names.
438
if re.search('\\.|-| ', name):
439
sanitised_name = re.sub('[-. ]', '_', name)
440
if sanitised_name.startswith('brz_'):
441
sanitised_name = sanitised_name[len('brz_'):]
442
trace.warning("Unable to load %r in %r as a plugin because the "
443
"file path isn't a valid module name; try renaming "
444
"it to %r." % (name, dir, sanitised_name))
446
return record_plugin_warning(
447
'Unable to load plugin %r from %r: %s' % (name, dir, e))
451
"""Return a dictionary of the plugins.
453
Each item in the dictionary is a PlugIn object.
456
for fullname in sys.modules:
457
if fullname.startswith(_MODULE_PREFIX):
458
name = fullname[len(_MODULE_PREFIX):]
459
if "." not in name and sys.modules[fullname] is not None:
460
result[name] = PlugIn(name, sys.modules[fullname])
464
def get_loaded_plugin(name):
465
"""Retrieve an already loaded plugin.
467
Returns None if there is no such plugin loaded
470
module = sys.modules[_MODULE_PREFIX + name]
475
return PlugIn(name, module)
478
def format_concise_plugin_list(state=None):
479
"""Return a string holding a concise list of plugins and their version.
482
state = breezy.get_global_state()
484
for name, a_plugin in sorted(getattr(state, 'plugins', {}).items()):
485
items.append("%s[%s]" %
486
(name, a_plugin.__version__))
487
return ', '.join(items)
490
class PluginsHelpIndex(object):
491
"""A help index that returns help topics for plugins."""
494
self.prefix = 'plugins/'
496
def get_topics(self, topic):
497
"""Search for topic in the loaded plugins.
499
This will not trigger loading of new plugins.
501
:param topic: A topic to search for.
502
:return: A list which is either empty or contains a single
503
RegisteredTopic entry.
507
if topic.startswith(self.prefix):
508
topic = topic[len(self.prefix):]
509
plugin_module_name = _MODULE_PREFIX + topic
511
module = sys.modules[plugin_module_name]
515
return [ModuleHelpTopic(module)]
518
class ModuleHelpTopic(object):
519
"""A help topic which returns the docstring for a module."""
521
def __init__(self, module):
524
:param module: The module for which help should be generated.
528
def get_help_text(self, additional_see_also=None, verbose=True):
529
"""Return a string with the help for this topic.
531
:param additional_see_also: Additional help topics to be
534
if not self.module.__doc__:
535
result = "Plugin '%s' has no docstring.\n" % self.module.__name__
537
result = self.module.__doc__
538
if result[-1] != '\n':
540
result += help_topics._format_see_also(additional_see_also)
543
def get_help_topic(self):
544
"""Return the module help topic: its basename."""
545
return self.module.__name__[len(_MODULE_PREFIX):]
548
class PlugIn(object):
549
"""The breezy representation of a plugin.
551
The PlugIn object provides a way to manipulate a given plugin module.
554
def __init__(self, name, module):
555
"""Construct a plugin for module."""
560
"""Get the path that this plugin was loaded from."""
561
if getattr(self.module, '__path__', None) is not None:
562
return os.path.abspath(self.module.__path__[0])
563
elif getattr(self.module, '__file__', None) is not None:
564
path = os.path.abspath(self.module.__file__)
565
if path[-4:] == COMPILED_EXT:
566
pypath = path[:-4] + '.py'
567
if os.path.isfile(pypath):
571
return repr(self.module)
574
return "<%s.%s name=%s, module=%s>" % (
575
self.__class__.__module__, self.__class__.__name__,
576
self.name, self.module)
578
def test_suite(self):
579
"""Return the plugin's test suite."""
580
if getattr(self.module, 'test_suite', None) is not None:
581
return self.module.test_suite()
585
def load_plugin_tests(self, loader):
586
"""Return the adapted plugin's test suite.
588
:param loader: The custom loader that should be used to load additional
591
if getattr(self.module, 'load_tests', None) is not None:
592
return loader.loadTestsFromModule(self.module)
596
def version_info(self):
597
"""Return the plugin's version_tuple or None if unknown."""
598
version_info = getattr(self.module, 'version_info', None)
599
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()
601
if isinstance(version_info, str):
602
version_info = version_info.split('.')
603
elif len(version_info) == 3:
604
version_info = tuple(version_info) + ('final', 0)
606
# The given version_info isn't even iteratible
607
trace.log_exception_quietly()
608
version_info = (version_info,)
612
def __version__(self):
613
version_info = self.version_info()
614
if version_info is None or len(version_info) == 0:
617
version_string = breezy._format_version_tuple(version_info)
618
except (ValueError, TypeError, IndexError):
619
trace.log_exception_quietly()
620
# Try to show something for the version anyway
621
version_string = '.'.join(map(str, version_info))
622
return version_string
625
class _PluginsAtFinder(object):
626
"""Meta path finder to support BRZ_PLUGINS_AT configuration."""
628
def __init__(self, prefix, names_and_paths):
630
self.names_to_path = dict((prefix + n, p) for n, p in names_and_paths)
633
return "<%s %r>" % (self.__class__.__name__, self.prefix)
635
def find_spec(self, fullname, paths, target=None):
636
"""New module spec returning find method."""
637
if fullname not in self.names_to_path:
639
path = self.names_to_path[fullname]
640
if os.path.isdir(path):
641
path = _get_package_init(path)
643
# GZ 2017-06-02: Any reason to block loading of the name from
644
# further down the path like this?
645
raise ImportError("Not loading namespace package %s as %s" % (
647
return importlib_util.spec_from_file_location(fullname, path)
649
def find_module(self, fullname, path):
650
"""Old PEP 302 import hook find_module method."""
651
if fullname not in self.names_to_path:
653
return _LegacyLoader(self.names_to_path[fullname])
656
class _LegacyLoader(object):
657
"""Source loader implementation for Python versions without importlib."""
659
def __init__(self, filepath):
660
self.filepath = filepath
663
return "<%s %r>" % (self.__class__.__name__, self.filepath)
665
def load_module(self, fullname):
666
"""Load a plugin from a specific directory (or file)."""
667
plugin_path = self.filepath
669
if os.path.isdir(plugin_path):
670
init_path = _get_package_init(plugin_path)
671
if init_path is not None:
672
loading_path = plugin_path
675
kind = imp.PKG_DIRECTORY
677
for suffix, mode, kind in imp.get_suffixes():
678
if plugin_path.endswith(suffix):
679
loading_path = plugin_path
681
if loading_path is None:
682
raise ImportError('%s cannot be loaded from %s'
683
% (fullname, plugin_path))
684
if kind is imp.PKG_DIRECTORY:
687
f = open(loading_path, mode)
689
mod = imp.load_module(fullname, f, loading_path,
690
(suffix, mode, kind))
691
mod.__package__ = fullname