Rev 5763: Merge config-section-matchers into config-stack resolving conflicts in file:///home/vila/src/bzr/experimental/config/
Vincent Ladeuil
v.ladeuil+lp at free.fr
Thu Apr 7 19:29:36 UTC 2011
At file:///home/vila/src/bzr/experimental/config/
------------------------------------------------------------
revno: 5763 [merge]
revision-id: v.ladeuil+lp at free.fr-20110407192936-uj1ai692yobw0kmx
parent: v.ladeuil+lp at free.fr-20110407074455-a5xhwpjzt1rdzi9z
parent: v.ladeuil+lp at free.fr-20110407171307-8oaze02li95rpvat
committer: Vincent Ladeuil <v.ladeuil+lp at free.fr>
branch nick: config-stack
timestamp: Thu 2011-04-07 21:29:36 +0200
message:
Merge config-section-matchers into config-stack resolving conflicts
modified:
bzrlib/config.py config.py-20051011043216-070c74f4e9e338e8
bzrlib/tests/test_config.py testconfig.py-20051011041908-742d0c15d8d8c8eb
-------------- next part --------------
=== modified file 'bzrlib/config.py'
--- a/bzrlib/config.py 2011-04-07 07:44:55 +0000
+++ b/bzrlib/config.py 2011-04-07 19:29:36 +0000
@@ -967,6 +967,49 @@
super(LockableConfig, self).remove_user_option(option_name,
section_name)
+def _match_section_by_parts(section, location):
+ location_parts = location.rstrip('/').split('/')
+
+ # location is a local path if possible, so we need
+ # to convert 'file://' urls to local paths if necessary.
+
+ # FIXME: I don't think the above comment is still up to date,
+ # LocationConfig is always instantiated with an url -- vila 2011-04-07
+
+ # This also avoids having file:///path be a more exact
+ # match than '/path'.
+
+ # FIXME: Not sure about the above either, but since the path components are
+ # compared in sync, adding two empty components (//) is likely to trick the
+ # comparison and also trick the check on the number of components, so we
+ # *should* take only the relevant part of the url. On the other hand, this
+ # means 'file://' urls *can't* be used in sections -- vila 2011-04-07
+
+ if section.startswith('file://'):
+ section_path = urlutils.local_path_from_url(section)
+ else:
+ section_path = section
+ section_parts = section_path.rstrip('/').split('/')
+
+ matched = True
+ if len(section_parts) > len(location_parts):
+ # More path components in the section, they can't match
+ matched = False
+ else:
+ # Rely on zip truncating in length to the length of the shortest
+ # argument sequence.
+ names = zip(location_parts, section_parts)
+ for name in names:
+ if not fnmatch.fnmatch(name[0], name[1]):
+ matched = False
+ break
+ if not matched:
+ return None
+ else:
+ # build the path difference between the section and the location
+ relpath = '/'.join(location_parts[len(section_parts):])
+ return len(section_parts), relpath
+
class LocationConfig(LockableConfig):
"""A configuration object that gives the policy for a location."""
@@ -1002,36 +1045,14 @@
def _get_matching_sections(self):
"""Return an ordered list of section names matching this location."""
sections = self._get_parser()
- location_names = self.location.split('/')
- if self.location.endswith('/'):
- del location_names[-1]
- matches=[]
+
+ matches = []
for section in sections:
- # location is a local path if possible, so we need
- # to convert 'file://' urls to local paths if necessary.
- # This also avoids having file:///path be a more exact
- # match than '/path'.
- if section.startswith('file://'):
- section_path = urlutils.local_path_from_url(section)
- else:
- section_path = section
- section_names = section_path.split('/')
- if section.endswith('/'):
- del section_names[-1]
- names = zip(location_names, section_names)
- matched = True
- for name in names:
- if not fnmatch.fnmatch(name[0], name[1]):
- matched = False
- break
- if not matched:
- continue
- # so, for the common prefix they matched.
- # if section is longer, no match.
- if len(section_names) > len(location_names):
- continue
- matches.append((len(section_names), section,
- '/'.join(location_names[len(section_names):])))
+ match = _match_section_by_parts(section, self.location)
+ if match is None:
+ continue
+ nb_parts, relpath = match
+ matches.append((nb_parts, section, relpath))
# put the longest (aka more specific) locations first
matches.sort(reverse=True)
sections = []
@@ -2296,6 +2317,74 @@
super(BranchStore, self).__init__(branch.control_transport,
'branch.conf')
+class SectionMatcher(object):
+ """Select sections into a given Store.
+
+ This intended to be used to postpone getting an iterable of sections from a
+ store.
+ """
+
+ def __init__(self, store):
+ self.store = store
+
+ def get_sections(self):
+ # This is where we requires loading the store so we can see all defined
+ # sections.
+ sections = self.store.get_sections()
+ # Walk the revisions in the order provided
+ for s in sections:
+ if self.match(s):
+ yield s
+
+ def match(self, secion):
+ raise NotImplementedError(self.match)
+
+
+class LocationSection(ReadOnlySection):
+
+ def __init__(self, section, length, extra_path):
+ super(LocationSection, self).__init__(section.id, section.options)
+ self.length = length
+ self.extra_path = extra_path
+
+ def get(self, name, default=None):
+ value = super(LocationSection, self).get(name, default)
+ if value is not None:
+ policy_name = self.get(name + ':policy', None)
+ policy = _policy_value.get(policy_name, POLICY_NONE)
+ if policy == POLICY_APPENDPATH:
+ value = urlutils.join(value, self.extra_path)
+ return value
+
+
+class LocationMatcher(SectionMatcher):
+
+ def __init__(self, store, location):
+ super(LocationMatcher, self).__init__(store)
+ self.location = location
+
+ def get_sections(self):
+ # Override the default implementation as we want to change the order
+ sections = []
+ for section in self.store.get_sections():
+ match = _match_section_by_parts(section.id, self.location)
+ if match is not None:
+ length, extra_path = match
+ sections.append(LocationSection(section, length, extra_path))
+ # We want the longest (aka more specific) locations first
+ sections = sorted(sections, key=lambda section: section.length,
+ reverse=True)
+ # Sections mentioning 'ignore_parents' restrict the selection
+ for section in sections:
+ # FIXME: We really want to use as_bool below -- vila 2011-04-07
+ ignore = section.get('ignore_parents', None)
+ if ignore is not None:
+ ignore = ui.bool_from_string(ignore)
+ if ignore:
+ break
+ # Finally, we have a valid section
+ yield section
+
class ConfigStack(object):
"""A stack of configurations where an option can be defined"""
=== modified file 'bzrlib/tests/test_config.py'
--- a/bzrlib/tests/test_config.py 2011-04-07 07:44:55 +0000
+++ b/bzrlib/tests/test_config.py 2011-04-07 19:29:36 +0000
@@ -1967,6 +1967,7 @@
scenarios = [('configobj', {'_get_store': get_ConfigObjStore})]
def get_store(self, file_name, content=None):
+ # Overriden to get a writable transport
return self._get_store(
self.get_transport(), file_name, content=content)
@@ -2076,8 +2077,14 @@
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
+ # writes. Since this requires a clearer rewrite, I'll just rely on using
+ # the same code in LockableConfigObjStore (copied from LockableConfig, but
+ # trivial enough, the main difference is that we add @needs_write_lock on
+ # save() instead of set_user_option() and remove_user_option()). The intent
+ # is to ensure that we always get a valid content for the store even when
+ # concurrent accesses occur, read/write, write/write. It may be worth
+ # looking into removing the lock dir when it;s not needed anymore and look
+ # at possible fallouts for concurrent lockers -- vila 20110-04-06
class TestConfigObjStore(tests.TestCaseWithTransport):
@@ -2093,6 +2100,72 @@
store = config.BranchStore(b)
+class TestSectionMatcher(TestStore):
+
+ scenarios = [('location', {'matcher': config.LocationMatcher})]
+
+ def get_store(self, file_name, content=None):
+ return get_ConfigObjStore(
+ self.get_readonly_transport(), file_name, content=content)
+
+ def test_no_matches_for_empty_stores(self):
+ store = self.get_store('foo.conf', '')
+ matcher = self.matcher(store, '/bar')
+ self.assertEquals([], list(matcher.get_sections()))
+
+ def test_build_doesnt_load_store(self):
+ store = self.get_store('foo.conf', '')
+ matcher = self.matcher(store, '/bar')
+ self.assertFalse(store.loaded)
+
+
+class TestLocationSection(tests.TestCase):
+
+ def get_section(self, options, extra_path):
+ section = config.ReadOnlySection('foo', options)
+ # We don't care about the length so we use '0'
+ return config.LocationSection(section, 0, extra_path)
+
+ def test_simple_option(self):
+ section = self.get_section({'foo': 'bar'}, '')
+ self.assertEquals('bar', section.get('foo'))
+
+ def test_option_with_extra_path(self):
+ section = self.get_section({'foo': 'bar', 'foo:policy': 'appendpath'},
+ 'baz')
+ self.assertEquals('bar/baz', section.get('foo'))
+
+ def test_invalid_policy(self):
+ section = self.get_section({'foo': 'bar', 'foo:policy': 'die'},
+ 'baz')
+ # invalid policies are ignored
+ self.assertEquals('bar', section.get('foo'))
+
+
+class TestLocationMatcher(TestStore):
+
+ def test_more_specific_sections_first(self):
+ store = config.ConfigObjStore.from_string(
+ '''
+[/foo]
+section=/foo
+[/foo/bar]
+section=/foo/bar
+''',
+ self.get_readonly_transport(), 'foo.conf', )
+ self.assertEquals(['/foo', '/foo/bar'],
+ [section.id for section in store.get_sections()])
+ matcher = config.LocationMatcher(store, '/foo/bar/baz')
+ sections = list(matcher.get_sections())
+ self.assertEquals([3, 2],
+ [section.length for section in sections])
+ self.assertEquals(['/foo/bar', '/foo'],
+ [section.id for section in sections])
+ self.assertEquals(['baz', 'bar/baz'],
+ [section.extra_path for section in sections])
+
+
+
class TestConfigStackGet(tests.TestCase):
# FIXME: This should be parametrized for all known ConfigStack or dedicated
@@ -2176,7 +2249,6 @@
super(TestConfigGetOptions, self).setUp()
create_configs(self)
- # One variable in none of the above
def test_no_variable(self):
# Using branch should query branch, locations and bazaar
self.assertOptions([], self.branch_config)
More information about the bazaar-commits
mailing list