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