Merge lp:~vila/bzr/config-expand-options into lp:bzr
- config-expand-options
- Merge into bzr.dev
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 |
Related bugs: |
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.
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_
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_
I also fixed get_user_
Vincent Ladeuil (vila) wrote : | # |
John A Meinel (jameinel) wrote : | # |
-----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:/
>
> 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.
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_
> 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...
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/
> 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_
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/
> 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...
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_
> 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
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...
Vincent Ladeuil (vila) wrote : | # |
Ping
Any objections to land this now ?
</ticking trigger>
Jelmer Vernooij (jelmer) wrote : | # |
I think this is good to land now.
Vincent Ladeuil (vila) wrote : | # |
sent to pqm by email
Preview Diff
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 |
For the record and context: http:// bazaar. launchpad. net/~bzr- core/bzr/ devnotes/ view/head: /configuration. txt