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