Rev 6401: (vila) Stores allow Stacks to control when values are quoted/unquoted in file:///srv/pqm.bazaar-vcs.org/archives/thelove/bzr/%2Btrunk/

Patch Queue Manager pqm at pqm.ubuntu.com
Wed Dec 21 21:27:35 UTC 2011


At file:///srv/pqm.bazaar-vcs.org/archives/thelove/bzr/%2Btrunk/

------------------------------------------------------------
revno: 6401 [merge]
revision-id: pqm at pqm.ubuntu.com-20111221212734-aea6s92gkpux3fky
parent: pqm at pqm.ubuntu.com-20111221191039-ajreq7qxwt64qzgw
parent: v.ladeuil+lp at free.fr-20111221205519-zljdvxe8ljssxfo0
committer: Patch Queue Manager <pqm at pqm.ubuntu.com>
branch nick: +trunk
timestamp: Wed 2011-12-21 21:27:34 +0000
message:
  (vila) Stores allow Stacks to control when values are quoted/unquoted
   (Vincent Ladeuil)
modified:
  bzrlib/branch.py               branch.py-20050309040759-e4baf4e0d046576e
  bzrlib/config.py               config.py-20051011043216-070c74f4e9e338e8
  bzrlib/plugins/po_merge/po_merge.py po_merge.py-20111123180440-la918t6t068pzacx-4
  bzrlib/tests/blackbox/test_config.py test_config.py-20100927150753-x6rf54uibd08r636-1
  bzrlib/tests/test_config.py    testconfig.py-20051011041908-742d0c15d8d8c8eb
  doc/developers/configuration.txt configuration.txt-20110408142435-korjxxnskvq44sta-1
  doc/en/release-notes/bzr-2.5.txt bzr2.5.txt-20110708125756-587p0hpw7oke4h05-1
=== modified file 'bzrlib/branch.py'
--- a/bzrlib/branch.py	2011-12-21 17:53:20 +0000
+++ b/bzrlib/branch.py	2011-12-21 20:43:29 +0000
@@ -1180,9 +1180,7 @@
         if config is None:
             config = self.get_config_stack()
         location = config.get(name)
-        # FIXME: There is a glitch around quoting/unquoting in config stores:
-        # an empty string can be seen as '""' instead of '' -- vila 2011-12-20
-        if location in ('', '""') :
+        if location == '':
             location = None
         return location
 

=== modified file 'bzrlib/config.py'
--- a/bzrlib/config.py	2011-12-21 17:53:20 +0000
+++ b/bzrlib/config.py	2011-12-21 20:55:19 +0000
@@ -2344,7 +2344,8 @@
     """
 
     def __init__(self, name, default=None, default_from_env=None,
-                 help=None, from_unicode=None, invalid=None):
+                 help=None, from_unicode=None, invalid=None,
+                 unquote=True):
         """Build an option definition.
 
         :param name: the name used to refer to the option.
@@ -2372,6 +2373,11 @@
             TypeError. Accepted values are: None (ignore invalid values),
             'warning' (emit a warning), 'error' (emit an error message and
             terminates).
+
+        :param unquote: should the unicode value be unquoted before conversion.
+           This should be used only when the store providing the values cannot
+           safely unquote them (see http://pad.lv/906897). It is provided so
+           daughter classes can handle the quoting themselves.
         """
         if default_from_env is None:
             default_from_env = []
@@ -2398,11 +2404,14 @@
         self.default_from_env = default_from_env
         self.help = help
         self.from_unicode = from_unicode
+        self.unquote = unquote
         if invalid and invalid not in ('warning', 'error'):
             raise AssertionError("%s not supported for 'invalid'" % (invalid,))
         self.invalid = invalid
 
-    def convert_from_unicode(self, unicode_value):
+    def convert_from_unicode(self, store, unicode_value):
+        if self.unquote and store is not None and unicode_value is not None:
+            unicode_value = store.unquote(unicode_value)
         if self.from_unicode is None or unicode_value is None:
             # Don't convert or nothing to convert
             return unicode_value
@@ -2459,7 +2468,7 @@
     return int(unicode_str)
 
 
-_unit_sfxs = dict(K=10**3, M=10**6, G=10**9)
+_unit_suffixes = dict(K=10**3, M=10**6, G=10**9)
 
 def int_SI_from_store(unicode_str):
     """Convert a human readable size in SI units, e.g 10MB into an integer.
@@ -2471,7 +2480,7 @@
     :return Integer, expanded to its base-10 value if a proper SI unit is 
         found, None otherwise.
     """
-    regexp = "^(\d+)(([" + ''.join(_unit_sfxs) + "])b?)?$"
+    regexp = "^(\d+)(([" + ''.join(_unit_suffixes) + "])b?)?$"
     p = re.compile(regexp, re.IGNORECASE)
     m = p.match(unicode_str)
     val = None
@@ -2480,7 +2489,7 @@
         val = int(val)
         if unit:
             try:
-                coeff = _unit_sfxs[unit.upper()]
+                coeff = _unit_suffixes[unit.upper()]
             except KeyError:
                 raise ValueError(gettext('{0} is not an SI unit.').format(unit))
             val *= coeff
@@ -2497,27 +2506,41 @@
     {}, encoding='utf-8', list_values=True, interpolation=False)
 
 
-def list_from_store(unicode_str):
-    if not isinstance(unicode_str, basestring):
-        raise TypeError
-    # Now inject our string directly as unicode. All callers got their value
-    # from configobj, so values that need to be quoted are already properly
-    # quoted.
-    _list_converter_config.reset()
-    _list_converter_config._parse([u"list=%s" % (unicode_str,)])
-    maybe_list = _list_converter_config['list']
-    if isinstance(maybe_list, basestring):
-        if maybe_list:
-            # A single value, most probably the user forgot (or didn't care to
-            # add) the final ','
-            l = [maybe_list]
+class ListOption(Option):
+
+    def __init__(self, name, default=None, default_from_env=None,
+                 help=None, invalid=None):
+        """A list Option definition.
+
+        This overrides the base class so the conversion from a unicode string
+        can take quoting into account.
+        """
+        super(ListOption, self).__init__(
+            name, default=default, default_from_env=default_from_env,
+            from_unicode=self.from_unicode, help=help,
+            invalid=invalid, unquote=False)
+
+    def from_unicode(self, unicode_str):
+        if not isinstance(unicode_str, basestring):
+            raise TypeError
+        # Now inject our string directly as unicode. All callers got their
+        # value from configobj, so values that need to be quoted are already
+        # properly quoted.
+        _list_converter_config.reset()
+        _list_converter_config._parse([u"list=%s" % (unicode_str,)])
+        maybe_list = _list_converter_config['list']
+        if isinstance(maybe_list, basestring):
+            if maybe_list:
+                # A single value, most probably the user forgot (or didn't care
+                # to add) the final ','
+                l = [maybe_list]
+            else:
+                # The empty string, convert to empty list
+                l = []
         else:
-            # The empty string, convert to empty list
-            l = []
-    else:
-        # We rely on ConfigObj providing us with a list already
-        l = maybe_list
-    return l
+            # We rely on ConfigObj providing us with a list already
+            l = maybe_list
+        return l
 
 
 class OptionRegistry(registry.Registry):
@@ -2573,8 +2596,8 @@
 existing mainline of the branch.
 '''))
 option_registry.register(
-    Option('acceptable_keys',
-           default=None, from_unicode=list_from_store,
+    ListOption('acceptable_keys',
+           default=None,
            help="""\
 List of GPG key patterns which are acceptable for verification.
 """))
@@ -2656,7 +2679,7 @@
 should not be lost if the machine crashes.  See also repository.fdatasync.
 '''))
 option_registry.register(
-    Option('debug_flags', default=[], from_unicode=list_from_store,
+    ListOption('debug_flags', default=[],
            help='Debug flags to activate.'))
 option_registry.register(
     Option('default_format', default='2a',
@@ -2910,6 +2933,20 @@
         """
         raise NotImplementedError(self.unload)
 
+    def quote(self, value):
+        """Quote a configuration option value for storing purposes.
+
+        This allows Stacks to present values as they will be stored.
+        """
+        return value
+
+    def unquote(self, value):
+        """Unquote a configuration option value into unicode.
+
+        The received value is quoted as stored.
+        """
+        return value
+
     def save(self):
         """Saves the Store to persistent storage."""
         raise NotImplementedError(self.save)
@@ -3082,6 +3119,20 @@
             section = self._config_obj.setdefault(section_id, {})
         return self.mutable_section_class(section_id, section)
 
+    def quote(self, value):
+        try:
+            # configobj conflates automagical list values and quoting
+            self._config_obj.list_values = True
+            return self._config_obj._quote(value)
+        finally:
+            self._config_obj.list_values = False
+
+    def unquote(self, value):
+        if value:
+            # _unquote doesn't handle None nor empty strings
+            value = self._config_obj._unquote(value)
+        return value
+
 
 class TransportIniFileStore(IniFileStore):
     """IniFileStore that loads files from a transport.
@@ -3416,10 +3467,12 @@
         # implies querying the persistent storage) until it can't be avoided
         # anymore by using callables to describe (possibly empty) section
         # lists.
+        found_store = None # Where the option value has been found
         for sections in self.sections_def:
             for store, section in sections():
                 value = section.get(name)
                 if value is not None:
+                    found_store = store
                     break
             if value is not None:
                 break
@@ -3441,8 +3494,10 @@
                         trace.warning('Cannot expand "%s":'
                                       ' %s does not support option expansion'
                                       % (name, type(val)))
-                if opt is not None:
-                    val = opt.convert_from_unicode(val)
+                if opt is None:
+                    val = found_store.unquote(val)
+                else:
+                    val = opt.convert_from_unicode(found_store, val)
             return val
         value = expand_and_convert(value)
         if opt is not None and value is None:
@@ -3526,19 +3581,20 @@
         or deleting an option. In practice the store will often be loaded but
         this helps catching some programming errors.
         """
-        section = self.store.get_mutable_section(self.mutable_section_id)
-        return section
+        store = self.store
+        section = store.get_mutable_section(self.mutable_section_id)
+        return store, section
 
     def set(self, name, value):
         """Set a new value for the option."""
-        section = self._get_mutable_section()
-        section.set(name, value)
+        store, section = self._get_mutable_section()
+        section.set(name, store.quote(value))
         for hook in ConfigHooks['set']:
             hook(self, name, value)
 
     def remove(self, name):
         """Remove an existing option."""
-        section = self._get_mutable_section()
+        _, section = self._get_mutable_section()
         section.remove(name)
         for hook in ConfigHooks['remove']:
             hook(self, name)
@@ -3727,7 +3783,7 @@
 # Use a an empty dict to initialize an empty configobj avoiding all
 # parsing and encoding checks
 _quoting_config = configobj.ConfigObj(
-    {}, encoding='utf-8', interpolation=False)
+    {}, encoding='utf-8', interpolation=False, list_values=True)
 
 class cmd_config(commands.Command):
     __doc__ = """Display, set or remove a configuration option.
@@ -3860,6 +3916,13 @@
                             self.outf.write('  [%s]\n' % (section.id,))
                             cur_section = section.id
                         value = section.get(oname, expand=False)
+                        # Since we don't use the stack, we need to restore a
+                        # proper quoting.
+                        try:
+                            opt = option_registry.get(oname)
+                            value = opt.convert_from_unicode(store, value)
+                        except KeyError:
+                            value = store.unquote(value)
                         value = _quoting_config._quote(value)
                         self.outf.write('  %s = %s\n' % (oname, value))
 

=== modified file 'bzrlib/plugins/po_merge/po_merge.py'
--- a/bzrlib/plugins/po_merge/po_merge.py	2011-12-19 13:23:58 +0000
+++ b/bzrlib/plugins/po_merge/po_merge.py	2011-12-21 10:42:34 +0000
@@ -57,9 +57,8 @@
 ''')
 
 
-po_dirs_option = config.Option(
+po_dirs_option = config.ListOption(
         'po_merge.po_dirs', default='po,debian/po',
-        from_unicode=config.list_from_store,
         help='List of dirs containing .po files that the hook applies to.')
 
 

=== modified file 'bzrlib/tests/blackbox/test_config.py'
--- a/bzrlib/tests/blackbox/test_config.py	2011-12-14 12:15:44 +0000
+++ b/bzrlib/tests/blackbox/test_config.py	2011-12-21 08:52:41 +0000
@@ -95,22 +95,22 @@
             ''')
 
     def test_list_all_values(self):
-        # FIXME: we should register the option as a list or it's displayed as
-        # astring and as such, quoted.
+        config.option_registry.register(config.ListOption('list'))
+        self.addCleanup(config.option_registry.remove, 'list')
         self.bazaar_config.set_user_option('list', [1, 'a', 'with, a comma'])
         script.run_script(self, '''\
             $ bzr config -d tree
             bazaar:
-              list = '1, a, "with, a comma"'
+              list = 1, a, "with, a comma"
             ''')
 
     def test_list_value_only(self):
-        # FIXME: we should register the option as a list or it's displayed as
-        # astring and as such, quoted.
+        config.option_registry.register(config.ListOption('list'))
+        self.addCleanup(config.option_registry.remove, 'list')
         self.bazaar_config.set_user_option('list', [1, 'a', 'with, a comma'])
         script.run_script(self, '''\
             $ bzr config -d tree list
-            '1, a, "with, a comma"'
+            1, a, "with, a comma"
             ''')
 
     def test_bazaar_config(self):
@@ -142,6 +142,7 @@
               hello = world
             ''')
 
+
 class TestConfigDisplayWithPolicy(tests.TestCaseWithTransport):
 
     def test_location_with_policy(self):

=== modified file 'bzrlib/tests/test_config.py'
--- a/bzrlib/tests/test_config.py	2011-12-21 17:53:20 +0000
+++ b/bzrlib/tests/test_config.py	2011-12-21 20:32:50 +0000
@@ -22,7 +22,6 @@
 import threading
 
 
-import testtools
 from testtools import matchers
 
 #import bzrlib specific imports here
@@ -2367,15 +2366,14 @@
 class TestOptionConverterMixin(object):
 
     def assertConverted(self, expected, opt, value):
-        self.assertEquals(expected, opt.convert_from_unicode(value),
-                          'Expecting %s, got %s' % (expected, value,))
+        self.assertEquals(expected, opt.convert_from_unicode(None, value))
 
     def assertWarns(self, opt, value):
         warnings = []
         def warning(*args):
             warnings.append(args[0] % args[1:])
         self.overrideAttr(trace, 'warning', warning)
-        self.assertEquals(None, opt.convert_from_unicode(value))
+        self.assertEquals(None, opt.convert_from_unicode(None, value))
         self.assertLength(1, warnings)
         self.assertEquals(
             'Value "%s" is not valid for "%s"' % (value, opt.name),
@@ -2383,12 +2381,11 @@
 
     def assertErrors(self, opt, value):
         self.assertRaises(errors.ConfigOptionValueError,
-                          opt.convert_from_unicode, value)
+                          opt.convert_from_unicode, None, value)
 
     def assertConvertInvalid(self, opt, invalid_value):
         opt.invalid = None
-        self.assertEquals(None, opt.convert_from_unicode(invalid_value),
-                          '%s is not None' % (invalid_value,))
+        self.assertEquals(None, opt.convert_from_unicode(None, invalid_value))
         opt.invalid = 'warning'
         self.assertWarns(opt, invalid_value)
         opt.invalid = 'error'
@@ -2458,105 +2455,10 @@
         self.assertConverted(100, opt, u'100')
 
 
-class TestOptionWithListConverter(tests.TestCase, TestOptionConverterMixin):
-
-    def get_option(self):
-        return config.Option('foo', help='A list.',
-                             from_unicode=config.list_from_store)
-
-    def test_convert_invalid(self):
-        # No string is invalid as all forms can be converted to a list
-        pass
-
-    def test_convert_valid(self):
-        opt = self.get_option()
-        # An empty string is an empty list
-        self.assertConverted([], opt, '') # Using a bare str() just in case
-        self.assertConverted([], opt, u'')
-        # A boolean
-        self.assertConverted([u'True'], opt, u'True')
-        # An integer
-        self.assertConverted([u'42'], opt, u'42')
-        # A single string
-        self.assertConverted([u'bar'], opt, u'bar')
-        # A list remains a list (configObj will turn a string containing commas
-        # into a list, but that's not what we're testing here)
-        self.assertConverted([u'foo', u'1', u'True'],
-                             opt, [u'foo', u'1', u'True'])
-
-
-class TestOptionConverterMixin(object):
-
-    def assertConverted(self, expected, opt, value):
-        self.assertEquals(expected, opt.convert_from_unicode(value))
-
-    def assertWarns(self, opt, value):
-        warnings = []
-        def warning(*args):
-            warnings.append(args[0] % args[1:])
-        self.overrideAttr(trace, 'warning', warning)
-        self.assertEquals(None, opt.convert_from_unicode(value))
-        self.assertLength(1, warnings)
-        self.assertEquals(
-            'Value "%s" is not valid for "%s"' % (value, opt.name),
-            warnings[0])
-
-    def assertErrors(self, opt, value):
-        self.assertRaises(errors.ConfigOptionValueError,
-                          opt.convert_from_unicode, value)
-
-    def assertConvertInvalid(self, opt, invalid_value):
-        opt.invalid = None
-        self.assertEquals(None, opt.convert_from_unicode(invalid_value))
-        opt.invalid = 'warning'
-        self.assertWarns(opt, invalid_value)
-        opt.invalid = 'error'
-        self.assertErrors(opt, invalid_value)
-
-
-class TestOptionWithBooleanConverter(tests.TestCase, TestOptionConverterMixin):
-
-    def get_option(self):
-        return config.Option('foo', help='A boolean.',
-                             from_unicode=config.bool_from_store)
-
-    def test_convert_invalid(self):
-        opt = self.get_option()
-        # A string that is not recognized as a boolean
-        self.assertConvertInvalid(opt, u'invalid-boolean')
-        # A list of strings is never recognized as a boolean
-        self.assertConvertInvalid(opt, [u'not', u'a', u'boolean'])
-
-    def test_convert_valid(self):
-        opt = self.get_option()
-        self.assertConverted(True, opt, u'True')
-        self.assertConverted(True, opt, u'1')
-        self.assertConverted(False, opt, u'False')
-
-
-class TestOptionWithIntegerConverter(tests.TestCase, TestOptionConverterMixin):
-
-    def get_option(self):
-        return config.Option('foo', help='An integer.',
-                             from_unicode=config.int_from_store)
-
-    def test_convert_invalid(self):
-        opt = self.get_option()
-        # A string that is not recognized as an integer
-        self.assertConvertInvalid(opt, u'forty-two')
-        # A list of strings is never recognized as an integer
-        self.assertConvertInvalid(opt, [u'a', u'list'])
-
-    def test_convert_valid(self):
-        opt = self.get_option()
-        self.assertConverted(16, opt, u'16')
-
-
-class TestOptionWithListConverter(tests.TestCase, TestOptionConverterMixin):
-
-    def get_option(self):
-        return config.Option('foo', help='A list.',
-                             from_unicode=config.list_from_store)
+class TestListOption(tests.TestCase, TestOptionConverterMixin):
+
+    def get_option(self):
+        return config.ListOption('foo', help='A list.')
 
     def test_convert_invalid(self):
         opt = self.get_option()
@@ -2712,6 +2614,7 @@
     def setUp(self):
         super(TestCommandLineStore, self).setUp()
         self.store = config.CommandLineStore()
+        self.overrideAttr(config, 'option_registry', config.OptionRegistry())
 
     def get_section(self):
         """Get the unique section for the command line overrides."""
@@ -2732,12 +2635,15 @@
         self.assertEqual('b', section.get('a'))
 
     def test_list_override(self):
+        opt = config.ListOption('l')
+        config.option_registry.register(opt)
         self.store._from_cmdline(['l=1,2,3'])
         val = self.get_section().get('l')
         self.assertEqual('1,2,3', val)
         # Reminder: lists should be registered as such explicitely, otherwise
         # the conversion needs to be done afterwards.
-        self.assertEqual(['1', '2', '3'], config.list_from_store(val))
+        self.assertEqual(['1', '2', '3'],
+                         opt.convert_from_unicode(self.store, val))
 
     def test_multiple_overrides(self):
         self.store._from_cmdline(['a=b', 'x=y'])
@@ -2797,6 +2703,63 @@
         self.assertRaises(AssertionError, store._load_from_string, 'bar=baz')
 
 
+class TestStoreQuoting(TestStore):
+
+    scenarios = [(key, {'get_store': builder}) for key, builder
+                 in config.test_store_builder_registry.iteritems()]
+
+    def setUp(self):
+        super(TestStoreQuoting, self).setUp()
+        self.store = self.get_store(self)
+        # We need a loaded store but any content will do
+        self.store._load_from_string('')
+
+    def assertIdempotent(self, s):
+        """Assert that quoting an unquoted string is a no-op and vice-versa.
+
+        What matters here is that option values, as they appear in a store, can
+        be safely round-tripped out of the store and back.
+
+        :param s: A string, quoted if required.
+        """
+        self.assertEquals(s, self.store.quote(self.store.unquote(s)))
+        self.assertEquals(s, self.store.unquote(self.store.quote(s)))
+
+    def test_empty_string(self):
+        if isinstance(self.store, config.IniFileStore):
+            # configobj._quote doesn't handle empty values
+            self.assertRaises(AssertionError,
+                              self.assertIdempotent, '')
+        else:
+            self.assertIdempotent('')
+        # But quoted empty strings are ok
+        self.assertIdempotent('""')
+
+    def test_embedded_spaces(self):
+        self.assertIdempotent('" a b c "')
+
+    def test_embedded_commas(self):
+        self.assertIdempotent('" a , b c "')
+
+    def test_simple_comma(self):
+        if isinstance(self.store, config.IniFileStore):
+            # configobj requires that lists are special-cased
+           self.assertRaises(AssertionError,
+                             self.assertIdempotent, ',')
+        else:
+            self.assertIdempotent(',')
+        # When a single comma is required, quoting is also required
+        self.assertIdempotent('","')
+
+    def test_list(self):
+        if isinstance(self.store, config.IniFileStore):
+            # configobj requires that lists are special-cased
+            self.assertRaises(AssertionError,
+                              self.assertIdempotent, 'a,b')
+        else:
+            self.assertIdempotent('a,b')
+
+
 class TestIniFileStoreContent(tests.TestCaseWithTransport):
     """Simulate loading a config store with content of various encodings.
 
@@ -3008,6 +2971,25 @@
         self.assertEquals((store,), calls[0])
 
 
+class TestQuotingIniFileStore(tests.TestCaseWithTransport):
+
+    def get_store(self):
+        return config.TransportIniFileStore(self.get_transport(), 'foo.conf')
+
+    def test_get_quoted_string(self):
+        store = self.get_store()
+        store._load_from_string('foo= " abc "')
+        stack = config.Stack([store.get_sections])
+        self.assertEquals(' abc ', stack.get('foo'))
+
+    def test_set_quoted_string(self):
+        store = self.get_store()
+        stack = config.Stack([store.get_sections], store)
+        stack.set('foo', ' a b c ')
+        store.save()
+        self.assertFileEqual('foo = " a b c "\n', 'foo.conf')
+
+
 class TestTransportIniFileStore(TestStore):
 
     def test_loading_unknown_file_fails(self):
@@ -3532,9 +3514,8 @@
         self.assertEquals(12, conf.get('foo'))
 
     def register_list_option(self, name, default=None, default_from_env=None):
-        l = config.Option(name, help='A list.',
-                          default=default, default_from_env=default_from_env,
-                          from_unicode=config.list_from_store)
+        l = config.ListOption(name, help='A list.', default=default,
+                              default_from_env=default_from_env)
         self.registry.register(l)
 
     def test_get_default_list_None(self):
@@ -3690,7 +3671,7 @@
 list={foo},{bar},{baz}
 ''')
         self.registry.register(
-            config.Option('list', from_unicode=config.list_from_store))
+            config.ListOption('list'))
         self.assertEquals(['start', 'middle', 'end'],
                            self.conf.get('list', expand=True))
 
@@ -3702,7 +3683,7 @@
 list={foo}
 ''')
         self.registry.register(
-            config.Option('list', from_unicode=config.list_from_store))
+            config.ListOption('list'))
         self.assertEquals(['start', 'middle', 'end'],
                            self.conf.get('list', expand=True))
 
@@ -3717,8 +3698,7 @@
 ''')
         # What matters is what the registration says, the conversion happens
         # only after all expansions have been performed
-        self.registry.register(
-            config.Option('hidden', from_unicode=config.list_from_store))
+        self.registry.register(config.ListOption('hidden'))
         self.assertEquals(['bin', 'go'],
                           self.conf.get('hidden', expand=True))
 

=== modified file 'doc/developers/configuration.txt'
--- a/doc/developers/configuration.txt	2011-12-21 14:25:26 +0000
+++ b/doc/developers/configuration.txt	2011-12-21 20:32:50 +0000
@@ -205,7 +205,10 @@
 
 The value of an option is a unicode string or ``None`` if it's not
 defined. By using ``from_unicode`` you can turn this string into a more
-appropriate representation (a list of unicode strings for example).
+appropriate representation.
+
+If you need a list value, you should use ``ListOption`` instead.
+
 
 Sections
 --------
@@ -249,6 +252,16 @@
 places to inherit from the existing basic tests and add their own specific
 ones.
 
+A ``Store`` defines how option values are stored, this includes:
+
+* defining the sections where the options are grouped,
+
+* defining how the values are quoted/unquoted for storage purposes. Stacks
+  use the unquoted values internally (default value handling and option
+  expansion are simpler this way) and ``bzr config`` quote them when they
+  need to be displayed.
+
+
 Filtering sections
 ------------------
 

=== modified file 'doc/en/release-notes/bzr-2.5.txt'
--- a/doc/en/release-notes/bzr-2.5.txt	2011-12-21 17:54:39 +0000
+++ b/doc/en/release-notes/bzr-2.5.txt	2011-12-21 20:32:50 +0000
@@ -64,6 +64,10 @@
 * Allow configuration option default value to be a python callable at
   registration. (Vincent Ladeuil, #832064)
 
+* Configuration stores can now provides a specific quoting mechanism. This
+  is required to workaround ``configobj`` conflating quoting and list values
+  automatic conversion. (Vincent Ladeuil, #906897)
+
 * Create obsolete_packs directory when repacking if it does not
   exist. (Jonathan Riddell, Jelmer Vernooij, #314314)
 




More information about the bazaar-commits mailing list