Rev 5540: Merge config-stack into doc-new-config in file:///home/vila/src/bzr/experimental/config/
Vincent Ladeuil
v.ladeuil+lp at free.fr
Thu Apr 7 07:44:14 UTC 2011
At file:///home/vila/src/bzr/experimental/config/
------------------------------------------------------------
revno: 5540 [merge]
revision-id: v.ladeuil+lp at free.fr-20110407074414-rsqszn9n24pr4ilg
parent: v.ladeuil+lp at free.fr-20110404092805-e50w1a213yq6esxz
parent: v.ladeuil+lp at free.fr-20110407074411-48xxlmuzz10j71d5
committer: Vincent Ladeuil <v.ladeuil+lp at free.fr>
branch nick: doc-new-config
timestamp: Thu 2011-04-07 09:44:14 +0200
message:
Merge config-stack into doc-new-config
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-07 07:44:11 +0000
@@ -848,7 +848,7 @@
# LockableConfig for other kind of transports, we will need to reuse
# whatever connection is already established -- vila 20100929
self.transport = transport.get_transport(self.dir)
- self._lock = lockdir.LockDir(self.transport, 'lock')
+ self._lock = lockdir.LockDir(self.transport, self.lock_name)
def _create_from_string(self, unicode_bytes, save):
super(LockableConfig, self)._create_from_string(unicode_bytes, False)
@@ -1988,7 +1988,6 @@
self._transport.put_file(self._filename, out_file)
-# FIXME: This is not lazy yet -- vila 2011-04-01
class ReadOnlySection(object):
"""A section defines a dict of options.
@@ -2030,24 +2029,255 @@
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.save)
+
+ def get_sections(self):
+ """Returns an ordered iterable of existing sections.
+
+ :returns: An iterable of (name, dict).
+ """
+ raise NotImplementedError(self.get_sections)
+
+ def get_mutable_section(self, section_name=None):
+ """Returns the specified mutable section.
+
+ :param section_name: The section identifier
+ """
+ raise NotImplementedError(self.get_mutable_section)
+
+
+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, allow_no_such_file=False):
+ """Load the store from the associated file.
+
+ :param allow_no_such_file: Swallow the NoSuchFile exception if True.
+ This allows delayed loading when creating the first option ever.
+ """
+ if self.loaded:
+ return
+ if self._content is not None:
+ co_input = self._content
+ else:
+ try:
+ content = self.transport.get_bytes(self.file_name)
+ except errors.NoSuchFile:
+ if allow_no_such_file:
+ content = ''
+ else:
+ raise
+ co_input = StringIO(content)
+ try:
+ # The config files are always stored utf8-encoded
+ 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 wawe>.
+ 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 ReadOnlySection(None, cobj)
+ for section_name in cobj.sections:
+ yield ReadOnlySection(section_name, cobj[section_name])
+
+ def get_mutable_section(self, section_name=None):
+ # We need a loaded store
+ self.load(allow_no_such_file=True)
+ if section_name is None:
+ section = self._config_obj
+ else:
+ section = self._config_obj.setdefault(section_name, {})
+ return MutableSection(section_name, section)
+
+
+class LockableConfigObjStore(ConfigObjStore):
+ """A ConfigObjStore using locks on save to ensure store integrity."""
+
+ def __init__(self, transport, file_name, lock_dir_name=None):
+ """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.
+ """
+ if lock_dir_name is None:
+ lock_dir_name = 'lock'
+ self.lock_dir_name = lock_dir_name
+ super(LockableConfigObjStore, self).__init__(transport, file_name)
+ self._lock = lockdir.LockDir(self.transport, self.lock_dir_name)
+
+ def lock_write(self, token=None):
+ """Takes a write lock in the directory containing the config file.
+
+ If the directory doesn't exist it is created.
+ """
+ # FIXME: This doesn't check the ownership of the created directories as
+ # ensure_config_dir_exists does. It should if the transport is local
+ # -- vila 2011-04-06
+ self.transport.create_prefix()
+ return self._lock.lock_write(token)
+
+ def unlock(self):
+ self._lock.unlock()
+
+ def break_lock(self):
+ self._lock.break_lock()
+
+ @needs_write_lock
+ def save(self):
+ super(LockableConfigObjStore, self).save()
+
+
+# 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(LockableConfigObjStore):
+
+ 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(LockableConfigObjStore):
+
+ 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"""
- def __init__(self, config_list):
- self.list = config_list
- for c in self.list:
- # Sanity check
- if not hasattr(c, 'get'):
- raise AssertionError("%r does not provide a 'get' method"
- % (c,))
+ def __init__(self, sections=None, get_mutable_section=None):
+ """Creates a stack of sections with an optional store for changes.
+
+ :param sections: A list of ReadOnlySection or callables that returns an
+ iterable of ReadOnlySection.
+
+ :param get_mutable_section: A callable that returns a MutableSection
+ where changes are recorded.
+ """
+ if sections is None:
+ sections = []
+ self.sections = sections
+ self.get_mutable_section = get_mutable_section
def get(self, name):
- """Return the value from the first definition found in the list"""
- for c in self.list:
- value = c.get(name)
- if value is not None:
- break
- return value
+ """Return the *first* option value found in the sections.
+
+ This is where we guarantee that sections coming from Store are loaded
+ lazily: the loading is delayed until we need to either check that an
+ option exists or get its value, which in turn may require to discover
+ in which sections it can be defined. Both of these (section and option
+ existence) require loading the store (even partially).
+ """
+ # Ensuring lazy loading is achieved by delaying section matching until
+ # it can be avoided anymore by using callables to describe (possibly
+ # empty) section lists.
+ for section_or_callable in self.sections:
+ # Each section can expand to multiple ones when a callable is used
+ if callable(section_or_callable):
+ sections = section_or_callable()
+ else:
+ sections = [section_or_callable]
+ for section in sections:
+ value = section.get(name)
+ if value is not None:
+ return value
+ # No definition was found
+ return None
+
+ def set(self, name, value):
+ """Set a new value for the option.
+
+ This is where we guarantee that the mutable section is lazily loaded:
+ this means we won't load the corresponding store before setting a value.
+ """
+ section = self.get_mutable_section()
+ section.set(name, value)
+
+ def remove(self, name):
+ """Remove an existing option.
+
+ This is where we guarantee that the mutable section is lazily loaded:
+ this means we won't load the correspoding store before trying to delete
+ an option. In practice the store will often be loaded but this allows
+ catching some programming errors.
+ """
+ section = self.get_mutable_section()
+ section.remove(name)
class cmd_config(commands.Command):
=== 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-06 17:33:55 +0000
@@ -831,7 +831,7 @@
def c1_write_config_file():
before_writing.set()
c1_orig()
- # The lock is held we wait for the main thread to decide when to
+ # The lock is held. We wait for the main thread to decide when to
# continue
after_writing.wait()
c1._write_config_file = c1_write_config_file
@@ -864,7 +864,7 @@
c1_orig = c1._write_config_file
def c1_write_config_file():
ready_to_write.set()
- # The lock is held we wait for the main thread to decide when to
+ # The lock is held. We wait for the main thread to decide when to
# continue
do_writing.wait()
c1_orig()
@@ -1889,14 +1889,214 @@
self.assertEquals(config._Created, section.orig['foo'])
+def get_ConfigObjStore(transport, file_name, content=None):
+ """Build a ConfigObjStore.
+
+ :param transport: The transport where the store lives.
+
+ :param file_name: The name of the store.
+
+ :param content: A provided content to inject into the built store.
+
+ If provided, the content is added to the store but without saving it on
+ disk. It should be a string or a unicode string in the ConfigObj syntax.
+ While this poses a constraint on other store implementations, it keeps a
+ simple syntax usable by test writers.
+ """
+ if content is None:
+ store = config.ConfigObjStore(transport, file_name)
+ else:
+ store = config.ConfigObjStore.from_string(content, transport, file_name)
+ return store
+
+
+class TestStore(tests.TestCaseWithTransport):
+
+ def assertSectionContent(self, expected, section):
+ """Assert that some options have the proper values in a section."""
+ expected_name, expected_options = expected
+ self.assertEquals(expected_name, section.id)
+ self.assertEquals(
+ expected_options,
+ dict([(k, section.get(k)) for k in expected_options.keys()]))
+
+
+class TestReadonlyStore(TestStore):
+
+ scenarios = [('configobj', {'_get_store': get_ConfigObjStore})]
+
+ def get_store(self, file_name, content=None):
+ return self._get_store(
+ self.get_readonly_transport(), file_name, content=content)
+
+ 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()
+ # We loaded the store from the provided content
+ self.assertEquals(True, store.loaded)
+
+ 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.assertSectionContent((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.assertSectionContent(('baz', {'foo': 'bar'}), sections[0])
+
+
+class TestMutableStore(TestStore):
+
+ scenarios = [('configobj', {'_get_store': get_ConfigObjStore})]
+
+ def get_store(self, file_name, content=None):
+ return self._get_store(
+ self.get_transport(), file_name, content=content)
+
+ def test_save_empty_succeeds(self):
+ store = self.get_store('foo.conf', '')
+ store.load()
+ self.assertEquals(False, self.get_transport().has('foo.conf'))
+ store.save()
+ self.assertEquals(True, self.get_transport().has('foo.conf'))
+
+ def test_save_with_content_succeeds(self):
+ store = self.get_store('foo.conf', 'foo=bar\n')
+ store.load()
+ self.assertEquals(False, self.get_transport().has('foo.conf'))
+ store.save()
+ self.assertEquals(True, self.get_transport().has('foo.conf'))
+ modified_store = self.get_store('foo.conf')
+ sections = list(modified_store.get_sections())
+ self.assertLength(1, sections)
+ self.assertSectionContent((None, {'foo': 'bar'}), sections[0])
+
+ def test_set_option_in_empty_store(self):
+ store = self.get_store('foo.conf')
+ section = store.get_mutable_section(None)
+ section.set('foo', 'bar')
+ store.save()
+ modified_store = self.get_store('foo.conf')
+ sections = list(modified_store.get_sections())
+ self.assertLength(1, sections)
+ self.assertSectionContent((None, {'foo': 'bar'}), sections[0])
+
+ def test_set_option_in_default_section(self):
+ store = self.get_store('foo.conf', '')
+ section = store.get_mutable_section(None)
+ section.set('foo', 'bar')
+ store.save()
+ modified_store = self.get_store('foo.conf')
+ sections = list(modified_store.get_sections())
+ self.assertLength(1, sections)
+ self.assertSectionContent((None, {'foo': 'bar'}), sections[0])
+
+ def test_set_option_in_named_section(self):
+ store = self.get_store('foo.conf', '')
+ section = store.get_mutable_section('baz')
+ section.set('foo', 'bar')
+ store.save()
+ modified_store = self.get_store('foo.conf')
+ sections = list(modified_store.get_sections())
+ self.assertLength(1, sections)
+ self.assertSectionContent(('baz', {'foo': 'bar'}), sections[0])
+
+
+class TestConfigObjStore(TestStore):
+
+ def test_loading_unknown_file_fails(self):
+ store = config.ConfigObjStore(self.get_transport(), 'I-do-not-exist')
+ self.assertRaises(errors.NoSuchFile, store.load)
+
+ def test_invalid_content(self):
+ store = config.ConfigObjStore.from_string(
+ 'this is invalid !', self.get_transport(), 'foo.conf', )
+ 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_get_embedded_sections(self):
+ # A more complicated example (which also shows that section names and
+ # option names share the same name space...)
+ # FIXME: This should be fixed by forbidding dicts as values ?
+ # -- vila 2011-04-05
+ store = config.ConfigObjStore.from_string('''
+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
+''', self.get_transport(), 'foo.conf')
+ sections = list(store.get_sections())
+ self.assertLength(4, sections)
+ # The default section has no name.
+ # List values are provided as lists
+ self.assertSectionContent((None, {'foo': 'bar', 'l': ['1', '2']}),
+ sections[0])
+ self.assertSectionContent(
+ ('DEFAULT', {'foo_in_DEFAULT': 'foo_DEFAULT'}), sections[1])
+ self.assertSectionContent(
+ ('bar', {'foo_in_bar': 'barbar'}), sections[2])
+ # sub sections are provided as embedded dicts.
+ self.assertSectionContent(
+ ('baz', {'foo_in_baz': 'barbaz', 'qux': {'foo_in_qux': 'quux'}}),
+ sections[3])
+
+
+class TestLockableConfigObjStore(TestStore):
+
+ def test_create_store_in_created_dir(self):
+ t = self.get_transport('dir/subdir')
+ store = config.LockableConfigObjStore(t, 'foo.conf')
+ store.get_mutable_section(None).set('foo', 'bar')
+ store.save()
+
+ # FIXME: We should adapt the tests in TestLockableConfig about concurrent
+ # writes, for now, I'll just rely on using the same code (copied, but
+ # pretty trivial) -- vila 20110-04-06
+
+
+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
# paramerized tests created to avoid bloating -- vila 2011-03-31
- def test_compatibility(self):
- self.assertRaises(AssertionError, config.ConfigStack, [object()])
-
def test_single_config_get(self):
conf = dict(foo='bar')
conf_stack = config.ConfigStack([conf])
@@ -1914,6 +2114,60 @@
conf_stack = config.ConfigStack([conf1, conf2])
self.assertEquals('baz', conf_stack.get('foo'))
+ def test_get_for_empty_stack(self):
+ conf_stack = config.ConfigStack()
+ self.assertEquals(None, conf_stack.get('foo'))
+
+ def test_get_for_empty_section_callable(self):
+ conf_stack = config.ConfigStack([lambda : []])
+ self.assertEquals(None, conf_stack.get('foo'))
+
+
+class TestConfigStackSet(tests.TestCaseWithTransport):
+
+ # FIXME: This should be parametrized for all known ConfigStack or dedicated
+ # paramerized tests created to avoid bloating -- vila 2011-04-05
+
+ def test_simple_set(self):
+ store = config.ConfigObjStore.from_string(
+ 'foo=bar', self.get_transport(), 'test.conf')
+ conf = config.ConfigStack(
+ [store.get_sections], store.get_mutable_section)
+ self.assertEquals('bar', conf.get('foo'))
+ conf.set('foo', 'baz')
+ # Did we get it back ?
+ self.assertEquals('baz', conf.get('foo'))
+
+ def test_set_creates_a_new_section(self):
+ store = config.ConfigObjStore.from_string(
+ '', self.get_transport(), 'test.conf')
+ conf = config.ConfigStack(
+ [store.get_sections], store.get_mutable_section)
+ conf.set('foo', 'baz')
+ self.assertEquals, 'baz', conf.get('foo')
+
+
+class TestConfigStackRemove(tests.TestCaseWithTransport):
+
+ # FIXME: This should be parametrized for all known ConfigStack or dedicated
+ # paramerized tests created to avoid bloating -- vila 2011-04-06
+
+ def test_remove_existing(self):
+ store = config.ConfigObjStore.from_string(
+ 'foo=bar', self.get_transport(), 'test.conf')
+ conf = config.ConfigStack(
+ [store.get_sections], store.get_mutable_section)
+ self.assertEquals('bar', conf.get('foo'))
+ conf.remove('foo')
+ # Did we get it back ?
+ self.assertEquals(None, conf.get('foo'))
+
+ def test_remove_unknown(self):
+ store = config.ConfigObjStore(self.get_transport(), 'test.conf')
+ conf = config.ConfigStack(
+ [store.get_sections], store.get_mutable_section)
+ self.assertRaises(KeyError, conf.remove, 'I_do_not_exist')
+
class TestConfigGetOptions(tests.TestCaseWithTransport, TestOptionsMixin):
More information about the bazaar-commits
mailing list