Rev 6084: Cargo-cult the config option expansion implementation with tweaks from the old to the new config design. in file:///home/vila/src/bzr/experimental/expand-in-stack/
Vincent Ladeuil
v.ladeuil+lp at free.fr
Fri Aug 19 17:10:44 UTC 2011
At file:///home/vila/src/bzr/experimental/expand-in-stack/
------------------------------------------------------------
revno: 6084
revision-id: v.ladeuil+lp at free.fr-20110819171043-5v7qkicv102ae4zj
parent: v.ladeuil+lp at free.fr-20110819131755-k7x6pvibgbvnxyze
committer: Vincent Ladeuil <v.ladeuil+lp at free.fr>
branch nick: expand-in-stack
timestamp: Fri 2011-08-19 19:10:43 +0200
message:
Cargo-cult the config option expansion implementation with tweaks from the old to the new config design.
-------------- next part --------------
=== modified file 'bzrlib/config.py'
--- a/bzrlib/config.py 2011-08-19 13:17:55 +0000
+++ b/bzrlib/config.py 2011-08-19 17:10:43 +0000
@@ -2881,8 +2881,11 @@
self.sections_def = sections_def
self.store = store
self.mutable_section_name = mutable_section_name
+ # Used to describe an expandable option reference (see
+ # _expand_options_in_string)
+ self._option_ref_re = None
- def get(self, name):
+ def get(self, name, expand=None):
"""Return the *first* option value found in the sections.
This is where we guarantee that sections coming from Store are loaded
@@ -2890,8 +2893,16 @@
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).
+
+ :param name: The queried option.
+
+ :param expand: Whether options references should be expanded.
+
+ :returns: The value of the option.
"""
# FIXME: No caching of options nor sections yet -- vila 20110503
+ if expand is None:
+ expand = _get_expand_default_value()
value = None
# Ensuring lazy loading is achieved by delaying section matching (which
# implies querying the persistent storage) until it can't be avoided
@@ -2938,6 +2949,137 @@
value = opt.get_default()
for hook in ConfigHooks['get']:
hook(self, name, value)
+ if expand:
+ if isinstance(value, list):
+ value = self._expand_options_in_list(value)
+ elif isinstance(value, dict):
+ trace.warning('Cannot expand "%s":'
+ ' Dicts do not support option expansion'
+ % (name,))
+ elif isinstance(value, (str, unicode)):
+ value = self._expand_options_in_string(value)
+ return value
+
+ def expand_options(self, string, env=None):
+ """Expand option references in the string in the configuration context.
+
+ :param string: The string containing option(s) to expand.
+
+ :param env: An option dict defining additional configuration options or
+ overriding existing ones.
+
+ :returns: The expanded string.
+ """
+ return self._expand_options_in_string(string, env)
+
+ def _expand_options_in_list(self, slist, env=None, _refs=None):
+ """Expand options in a list of strings in the configuration context.
+
+ :param slist: A list of strings.
+
+ :param env: An option dict defining additional configuration options or
+ overriding existing ones.
+
+ :param _refs: Private list (FIFO) containing the options being
+ expanded to detect loops.
+
+ :returns: The flatten list of expanded strings.
+ """
+ # expand options in each value separately flattening lists
+ result = []
+ for s in slist:
+ value = self._expand_options_in_string(s, env, _refs)
+ if isinstance(value, list):
+ result.extend(value)
+ else:
+ result.append(value)
+ return result
+
+ def _expand_options_in_string(self, string, env=None, _refs=None):
+ """Expand options in the string in the configuration context.
+
+ :param string: The string to be expanded.
+
+ :param env: An option dict defining additional configuration options or
+ overriding existing ones.
+
+ :param _refs: Private list (FIFO) containing the options being expanded
+ to detect loops.
+
+ :returns: The expanded string.
+ """
+ if string is None:
+ # Not much to expand there
+ return None
+ if _refs is None:
+ # What references are currently resolved (to detect loops)
+ _refs = []
+ if self._option_ref_re is None:
+ # We want to match the most embedded reference first (i.e. for
+ # '{{foo}}' we will get '{foo}',
+ # for '{bar{baz}}' we will get '{baz}'
+ self._option_ref_re = re.compile('({[^{}]+})')
+ result = string
+ # We need to iterate until no more refs appear ({{foo}} will need two
+ # iterations for example).
+ while True:
+ raw_chunks = self._option_ref_re.split(result)
+ if len(raw_chunks) == 1:
+ # Shorcut the trivial case: no refs
+ return result
+ chunks = []
+ list_value = False
+ # Split will isolate refs so that every other chunk is a ref
+ chunk_is_ref = False
+ for chunk in raw_chunks:
+ if not chunk_is_ref:
+ if chunk:
+ # Keep only non-empty strings (or we get bogus empty
+ # slots when a list value is involved).
+ chunks.append(chunk)
+ chunk_is_ref = True
+ else:
+ name = chunk[1:-1]
+ if name in _refs:
+ raise errors.OptionExpansionLoop(string, _refs)
+ _refs.append(name)
+ value = self._expand_option(name, env, _refs)
+ if value is None:
+ raise errors.ExpandingUnknownOption(name, string)
+ if isinstance(value, list):
+ list_value = True
+ chunks.extend(value)
+ else:
+ chunks.append(value)
+ _refs.pop()
+ chunk_is_ref = False
+ if list_value:
+ # Once a list appears as the result of an expansion, all
+ # callers will get a list result. This allows a consistent
+ # behavior even when some options in the expansion chain
+ # defined as strings (no comma in their value) but their
+ # expanded value is a list.
+ return self._expand_options_in_list(chunks, env, _refs)
+ else:
+ result = ''.join(chunks)
+ return result
+
+ def _expand_option(self, name, env, _refs):
+ if env is not None and name in env:
+ # Special case, values provided in env takes precedence over
+ # anything else
+ value = env[name]
+ else:
+ # FIXME: This is a limited implementation, what we really need is a
+ # way to query the bzr config for the value of an option,
+ # respecting the scope rules (That is, once we implement fallback
+ # configs, getting the option value should restart from the top
+ # config, not the current one) -- vila 20101222
+ value = self.get(name, expand=False)
+ if isinstance(value, list):
+ value = self._expand_options_in_list(value, env, _refs)
+ else:
+ value = self._expand_options_in_string(value, env, _refs)
return value
def _get_mutable_section(self):
=== modified file 'bzrlib/tests/test_config.py'
--- a/bzrlib/tests/test_config.py 2011-08-19 13:17:55 +0000
+++ b/bzrlib/tests/test_config.py 2011-08-19 17:10:43 +0000
@@ -819,6 +819,7 @@
self.assertEquals(['{foo', '}', '{', 'bar}'],
conf.get_user_option('hidden', expand=True))
+
class TestLocationConfigOptionExpansion(tests.TestCaseInTempDir):
def get_config(self, location, string=None):
@@ -3107,6 +3108,134 @@
self.assertEquals(['m', 'o', 'r', 'e'], self.conf.get('foo'))
+class TestStackExpandOptions(tests.TestCaseWithTransport):
+
+ def setUp(self):
+ super(TestStackExpandOptions, self).setUp()
+ self.conf = build_branch_stack(self)
+
+ def assertExpansion(self, expected, string, env=None):
+ self.assertEquals(expected, self.conf.expand_options(string, env))
+
+ def test_no_expansion(self):
+ self.assertExpansion('foo', 'foo')
+
+ def test_env_adding_options(self):
+ self.assertExpansion('bar', '{foo}', {'foo': 'bar'})
+
+ def test_env_overriding_options(self):
+ self.conf.store._load_from_string('foo=baz')
+ self.assertExpansion('bar', '{foo}', {'foo': 'bar'})
+
+ def test_simple_ref(self):
+ self.conf.store._load_from_string('foo=xxx')
+ self.assertExpansion('xxx', '{foo}')
+
+ def test_unknown_ref(self):
+ self.assertRaises(errors.ExpandingUnknownOption,
+ self.conf.expand_options, '{foo}')
+
+ def test_indirect_ref(self):
+ self.conf.store._load_from_string('''
+foo=xxx
+bar={foo}
+''')
+ self.assertExpansion('xxx', '{bar}')
+
+ def test_embedded_ref(self):
+ self.conf.store._load_from_string('''
+foo=xxx
+bar=foo
+''')
+ self.assertExpansion('xxx', '{{bar}}')
+
+ def test_simple_loop(self):
+ self.conf.store._load_from_string('foo={foo}')
+ self.assertRaises(errors.OptionExpansionLoop,
+ self.conf.expand_options, '{foo}')
+
+ def test_indirect_loop(self):
+ self.conf.store._load_from_string('''
+foo={bar}
+bar={baz}
+baz={foo}''')
+ e = self.assertRaises(errors.OptionExpansionLoop,
+ self.conf.expand_options, '{foo}')
+ self.assertEquals('foo->bar->baz', e.refs)
+ self.assertEquals('{foo}', e.string)
+
+ def test_list(self):
+ self.conf.store._load_from_string('''
+foo=start
+bar=middle
+baz=end
+list={foo},{bar},{baz}
+''')
+ self.assertEquals(['start', 'middle', 'end'],
+ self.conf.get('list', expand=True))
+
+ def test_cascading_list(self):
+ self.conf.store._load_from_string('''
+foo=start,{bar}
+bar=middle,{baz}
+baz=end
+list={foo}
+''')
+ self.assertEquals(['start', 'middle', 'end'],
+ self.conf.get('list', expand=True))
+
+ def test_pathologically_hidden_list(self):
+ self.conf.store._load_from_string('''
+foo=bin
+bar=go
+start={foo
+middle=},{
+end=bar}
+hidden={start}{middle}{end}
+''')
+ # Nope, it's either a string or a list, and the list wins as soon as a
+ # ',' appears, so the string concatenation never occur.
+ self.assertEquals(['{foo', '}', '{', 'bar}'],
+ self.conf.get('hidden', expand=True))
+
+
+class TestStackCrossSectionsExpand(tests.TestCaseWithTransport):
+
+ def setUp(self):
+ super(TestStackCrossSectionsExpand, self).setUp()
+
+ def get_config(self, location, string):
+ if string is None:
+ string = ''
+ # Since we don't save the config we won't strictly require to inherit
+ # from TestCaseInTempDir, but an error occurs so quickly...
+ c = config.LocationStack(location)
+ c.store._load_from_string(string)
+ return c
+
+ def test_dont_cross_unrelated_section(self):
+ c = self.get_config('/another/branch/path','''
+[/one/branch/path]
+foo = hello
+bar = {foo}/2
+
+[/another/branch/path]
+bar = {foo}/2
+''')
+ self.assertRaises(errors.ExpandingUnknownOption,
+ c.get, 'bar', expand=True)
+
+ def test_cross_related_sections(self):
+ c = self.get_config('/project/branch/path','''
+[/project]
+foo = qu
+
+[/project/branch/path]
+bar = {foo}ux
+''')
+ self.assertEquals('quux', c.get('bar', expand=True))
+
+
class TestStackSet(TestStackWithTransport):
def test_simple_set(self):
More information about the bazaar-commits
mailing list