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