Rev 5748: Merge config-concrete-stores into config-stack resolving conflicts in file:///home/vila/src/bzr/experimental/config/

Vincent Ladeuil v.ladeuil+lp at free.fr
Tue Apr 5 10:54:22 UTC 2011


At file:///home/vila/src/bzr/experimental/config/

------------------------------------------------------------
revno: 5748 [merge]
revision-id: v.ladeuil+lp at free.fr-20110405105422-mra9l3b1ltuaxchy
parent: v.ladeuil+lp at free.fr-20110404092800-mp1n863ton7poux2
parent: v.ladeuil+lp at free.fr-20110405105300-kxhqe3dwo14x0xx4
committer: Vincent Ladeuil <v.ladeuil+lp at free.fr>
branch nick: config-stack
timestamp: Tue 2011-04-05 12:54:22 +0200
message:
  Merge config-concrete-stores into config-stack resolving conflicts
modified:
  bzrlib/config.py               config.py-20051011043216-070c74f4e9e338e8
  bzrlib/errors.py               errors.py-20050309040759-20512168c4e14fbd
  bzrlib/tests/test_config.py    testconfig.py-20051011041908-742d0c15d8d8c8eb
-------------- next part --------------
=== modified file 'bzrlib/config.py'
--- a/bzrlib/config.py	2011-04-04 09:28:00 +0000
+++ b/bzrlib/config.py	2011-04-05 10:54:22 +0000
@@ -2030,6 +2030,141 @@
         del self.options[name]
 
 
+class Store(object):
+    """Abstract interface to persistent storage for configuration options."""
+
+    def __init__(self):
+        self.loaded = False
+
+    def load(self):
+        raise NotImplementedError(self.load)
+
+    def save(self):
+        raise NotImplementedError(self.load)
+
+    def get_sections(self):
+        """Returns an ordered iterable of existing sections.
+
+        :returns: An iterable of (name, dict).
+        """
+        raise NotImplementedError(self.get_sections)
+
+    def set_option(self, name, value, section_name=None):
+        raise NotImplementedError(self.set_option)
+
+
+class ConfigObjStore(Store):
+
+    def __init__(self, transport, file_name):
+        """A config Store using ConfigObj for storage.
+
+        :param transport: The transport object where the config file is located.
+
+        :param file_name: The config file basename in the transport directory.
+        """
+        super(ConfigObjStore, self).__init__()
+        self.transport = transport
+        self.file_name = file_name
+        # No transient content is known initially
+        self._content = None
+
+    @classmethod
+    def from_string(cls, str_or_unicode, transport, file_name):
+        """Create a config store from a string.
+
+        :param str_or_unicode: A string representing the file content. This will
+            be utf-8 encoded internally.
+
+        :param transport: The transport object where the config file is located.
+
+        :param file_name: The configuration file basename.
+        """
+        conf = cls(transport=transport, file_name=file_name)
+        conf._create_from_string(str_or_unicode)
+        return conf
+
+    def _create_from_string(self, str_or_unicode):
+        # We just keep the content waiting for load() to be called when needed
+        self._content = StringIO(str_or_unicode.encode('utf-8'))
+
+    def load(self):
+        """Load the store from the associated file."""
+        if self.loaded:
+            return
+        if self._content is not None:
+            co_input = self._content
+        else:
+            # The config files are always stored utf8-encoded
+            co_input =  StringIO(self.transport.get_bytes(self.file_name))
+        try:
+            self._config_obj = ConfigObj(co_input, encoding='utf-8')
+        except configobj.ConfigObjError, e:
+            # FIXME: external_url should really accepts an optional relpath
+            # parameter (bug #750169) :-/ -- vila 2011-04-04
+            # The following will do in the interim but maybe we don't want to
+            # expose a path here but rather a config ID and its associated
+            # object (hand wawing).
+            file_path = os.path.join(self.transport.external_url(),
+                                     self.file_name)
+            raise errors.ParseConfigError(e.errors, file_path)
+        self.loaded = True
+
+    def save(self):
+        out = StringIO()
+        self._config_obj.write(out)
+        self.transport.put_bytes(self.file_name, out.getvalue())
+        # We don't need the transient content anymore
+        self._content = None
+
+    def get_sections(self):
+        """Get the configobj section in the file order.
+
+        :returns: An iterable of (name, dict).
+        """
+        # We need a loaded store
+        self.load()
+        cobj = self._config_obj
+        if cobj.scalars:
+            yield None, dict([(k, cobj[k]) for k in cobj.scalars])
+        for section_name in cobj.sections:
+            yield section_name, dict(cobj[section_name])
+
+    def set_option(self, name, value, section_name=None):
+        # We need a loaded store
+        self.load()
+        if section_name is None:
+            section = self._config_obj
+        else:
+            section = self._config_obj.setdefault(section_name, {})
+        section[name] = value
+
+
+# FIXME: global, bazaar, shouldn't that be 'user' instead or even
+# 'user_defaults' as opposed to 'user_overrides', 'system_defaults'
+# (/etc/bzr/bazaar.conf) and 'system_overrides' ? -- vila 2011-04-05
+class GlobalStore(ConfigObjStore):
+
+    def __init__(self, possible_transports=None):
+        t = transport.get_transport(config_dir(),
+                                    possible_transports=possible_transports)
+        super(GlobalStore, self).__init__(t, 'bazaar.conf')
+
+
+class LocationStore(ConfigObjStore):
+
+    def __init__(self, possible_transports=None):
+        t = transport.get_transport(config_dir(),
+                                    possible_transports=possible_transports)
+        super(LocationStore, self).__init__(transport, 'locations.conf')
+
+
+class BranchStore(ConfigObjStore):
+
+    def __init__(self, branch):
+        super(BranchStore, self).__init__(branch.control_transport,
+                                          'branch.conf')
+
+
 class ConfigStack(object):
     """A stack of configurations where an option can be defined"""
 

=== modified file 'bzrlib/errors.py'
--- a/bzrlib/errors.py	2011-03-12 23:58:55 +0000
+++ b/bzrlib/errors.py	2011-04-04 12:12:57 +0000
@@ -1766,12 +1766,12 @@
 
 class ParseConfigError(BzrError):
 
+    _fmt = "Error(s) parsing config file %(filename)s:\n%(errors)s"
+
     def __init__(self, errors, filename):
-        if filename is None:
-            filename = ""
-        message = "Error(s) parsing config file %s:\n%s" % \
-            (filename, ('\n'.join(e.msg for e in errors)))
-        BzrError.__init__(self, message)
+        BzrError.__init__(self)
+        self.filename = filename
+        self.errors = '\n'.join(e.msg for e in errors)
 
 
 class NoEmailInUsername(BzrError):

=== modified file 'bzrlib/tests/test_config.py'
--- a/bzrlib/tests/test_config.py	2011-04-04 09:28:00 +0000
+++ b/bzrlib/tests/test_config.py	2011-04-05 10:54:22 +0000
@@ -1889,6 +1889,129 @@
         self.assertEquals(config._Created, section.orig['foo'])
 
 
+class TestStore(tests.TestCaseWithTransport):
+
+    # FIXME: parametrize against all valid (store, transport) combinations
+
+    def get_store(self, name=None, content=None):
+        if name is None:
+            name = 'foo.conf'
+        if content is None:
+            store = config.ConfigObjStore(self.get_transport(), name)
+        else:
+            store = config.ConfigObjStore.from_string(
+                content, self.get_transport(), name)
+        return store
+
+    def test_delayed_load(self):
+        self.build_tree_contents([('foo.conf', '')])
+        store = self.get_store('foo.conf')
+        self.assertEquals(False, store.loaded)
+        store.load()
+        self.assertEquals(True, store.loaded)
+
+    def test_from_string_delayed_load(self):
+        store = self.get_store('foo.conf', '')
+        self.assertEquals(False, store.loaded)
+        store.load()
+        self.assertEquals(True, store.loaded)
+        # We use from_string and don't save, so the file shouldn't be created
+        self.failIfExists('foo.conf')
+
+    def test_invalid_content(self):
+        store = self.get_store('foo.conf', 'this is invalid !')
+        self.assertEquals(False, store.loaded)
+        exc = self.assertRaises(errors.ParseConfigError, store.load)
+        self.assertEndsWith(exc.filename, 'foo.conf')
+        # And the load failed
+        self.assertEquals(False, store.loaded)
+
+    def test_save_empty_succeeds(self):
+        store = self.get_store('foo.conf', '')
+        store.load()
+        self.failIfExists('foo.conf')
+        store.save()
+        self.failUnlessExists('foo.conf')
+
+    def test_save_with_content_succeeds(self):
+        store = self.get_store('foo.conf', 'foo=bar\n')
+        store.load()
+        self.failIfExists('foo.conf')
+        store.save()
+        self.failUnlessExists('foo.conf')
+        # FIXME: Far too ConfigObj specific
+        self.assertFileEqual('foo = bar\n', 'foo.conf')
+
+    def test_get_no_sections_for_empty(self):
+        store = self.get_store('foo.conf', '')
+        store.load()
+        self.assertEquals([], list(store.get_sections()))
+
+    def test_get_default_section(self):
+        store = self.get_store('foo.conf', 'foo=bar')
+        sections = list(store.get_sections())
+        self.assertLength(1, sections)
+        self.assertEquals((None, {'foo': 'bar'}), sections[0])
+
+    def test_get_named_section(self):
+        store = self.get_store('foo.conf', '[baz]\nfoo=bar')
+        sections = list(store.get_sections())
+        self.assertLength(1, sections)
+        self.assertEquals(('baz', {'foo': 'bar'}), sections[0])
+
+    def test_get_embedded_sections(self):
+        # A more complicated example (which also shows that section names and
+        # option names share the same name space...)
+        store = self.get_store('foo.conf', '''
+foo=bar
+l=1,2
+[DEFAULT]
+foo_in_DEFAULT=foo_DEFAULT
+[bar]
+foo_in_bar=barbar
+[baz]
+foo_in_baz=barbaz
+[[qux]]
+foo_in_qux=quux
+''')
+        sections = list(store.get_sections())
+        self.assertLength(4, sections)
+        # The default section has no name.
+        # List values are provided as lists
+        self.assertEquals((None, {'foo': 'bar', 'l': ['1', '2']}), sections[0])
+        self.assertEquals(('DEFAULT', {'foo_in_DEFAULT': 'foo_DEFAULT'}),
+                          sections[1])
+        self.assertEquals(('bar', {'foo_in_bar': 'barbar'}), sections[2])
+        # sub sections are provided as embedded dicts.
+        self.assertEquals(('baz', {'foo_in_baz': 'barbaz',
+                                   'qux': {'foo_in_qux': 'quux'}}),
+                          sections[3])
+
+    def test_set_option_in_default_section(self):
+        store = self.get_store('foo.conf', '')
+        store.set_option('foo', 'bar')
+        store.save()
+        self.assertFileEqual('foo = bar\n', 'foo.conf')
+
+    def test_set_option_in_named_section(self):
+        store = self.get_store('foo.conf', '')
+        store.set_option('foo', 'bar', 'baz')
+        store.save()
+        self.assertFileEqual('[baz]\nfoo = bar\n', 'foo.conf')
+
+
+class TestConfigObjStore(tests.TestCaseWithTransport):
+
+    def test_global_store(self):
+        store = config.GlobalStore()
+
+    def test_location_store(self):
+        store = config.LocationStore()
+
+    def test_branch_store(self):
+        b = self.make_branch('.')
+        store = config.BranchStore(b)
+
 class TestConfigStackGet(tests.TestCase):
 
     # FIXME: This should be parametrized for all known ConfigStack or dedicated



More information about the bazaar-commits mailing list