Rev 5108: (vila) Add BZR_PLUGINS_AT support to specify a directory for a given in file:///home/pqm/archives/thelove/bzr/%2Btrunk/
Canonical.com Patch Queue Manager
pqm at pqm.ubuntu.com
Wed Mar 24 14:50:57 GMT 2010
At file:///home/pqm/archives/thelove/bzr/%2Btrunk/
------------------------------------------------------------
revno: 5108 [merge]
revision-id: pqm at pqm.ubuntu.com-20100324145053-1edalqun0nesmbeh
parent: pqm at pqm.ubuntu.com-20100324072744-2fod6mq1jqijs7c0
parent: v.ladeuil+lp at free.fr-20100324135837-06hiz4gj3b29zlr0
committer: Canonical.com Patch Queue Manager <pqm at pqm.ubuntu.com>
branch nick: +trunk
timestamp: Wed 2010-03-24 14:50:53 +0000
message:
(vila) Add BZR_PLUGINS_AT support to specify a directory for a given
plugin
modified:
NEWS NEWS-20050323055033-4e00b5db738777ff
bzrlib/help_topics/en/configuration.txt configuration.txt-20060314161707-868350809502af01
bzrlib/plugin.py plugin.py-20050622060424-829b654519533d69
bzrlib/tests/__init__.py selftest.py-20050531073622-8d0e3c8845c97a64
bzrlib/tests/test_plugins.py plugins.py-20050622075746-32002b55e5e943e9
=== modified file 'NEWS'
--- a/NEWS 2010-03-24 07:27:44 +0000
+++ b/NEWS 2010-03-24 13:58:37 +0000
@@ -58,6 +58,13 @@
a list of plugin names separated by ':' (';' on windows).
(Vincent Ladeuil, #411413)
+* Plugins can be loaded from arbitrary locations by defining
+ ``BZR_PLUGINS_AT`` as a list of name at path separated by ':' (';' on
+ windows). This takes precedence over ``BZR_PLUGIN_PATH`` for the
+ specified plugins. This is targeted at plugin developers for punctual
+ needs and *not* intended to replace ``BZR_PLUGIN_PATH``.
+ (Vincent Ladeuil, #82693)
+
* Tag names can now be determined automatically by ``automatic_tag_name``
hooks on ``Branch`` if they are not specified on the command line.
(Jelmer Vernooij)
=== modified file 'bzrlib/help_topics/en/configuration.txt'
--- a/bzrlib/help_topics/en/configuration.txt 2010-03-19 12:09:05 +0000
+++ b/bzrlib/help_topics/en/configuration.txt 2010-03-24 13:58:37 +0000
@@ -120,10 +120,10 @@
BZR_DISABLE_PLUGINS
~~~~~~~~~~~~~~~~~~~
-Under special circumstances, it's better to disable a plugin (or
-several) rather than uninstalling them completely. Such plugins
-can be specified in the ``BZR_DISABLE_PLUGINS`` environment
-variable.
+Under special circumstances (mostly when trying to diagnose a
+bug), it's better to disable a plugin (or several) rather than
+uninstalling them completely. Such plugins can be specified in
+the ``BZR_DISABLE_PLUGINS`` environment variable.
In that case, ``bzr`` will stop loading the specified plugins and
will raise an import error if they are explicitly imported (by
@@ -133,6 +133,32 @@
BZR_DISABLE_PLUGINS='myplugin:yourplugin'
+BZR_PLUGINS_AT
+~~~~~~~~~~~~~~
+
+When adding a new feature or working on a bug in a plugin,
+developers often need to use a specific version of a given
+plugin. Since python requires that the directory containing the
+code is named like the plugin itself this make it impossible to
+use arbitrary directory names (using a two-level directory scheme
+is inconvenient). ``BZR_PLUGINS_AT`` allows such directories even
+if they don't appear in ``BZR_PLUGIN_PATH`` .
+
+Plugins specified in this environment variable takes precedence
+over the ones in ``BZR_PLUGIN_PATH``.
+
+The variable specified a list of ``plugin_name at plugin path``,
+``plugin_name`` being the name of the plugin as it appears in
+python module paths, ``plugin_path`` being the path to the
+directory containing the plugin code itself
+(i.e. ``plugins/myplugin`` not ``plugins``). Use ':' as the list
+separator, use ';' on windows.
+
+Example:
+~~~~~~~~
+
+Using a specific version of ``myplugin``:
+``BZR_PLUGINS_AT='myplugin@/home/me/bugfixes/123456-myplugin``
BZRPATH
~~~~~~~
=== modified file 'bzrlib/plugin.py'
--- a/bzrlib/plugin.py 2010-03-17 07:16:32 +0000
+++ b/bzrlib/plugin.py 2010-03-24 13:57:35 +0000
@@ -91,12 +91,19 @@
if path is None:
path = get_standard_plugins_path()
_mod_plugins.__path__ = path
- # Set up a blacklist for disabled plugins if any
- PluginBlackListImporter.blacklist = {}
+ PluginImporter.reset()
+ # Set up a blacklist for disabled plugins
disabled_plugins = os.environ.get('BZR_DISABLE_PLUGINS', None)
if disabled_plugins is not None:
for name in disabled_plugins.split(os.pathsep):
- PluginBlackListImporter.blacklist['bzrlib.plugins.' + name] = True
+ PluginImporter.blacklist.add('bzrlib.plugins.' + name)
+ # Set up a the specific paths for plugins
+ specific_plugins = os.environ.get('BZR_PLUGINS_AT', None)
+ if specific_plugins is not None:
+ for spec in specific_plugins.split(os.pathsep):
+ plugin_name, plugin_path = spec.split('@')
+ PluginImporter.specific_paths[
+ 'bzrlib.plugins.%s' % plugin_name] = plugin_path
return path
@@ -237,6 +244,11 @@
The python module path for bzrlib.plugins will be modified to be 'dirs'.
"""
+ # Explicitly load the plugins with a specific path
+ for fullname, path in PluginImporter.specific_paths.iteritems():
+ name = fullname[len('bzrlib.plugins.'):]
+ _load_plugin_module(name, path)
+
# We need to strip the trailing separators here as well as in the
# set_plugins_path function because calling code can pass anything in to
# this function, and since it sets plugins.__path__, it should set it to
@@ -256,72 +268,99 @@
load_from_dirs = load_from_path
+def _find_plugin_module(dir, name):
+ """Check if there is a valid python module that can be loaded as a plugin.
+
+ :param dir: The directory where the search is performed.
+ :param path: An existing file path, either a python file or a package
+ directory.
+
+ :return: (name, path, description) name is the module name, path is the
+ file to load and description is the tuple returned by
+ imp.get_suffixes().
+ """
+ path = osutils.pathjoin(dir, name)
+ if os.path.isdir(path):
+ # Check for a valid __init__.py file, valid suffixes depends on -O and
+ # can be .py, .pyc and .pyo
+ for suffix, mode, kind in imp.get_suffixes():
+ if kind not in (imp.PY_SOURCE, imp.PY_COMPILED):
+ # We don't recognize compiled modules (.so, .dll, etc)
+ continue
+ init_path = osutils.pathjoin(path, '__init__' + suffix)
+ if os.path.isfile(init_path):
+ return name, init_path, (suffix, mode, kind)
+ else:
+ for suffix, mode, kind in imp.get_suffixes():
+ if name.endswith(suffix):
+ # Clean up the module name
+ name = name[:-len(suffix)]
+ if kind == imp.C_EXTENSION and name.endswith('module'):
+ name = name[:-len('module')]
+ return name, path, (suffix, mode, kind)
+ # There is no python module here
+ return None, None, (None, None, None)
+
+
+def _load_plugin_module(name, dir):
+ """Load plugine name from dir.
+
+ :param name: The plugin name in the bzrlib.plugins namespace.
+ :param dir: The directory the plugin is loaded from for error messages.
+ """
+ if ('bzrlib.plugins.%s' % name) in PluginImporter.blacklist:
+ return
+ try:
+ exec "import bzrlib.plugins.%s" % name in {}
+ except KeyboardInterrupt:
+ raise
+ except errors.IncompatibleAPI, e:
+ trace.warning("Unable to load plugin %r. It requested API version "
+ "%s of module %s but the minimum exported version is %s, and "
+ "the maximum is %s" %
+ (name, e.wanted, e.api, e.minimum, e.current))
+ except Exception, e:
+ trace.warning("%s" % e)
+ if re.search('\.|-| ', name):
+ sanitised_name = re.sub('[-. ]', '_', name)
+ if sanitised_name.startswith('bzr_'):
+ sanitised_name = sanitised_name[len('bzr_'):]
+ trace.warning("Unable to load %r in %r as a plugin because the "
+ "file path isn't a valid module name; try renaming "
+ "it to %r." % (name, dir, sanitised_name))
+ else:
+ trace.warning('Unable to load plugin %r from %r' % (name, dir))
+ trace.log_exception_quietly()
+ if 'error' in debug.debug_flags:
+ trace.print_exception(sys.exc_info(), sys.stderr)
+
+
def load_from_dir(d):
"""Load the plugins in directory d.
d must be in the plugins module path already.
+ This function is called once for each directory in the module path.
"""
- # Get the list of valid python suffixes for __init__.py?
- # this includes .py, .pyc, and .pyo (depending on if we are running -O)
- # but it doesn't include compiled modules (.so, .dll, etc)
- valid_suffixes = [suffix for suffix, mod_type, flags in imp.get_suffixes()
- if flags in (imp.PY_SOURCE, imp.PY_COMPILED)]
- package_entries = ['__init__'+suffix for suffix in valid_suffixes]
plugin_names = set()
- for f in os.listdir(d):
- path = osutils.pathjoin(d, f)
- if os.path.isdir(path):
- for entry in package_entries:
- # This directory should be a package, and thus added to
- # the list
- if os.path.isfile(osutils.pathjoin(path, entry)):
- break
- else: # This directory is not a package
- continue
- else:
- for suffix_info in imp.get_suffixes():
- if f.endswith(suffix_info[0]):
- f = f[:-len(suffix_info[0])]
- if suffix_info[2] == imp.C_EXTENSION and f.endswith('module'):
- f = f[:-len('module')]
- break
+ for p in os.listdir(d):
+ name, path, desc = _find_plugin_module(d, p)
+ if name is not None:
+ if name == '__init__':
+ # We do nothing with the __init__.py file in directories from
+ # the bzrlib.plugins module path, we may want to, one day
+ # -- vila 20100316.
+ continue # We don't load __init__.py in the plugins dirs
+ elif getattr(_mod_plugins, name, None) is not None:
+ # The module has already been loaded from another directory
+ # during a previous call.
+ # FIXME: There should be a better way to report masked plugins
+ # -- vila 20100316
+ trace.mutter('Plugin name %s already loaded', name)
else:
- continue
- if f == '__init__':
- continue # We don't load __init__.py again in the plugin dir
- elif getattr(_mod_plugins, f, None):
- trace.mutter('Plugin name %s already loaded', f)
- else:
- # trace.mutter('add plugin name %s', f)
- plugin_names.add(f)
+ plugin_names.add(name)
for name in plugin_names:
- if ('bzrlib.plugins.%s' % name) in PluginBlackListImporter.blacklist:
- continue
- try:
- exec "import bzrlib.plugins.%s" % name in {}
- except KeyboardInterrupt:
- raise
- except errors.IncompatibleAPI, e:
- trace.warning("Unable to load plugin %r. It requested API version "
- "%s of module %s but the minimum exported version is %s, and "
- "the maximum is %s" %
- (name, e.wanted, e.api, e.minimum, e.current))
- except Exception, e:
- trace.warning("%s" % e)
- ## import pdb; pdb.set_trace()
- if re.search('\.|-| ', name):
- sanitised_name = re.sub('[-. ]', '_', name)
- if sanitised_name.startswith('bzr_'):
- sanitised_name = sanitised_name[len('bzr_'):]
- trace.warning("Unable to load %r in %r as a plugin because the "
- "file path isn't a valid module name; try renaming "
- "it to %r." % (name, d, sanitised_name))
- else:
- trace.warning('Unable to load plugin %r from %r' % (name, d))
- trace.log_exception_quietly()
- if 'error' in debug.debug_flags:
- trace.print_exception(sys.exc_info(), sys.stderr)
+ _load_plugin_module(name, d)
def plugins():
@@ -486,17 +525,73 @@
__version__ = property(_get__version__)
-class _PluginBlackListImporter(object):
+class _PluginImporter(object):
+ """An importer tailored to bzr specific needs.
+
+ This is a singleton that takes care of:
+ - disabled plugins specified in 'blacklist',
+ - plugins that needs to be loaded from specific directories.
+ """
def __init__(self):
- self.blacklist = {}
+ self.reset()
+
+ def reset(self):
+ self.blacklist = set()
+ self.specific_paths = {}
def find_module(self, fullname, parent_path=None):
+ """Search a plugin module.
+
+ Disabled plugins raise an import error, plugins with specific paths
+ returns a specific loader.
+
+ :return: None if the plugin doesn't need special handling, self
+ otherwise.
+ """
+ if not fullname.startswith('bzrlib.plugins.'):
+ return None
if fullname in self.blacklist:
raise ImportError('%s is disabled' % fullname)
+ if fullname in self.specific_paths:
+ return self
return None
-PluginBlackListImporter = _PluginBlackListImporter()
-sys.meta_path.append(PluginBlackListImporter)
-
-
+ def load_module(self, fullname):
+ """Load a plugin from a specific directory."""
+ # We are called only for specific paths
+ plugin_dir = self.specific_paths[fullname]
+ candidate = None
+ maybe_package = False
+ for p in os.listdir(plugin_dir):
+ if os.path.isdir(osutils.pathjoin(plugin_dir, p)):
+ # We're searching for files only and don't want submodules to
+ # be recognized as plugins (they are submodules inside the
+ # plugin).
+ continue
+ name, path, (
+ suffix, mode, kind) = _find_plugin_module(plugin_dir, p)
+ if name is not None:
+ candidate = (name, path, suffix, mode, kind)
+ if kind == imp.PY_SOURCE:
+ # We favour imp.PY_SOURCE (which will use the compiled
+ # version if available) over imp.PY_COMPILED (which is used
+ # only if the source is not available)
+ break
+ if candidate is None:
+ raise ImportError('%s cannot be loaded from %s'
+ % (fullname, plugin_dir))
+ f = open(path, mode)
+ try:
+ mod = imp.load_module(fullname, f, path, (suffix, mode, kind))
+ # The plugin can contain modules, so be ready
+ mod.__path__ = [plugin_dir]
+ mod.__package__ = fullname
+ return mod
+ finally:
+ f.close()
+
+
+# Install a dedicated importer for plugins requiring special handling
+PluginImporter = _PluginImporter()
+sys.meta_path.append(PluginImporter)
=== modified file 'bzrlib/tests/__init__.py'
--- a/bzrlib/tests/__init__.py 2010-03-24 07:27:44 +0000
+++ b/bzrlib/tests/__init__.py 2010-03-24 13:58:37 +0000
@@ -1520,6 +1520,7 @@
'BZR_LOG': None,
'BZR_PLUGIN_PATH': None,
'BZR_DISABLE_PLUGINS': None,
+ 'BZR_PLUGINS_AT': None,
'BZR_CONCURRENCY': None,
# Make sure that any text ui tests are consistent regardless of
# the environment the test case is run in; you may want tests that
=== modified file 'bzrlib/tests/test_plugins.py'
--- a/bzrlib/tests/test_plugins.py 2010-03-17 07:16:32 +0000
+++ b/bzrlib/tests/test_plugins.py 2010-03-24 11:55:49 +0000
@@ -39,7 +39,11 @@
class TestPluginMixin(object):
- def create_plugin(self, name, source='', dir='.', file_name=None):
+ def create_plugin(self, name, source=None, dir='.', file_name=None):
+ if source is None:
+ source = '''\
+"""This is the doc for %s"""
+''' % (name)
if file_name is None:
file_name = name + '.py'
# 'source' must not fail to load
@@ -51,11 +55,20 @@
finally:
f.close()
- def create_plugin_package(self, name, source='', dir='.'):
- plugin_dir = osutils.pathjoin(dir, name)
- os.mkdir(plugin_dir)
- self.addCleanup(osutils.rmtree, plugin_dir)
- self.create_plugin(name, source, dir=plugin_dir,
+ def create_plugin_package(self, name, dir=None, source=None):
+ if dir is None:
+ dir = name
+ if source is None:
+ source = '''\
+"""This is the doc for %s"""
+dir_source = '%s'
+''' % (name, dir)
+ os.makedirs(dir)
+ def cleanup():
+ # Workaround lazy import random? madness
+ osutils.rmtree(dir)
+ self.addCleanup(cleanup)
+ self.create_plugin(name, source, dir,
file_name='__init__.py')
def _unregister_plugin(self, name):
@@ -767,16 +780,89 @@
self.overrideAttr(plugin, '_loaded', False)
plugin.load_plugins(['.'])
self.assertPluginKnown('test_foo')
+ self.assertEqual("This is the doc for test_foo",
+ bzrlib.plugins.test_foo.__doc__)
def test_not_loaded(self):
self.warnings = []
def captured_warning(*args, **kwargs):
self.warnings.append((args, kwargs))
self.overrideAttr(trace, 'warning', captured_warning)
+ # Reset the flag that protect against double loading
self.overrideAttr(plugin, '_loaded', False)
osutils.set_or_unset_env('BZR_DISABLE_PLUGINS', 'test_foo')
- plugin.load_plugins(plugin.set_plugins_path(['.']))
+ plugin.load_plugins(['.'])
self.assertPluginUnknown('test_foo')
# Make sure we don't warn about the plugin ImportError since this has
# been *requested* by the user.
self.assertLength(0, self.warnings)
+
+
+class TestLoadPluginAt(tests.TestCaseInTempDir, TestPluginMixin):
+
+ def setUp(self):
+ super(TestLoadPluginAt, self).setUp()
+ # Make sure we don't pollute the plugins namespace
+ self.overrideAttr(plugins, '__path__')
+ # Be paranoid in case a test fail
+ self.addCleanup(self._unregister_plugin, 'test_foo')
+ # Reset the flag that protect against double loading
+ self.overrideAttr(plugin, '_loaded', False)
+ # Create the same plugin in two directories
+ self.create_plugin_package('test_foo', dir='non-standard-dir')
+ self.create_plugin_package('test_foo', dir='b/test_foo')
+
+ def assertTestFooLoadedFrom(self, dir):
+ self.assertPluginKnown('test_foo')
+ self.assertEqual('This is the doc for test_foo',
+ bzrlib.plugins.test_foo.__doc__)
+ self.assertEqual(dir, bzrlib.plugins.test_foo.dir_source)
+
+ def test_regular_load(self):
+ plugin.load_plugins(['b'])
+ self.assertTestFooLoadedFrom('b/test_foo')
+
+ def test_import(self):
+ osutils.set_or_unset_env('BZR_PLUGINS_AT', 'test_foo at non-standard-dir')
+ plugin.set_plugins_path(['b'])
+ try:
+ import bzrlib.plugins.test_foo
+ except ImportError:
+ pass
+ self.assertTestFooLoadedFrom('non-standard-dir')
+
+ def test_loading(self):
+ osutils.set_or_unset_env('BZR_PLUGINS_AT', 'test_foo at non-standard-dir')
+ plugin.load_plugins(['b'])
+ self.assertTestFooLoadedFrom('non-standard-dir')
+
+ def test_compiled_loaded(self):
+ osutils.set_or_unset_env('BZR_PLUGINS_AT', 'test_foo at non-standard-dir')
+ plugin.load_plugins(['b'])
+ self.assertTestFooLoadedFrom('non-standard-dir')
+ self.assertEqual('non-standard-dir/__init__.py',
+ bzrlib.plugins.test_foo.__file__)
+
+ # Try importing again now that the source has been compiled
+ self._unregister_plugin('test_foo')
+ plugin._loaded = False
+ plugin.load_plugins(['b'])
+ self.assertTestFooLoadedFrom('non-standard-dir')
+ if __debug__:
+ suffix = 'pyc'
+ else:
+ suffix = 'pyo'
+ self.assertEqual('non-standard-dir/__init__.%s' % suffix,
+ bzrlib.plugins.test_foo.__file__)
+
+ def test_submodule_loading(self):
+ # We create an additional directory under the one for test_foo
+ self.create_plugin_package('test_bar', dir='non-standard-dir/test_bar')
+ osutils.set_or_unset_env('BZR_PLUGINS_AT', 'test_foo at non-standard-dir')
+ plugin.set_plugins_path(['b'])
+ import bzrlib.plugins.test_foo
+ self.assertEqual('bzrlib.plugins.test_foo',
+ bzrlib.plugins.test_foo.__package__)
+ import bzrlib.plugins.test_foo.test_bar
+ self.assertEqual('non-standard-dir/test_bar/__init__.py',
+ bzrlib.plugins.test_foo.test_bar.__file__)
More information about the bazaar-commits
mailing list