Merge lp:~mbp/bzr/704195-plugin-warnings into lp:bzr

Proposed by Martin Pool
Status: Merged
Approved by: Martin Pool
Approved revision: no longer in the source branch.
Merged at revision: 5630
Proposed branch: lp:~mbp/bzr/704195-plugin-warnings
Merge into: lp:bzr
Diff against target: 332 lines (+119/-38)
6 files modified
bzrlib/builtins.py (+3/-20)
bzrlib/crash.py (+2/-6)
bzrlib/plugin.py (+54/-3)
bzrlib/tests/test_crash.py (+9/-0)
bzrlib/tests/test_plugins.py (+46/-9)
doc/en/release-notes/bzr-2.4.txt (+5/-0)
To merge this branch: bzr merge lp:~mbp/bzr/704195-plugin-warnings
Reviewer Review Type Date Requested Status
Jelmer Vernooij (community) Approve
Bazaar Developers Pending
Review via email: mp+46771@code.launchpad.net

This proposal supersedes a proposal from 2011-01-19.

Commit message

Don't show messages about plugins incompatible with bzr

Description of the change

This stops bzr spamming you about out-of-date plugins. Telling people on every single invocation is not helpful. Now we just tell you on in `bzr plugins`.

As a follow-on in bug 704238 we can try to load them anyhow.

To post a comment you must log in.
Revision history for this message
Martin Pool (mbp) wrote :

I previously targeted 2.3 but this is more useful in 2.4. The NEWS probably needs to be fixed before landing.

Revision history for this message
Jelmer Vernooij (jelmer) :
review: Approve
Revision history for this message
Martin Pool (mbp) wrote :

I'll update a couple of things before landing based on conversations
with Jelmer:

 * the code that formats the list could fold both the loaded and
failed-to-load plugins together, and be a bit simpler
 * should move into the 2.4 NEWS

Note that with this change, missing-dependency plugins will continue
to not load; we just represent that differently.

--
Martin

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

sent to pqm by email

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'bzrlib/builtins.py'
2--- bzrlib/builtins.py 2011-01-19 18:40:15 +0000
3+++ bzrlib/builtins.py 2011-01-20 23:09:55 +0000
4@@ -4587,26 +4587,9 @@
5
6 @display_command
7 def run(self, verbose=False):
8- import bzrlib.plugin
9- from inspect import getdoc
10- result = []
11- for name, plugin in bzrlib.plugin.plugins().items():
12- version = plugin.__version__
13- if version == 'unknown':
14- version = ''
15- name_ver = '%s %s' % (name, version)
16- d = getdoc(plugin.module)
17- if d:
18- doc = d.split('\n')[0]
19- else:
20- doc = '(no description)'
21- result.append((name_ver, doc, plugin.path()))
22- for name_ver, doc, path in sorted(result):
23- self.outf.write("%s\n" % name_ver)
24- self.outf.write(" %s\n" % doc)
25- if verbose:
26- self.outf.write(" %s\n" % path)
27- self.outf.write("\n")
28+ from bzrlib import plugin
29+ self.outf.writelines(
30+ plugin.describe_plugins(show_paths=verbose))
31
32
33 class cmd_testament(Command):
34
35=== modified file 'bzrlib/crash.py'
36--- bzrlib/crash.py 2011-01-18 00:25:04 +0000
37+++ bzrlib/crash.py 2011-01-20 23:09:55 +0000
38@@ -1,4 +1,4 @@
39-# Copyright (C) 2009, 2010 Canonical Ltd
40+# Copyright (C) 2009, 2010, 2011 Canonical Ltd
41 #
42 # This program is free software; you can redistribute it and/or modify
43 # it under the terms of the GNU General Public License as published by
44@@ -255,11 +255,7 @@
45
46
47 def _format_plugin_list():
48- plugin_lines = []
49- for name, a_plugin in sorted(plugin.plugins().items()):
50- plugin_lines.append(" %-20s %s [%s]" %
51- (name, a_plugin.path(), a_plugin.__version__))
52- return '\n'.join(plugin_lines)
53+ return ''.join(plugin.describe_plugins(show_paths=True))
54
55
56 def _format_module_list():
57
58=== modified file 'bzrlib/plugin.py'
59--- bzrlib/plugin.py 2010-06-11 08:02:42 +0000
60+++ bzrlib/plugin.py 2011-01-20 23:09:55 +0000
61@@ -1,4 +1,4 @@
62-# Copyright (C) 2005-2010 Canonical Ltd
63+# Copyright (C) 2005-2011 Canonical Ltd
64 #
65 # This program is free software; you can redistribute it and/or modify
66 # it under the terms of the GNU General Public License as published by
67@@ -63,6 +63,11 @@
68 _plugins_disabled = False
69
70
71+plugin_warnings = {}
72+# Map from plugin name, to list of string warnings about eg plugin
73+# dependencies.
74+
75+
76 def are_plugins_disabled():
77 return _plugins_disabled
78
79@@ -77,6 +82,43 @@
80 load_plugins([])
81
82
83+def describe_plugins(show_paths=False):
84+ """Generate text description of plugins.
85+
86+ Includes both those that have loaded, and those that failed to
87+ load.
88+
89+ :param show_paths: If true,
90+ :returns: Iterator of text lines (including newlines.)
91+ """
92+ from inspect import getdoc
93+ loaded_plugins = plugins()
94+ all_names = sorted(list(set(
95+ loaded_plugins.keys() + plugin_warnings.keys())))
96+ for name in all_names:
97+ if name in loaded_plugins:
98+ plugin = loaded_plugins[name]
99+ version = plugin.__version__
100+ if version == 'unknown':
101+ version = ''
102+ yield '%s %s\n' % (name, version)
103+ d = getdoc(plugin.module)
104+ if d:
105+ doc = d.split('\n')[0]
106+ else:
107+ doc = '(no description)'
108+ yield (" %s\n" % doc)
109+ if show_paths:
110+ yield (" %s\n" % plugin.path())
111+ del plugin
112+ else:
113+ yield "%s (failed to load)\n" % name
114+ if name in plugin_warnings:
115+ for line in plugin_warnings[name]:
116+ yield " ** " + line + '\n'
117+ yield '\n'
118+
119+
120 def _strip_trailing_sep(path):
121 return path.rstrip("\\/")
122
123@@ -327,6 +369,11 @@
124 return None, None, (None, None, None)
125
126
127+def record_plugin_warning(plugin_name, warning_message):
128+ trace.mutter(warning_message)
129+ plugin_warnings.setdefault(plugin_name, []).append(warning_message)
130+
131+
132 def _load_plugin_module(name, dir):
133 """Load plugin name from dir.
134
135@@ -340,10 +387,12 @@
136 except KeyboardInterrupt:
137 raise
138 except errors.IncompatibleAPI, e:
139- trace.warning("Unable to load plugin %r. It requested API version "
140+ warning_message = (
141+ "Unable to load plugin %r. It requested API version "
142 "%s of module %s but the minimum exported version is %s, and "
143 "the maximum is %s" %
144 (name, e.wanted, e.api, e.minimum, e.current))
145+ record_plugin_warning(name, warning_message)
146 except Exception, e:
147 trace.warning("%s" % e)
148 if re.search('\.|-| ', name):
149@@ -354,7 +403,9 @@
150 "file path isn't a valid module name; try renaming "
151 "it to %r." % (name, dir, sanitised_name))
152 else:
153- trace.warning('Unable to load plugin %r from %r' % (name, dir))
154+ record_plugin_warning(
155+ name,
156+ 'Unable to load plugin %r from %r' % (name, dir))
157 trace.log_exception_quietly()
158 if 'error' in debug.debug_flags:
159 trace.print_exception(sys.exc_info(), sys.stderr)
160
161=== modified file 'bzrlib/tests/test_crash.py'
162--- bzrlib/tests/test_crash.py 2011-01-12 01:01:53 +0000
163+++ bzrlib/tests/test_crash.py 2011-01-20 23:09:55 +0000
164@@ -26,6 +26,7 @@
165 config,
166 crash,
167 osutils,
168+ plugin,
169 tests,
170 )
171
172@@ -42,6 +43,11 @@
173 self.overrideEnv('APPORT_CRASH_DIR', crash_dir)
174 self.assertEquals(crash_dir, config.crash_dir())
175
176+ self.overrideAttr(
177+ plugin,
178+ 'plugin_warnings',
179+ {'example': ['Failed to load plugin foo']})
180+
181 stderr = StringIO()
182
183 try:
184@@ -71,3 +77,6 @@
185 self.assertContainsRe(report, 'test_apport_report')
186 # should also be in there
187 self.assertContainsRe(report, '(?m)^CommandLine:')
188+ self.assertContainsRe(
189+ report,
190+ 'Failed to load plugin foo')
191
192=== modified file 'bzrlib/tests/test_plugins.py'
193--- bzrlib/tests/test_plugins.py 2011-01-10 22:20:12 +0000
194+++ bzrlib/tests/test_plugins.py 2011-01-20 23:09:55 +0000
195@@ -38,7 +38,7 @@
196
197 # TODO: Write a test for plugin decoration of commands.
198
199-class TestPluginMixin(object):
200+class BaseTestPlugins(tests.TestCaseInTempDir):
201
202 def create_plugin(self, name, source=None, dir='.', file_name=None):
203 if source is None:
204@@ -99,7 +99,7 @@
205 self.failUnless('bzrlib.plugins.%s' % name in sys.modules)
206
207
208-class TestLoadingPlugins(tests.TestCaseInTempDir, TestPluginMixin):
209+class TestLoadingPlugins(BaseTestPlugins):
210
211 activeattributes = {}
212
213@@ -267,8 +267,13 @@
214 stream.close()
215
216 def test_plugin_with_bad_api_version_reports(self):
217- # This plugin asks for bzrlib api version 1.0.0, which is not supported
218- # anymore.
219+ """Try loading a plugin that requests an unsupported api.
220+
221+ Observe that it records the problem but doesn't complain on stderr.
222+
223+ See https://bugs.launchpad.net/bzr/+bug/704195
224+ """
225+ self.overrideAttr(plugin, 'plugin_warnings', {})
226 name = 'wants100.py'
227 f = file(name, 'w')
228 try:
229@@ -276,9 +281,14 @@
230 "bzrlib.api.require_any_api(bzrlib, [(1, 0, 0)])\n")
231 finally:
232 f.close()
233-
234 log = self.load_and_capture(name)
235- self.assertContainsRe(log,
236+ self.assertNotContainsRe(log,
237+ r"It requested API version")
238+ self.assertEquals(
239+ ['wants100'],
240+ plugin.plugin_warnings.keys())
241+ self.assertContainsRe(
242+ plugin.plugin_warnings['wants100'][0],
243 r"It requested API version")
244
245 def test_plugin_with_bad_name_does_not_load(self):
246@@ -292,7 +302,7 @@
247 "it to 'bad_plugin_name_'\.")
248
249
250-class TestPlugins(tests.TestCaseInTempDir, TestPluginMixin):
251+class TestPlugins(BaseTestPlugins):
252
253 def setup_plugin(self, source=""):
254 # This test tests a new plugin appears in bzrlib.plugin.plugins().
255@@ -768,7 +778,7 @@
256 ['+foo', '-bar'])
257
258
259-class TestDisablePlugin(tests.TestCaseInTempDir, TestPluginMixin):
260+class TestDisablePlugin(BaseTestPlugins):
261
262 def setUp(self):
263 super(TestDisablePlugin, self).setUp()
264@@ -809,6 +819,7 @@
265 self.assertLength(0, self.warnings)
266
267
268+
269 class TestLoadPluginAtSyntax(tests.TestCase):
270
271 def _get_paths(self, paths):
272@@ -832,7 +843,7 @@
273 os.pathsep.join(['batman@cave', '', 'robin@mobile']))
274
275
276-class TestLoadPluginAt(tests.TestCaseInTempDir, TestPluginMixin):
277+class TestLoadPluginAt(BaseTestPlugins):
278
279 def setUp(self):
280 super(TestLoadPluginAt, self).setUp()
281@@ -847,6 +858,9 @@
282 self.create_plugin_package('test_foo', dir='standard/test_foo')
283 # All the tests will load the 'test_foo' plugin from various locations
284 self.addCleanup(self._unregister_plugin, 'test_foo')
285+ # Unfortunately there's global cached state for the specific
286+ # registered paths.
287+ self.addCleanup(plugin.PluginImporter.reset)
288
289 def assertTestFooLoadedFrom(self, path):
290 self.assertPluginKnown('test_foo')
291@@ -945,3 +959,26 @@
292 self.overrideEnv('BZR_PLUGINS_AT', 'test_foo@%s' % plugin_path)
293 plugin.load_plugins(['standard'])
294 self.assertTestFooLoadedFrom(plugin_path)
295+
296+
297+class TestDescribePlugins(BaseTestPlugins):
298+
299+ def test_describe_plugins(self):
300+ class DummyModule(object):
301+ __doc__ = 'Hi there'
302+ class DummyPlugin(object):
303+ __version__ = '0.1.0'
304+ module = DummyModule()
305+ def dummy_plugins():
306+ return { 'good': DummyPlugin() }
307+ self.overrideAttr(plugin, 'plugin_warnings',
308+ {'bad': ['Failed to load (just testing)']})
309+ self.overrideAttr(plugin, 'plugins', dummy_plugins)
310+ self.assertEquals("""\
311+bad (failed to load)
312+ ** Failed to load (just testing)
313+
314+good 0.1.0
315+ Hi there
316+
317+""", ''.join(plugin.describe_plugins()))
318
319=== modified file 'doc/en/release-notes/bzr-2.4.txt'
320--- doc/en/release-notes/bzr-2.4.txt 2011-01-19 18:18:48 +0000
321+++ doc/en/release-notes/bzr-2.4.txt 2011-01-20 23:09:55 +0000
322@@ -54,6 +54,11 @@
323 * ``bzr whoami`` will now display an error if both a new identity and ``--email``
324 were specified. (Jelmer Vernooij, #680449)
325
326+* Plugins incompatible with the current version of bzr no longer produce a
327+ warning on every command invocation. Instead, a message is shown by
328+ ``bzr plugins`` and in crash reports.
329+ (#704195, Martin Pool)
330+
331 Documentation
332 *************
333