Merge lp:~vila/bzr/config-expand-options into lp:bzr

Proposed by Vincent Ladeuil
Status: Merged
Approved by: Jelmer Vernooij
Approved revision: no longer in the source branch.
Merged at revision: 5684
Proposed branch: lp:~vila/bzr/config-expand-options
Merge into: lp:bzr
Diff against target: 647 lines (+457/-36)
8 files modified
bzrlib/bugtracker.py (+9/-5)
bzrlib/config.py (+208/-30)
bzrlib/errors.py (+19/-0)
bzrlib/help_topics/en/configuration.txt (+7/-0)
bzrlib/tests/__init__.py (+4/-0)
bzrlib/tests/test_config.py (+194/-1)
doc/en/release-notes/bzr-2.4.txt (+8/-0)
doc/en/whats-new/whats-new-in-2.4.txt (+8/-0)
To merge this branch: bzr merge lp:~vila/bzr/config-expand-options
Reviewer Review Type Date Requested Status
Jelmer Vernooij (community) Approve
Review via email: mp+50355@code.launchpad.net

Commit message

Add the opt-in ability to expand options in config files

Description of the change

This implement option expansion in config files.

This is a limited implementation that only support references inside a given file. More steps will be necessary to allow cross-files references but this is a first significant steps and should already address many needs (including configuring the udd package importer both in production and for tests).

This proposed implementation detects loops and unknown references.

An exception is raised in the later case. There are possible variations there that we may prefer, feedback welcome !

For a first proposal I went with the conservative approach of throwing an exception but we can also:

- silently leave the unresolved references in place (the current version is a good testbed for this scenario as I realize while trying to submit this proposal :), we tend to display config options when something goes wrong and it's quite obvious then to catch typos

- replace the unresolved references by an empty string. This avoid exceptions but errors may be hard to diagnose. Not that this is what bash and other shells tends to implement though so people may prefer that.

My preference would be to leave the unresolved references in place in the future.

I didn't implement option expansion for dict values. I was convinced nobody ever used them until a test broke (per_branch.test_config) which has been introduced to fix bug #430382).

Such values are implemented as sub sections and supporting them seems unnecessary when they can be expressed with separate options (i.e. by concatenating the option name and the dict key).

Another issue with expanding options in dicts is that this can't be done for keys (even if the keys are unique without expansion they can collide after expansion).

Bottom line: options are basically strings, list of strings at most, trying to turn them into a database doesn't sound like somthing we want to pursue.

In the long term, I'd like to completely deprecate dicts as supported values in config files.

For now, a warning is emitted.

Back to the option expansion: get_user_option() now has a new 'expand' attribute to control the expansion and allow templates to be retrieved (with expand=False).

There is also a new expand_options(strings, env=None) method on the Config object (so all daughter alsses can use it) whose purpose is to expand options *or* keys from the additional 'env'.
mergetools and difftools are the primary targets for this method since they rely on templates embedding references to domain specific variables (paths mainly).
bugtrackers could also use this (but the current implementation is so simple I didn't change it except for calling get_user_option(..., expand=False).

I also fixed get_user_option_as_{bool|list} to use the public get_user_option so they benefit from the expansion too.

To post a comment you must log in.
Revision history for this message
Vincent Ladeuil (vila) wrote :
Revision history for this message
John A Meinel (jameinel) wrote :
Download full text (4.7 KiB)

-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1

On 2/18/2011 10:22 AM, Vincent Ladeuil wrote:
> Vincent Ladeuil has proposed merging lp:~vila/bzr/config-expand-options into lp:bzr.
>
> Requested reviews:
> bzr-core (bzr-core)
>
> For more details, see:
> https://code.launchpad.net/~vila/bzr/config-expand-options/+merge/50355
>
> This implement option expansion in config files.
>
> This is a limited implementation that only support references inside a given file. More steps will be necessary to allow cross-files references but this is a first significant steps and should already address many needs (including configuring the udd package importer both in production and for tests).
>
> This proposed implementation detects loops and unknown references.
>
> An exception is raised in the later case. There are possible variations there that we may prefer, feedback welcome !
>
> For a first proposal I went with the conservative approach of throwing an exception but we can also:
>
> - silently leave the unresolved references in place (the current version is a good testbed for this scenario as I realize while trying to submit this proposal :), we tend to display config options when something goes wrong and it's quite obvious then to catch typos
>
> - replace the unresolved references by an empty string. This avoid exceptions but errors may be hard to diagnose. Not that this is what bash and other shells tends to implement though so people may prefer that.
>
> My preference would be to leave the unresolved references in place in the future.
>
> I didn't implement option expansion for dict values. I was convinced nobody ever used them until a test broke (per_branch.test_config) which has been introduced to fix bug #430382).

qbzr uses a dict for some of its stuff. I think the commit message
backlog is stored that way. (Because we've had failures for 'uncommit'
in lightweight checkouts because RemoteConfig didn't support dict
values, etc.)

>
> Such values are implemented as sub sections and supporting them seems unnecessary when they can be expressed with separate options (i.e. by concatenating the option name and the dict key).
>
> Another issue with expanding options in dicts is that this can't be done for keys (even if the keys are unique without expansion they can collide after expansion).
>
> Bottom line: options are basically strings, list of strings at most, trying to turn them into a database doesn't sound like somthing we want to pursue.
>
> In the long term, I'd like to completely deprecate dicts as supported values in config files.
>
> For now, a warning is emitted.
>
> Back to the option expansion: get_user_option() now has a new 'expand' attribute to control the expansion and allow templates to be retrieved (with expand=False).
>
> There is also a new expand_options(strings, env=None) method on the Config object (so all daughter alsses can use it) whose purpose is to expand options *or* keys from the additional 'env'.
> mergetools and difftools are the primary targets for this method since they rely on templates embedding references to domain specific variables (paths mainly).
> bugtrackers could also use this (but the cur...

Read more...

Revision history for this message
Vincent Ladeuil (vila) wrote :

> I'm a bit concerned about scope here. For example if in locations.conf I do:
>
> [/one/branch/path]
> foo = hello
> bar = {foo}/2
>
> [/another/branch/path]
> bar = {foo}/2
>
> Does it also get expanded? or do we only expand within the given scope
> of a branch?

It shouldn't. Worth adding a test. Did you try ?

>
> For big changes like this, I tend to err on the side of 'fail noisily'
> until we figure out that we really want to do it differently. So a {foo}
> in a string with no option {foo} will fail, rather than silently get
> replaced by '' or left in line.

That's indeed the current proposal. You don't say whether you want to keep it in the long term or which alternative you prefer otherwise.

>
> Then again, maybe you can just notice that your path isn't correct.
>
>
> I'm a little concerned with stuff like 'uncommit' messages that get
> stored in the config. If I do:
>
>
> bzr commit -m "{foo} is now available for config entries"
> bzr uncommit
>
> # Note this needs something like qbzr to actually store the old commit
> # message
>

Yes, that was bug #430382 and how I discover there was at least one use of dict value in the wild.

> Will that new entry try to expand {foo}?

No. Because it's part of a dict value.

The correct way to deal with that sort of option *will* be to either properly quote them (or bencode them or whatever) or use get_user_option(..., expand=False) which makes sense. Either an option is used as a template or an arbitrary content and you should take care of what can be embedded there or it's a simple option where no references can appear.

Revision history for this message
Vincent Ladeuil (vila) wrote :

>>>>> John A Meinel <email address hidden> writes:

<snip/>
    > I'm a bit concerned about scope here. For example if in locations.conf I do:

    > [/one/branch/path]
    > foo = hello
    > bar = {foo}/2

    > [/another/branch/path]
    > bar = {foo}/2

A trickier example would be:

    > [/project]
    > foo = hello
    > bar = {foo}/2

    > [/project/branch]
    > bar = {foo}/3

And this case should be valid.

Tests coming soon...

Revision history for this message
Vincent Ladeuil (vila) wrote :

> The correct way to deal with that sort of option *will* be to either properly
> quote them (or bencode them or whatever) or use get_user_option(...,
> expand=False) which makes sense. Either an option is used as a template or an
> arbitrary content and you should take care of what can be embedded there or
> it's a simple option where no references can appear.

*blink*

Who am I to chose here ?

The default behaviour is not for me to decide:

  bzr.config.expand = <your choice>

... available in your nearest config file (RSN) :D

Revision history for this message
Vincent Ladeuil (vila) wrote :

And here comes the option controlling the option expansion.

Note that configobj *was* implementing expansion for ${option} so we're not entering a completely unexplored territory when it comes to protection values against expansion.

We never had reports about expansion corrupting data since bzr started using configobj so I'm not *that* concerned about such occurrences with the {option} syntax, but if it can help landing this proposal...

Revision history for this message
Vincent Ladeuil (vila) wrote :

Ping

Any objections to land this now ?

</ticking trigger>

Revision history for this message
Jelmer Vernooij (jelmer) wrote :

I think this is good to land now.

review: Approve
Revision history for this message
Vincent Ladeuil (vila) wrote :

sent to pqm by email

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'bzrlib/bugtracker.py'
2--- bzrlib/bugtracker.py 2010-09-25 00:42:22 +0000
3+++ bzrlib/bugtracker.py 2011-02-20 11:14:15 +0000
4@@ -231,7 +231,8 @@
5
6
7 tracker_registry.register('gnome',
8- UniqueIntegerBugTracker('gnome', 'http://bugzilla.gnome.org/show_bug.cgi?id='))
9+ UniqueIntegerBugTracker('gnome',
10+ 'http://bugzilla.gnome.org/show_bug.cgi?id='))
11
12
13 class URLParametrizedBugTracker(BugTracker):
14@@ -246,7 +247,7 @@
15 def get(self, abbreviation, branch):
16 config = branch.get_config()
17 url = config.get_user_option(
18- "%s_%s_url" % (self.type_name, abbreviation))
19+ "%s_%s_url" % (self.type_name, abbreviation), expand=False)
20 if url is None:
21 return None
22 self._base_url = url
23@@ -261,9 +262,12 @@
24 return urlutils.join(self._base_url, self._bug_area) + str(bug_id)
25
26
27-class URLParametrizedIntegerBugTracker(IntegerBugTracker, URLParametrizedBugTracker):
28- """A type of bug tracker that can be found on a variety of different sites,
29- and thus needs to have the base URL configured, but only allows integer bug IDs.
30+class URLParametrizedIntegerBugTracker(IntegerBugTracker,
31+ URLParametrizedBugTracker):
32+ """A type of bug tracker that only allows integer bug IDs.
33+
34+ This can be found on a variety of different sites, and thus needs to have
35+ the base URL configured.
36
37 Looks for a config setting in the form '<type_name>_<abbreviation>_url'.
38 `type_name` is the name of the type of tracker (e.g. 'bugzilla' or 'trac')
39
40=== modified file 'bzrlib/config.py'
41--- bzrlib/config.py 2011-01-20 04:44:14 +0000
42+++ bzrlib/config.py 2011-02-20 11:14:15 +0000
43@@ -129,25 +129,50 @@
44 STORE_BRANCH = 3
45 STORE_GLOBAL = 4
46
47-_ConfigObj = None
48-def ConfigObj(*args, **kwargs):
49- global _ConfigObj
50- if _ConfigObj is None:
51- class ConfigObj(configobj.ConfigObj):
52-
53- def get_bool(self, section, key):
54- return self[section].as_bool(key)
55-
56- def get_value(self, section, name):
57- # Try [] for the old DEFAULT section.
58- if section == "DEFAULT":
59- try:
60- return self[name]
61- except KeyError:
62- pass
63- return self[section][name]
64- _ConfigObj = ConfigObj
65- return _ConfigObj(*args, **kwargs)
66+
67+class ConfigObj(configobj.ConfigObj):
68+
69+ def __init__(self, infile=None, **kwargs):
70+ # We define our own interpolation mechanism calling it option expansion
71+ super(ConfigObj, self).__init__(infile=infile,
72+ interpolation=False,
73+ **kwargs)
74+
75+
76+ def get_bool(self, section, key):
77+ return self[section].as_bool(key)
78+
79+ def get_value(self, section, name):
80+ # Try [] for the old DEFAULT section.
81+ if section == "DEFAULT":
82+ try:
83+ return self[name]
84+ except KeyError:
85+ pass
86+ return self[section][name]
87+
88+
89+# FIXME: Until we can guarantee that each config file is loaded once and and
90+# only once for a given bzrlib session, we don't want to re-read the file every
91+# time we query for an option so we cache the value (bad ! watch out for tests
92+# needing to restore the proper value).This shouldn't be part of 2.4.0 final,
93+# yell at mgz^W vila and the RM if this is still present at that time
94+# -- vila 20110219
95+_expand_default_value = None
96+def _get_expand_default_value():
97+ global _expand_default_value
98+ if _expand_default_value is not None:
99+ return _expand_default_value
100+ conf = GlobalConfig()
101+ # Note that we must not use None for the expand value below or we'll run
102+ # into infinite recursion. Using False really would be quite silly ;)
103+ expand = conf.get_user_option_as_bool('bzr.config.expand', expand=True)
104+ if expand is None:
105+ # This is an opt-in feature, you *really* need to clearly say you want
106+ # to activate it !
107+ expand = False
108+ _expand_default_value = expand
109+ return expand
110
111
112 class Config(object):
113@@ -189,21 +214,167 @@
114 def _get_signing_policy(self):
115 """Template method to override signature creation policy."""
116
117+ option_ref_re = None
118+
119+ def expand_options(self, string, env=None):
120+ """Expand option references in the string in the configuration context.
121+
122+ :param string: The string containing option to expand.
123+
124+ :param env: An option dict defining additional configuration options or
125+ overriding existing ones.
126+
127+ :returns: The expanded string.
128+ """
129+ return self._expand_options_in_string(string, env)
130+
131+ def _expand_options_in_list(self, slist, env=None, _ref_stack=None):
132+ """Expand options in a list of strings in the configuration context.
133+
134+ :param slist: A list of strings.
135+
136+ :param env: An option dict defining additional configuration options or
137+ overriding existing ones.
138+
139+ :param _ref_stack: Private list containing the options being
140+ expanded to detect loops.
141+
142+ :returns: The flatten list of expanded strings.
143+ """
144+ # expand options in each value separately flattening lists
145+ result = []
146+ for s in slist:
147+ value = self._expand_options_in_string(s, env, _ref_stack)
148+ if isinstance(value, list):
149+ result.extend(value)
150+ else:
151+ result.append(value)
152+ return result
153+
154+ def _expand_options_in_string(self, string, env=None, _ref_stack=None):
155+ """Expand options in the string in the configuration context.
156+
157+ :param string: The string to be expanded.
158+
159+ :param env: An option dict defining additional configuration options or
160+ overriding existing ones.
161+
162+ :param _ref_stack: Private list containing the options being
163+ expanded to detect loops.
164+
165+ :returns: The expanded string.
166+ """
167+ if string is None:
168+ # Not much to expand there
169+ return None
170+ if _ref_stack is None:
171+ # What references are currently resolved (to detect loops)
172+ _ref_stack = []
173+ if self.option_ref_re is None:
174+ # We want to match the most embedded reference first (i.e. for
175+ # '{{foo}}' we will get '{foo}',
176+ # for '{bar{baz}}' we will get '{baz}'
177+ self.option_ref_re = re.compile('({[^{}]+})')
178+ result = string
179+ # We need to iterate until no more refs appear ({{foo}} will need two
180+ # iterations for example).
181+ while True:
182+ try:
183+ raw_chunks = self.option_ref_re.split(result)
184+ except TypeError:
185+ import pdb; pdb.set_trace()
186+ if len(raw_chunks) == 1:
187+ # Shorcut the trivial case: no refs
188+ return result
189+ chunks = []
190+ list_value = False
191+ # Split will isolate refs so that every other chunk is a ref
192+ chunk_is_ref = False
193+ for chunk in raw_chunks:
194+ if not chunk_is_ref:
195+ if chunk:
196+ # Keep only non-empty strings (or we get bogus empty
197+ # slots when a list value is involved).
198+ chunks.append(chunk)
199+ chunk_is_ref = True
200+ else:
201+ name = chunk[1:-1]
202+ if name in _ref_stack:
203+ raise errors.OptionExpansionLoop(string, _ref_stack)
204+ _ref_stack.append(name)
205+ value = self._expand_option(name, env, _ref_stack)
206+ if value is None:
207+ raise errors.ExpandingUnknownOption(name, string)
208+ if isinstance(value, list):
209+ list_value = True
210+ chunks.extend(value)
211+ else:
212+ chunks.append(value)
213+ _ref_stack.pop()
214+ chunk_is_ref = False
215+ if list_value:
216+ # Once a list appears as the result of an expansion, all
217+ # callers will get a list result. This allows a consistent
218+ # behavior even when some options in the expansion chain
219+ # defined as strings (no comma in their value) but their
220+ # expanded value is a list.
221+ return self._expand_options_in_list(chunks, env, _ref_stack)
222+ else:
223+ result = ''.join(chunks)
224+ return result
225+
226+ def _expand_option(self, name, env, _ref_stack):
227+ if env is not None and name in env:
228+ # Special case, values provided in env takes precedence over
229+ # anything else
230+ value = env[name]
231+ else:
232+ # FIXME: This is a limited implementation, what we really need is a
233+ # way to query the bzr config for the value of an option,
234+ # respecting the scope rules (That is, once we implement fallback
235+ # configs, getting the option value should restart from the top
236+ # config, not the current one) -- vila 20101222
237+ value = self.get_user_option(name, expand=False)
238+ if isinstance(value, list):
239+ value = self._expand_options_in_list(value, env, _ref_stack)
240+ else:
241+ value = self._expand_options_in_string(value, env, _ref_stack)
242+ return value
243+
244 def _get_user_option(self, option_name):
245 """Template method to provide a user option."""
246 return None
247
248- def get_user_option(self, option_name):
249- """Get a generic option - no special process, no default."""
250- return self._get_user_option(option_name)
251-
252- def get_user_option_as_bool(self, option_name):
253+ def get_user_option(self, option_name, expand=None):
254+ """Get a generic option - no special process, no default.
255+
256+ :param option_name: The queried option.
257+
258+ :param expand: Whether options references should be expanded.
259+
260+ :returns: The value of the option.
261+ """
262+ if expand is None:
263+ expand = _get_expand_default_value()
264+ value = self._get_user_option(option_name)
265+ if expand:
266+ if isinstance(value, list):
267+ value = self._expand_options_in_list(value)
268+ elif isinstance(value, dict):
269+ trace.warning('Cannot expand "%s":'
270+ ' Dicts do not support option expansion'
271+ % (option_name,))
272+ else:
273+ value = self._expand_options_in_string(value)
274+ return value
275+
276+ def get_user_option_as_bool(self, option_name, expand=None):
277 """Get a generic option as a boolean - no special process, no default.
278
279 :return None if the option doesn't exist or its value can't be
280 interpreted as a boolean. Returns True or False otherwise.
281 """
282- s = self._get_user_option(option_name)
283+ s = self.get_user_option(option_name, expand=expand)
284 if s is None:
285 # The option doesn't exist
286 return None
287@@ -214,15 +385,16 @@
288 s, option_name)
289 return val
290
291- def get_user_option_as_list(self, option_name):
292+ def get_user_option_as_list(self, option_name, expand=None):
293 """Get a generic option as a list - no special process, no default.
294
295 :return None if the option doesn't exist. Returns the value as a list
296 otherwise.
297 """
298- l = self._get_user_option(option_name)
299+ l = self.get_user_option(option_name, expand=expand)
300 if isinstance(l, (str, unicode)):
301- # A single value, most probably the user forgot the final ','
302+ # A single value, most probably the user forgot (or didn't care to
303+ # add) the final ','
304 l = [l]
305 return l
306
307@@ -372,8 +544,9 @@
308 # be found in the known_merge_tools if it's not found in the config.
309 # This should be done through the proposed config defaults mechanism
310 # when it becomes available in the future.
311- command_line = (self.get_user_option('bzr.mergetool.%s' % name) or
312- mergetools.known_merge_tools.get(name, None))
313+ command_line = (self.get_user_option('bzr.mergetool.%s' % name,
314+ expand=False)
315+ or mergetools.known_merge_tools.get(name, None))
316 return command_line
317
318
319@@ -672,6 +845,11 @@
320 def __init__(self, file_name):
321 super(LockableConfig, self).__init__(file_name=file_name)
322 self.dir = osutils.dirname(osutils.safe_unicode(self.file_name))
323+ # FIXME: It doesn't matter that we don't provide possible_transports
324+ # below since this is currently used only for local config files ;
325+ # local transports are not shared. But if/when we start using
326+ # LockableConfig for other kind of transports, we will need to reuse
327+ # whatever connection is already established -- vila 20100929
328 self.transport = transport.get_transport(self.dir)
329 self._lock = lockdir.LockDir(self.transport, 'lock')
330
331
332=== modified file 'bzrlib/errors.py'
333--- bzrlib/errors.py 2011-01-26 20:02:52 +0000
334+++ bzrlib/errors.py 2011-02-20 11:14:15 +0000
335@@ -3221,3 +3221,22 @@
336 def __init__(self, branch_url):
337 self.branch_url = branch_url
338
339+
340+# FIXME: I would prefer to define the config related exception classes in
341+# config.py but the lazy import mechanism proscribes this -- vila 20101222
342+class OptionExpansionLoop(BzrError):
343+
344+ _fmt = 'Loop involving %(refs)r while expanding "%(string)s".'
345+
346+ def __init__(self, string, refs):
347+ self.string = string
348+ self.refs = '->'.join(refs)
349+
350+
351+class ExpandingUnknownOption(BzrError):
352+
353+ _fmt = 'Option %(name)s is not defined while expanding "%(string)s".'
354+
355+ def __init__(self, name, string):
356+ self.name = name
357+ self.string = string
358
359=== modified file 'bzrlib/help_topics/en/configuration.txt'
360--- bzrlib/help_topics/en/configuration.txt 2011-01-16 01:12:01 +0000
361+++ bzrlib/help_topics/en/configuration.txt 2011-02-20 11:14:15 +0000
362@@ -258,6 +258,13 @@
363 email = John Doe <jdoe@isp.com>
364 check_signatures = require
365
366+A variable can reference other variables **in the same configuration file** by
367+enclosing them in curly brackets::
368+
369+ my_branch_name = feature_x
370+ my_server = bzr+ssh://example.com
371+ push_location = {my_server}/project/{my_branch_name}
372+
373
374 Variable policies
375 ^^^^^^^^^^^^^^^^^
376
377=== modified file 'bzrlib/tests/__init__.py'
378--- bzrlib/tests/__init__.py 2011-02-11 17:12:35 +0000
379+++ bzrlib/tests/__init__.py 2011-02-20 11:14:15 +0000
380@@ -954,6 +954,10 @@
381 # between tests. We should get rid of this altogether: bug 656694. --
382 # mbp 20101008
383 self.overrideAttr(bzrlib.trace, '_verbosity_level', 0)
384+ # Isolate config option expansion until its default value for bzrlib is
385+ # settled on or a the FIXME associated with _get_expand_default_value
386+ # is addressed -- vila 20110219
387+ self.overrideAttr(config, '_expand_default_value', None)
388
389 def debug(self):
390 # debug a frame up.
391
392=== modified file 'bzrlib/tests/test_config.py'
393--- bzrlib/tests/test_config.py 2011-01-20 04:44:14 +0000
394+++ bzrlib/tests/test_config.py 2011-02-20 11:14:15 +0000
395@@ -514,6 +514,7 @@
396 ' Use IniBasedConfig(_content=xxx) instead.'],
397 conf._get_parser, file=config_file)
398
399+
400 class TestIniConfigSaving(tests.TestCaseInTempDir):
401
402 def test_cant_save_without_a_file_name(self):
403@@ -527,6 +528,198 @@
404 self.assertFileEqual(content, 'test.conf')
405
406
407+class TestIniConfigOptionExpansionDefaultValue(tests.TestCaseInTempDir):
408+ """What is the default value of expand for config options.
409+
410+ This is an opt-in beta feature used to evaluate whether or not option
411+ references can appear in dangerous place raising exceptions, disapearing
412+ (and as such corrupting data) or if it's safe to activate the option by
413+ default.
414+
415+ Note that these tests relies on config._expand_default_value being already
416+ overwritten in the parent class setUp.
417+ """
418+
419+ def setUp(self):
420+ super(TestIniConfigOptionExpansionDefaultValue, self).setUp()
421+ self.config = None
422+ self.warnings = []
423+ def warning(*args):
424+ self.warnings.append(args[0] % args[1:])
425+ self.overrideAttr(trace, 'warning', warning)
426+
427+ def get_config(self, expand):
428+ c = config.GlobalConfig.from_string('bzr.config.expand=%s' % (expand,),
429+ save=True)
430+ return c
431+
432+ def assertExpandIs(self, expected):
433+ actual = config._get_expand_default_value()
434+ #self.config.get_user_option_as_bool('bzr.config.expand')
435+ self.assertEquals(expected, actual)
436+
437+ def test_default_is_None(self):
438+ self.assertEquals(None, config._expand_default_value)
439+
440+ def test_default_is_False_even_if_None(self):
441+ self.config = self.get_config(None)
442+ self.assertExpandIs(False)
443+
444+ def test_default_is_False_even_if_invalid(self):
445+ self.config = self.get_config('<your choice>')
446+ self.assertExpandIs(False)
447+ # ...
448+ # Huh ? My choice is False ? Thanks, always happy to hear that :D
449+ # Wait, you've been warned !
450+ self.assertLength(1, self.warnings)
451+ self.assertEquals(
452+ 'Value "<your choice>" is not a boolean for "bzr.config.expand"',
453+ self.warnings[0])
454+
455+ def test_default_is_True(self):
456+ self.config = self.get_config(True)
457+ self.assertExpandIs(True)
458+
459+ def test_default_is_False(self):
460+ self.config = self.get_config(False)
461+ self.assertExpandIs(False)
462+
463+
464+class TestIniConfigOptionExpansion(tests.TestCase):
465+ """Test option expansion from the IniConfig level.
466+
467+ What we really want here is to test the Config level, but the class being
468+ abstract as far as storing values is concerned, this can't be done
469+ properly (yet).
470+ """
471+ # FIXME: This should be rewritten when all configs share a storage
472+ # implementation -- vila 2011-02-18
473+
474+ def get_config(self, string=None):
475+ if string is None:
476+ string = ''
477+ c = config.IniBasedConfig.from_string(string)
478+ return c
479+
480+ def assertExpansion(self, expected, conf, string, env=None):
481+ self.assertEquals(expected, conf.expand_options(string, env))
482+
483+ def test_no_expansion(self):
484+ c = self.get_config('')
485+ self.assertExpansion('foo', c, 'foo')
486+
487+ def test_env_adding_options(self):
488+ c = self.get_config('')
489+ self.assertExpansion('bar', c, '{foo}', {'foo': 'bar'})
490+
491+ def test_env_overriding_options(self):
492+ c = self.get_config('foo=baz')
493+ self.assertExpansion('bar', c, '{foo}', {'foo': 'bar'})
494+
495+ def test_simple_ref(self):
496+ c = self.get_config('foo=xxx')
497+ self.assertExpansion('xxx', c, '{foo}')
498+
499+ def test_unknown_ref(self):
500+ c = self.get_config('')
501+ self.assertRaises(errors.ExpandingUnknownOption,
502+ c.expand_options, '{foo}')
503+
504+ def test_indirect_ref(self):
505+ c = self.get_config('''
506+foo=xxx
507+bar={foo}
508+''')
509+ self.assertExpansion('xxx', c, '{bar}')
510+
511+ def test_embedded_ref(self):
512+ c = self.get_config('''
513+foo=xxx
514+bar=foo
515+''')
516+ self.assertExpansion('xxx', c, '{{bar}}')
517+
518+ def test_simple_loop(self):
519+ c = self.get_config('foo={foo}')
520+ self.assertRaises(errors.OptionExpansionLoop, c.expand_options, '{foo}')
521+
522+ def test_indirect_loop(self):
523+ c = self.get_config('''
524+foo={bar}
525+bar={baz}
526+baz={foo}''')
527+ e = self.assertRaises(errors.OptionExpansionLoop,
528+ c.expand_options, '{foo}')
529+ self.assertEquals('foo->bar->baz', e.refs)
530+ self.assertEquals('{foo}', e.string)
531+
532+ def test_list(self):
533+ conf = self.get_config('''
534+foo=start
535+bar=middle
536+baz=end
537+list={foo},{bar},{baz}
538+''')
539+ self.assertEquals(['start', 'middle', 'end'],
540+ conf.get_user_option('list', expand=True))
541+
542+ def test_cascading_list(self):
543+ conf = self.get_config('''
544+foo=start,{bar}
545+bar=middle,{baz}
546+baz=end
547+list={foo}
548+''')
549+ self.assertEquals(['start', 'middle', 'end'],
550+ conf.get_user_option('list', expand=True))
551+
552+ def test_pathological_hidden_list(self):
553+ conf = self.get_config('''
554+foo=bin
555+bar=go
556+start={foo
557+middle=},{
558+end=bar}
559+hidden={start}{middle}{end}
560+''')
561+ # Nope, it's either a string or a list, and the list wins as soon as a
562+ # ',' appears, so the string concatenation never occur.
563+ self.assertEquals(['{foo', '}', '{', 'bar}'],
564+ conf.get_user_option('hidden', expand=True))
565+
566+class TestLocationConfigOptionExpansion(tests.TestCaseInTempDir):
567+
568+ def get_config(self, location, string=None):
569+ if string is None:
570+ string = ''
571+ # Since we don't save the config we won't strictly require to inherit
572+ # from TestCaseInTempDir, but an error occurs so quickly...
573+ c = config.LocationConfig.from_string(string, location)
574+ return c
575+
576+ def test_dont_cross_unrelated_section(self):
577+ c = self.get_config('/another/branch/path','''
578+[/one/branch/path]
579+foo = hello
580+bar = {foo}/2
581+
582+[/another/branch/path]
583+bar = {foo}/2
584+''')
585+ self.assertRaises(errors.ExpandingUnknownOption,
586+ c.get_user_option, 'bar', expand=True)
587+
588+ def test_cross_related_sections(self):
589+ c = self.get_config('/project/branch/path','''
590+[/project]
591+foo = qu
592+
593+[/project/branch/path]
594+bar = {foo}ux
595+''')
596+ self.assertEquals('quux', c.get_user_option('bar', expand=True))
597+
598+
599 class TestIniBaseConfigOnDisk(tests.TestCaseInTempDir):
600
601 def test_cannot_reload_without_name(self):
602@@ -985,7 +1178,7 @@
603 conf = self._get_empty_config()
604 cmdline = conf.find_merge_tool('kdiff3')
605 self.assertEquals('kdiff3 {base} {this} {other} -o {result}', cmdline)
606-
607+
608 def test_find_merge_tool_override_known(self):
609 conf = self._get_empty_config()
610 conf.set_user_option('bzr.mergetool.kdiff3', 'kdiff3 blah')
611
612=== modified file 'doc/en/release-notes/bzr-2.4.txt'
613--- doc/en/release-notes/bzr-2.4.txt 2011-02-19 17:37:45 +0000
614+++ doc/en/release-notes/bzr-2.4.txt 2011-02-20 11:14:15 +0000
615@@ -26,6 +26,14 @@
616 * External merge tools can now be configured in bazaar.conf. See
617 ``bzr help configuration`` for more information. (Gordon Tyler, #489915)
618
619+* Configuration options can now use references to other options in the same
620+ file by enclosing them with curly brackets (``{other_opt}``). This makes it
621+ possible to use, for example,
622+ ``push_location=lp:~vila/bzr/config-{nickname}`` in ``branch.conf`` when
623+ using a loom. During the beta period, the default behaviour is to disable
624+ this feature. It can be activated by declaring ``bzr.config.expand = True``
625+ in ``bazaar.conf``. (Vincent Ladeuil)
626+
627 Improvements
628 ************
629
630
631=== modified file 'doc/en/whats-new/whats-new-in-2.4.txt'
632--- doc/en/whats-new/whats-new-in-2.4.txt 2011-02-07 04:29:44 +0000
633+++ doc/en/whats-new/whats-new-in-2.4.txt 2011-02-20 11:14:15 +0000
634@@ -32,6 +32,14 @@
635 revisions from tags will always be present, so that operations like ``bzr
636 log -r tag:foo`` will always work.
637
638+Configuration files
639+*******************
640+
641+Option values can now refer to other options in the same configuration file by
642+enclosing them in curly brackets (``{option}``). This is an opt-in feature
643+during the beta period controlled by the ``bzr.config.expand`` option that
644+should be declared in ``bazaar.conf`` and no other file.
645+
646 Further information
647 *******************
648