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_from_path(state, path)
105
if (None, 'entrypoints') in _env_plugin_path():
106
_load_plugins_from_entrypoints(state)
107
state.plugins = plugins()
110
def _load_plugins_from_entrypoints(state):
114
# No pkg_resources, no entrypoints.
117
for ep in pkg_resources.iter_entry_points('breezy.plugin'):
118
fullname = _MODULE_PREFIX + ep.name
119
if fullname in sys.modules:
121
sys.modules[fullname] = ep.load()
124
def plugin_name(module_name):
125
"""Gives unprefixed name from module_name or None."""
126
if module_name.startswith(_MODULE_PREFIX):
127
parts = module_name.split(".")
133
def extend_path(path, name):
134
"""Helper so breezy.plugins can be a sort of namespace package.
136
To be used in similar fashion to pkgutil.extend_path:
138
from breezy.plugins import extend_path
139
__path__ = extend_path(__path__, __name__)
141
Inspects the BRZ_PLUGIN* envvars, sys.path, and the filesystem to find
142
plugins. May mutate sys.modules in order to block plugin loading, and may
143
append a new meta path finder to sys.meta_path for plugins@ loading.
145
Returns a list of paths to import from, as an enhanced object that also
146
contains details of the other configuration used.
148
blocks = _env_disable_plugins()
149
_block_plugins(blocks)
151
extra_details = _env_plugins_at()
152
_install_importer_if_needed(extra_details)
154
paths = _iter_plugin_paths(_env_plugin_path(), path)
156
return _Path(name, blocks, extra_details, paths)
160
"""List type to use as __path__ but containing additional details.
162
Python 3 allows any iterable for __path__ but Python 2 is more fussy.
165
def __init__(self, package_name, blocked, extra, paths):
166
super(_Path, self).__init__(paths)
167
self.package_name = package_name
168
self.blocked_names = blocked
169
self.extra_details = extra
172
return "%s(%r, %r, %r, %s)" % (
173
self.__class__.__name__, self.package_name, self.blocked_names,
174
self.extra_details, list.__repr__(self))
177
def _expect_identifier(name, env_key, env_value):
178
"""Validate given name from envvar is usable as a Python identifier.
180
Returns the name as a native str, or None if it was invalid.
182
Per PEP 3131 this is no longer strictly correct for Python 3, but as MvL
183
didn't include a neat way to check except eval, this enforces ascii.
185
if re.match(r"^[A-Za-z_][A-Za-z0-9_]*$", name) is None:
186
trace.warning("Invalid name '%s' in %s='%s'", name, env_key, env_value)
191
def _env_disable_plugins(key='BRZ_DISABLE_PLUGINS'):
192
"""Gives list of names for plugins to disable from environ key."""
194
env = osutils.path_from_environ(key)
196
for name in env.split(os.pathsep):
197
name = _expect_identifier(name, key, env)
199
disabled_names.append(name)
200
return disabled_names
203
def _env_plugins_at(key='BRZ_PLUGINS_AT'):
204
"""Gives list of names and paths of specific plugins from environ key."""
206
env = osutils.path_from_environ(key)
208
for pair in env.split(os.pathsep):
210
name, path = pair.split('@', 1)
213
name = osutils.basename(path).split('.', 1)[0]
214
name = _expect_identifier(name, key, env)
216
plugin_details.append((name, path))
217
return plugin_details
220
def _env_plugin_path(key='BRZ_PLUGIN_PATH'):
221
"""Gives list of paths and contexts for plugins from environ key.
223
Each entry is either a specific path to load plugins from and the value
224
'path', or None and one of the values 'user', 'core', 'entrypoints', 'site'.
227
env = osutils.path_from_environ(key)
228
defaults = {"user": not env, "core": True, "site": True, 'entrypoints': True}
230
# Add paths specified by user in order
231
for p in env.split(os.pathsep):
232
flag, name = p[:1], p[1:]
233
if flag in ("+", "-") and name in defaults:
234
if flag == "+" and defaults[name] is not None:
235
path_details.append((None, name))
236
defaults[name] = None
238
path_details.append((p, 'path'))
240
# Add any remaining default paths
241
for name in ('user', 'core', 'entrypoints', 'site'):
243
path_details.append((None, name))
248
def _iter_plugin_paths(paths_from_env, core_paths):
249
"""Generate paths using paths_from_env and core_paths."""
250
# GZ 2017-06-02: This is kinda horrid, should make better.
251
for path, context in paths_from_env:
252
if context == 'path':
254
elif context == 'user':
255
path = get_user_plugin_path()
131
256
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:
258
elif context == 'core':
259
for path in _get_core_plugin_paths(core_paths):
261
elif context == 'site':
262
for path in _get_site_plugin_paths(sys.path):
263
if os.path.isdir(path):
267
def _install_importer_if_needed(plugin_details):
268
"""Install a meta path finder to handle plugin_details if any."""
270
finder = _PluginsAtFinder(_MODULE_PREFIX, plugin_details)
271
# For Python 3, must insert before default PathFinder to override.
272
sys.meta_path.insert(2, finder)
275
def _load_plugins_from_path(state, paths):
276
"""Do the importing all plugins from paths."""
277
imported_names = set()
278
for name, path in _iter_possible_plugins(paths):
279
if name not in imported_names:
280
msg = _load_plugin_module(name, path)
282
state.plugin_warnings.setdefault(name, []).append(msg)
283
imported_names.add(name)
286
def _block_plugins(names):
287
"""Add names to sys.modules to block future imports."""
289
package_name = _MODULE_PREFIX + name
290
if sys.modules.get(package_name) is not None:
291
trace.mutter("Blocked plugin %s already loaded.", name)
292
sys.modules[package_name] = None
295
def _get_package_init(package_path):
296
"""Get path of __init__ file from package_path or None if not a package."""
297
init_path = osutils.pathjoin(package_path, "__init__.py")
298
if os.path.exists(init_path):
300
init_path = init_path[:-3] + COMPILED_EXT
301
if os.path.exists(init_path):
306
def _iter_possible_plugins(plugin_paths):
307
"""Generate names and paths of possible plugins from plugin_paths."""
308
# Inspect any from BRZ_PLUGINS_AT first.
309
for name, path in getattr(plugin_paths, "extra_details", ()):
311
# Then walk over files and directories in the paths from the package.
312
for path in plugin_paths:
313
if os.path.isfile(path):
314
if path.endswith(".zip"):
315
trace.mutter("Don't yet support loading plugins from zip.")
317
for name, path in _walk_modules(path):
321
def _walk_modules(path):
322
"""Generate name and path of modules and packages on path."""
323
for root, dirs, files in os.walk(path):
327
if f.endswith((".py", COMPILED_EXT)):
328
yield f.rsplit(".", 1)[0], root
332
package_dir = osutils.pathjoin(root, d)
333
fullpath = _get_package_init(package_dir)
334
if fullpath is not None:
336
# Don't descend into subdirectories
340
def describe_plugins(show_paths=False, state=None):
341
"""Generate text description of plugins.
343
Includes both those that have loaded, and those that failed to load.
345
:param show_paths: If true, include the plugin path.
346
:param state: The library state object to inspect.
347
:returns: Iterator of text lines (including newlines.)
350
state = breezy.get_global_state()
351
loaded_plugins = getattr(state, 'plugins', {})
352
plugin_warnings = set(getattr(state, 'plugin_warnings', []))
353
all_names = sorted(set(loaded_plugins.keys()).union(plugin_warnings))
354
for name in all_names:
355
if name in loaded_plugins:
356
plugin = loaded_plugins[name]
357
version = plugin.__version__
358
if version == 'unknown':
360
yield '%s %s\n' % (name, version)
361
d = plugin.module.__doc__
363
doc = d.split('\n')[0]
365
doc = '(no description)'
366
yield (" %s\n" % doc)
368
yield (" %s\n" % plugin.path())
370
yield "%s (failed to load)\n" % name
371
if name in state.plugin_warnings:
372
for line in state.plugin_warnings[name]:
373
yield " ** " + line + '\n'
377
def _get_core_plugin_paths(existing_paths):
378
"""Generate possible locations for plugins based on existing_paths."""
379
if getattr(sys, 'frozen', False):
380
# We need to use relative path to system-wide plugin
381
# directory because breezy from standalone brz.exe
382
# could be imported by another standalone program
383
# (e.g. brz-config; or TortoiseBzr/Olive if/when they
384
# will become standalone exe). [bialix 20071123]
385
# __file__ typically is
386
# C:\Program Files\Bazaar\lib\library.zip\breezy\plugin.pyc
387
# then plugins directory is
388
# C:\Program Files\Bazaar\plugins
389
# so relative path is ../../../plugins
390
yield osutils.abspath(osutils.pathjoin(
391
osutils.dirname(__file__), '../../../plugins'))
392
else: # don't look inside library.zip
393
for path in existing_paths:
397
def _get_site_plugin_paths(sys_paths):
398
"""Generate possible locations for plugins from given sys_paths."""
399
for path in sys_paths:
400
if os.path.basename(path) in ('dist-packages', 'site-packages'):
401
yield osutils.pathjoin(path, 'breezy', 'plugins')
404
def get_user_plugin_path():
405
return osutils.pathjoin(bedding.config_dir(), 'plugins')
408
def record_plugin_warning(warning_message):
409
trace.mutter(warning_message)
410
return warning_message
413
def _load_plugin_module(name, dir):
414
"""Load plugin by name.
416
:param name: The plugin name in the breezy.plugins namespace.
417
:param dir: The directory the plugin is loaded from for error messages.
419
if _MODULE_PREFIX + name in sys.modules:
422
__import__(_MODULE_PREFIX + name)
423
except errors.IncompatibleVersion as e:
425
"Unable to load plugin %r. It supports %s "
426
"versions %r but the current version is %s" %
427
(name, e.api.__name__, e.wanted, e.current))
428
return record_plugin_warning(warning_message)
429
except Exception as e:
430
trace.log_exception_quietly()
431
if 'error' in debug.debug_flags:
432
trace.print_exception(sys.exc_info(), sys.stderr)
433
# GZ 2017-06-02: Move this name checking up a level, no point trying
434
# to import things with bad names.
435
if re.search('\\.|-| ', name):
436
sanitised_name = re.sub('[-. ]', '_', name)
437
if sanitised_name.startswith('brz_'):
438
sanitised_name = sanitised_name[len('brz_'):]
439
trace.warning("Unable to load %r in %r as a plugin because the "
440
"file path isn't a valid module name; try renaming "
441
"it to %r." % (name, dir, sanitised_name))
443
return record_plugin_warning(
444
'Unable to load plugin %r from %r: %s' % (name, dir, e))
448
"""Return a dictionary of the plugins.
450
Each item in the dictionary is a PlugIn object.
453
for fullname in sys.modules:
454
if fullname.startswith(_MODULE_PREFIX):
455
name = fullname[len(_MODULE_PREFIX):]
456
if "." not in name and sys.modules[fullname] is not None:
457
result[name] = PlugIn(name, sys.modules[fullname])
461
def get_loaded_plugin(name):
462
"""Retrieve an already loaded plugin.
464
Returns None if there is no such plugin loaded
467
module = sys.modules[_MODULE_PREFIX + name]
472
return PlugIn(name, module)
475
def format_concise_plugin_list(state=None):
476
"""Return a string holding a concise list of plugins and their version.
479
state = breezy.get_global_state()
481
for name, a_plugin in sorted(getattr(state, 'plugins', {}).items()):
482
items.append("%s[%s]" %
483
(name, a_plugin.__version__))
484
return ', '.join(items)
487
class PluginsHelpIndex(object):
488
"""A help index that returns help topics for plugins."""
491
self.prefix = 'plugins/'
493
def get_topics(self, topic):
494
"""Search for topic in the loaded plugins.
496
This will not trigger loading of new plugins.
498
:param topic: A topic to search for.
499
:return: A list which is either empty or contains a single
500
RegisteredTopic entry.
504
if topic.startswith(self.prefix):
505
topic = topic[len(self.prefix):]
506
plugin_module_name = _MODULE_PREFIX + topic
508
module = sys.modules[plugin_module_name]
512
return [ModuleHelpTopic(module)]
515
class ModuleHelpTopic(object):
516
"""A help topic which returns the docstring for a module."""
518
def __init__(self, module):
521
:param module: The module for which help should be generated.
525
def get_help_text(self, additional_see_also=None, verbose=True):
526
"""Return a string with the help for this topic.
528
:param additional_see_also: Additional help topics to be
531
if not self.module.__doc__:
532
result = "Plugin '%s' has no docstring.\n" % self.module.__name__
534
result = self.module.__doc__
535
if result[-1] != '\n':
537
result += help_topics._format_see_also(additional_see_also)
540
def get_help_topic(self):
541
"""Return the module help topic: its basename."""
542
return self.module.__name__[len(_MODULE_PREFIX):]
545
class PlugIn(object):
546
"""The breezy representation of a plugin.
548
The PlugIn object provides a way to manipulate a given plugin module.
551
def __init__(self, name, module):
552
"""Construct a plugin for module."""
557
"""Get the path that this plugin was loaded from."""
558
if getattr(self.module, '__path__', None) is not None:
559
return os.path.abspath(self.module.__path__[0])
560
elif getattr(self.module, '__file__', None) is not None:
561
path = os.path.abspath(self.module.__file__)
562
if path[-4:] == COMPILED_EXT:
563
pypath = path[:-4] + '.py'
564
if os.path.isfile(pypath):
568
return repr(self.module)
571
return "<%s.%s name=%s, module=%s>" % (
572
self.__class__.__module__, self.__class__.__name__,
573
self.name, self.module)
575
def test_suite(self):
576
"""Return the plugin's test suite."""
577
if getattr(self.module, 'test_suite', None) is not None:
578
return self.module.test_suite()
582
def load_plugin_tests(self, loader):
583
"""Return the adapted plugin's test suite.
585
:param loader: The custom loader that should be used to load additional
588
if getattr(self.module, 'load_tests', None) is not None:
589
return loader.loadTestsFromModule(self.module)
593
def version_info(self):
594
"""Return the plugin's version_tuple or None if unknown."""
595
version_info = getattr(self.module, 'version_info', None)
596
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()
598
if isinstance(version_info, str):
599
version_info = version_info.split('.')
600
elif len(version_info) == 3:
601
version_info = tuple(version_info) + ('final', 0)
603
# The given version_info isn't even iteratible
604
trace.log_exception_quietly()
605
version_info = (version_info,)
609
def __version__(self):
610
version_info = self.version_info()
611
if version_info is None or len(version_info) == 0:
614
version_string = breezy._format_version_tuple(version_info)
615
except (ValueError, TypeError, IndexError):
616
trace.log_exception_quietly()
617
# Try to show something for the version anyway
618
version_string = '.'.join(map(str, version_info))
619
return version_string
622
class _PluginsAtFinder(object):
623
"""Meta path finder to support BRZ_PLUGINS_AT configuration."""
625
def __init__(self, prefix, names_and_paths):
627
self.names_to_path = dict((prefix + n, p) for n, p in names_and_paths)
630
return "<%s %r>" % (self.__class__.__name__, self.prefix)
632
def find_spec(self, fullname, paths, target=None):
633
"""New module spec returning find method."""
634
if fullname not in self.names_to_path:
636
path = self.names_to_path[fullname]
637
if os.path.isdir(path):
638
path = _get_package_init(path)
640
# GZ 2017-06-02: Any reason to block loading of the name from
641
# further down the path like this?
642
raise ImportError("Not loading namespace package %s as %s" % (
644
return importlib_util.spec_from_file_location(fullname, path)
646
def find_module(self, fullname, path):
647
"""Old PEP 302 import hook find_module method."""
648
if fullname not in self.names_to_path:
650
return _LegacyLoader(self.names_to_path[fullname])
653
class _LegacyLoader(object):
654
"""Source loader implementation for Python versions without importlib."""
656
def __init__(self, filepath):
657
self.filepath = filepath
660
return "<%s %r>" % (self.__class__.__name__, self.filepath)
662
def load_module(self, fullname):
663
"""Load a plugin from a specific directory (or file)."""
664
plugin_path = self.filepath
666
if os.path.isdir(plugin_path):
667
init_path = _get_package_init(plugin_path)
668
if init_path is not None:
669
loading_path = plugin_path
672
kind = imp.PKG_DIRECTORY
674
for suffix, mode, kind in imp.get_suffixes():
675
if plugin_path.endswith(suffix):
676
loading_path = plugin_path
678
if loading_path is None:
679
raise ImportError('%s cannot be loaded from %s'
680
% (fullname, plugin_path))
681
if kind is imp.PKG_DIRECTORY:
684
f = open(loading_path, mode)
686
mod = imp.load_module(fullname, f, loading_path,
687
(suffix, mode, kind))
688
mod.__package__ = fullname