Merge lp:~vila/bzr/930182-display-reg-options into lp:bzr

Proposed by Vincent Ladeuil
Status: Merged
Approved by: Jelmer Vernooij
Approved revision: no longer in the source branch.
Merged at revision: 6471
Proposed branch: lp:~vila/bzr/930182-display-reg-options
Merge into: lp:bzr
Diff against target: 272 lines (+106/-49)
4 files modified
bzrlib/config.py (+43/-45)
bzrlib/tests/blackbox/test_config.py (+20/-2)
bzrlib/tests/test_config.py (+40/-2)
doc/en/release-notes/bzr-2.6.txt (+3/-0)
To merge this branch: bzr merge lp:~vila/bzr/930182-display-reg-options
Reviewer Review Type Date Requested Status
Jelmer Vernooij (community) Needs Fixing
Review via email: mp+92807@code.launchpad.net

Commit message

Fix RegistryOption display in bzr config output

Description of the change

bzr config was still relying too much on configobj for proper quoting.

After a bit if ping-pong to fix quoting fallouts, this proposal fixes bzr
config display for all values by *not* calling (indirectly) the
``from_unicode`` method for registered options.

Thanks to Jelmer for pushing me forward on this one as I first mistakenly
thought that stack.get() and stack.set() should be symmetric.

For the record, they are not and 'from_unicode' have been introduced
precisely to make dev's life easier. ``bool_from_store`` for example accepts
'off' to means False which already breaks this symmetry ('False' is an
accepted string for booleans which makes this fact a little less obvious).

stack.set() still relies on stringification for bool, int and lists and that
seem to be the most pragmatic thing to do.

A little care should still be taken when setting values for RegistryOptions
and devs have to remember that the values stored are keys for the registry
not object themselves (we can't (or rather don't want to)) store objects in
text files but this doesn't have to force them to avoid writing from_unicode
helpers.

To post a comment you must log in.
Revision history for this message
Vincent Ladeuil (vila) wrote :

Wow, I just realized there was a nasty potential bug in the option expansion: conversion should never be attempted during expansion where all values should remain strings. The conversion should be done only for the outer get() call.

The fix is a one-liner.

Revision history for this message
Jelmer Vernooij (jelmer) wrote :

+ # Ensuring lazy loading is achieved by delaying section matching (which
s/Ensuring/Ensure/

+ # happen while expanding. Conversion should only occur for the origianl
^ typo

It would be nice to have some sort of test for iter_sections().

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

> + # Ensuring lazy loading is achieved by delaying section matching
> (which
> s/Ensuring/Ensure/
 Are you sure ? 'Ensure ... is achieved' sounds weird.

>
> + # happen while expanding. Conversion should only occur for the
> origianl
> ^ typo

Thanks.
>
> It would be nice to have some sort of test for iter_sections().

Will do.

Revision history for this message
Vincent Ladeuil (vila) 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/config.py'
2--- bzrlib/config.py 2012-02-18 16:55:04 +0000
3+++ bzrlib/config.py 2012-02-20 17:21:48 +0000
4@@ -3645,7 +3645,17 @@
5 self.store = store
6 self.mutable_section_id = mutable_section_id
7
8- def get(self, name, expand=None):
9+ def iter_sections(self):
10+ """Iterate all the defined sections."""
11+ # Ensuring lazy loading is achieved by delaying section matching (which
12+ # implies querying the persistent storage) until it can't be avoided
13+ # anymore by using callables to describe (possibly empty) section
14+ # lists.
15+ for sections in self.sections_def:
16+ for store, section in sections():
17+ yield store, section
18+
19+ def get(self, name, expand=None, convert=True):
20 """Return the *first* option value found in the sections.
21
22 This is where we guarantee that sections coming from Store are loaded
23@@ -3658,6 +3668,9 @@
24
25 :param expand: Whether options references should be expanded.
26
27+ :param convert: Whether the option value should be converted from
28+ unicode (do nothing for non-registered options).
29+
30 :returns: The value of the option.
31 """
32 # FIXME: No caching of options nor sections yet -- vila 20110503
33@@ -3686,7 +3699,7 @@
34 % (name, type(val)))
35 if opt is None:
36 val = found_store.unquote(val)
37- else:
38+ elif convert:
39 val = opt.convert_from_unicode(found_store, val)
40 return val
41
42@@ -3696,17 +3709,10 @@
43 value = opt.get_override()
44 value = expand_and_convert(value)
45 if value is None:
46- # Ensuring lazy loading is achieved by delaying section matching
47- # (which implies querying the persistent storage) until it can't be
48- # avoided anymore by using callables to describe (possibly empty)
49- # section lists.
50- for sections in self.sections_def:
51- for store, section in sections():
52- value = section.get(name)
53- if value is not None:
54- found_store = store
55- break
56+ for store, section in self.iter_sections():
57+ value = section.get(name)
58 if value is not None:
59+ found_store = store
60 break
61 value = expand_and_convert(value)
62 if opt is not None and value is None:
63@@ -3778,7 +3784,7 @@
64 # anything else
65 value = env[name]
66 else:
67- value = self.get(name, expand=False)
68+ value = self.get(name, expand=False, convert=False)
69 value = self._expand_options_in_string(value, env, _refs)
70 return value
71
72@@ -4025,11 +4031,6 @@
73 self.store.save_changes()
74
75
76-# Use a an empty dict to initialize an empty configobj avoiding all
77-# parsing and encoding checks
78-_quoting_config = configobj.ConfigObj(
79- {}, encoding='utf-8', interpolation=False, list_values=True)
80-
81 class cmd_config(commands.Command):
82 __doc__ = """Display, set or remove a configuration option.
83
84@@ -4133,12 +4134,17 @@
85 except errors.NotBranchError:
86 return LocationStack(directory)
87
88+ def _quote_multiline(self, value):
89+ if '\n' in value:
90+ value = '"""' + value + '"""'
91+ return value
92+
93 def _show_value(self, name, directory, scope):
94 conf = self._get_stack(directory, scope)
95- value = conf.get(name, expand=True)
96+ value = conf.get(name, expand=True, convert=False)
97 if value is not None:
98 # Quote the value appropriately
99- value = _quoting_config._quote(value)
100+ value = self._quote_multiline(value)
101 self.outf.write('%s\n' % (value,))
102 else:
103 raise errors.NoSuchConfigOption(name)
104@@ -4152,31 +4158,23 @@
105 cur_store_id = None
106 cur_section = None
107 conf = self._get_stack(directory, scope)
108- for sections in conf.sections_def:
109- for store, section in sections():
110- for oname in section.iter_option_names():
111- if name.search(oname):
112- if cur_store_id != store.id:
113- # Explain where the options are defined
114- self.outf.write('%s:\n' % (store.id,))
115- cur_store_id = store.id
116- cur_section = None
117- if (section.id is not None
118- and cur_section != section.id):
119- # Display the section id as it appears in the store
120- # (None doesn't appear by definition)
121- self.outf.write(' [%s]\n' % (section.id,))
122- cur_section = section.id
123- value = section.get(oname, expand=False)
124- # Since we don't use the stack, we need to restore a
125- # proper quoting.
126- try:
127- opt = option_registry.get(oname)
128- value = opt.convert_from_unicode(store, value)
129- except KeyError:
130- value = store.unquote(value)
131- value = _quoting_config._quote(value)
132- self.outf.write(' %s = %s\n' % (oname, value))
133+ for store, section in conf.iter_sections():
134+ for oname in section.iter_option_names():
135+ if name.search(oname):
136+ if cur_store_id != store.id:
137+ # Explain where the options are defined
138+ self.outf.write('%s:\n' % (store.id,))
139+ cur_store_id = store.id
140+ cur_section = None
141+ if (section.id is not None and cur_section != section.id):
142+ # Display the section id as it appears in the store
143+ # (None doesn't appear by definition)
144+ self.outf.write(' [%s]\n' % (section.id,))
145+ cur_section = section.id
146+ value = section.get(oname, expand=False)
147+ # Quote the value appropriately
148+ value = self._quote_multiline(value)
149+ self.outf.write(' %s = %s\n' % (oname, value))
150
151 def _set_config_option(self, name, value, directory, scope):
152 conf = self._get_stack(directory, scope, write_access=True)
153
154=== modified file 'bzrlib/tests/blackbox/test_config.py'
155--- bzrlib/tests/blackbox/test_config.py 2012-01-03 12:56:06 +0000
156+++ bzrlib/tests/blackbox/test_config.py 2012-02-20 17:21:48 +0000
157@@ -95,7 +95,7 @@
158 """
159 ''')
160
161- def test_list_all_values(self):
162+ def test_list_value_all(self):
163 config.option_registry.register(config.ListOption('list'))
164 self.addCleanup(config.option_registry.remove, 'list')
165 self.bazaar_config.set_user_option('list', [1, 'a', 'with, a comma'])
166@@ -106,7 +106,7 @@
167 list = 1, a, "with, a comma"
168 ''')
169
170- def test_list_value_only(self):
171+ def test_list_value_one(self):
172 config.option_registry.register(config.ListOption('list'))
173 self.addCleanup(config.option_registry.remove, 'list')
174 self.bazaar_config.set_user_option('list', [1, 'a', 'with, a comma'])
175@@ -115,6 +115,24 @@
176 1, a, "with, a comma"
177 ''')
178
179+ def test_registry_value_all(self):
180+ self.bazaar_config.set_user_option('bzr.transform.orphan_policy',
181+ u'move')
182+ script.run_script(self, '''\
183+ $ bzr config -d tree
184+ bazaar:
185+ [DEFAULT]
186+ bzr.transform.orphan_policy = move
187+ ''')
188+
189+ def test_registry_value_one(self):
190+ self.bazaar_config.set_user_option('bzr.transform.orphan_policy',
191+ u'move')
192+ script.run_script(self, '''\
193+ $ bzr config -d tree bzr.transform.orphan_policy
194+ move
195+ ''')
196+
197 def test_bazaar_config(self):
198 self.bazaar_config.set_user_option('hello', 'world')
199 script.run_script(self, '''\
200
201=== modified file 'bzrlib/tests/test_config.py'
202--- bzrlib/tests/test_config.py 2012-02-18 16:55:04 +0000
203+++ bzrlib/tests/test_config.py 2012-02-20 17:21:48 +0000
204@@ -3674,6 +3674,41 @@
205 self.assertEquals('bar', conf.get('foo'))
206
207
208+class TestStackIterSections(tests.TestCase):
209+
210+ def test_empty_stack(self):
211+ conf = config.Stack([])
212+ sections = list(conf.iter_sections())
213+ self.assertLength(0, sections)
214+
215+ def test_empty_store(self):
216+ store = config.IniFileStore()
217+ store._load_from_string('')
218+ conf = config.Stack([store.get_sections])
219+ sections = list(conf.iter_sections())
220+ self.assertLength(0, sections)
221+
222+ def test_simple_store(self):
223+ store = config.IniFileStore()
224+ store._load_from_string('foo=bar')
225+ conf = config.Stack([store.get_sections])
226+ tuples = list(conf.iter_sections())
227+ self.assertLength(1, tuples)
228+ (found_store, found_section) = tuples[0]
229+ self.assertIs(store, found_store)
230+
231+ def test_two_stores(self):
232+ store1 = config.IniFileStore()
233+ store1._load_from_string('foo=bar')
234+ store2 = config.IniFileStore()
235+ store2._load_from_string('bar=qux')
236+ conf = config.Stack([store1.get_sections, store2.get_sections])
237+ tuples = list(conf.iter_sections())
238+ self.assertLength(2, tuples)
239+ self.assertIs(store1, tuples[0][0])
240+ self.assertIs(store2, tuples[1][0])
241+
242+
243 class TestStackWithTransport(tests.TestCaseWithTransport):
244
245 scenarios = [(key, {'get_stack': builder}) for key, builder
246@@ -3959,8 +3994,11 @@
247 baz=end
248 list={foo}
249 ''')
250- self.registry.register(
251- config.ListOption('list'))
252+ self.registry.register(config.ListOption('list'))
253+ # Register an intermediate option as a list to ensure no conversion
254+ # happen while expanding. Conversion should only occur for the original
255+ # option ('list' here).
256+ self.registry.register(config.ListOption('baz'))
257 self.assertEquals(['start', 'middle', 'end'],
258 self.conf.get('list', expand=True))
259
260
261=== modified file 'doc/en/release-notes/bzr-2.6.txt'
262--- doc/en/release-notes/bzr-2.6.txt 2012-02-14 17:49:28 +0000
263+++ doc/en/release-notes/bzr-2.6.txt 2012-02-20 17:21:48 +0000
264@@ -40,6 +40,9 @@
265 .. Fixes for situations where bzr would previously crash or give incorrect
266 or undesirable results.
267
268+* Fix ``bzr config`` display for ``RegistryOption`` values.
269+ (Vincent Ladeuil, #930182)
270+
271 Documentation
272 *************
273