Merge lp:~vila/bzr/411413-disable-plugin into lp:bzr

Proposed by Vincent Ladeuil
Status: Merged
Merged at revision: not available
Proposed branch: lp:~vila/bzr/411413-disable-plugin
Merge into: lp:bzr
Diff against target: 212 lines (+107/-5)
5 files modified
NEWS (+4/-0)
bzrlib/help_topics/en/configuration.txt (+16/-0)
bzrlib/plugin.py (+27/-3)
bzrlib/tests/__init__.py (+1/-0)
bzrlib/tests/test_plugins.py (+59/-2)
To merge this branch: bzr merge lp:~vila/bzr/411413-disable-plugin
Reviewer Review Type Date Requested Status
Martin Pool Needs Fixing
Review via email: mp+21435@code.launchpad.net

Description of the change

This fix bug #411413 by allowing the '-<plugin_name>' syntax to be used in BZR_PLUGIN_PATH.

Note that it works for:
- the automatic loading triggered by bzrlib.plugin.load_plugins([path])
- direct imports from other plugins, import bzrlib.plugins.myplugin
  will now raise an import error

To post a comment you must log in.
Revision history for this message
Matthew Fuller (fullermd) wrote :

> [...] allowing the '-<plugin_name>' syntax to be used in
> BZR_PLUGIN_PATH.

That seems... unclean.

Revision history for this message
Vincent Ladeuil (vila) wrote :

> > [...] allowing the '-<plugin_name>' syntax to be used in
> > BZR_PLUGIN_PATH.
>
> That seems... unclean.

More precisely ? Any better idea ?

We already use +user/-site/+core to give access to the "standard" parts of the paths.
Disabling a plugin is not something you do on a regular basis (or you'd better
just uninstall it).

Revision history for this message
Martin Pool (mbp) wrote :

On 17 March 2010 03:09, Vincent Ladeuil <email address hidden> wrote:
>> > [...] allowing the '-<plugin_name>' syntax to be used in
>> > BZR_PLUGIN_PATH.
>>
>> That seems...   unclean.

Matthew's right, that's just pretty weird to allow a mix of plugin
names and paths.

>
> More precisely ? Any better idea ?
>
> We already use +user/-site/+core to give access to the "standard" parts of the paths.
> Disabling a plugin is not something you do on a regular basis (or you'd better
> just uninstall it).

BZR_DISABLE_PLUGINS=svn:git:qbzr

--
Martin <http://launchpad.net/~mbp/>

Revision history for this message
Martin Pool (mbp) :
review: Needs Fixing
Revision history for this message
Matthew Fuller (fullermd) wrote :

> More precisely ? Any better idea ?

I'd have a separate env var for it.

> We already use +user/-site/+core to give access to the "standard"
> parts of the paths.

Well, but it doesn't give paths to plugins. It gives paths to dirs
CONTAINING plugins. To then mix dirs with plugins, with names of
plugins, is icky.

Revision history for this message
Vincent Ladeuil (vila) wrote :

> BZR_DISABLE_PLUGINS=svn:git:qbzr

Bah, of course, fixed.

Revision history for this message
Martin Pool (mbp) wrote :

23 +BZR_DISABLE_PLUGINS
24 +~~~~~~~~~~~~~~~~~~~
25 +
26 +Under special circumstances, it's better to disable a plugin (or
27 +several) rather than uninstalling them completely. Such plugins
28 +can be specified in the ``BZR_DISABLE_PLUGINS`` environment
29 +variable.
30 +
31 +In that case, ``bzr`` will stop loading the specified plugins and
32 +will raise an import error if you try to import them explicitly.
33 +
34 +Example:
35 +~~~~~~~~
36 +

The ReST syntax here looks strange, because "Example" will be another heading at the same level as BZR_DISABLE_PLUGINS. Just leave it out and put a :: after the word "explicitly".

Users may not understand what "if you try to import them explicitly" means - indeed it doesn't mean a lot at the external user level. So you could say they will error if for example another plugin depends upon them?

202 + self.warnings = []
203 + def captured_warning(*args, **kwargs):
204 + self.warnings.append((args, kwargs))
205 + self.overrideAttr(trace, 'warning', captured_warning)

It seems like there should already be a facility for this, but maybe not. Maybe you could lift it to a base class to save time?

Otherwise very good, thanks for fixing this!

bb:tweak

review: Needs Fixing
Revision history for this message
Vincent Ladeuil (vila) wrote :

> The ReST syntax here looks strange, because "Example" will be another heading
> at the same level as BZR_DISABLE_PLUGINS. Just leave it out and put a ::
> after the word "explicitly".
>

Ok, after some wtf instants and some help from igc, I've fixed that and checked
the results in the sphinx generated html.

> Users may not understand what "if you try to import them explicitly" means -
> indeed it doesn't mean a lot at the external user level. So you could say
> they will error if for example another plugin depends upon them?

Added.

>
> 202 + self.warnings = []
> 203 + def captured_warning(*args, **kwargs):
> 204 + self.warnings.append((args, kwargs))
> 205 + self.overrideAttr(trace, 'warning', captured_warning)
>
> It seems like there should already be a facility for this, but maybe not.
> Maybe you could lift it to a base class to save time?

Well, I struggled a bit with _capture_deprecation_warnings (for symbol_versioning.warn) and callCatchWarnings (for warnings.showwarning) before realizing I wanted *trace.warning*.

So I agree we should provide an easier way :)
May be if all warnings, note, mutter was controlled by UIFactory and friends things will be clearer ?

Since that's the first time I know of that we need to capture trace.warning, I'd rather punt on this one.

I could make a further submission if you prefer, but *I*'d prefer some unification
of various warnings first (if only by providing more explicit names).

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'NEWS'
--- NEWS 2010-03-12 19:59:50 +0000
+++ NEWS 2010-03-17 08:15:36 +0000
@@ -51,6 +51,10 @@
51 treats backslash as an escape character on Windows. (Gordon Tyler,51 treats backslash as an escape character on Windows. (Gordon Tyler,
52 #392248)52 #392248)
5353
54* Plugins can be disabled by defining ``BZR_DISABLE_PLUGINS`` as
55 a list of plugin names separated by ':' (';' on windows).
56 (Vincent Ladeuil, #411413)
57
54* Tree-shape conflicts can be resolved by providing ``--take-this`` and58* Tree-shape conflicts can be resolved by providing ``--take-this`` and
55 ``--take-other`` to the ``bzr resolve`` command. Just marking the conflict59 ``--take-other`` to the ``bzr resolve`` command. Just marking the conflict
56 as resolved is still accessible via the ``--done`` default action.60 as resolved is still accessible via the ``--done`` default action.
5761
=== modified file 'bzrlib/help_topics/en/configuration.txt'
--- bzrlib/help_topics/en/configuration.txt 2010-01-03 03:33:10 +0000
+++ bzrlib/help_topics/en/configuration.txt 2010-03-17 08:15:36 +0000
@@ -116,6 +116,22 @@
116Overriding the default site plugin directory:116Overriding the default site plugin directory:
117``BZR_PLUGIN_PATH='/path/to/my/site/plugins:-site':+user``117``BZR_PLUGIN_PATH='/path/to/my/site/plugins:-site':+user``
118118
119BZR_DISABLE_PLUGINS
120~~~~~~~~~~~~~~~~~~~
121
122Under special circumstances, it's better to disable a plugin (or
123several) rather than uninstalling them completely. Such plugins
124can be specified in the ``BZR_DISABLE_PLUGINS`` environment
125variable.
126
127In that case, ``bzr`` will stop loading the specified plugins and
128will raise an import error if you try to import them explicitly.
129
130Example:
131~~~~~~~~
132
133Disabling ``myplugin`` and ``yourplugin``:
134``BZR_DISABLE_PLUGINS='myplugin:yourplugin'``
119135
120136
121BZRPATH137BZRPATH
122138
=== modified file 'bzrlib/plugin.py'
--- bzrlib/plugin.py 2010-03-12 13:41:08 +0000
+++ bzrlib/plugin.py 2010-03-17 08:15:36 +0000
@@ -91,6 +91,12 @@
91 if path is None:91 if path is None:
92 path = get_standard_plugins_path()92 path = get_standard_plugins_path()
93 _mod_plugins.__path__ = path93 _mod_plugins.__path__ = path
94 # Set up a blacklist for disabled plugins if any
95 PluginBlackListImporter.blacklist = {}
96 disabled_plugins = os.environ.get('BZR_DISABLE_PLUGINS', None)
97 if disabled_plugins is not None:
98 for name in disabled_plugins.split(os.pathsep):
99 PluginBlackListImporter.blacklist['bzrlib.plugins.' + name] = True
94 return path100 return path
95101
96102
@@ -183,8 +189,8 @@
183 try:189 try:
184 p = refs[p[1:]]190 p = refs[p[1:]]
185 except KeyError:191 except KeyError:
186 # Leave them untouched otherwise, user may have paths starting192 # Leave them untouched so user can still use paths starting
187 # with '+'...193 # with '+'
188 pass194 pass
189 _append_new_path(paths, p)195 _append_new_path(paths, p)
190196
@@ -202,7 +208,7 @@
202 files (and whatever other extensions are used in the platform,208 files (and whatever other extensions are used in the platform,
203 such as *.pyd).209 such as *.pyd).
204210
205 load_from_dirs() provides the underlying mechanism and is called with211 load_from_path() provides the underlying mechanism and is called with
206 the default directory list to provide the normal behaviour.212 the default directory list to provide the normal behaviour.
207213
208 :param path: The list of paths to search for plugins. By default,214 :param path: The list of paths to search for plugins. By default,
@@ -290,6 +296,8 @@
290 plugin_names.add(f)296 plugin_names.add(f)
291297
292 for name in plugin_names:298 for name in plugin_names:
299 if ('bzrlib.plugins.%s' % name) in PluginBlackListImporter.blacklist:
300 continue
293 try:301 try:
294 exec "import bzrlib.plugins.%s" % name in {}302 exec "import bzrlib.plugins.%s" % name in {}
295 except KeyboardInterrupt:303 except KeyboardInterrupt:
@@ -476,3 +484,19 @@
476 return version_string484 return version_string
477485
478 __version__ = property(_get__version__)486 __version__ = property(_get__version__)
487
488
489class _PluginBlackListImporter(object):
490
491 def __init__(self):
492 self.blacklist = {}
493
494 def find_module(self, fullname, parent_path=None):
495 if fullname in self.blacklist:
496 raise ImportError('%s is disabled' % fullname)
497 return None
498
499PluginBlackListImporter = _PluginBlackListImporter()
500sys.meta_path.append(PluginBlackListImporter)
501
502
479503
=== modified file 'bzrlib/tests/__init__.py'
--- bzrlib/tests/__init__.py 2010-03-10 06:02:51 +0000
+++ bzrlib/tests/__init__.py 2010-03-17 08:15:36 +0000
@@ -1519,6 +1519,7 @@
1519 'BZR_PROGRESS_BAR': None,1519 'BZR_PROGRESS_BAR': None,
1520 'BZR_LOG': None,1520 'BZR_LOG': None,
1521 'BZR_PLUGIN_PATH': None,1521 'BZR_PLUGIN_PATH': None,
1522 'BZR_DISABLE_PLUGINS': None,
1522 'BZR_CONCURRENCY': None,1523 'BZR_CONCURRENCY': None,
1523 # Make sure that any text ui tests are consistent regardless of1524 # Make sure that any text ui tests are consistent regardless of
1524 # the environment the test case is run in; you may want tests that1525 # the environment the test case is run in; you may want tests that
15251526
=== modified file 'bzrlib/tests/test_plugins.py'
--- bzrlib/tests/test_plugins.py 2010-03-15 11:17:44 +0000
+++ bzrlib/tests/test_plugins.py 2010-03-17 08:15:36 +0000
@@ -31,6 +31,7 @@
31 plugin,31 plugin,
32 plugins,32 plugins,
33 tests,33 tests,
34 trace,
34 )35 )
3536
3637
@@ -38,6 +39,25 @@
3839
39class TestPluginMixin(object):40class TestPluginMixin(object):
4041
42 def create_plugin(self, name, source='', dir='.', file_name=None):
43 if file_name is None:
44 file_name = name + '.py'
45 # 'source' must not fail to load
46 path = osutils.pathjoin(dir, file_name)
47 f = open(path, 'w')
48 self.addCleanup(os.unlink, path)
49 try:
50 f.write(source + '\n')
51 finally:
52 f.close()
53
54 def create_plugin_package(self, name, source='', dir='.'):
55 plugin_dir = osutils.pathjoin(dir, name)
56 os.mkdir(plugin_dir)
57 self.addCleanup(osutils.rmtree, plugin_dir)
58 self.create_plugin(name, source, dir=plugin_dir,
59 file_name='__init__.py')
60
41 def _unregister_plugin(self, name):61 def _unregister_plugin(self, name):
42 """Remove the plugin from sys.modules and the bzrlib namespace."""62 """Remove the plugin from sys.modules and the bzrlib namespace."""
43 py_name = 'bzrlib.plugins.%s' % name63 py_name = 'bzrlib.plugins.%s' % name
@@ -47,11 +67,11 @@
47 delattr(bzrlib.plugins, name)67 delattr(bzrlib.plugins, name)
4868
49 def assertPluginUnknown(self, name):69 def assertPluginUnknown(self, name):
50 self.failIf(getattr(bzrlib.plugins, 'plugin', None) is not None)70 self.failIf(getattr(bzrlib.plugins, name, None) is not None)
51 self.failIf('bzrlib.plugins.%s' % name in sys.modules)71 self.failIf('bzrlib.plugins.%s' % name in sys.modules)
5272
53 def assertPluginKnown(self, name):73 def assertPluginKnown(self, name):
54 self.failUnless(getattr(bzrlib.plugins, 'plugin', None) is not None)74 self.failUnless(getattr(bzrlib.plugins, name, None) is not None)
55 self.failUnless('bzrlib.plugins.%s' % name in sys.modules)75 self.failUnless('bzrlib.plugins.%s' % name in sys.modules)
5676
5777
@@ -723,3 +743,40 @@
723 self.check_path(['+foo', '-bar', self.core, self.site],743 self.check_path(['+foo', '-bar', self.core, self.site],
724 ['+foo', '-bar'])744 ['+foo', '-bar'])
725745
746
747class TestDisablePlugin(tests.TestCaseInTempDir, TestPluginMixin):
748
749 def setUp(self):
750 super(TestDisablePlugin, self).setUp()
751 self.create_plugin_package('test_foo')
752 # Make sure we don't pollute the plugins namespace
753 self.overrideAttr(plugins, '__path__')
754 # Be paranoid in case a test fail
755 self.addCleanup(self._unregister_plugin, 'test_foo')
756
757 def test_cannot_import(self):
758 osutils.set_or_unset_env('BZR_DISABLE_PLUGINS', 'test_foo')
759 plugin.set_plugins_path(['.'])
760 try:
761 import bzrlib.plugins.test_foo
762 except ImportError:
763 pass
764 self.assertPluginUnknown('test_foo')
765
766 def test_regular_load(self):
767 self.overrideAttr(plugin, '_loaded', False)
768 plugin.load_plugins(['.'])
769 self.assertPluginKnown('test_foo')
770
771 def test_not_loaded(self):
772 self.warnings = []
773 def captured_warning(*args, **kwargs):
774 self.warnings.append((args, kwargs))
775 self.overrideAttr(trace, 'warning', captured_warning)
776 self.overrideAttr(plugin, '_loaded', False)
777 osutils.set_or_unset_env('BZR_DISABLE_PLUGINS', 'test_foo')
778 plugin.load_plugins(plugin.set_plugins_path(['.']))
779 self.assertPluginUnknown('test_foo')
780 # Make sure we don't warn about the plugin ImportError since this has
781 # been *requested* by the user.
782 self.assertLength(0, self.warnings)