Merge lp:~vila/bzr/new-config into lp:bzr

Proposed by Vincent Ladeuil
Status: Merged
Merged at revision: 5886
Proposed branch: lp:~vila/bzr/new-config
Merge into: lp:bzr
Diff against target: 1309 lines (+1074/-67)
6 files modified
bzrlib/config.py (+479/-37)
bzrlib/errors.py (+5/-5)
bzrlib/tests/test_config.py (+468/-24)
doc/developers/configuration.txt (+102/-0)
doc/developers/index.txt (+2/-1)
doc/en/user-guide/configuring_bazaar.txt (+18/-0)
To merge this branch: bzr merge lp:~vila/bzr/new-config
Reviewer Review Type Date Requested Status
bzr-core Pending
Review via email: mp+56887@code.launchpad.net

Description of the change

This is not for review, more like a sneak preview.

If you want to look at it, you'd probably want to branch it locally (beware, it's a loom) and look at the contained threads:

   next
  doc-new-config
=>config-concrete-stacks
  config-stack
  config-section-matchers
  config-concrete-stores
  config-abstract-store
  config-section
  refactor-locationconfig-matching
  bzr.dev

Forget about doc-new-config and next.

This provides the building blocks to replace our actual config files, mainly:

- Store objects which are associated with persistent config files (the only implementation so far is based on ConfigObj)

- Stack objects which should replace GlobalConfig, LocationConfig and BranchConfig objects

- some test infrastructure to make it easier to write new config files or add features

- ensure that all config stores use the transport API

What is still missing:

- user and dev docs (in progress)

- option value converters (mainly for boolean, the plan is to add a parameter to Stack.get())

- an implementation mimicking the actual behavior regarding IOs: Stack.get() should force a load of the config file, Stack.set() should force a write of the config file,

With this, we get an implementation that can replace:
 GlobalConfig().get_user_option('blah')
with
 config.GlobalStack().get('blah')
or maybe even
 config.Global.get('blah')
but the later probably needs discussionS

With that, it becomes possible to progressively deploy the new implementation option by option, feature by feature, discussing issues as they are discovered.

I'll file more mps for each thread later.

To post a comment you must log in.
Revision history for this message
Martin Pool (mbp) wrote :

> config.GlobalStack().get('blah')
> or maybe even
> config.Global.get('blah')

I'd like to talk about this; istm if we have the right expression to put in config-using code we will know some aspects of the infrastructure are also right (or can be gradually made right.)

'FooBar()' should almost always construct an object of that class; cases where we've fudged that to actually construct something else cause unclearness. If it might need to do something else (either make a different class, or return an existing object), we should use a factory function.

In these cases we generally want to be reusing existing objects, not making new ones.

If this is a new api maybe it should hang off the library_state object; though there can be a convenient caller interface that doesn't explicitly need to do so.

I guess 'global' here means (eventually): from the command line, environment variables, user config, system-wide config, built in defaults. Perhaps others. Callers ought to be expressing essentially constraints on what values could be relevant, without knowing the policy of how that's matched up.

The more interesting case there is something more restricted than global. I was contemplating perhaps having them say

config_stack_for([branch, working_tree, destination_transport])

where they pass in a list of objects that are relevant to their current scope, and then the config mechanism works out which sections/sources to use from that.

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

> I'd like to talk about this; istm if we have the right expression to put in config-using code we will know some aspects of the infrastructure are also right (or can be gradually made right.)

+1

My plan is to make small proposals on top of this one to try various approaches.

Things like:
- adding helpers to define Option with policies (boolean or registry values)
- new SectionMatchers (I find the current LocationMatcher hard to understand for users in several edge cases, istm that real PathMatcher or GlobMatcher would be clearer)
- the new locking scheme described in the devnotes

> 'FooBar()' should almost always construct an object of that class; cases where we've fudged that to actually construct something else cause unclearness. If it might need to do something else (either make a different class, or return an existing object), we should use a factory function.

I'm not sure what you're targeting here, but yeah, some registries are still missing in the proposed design (because they weren't required to reach an ~equivalent implementation).

> In these cases we generally want to be reusing existing objects, not making new ones.

If by that you mean making sure we don't have to create a ConfigStack each time we want to get an option value, I fully agree.

> If this is a new api maybe it should hang off the library_state object; though there can be a convenient caller interface that doesn't explicitly need to do so.

For GlobalStack and LocationStack, that's indeed the plan.

Also note that it should be possible to add some sort of cloning facility at this level without needing to re-read any config file: say you have a LocationStack and you want one but for a different location.

> I guess 'global' here means (eventually): from the command line, environment variables, user config, system-wide config, built in defaults. Perhaps others. Callers ought to be expressing essentially constraints on what values could be relevant, without knowing the policy of how that's matched up.

Roughly yes. Since os.environ and command-line options are *not* supported today, the proposal didn't mention them.

But the idea is that they should be added at the stack level (just look at the concrete stacks there, adding them is just adding the right dict (yes, the python object) into the stack.sections list at the right place.

> Making it easy to share and reuse these facilities when creating different stacks is not implemented here but shouldn't be hard either.

> The more interesting case there is something more restricted than global. I was contemplating perhaps having them say

> config_stack_for([branch, working_tree, destination_transport])

> where they pass in a list of objects that are relevant to their current scope, and then the config mechanism works out which sections/sources to use from that.

Yes, that's the idea, I don't know yet how this will be implemented but some patterns are already known and mentioned in the devnotes, the relevant ones should naturally emerge as we deploy.

Revision history for this message
Martin Pool (mbp) wrote :
Download full text (3.9 KiB)

My point about using a camelcaps name in the api is

1- the expression 'GlobalConfig()' should construct a new GlobalConfig object
2- we don't want to build new objects every time someone wants to look
up a value
therefore
3- the expression to look up a value should not include something that
looks like object construction

you also mention

> config.Global.get('blah')

which looks like access to a class method, and I think we don't want
that either, because it won't scale when we need access to a
particular instance of a type of configuration.

That's why I come back to saying there should be something like a
function that gives you the stack relevant to particular scopes.

Martin

On 8 April 2011 18:12, Vincent Ladeuil <email address hidden> wrote:
>> I'd like to talk about this; istm if we have the right expression to put in config-using code we will know some aspects of the infrastructure are also right (or can be gradually made right.)
>
> +1
>
> My plan is to make small proposals on top of this one to try various approaches.
>
> Things like:
> - adding helpers to define Option with policies (boolean or registry values)
> - new SectionMatchers (I find the current LocationMatcher hard to understand for users in several edge cases, istm that real PathMatcher or GlobMatcher would be clearer)
> - the new locking scheme described in the devnotes
>
>
>> 'FooBar()' should almost always construct an object of that class; cases where we've fudged that to actually construct something else cause unclearness.  If it might need to do something else (either make a different class, or return an existing object), we should use a factory function.
>
> I'm not sure what you're targeting here, but yeah, some registries are still missing in the proposed design (because they weren't required to reach an ~equivalent implementation).
>
>> In these cases we generally want to be reusing existing objects, not making new ones.
>
> If by that you mean making sure we don't have to create a ConfigStack each time we want to get an option value, I fully agree.
>
>> If this is a new api maybe it should hang off the library_state object; though there can be a convenient caller interface that doesn't explicitly need to do so.
>
> For GlobalStack and LocationStack, that's indeed the plan.
>
> Also note that it should be possible to add some sort of cloning facility at this level without needing to re-read any config file: say you have a LocationStack and you want one but for a different location.
>
>> I guess 'global' here means (eventually): from the command line, environment variables, user config, system-wide config, built in defaults.  Perhaps others.  Callers ought to be expressing essentially constraints on what values could be relevant, without knowing the policy of how that's matched up.
>
> Roughly yes. Since os.environ and command-line options are *not* supported today, the proposal didn't mention them.
>
> But the idea is that they should be added at the stack level (just look at the concrete stacks there, adding them is just adding the right dict (yes, the python object) into the stack.sections list at the right place.
>
>> Making it easy to share and reuse ...

Read more...

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

> My point about using a camelcaps name in the api is
>
> 1- the expression 'GlobalConfig()' should construct a new GlobalConfig object
> 2- we don't want to build new objects every time someone wants to look
> up a value
> therefore
> 3- the expression to look up a value should not include something that
> looks like object construction
>
> you also mention
>
> > config.Global.get('blah')
>

Full agreement there. What I was confusingly trying to express was that either we let people build their own objects (even for GlobalStack) or we end up being able to control that there only one GLobalStack, in which case we may achieve an even simpler syntax. We'll see.

Revision history for this message
Martin Pool (mbp) wrote :

I've read all the individual branches, and though there are a few questions in detail, I think the overall shape looks pretty nice.

I am finishing off Sergei's rules patch, and in the context of that I was wondering whether we should really have a major structural separation between per-file rules and the other configuration schemes. They are all just name-value pairs. The query to get the value for a particular file in a particular well could equally well go through a config-type api, and also read the general configuration stuff as well as per-file rules. (Indeed it can do either of them but I think quite reasonably it could do both.)

So for instance if you wanted to set eol=native just globally or in locations.conf you could do that.

Obviously not all settings make sense to read per-file. Also, as we will eventually read some settings from the tree contents, not all of them are safe to read from the untrusted content in there. But that should be pretty easy to do, just by constructing the right stack. We can perhaps also constrain this when we later get a registry of available options: mark some of them as per-file and some as safe. (eol conversion is safe; sending email or running external commands is not safe.)

We would have to think carefully about the performance considerations because we couldn't afford to construct a new section or stack for every file we want to find out about. But if there is a get_tree_file_config_stack(...) then perhaps that can be smart about reusing existing objects.

lp:~vila/bzr/new-config updated
5472. By Vincent Ladeuil

Merge doc-new-config into next

5473. By Vincent Ladeuil

Merge doc-new-config into next

5474. By Vincent Ladeuil

Merge doc-new-config into next

5475. By Vincent Ladeuil

Merge doc-new-config into next

5476. By Vincent Ladeuil

Merge config-editor-option into next

5477. By Vincent Ladeuil

Mention that the config lazy loading is defeated.

5478. By Vincent Ladeuil

Merge config-editor-option into next

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'bzrlib/config.py'
2--- bzrlib/config.py 2011-04-05 14:47:26 +0000
3+++ bzrlib/config.py 2011-04-12 12:36:46 +0000
4@@ -848,7 +848,7 @@
5 # LockableConfig for other kind of transports, we will need to reuse
6 # whatever connection is already established -- vila 20100929
7 self.transport = transport.get_transport(self.dir)
8- self._lock = lockdir.LockDir(self.transport, 'lock')
9+ self._lock = lockdir.LockDir(self.transport, self.lock_name)
10
11 def _create_from_string(self, unicode_bytes, save):
12 super(LockableConfig, self)._create_from_string(unicode_bytes, False)
13@@ -967,6 +967,61 @@
14 super(LockableConfig, self).remove_user_option(option_name,
15 section_name)
16
17+def _iter_for_location_by_parts(sections, location):
18+ """Keep only the sessions matching the specified location.
19+
20+ :param sections: An iterable of section names.
21+
22+ :param location: An url or a local path to match against.
23+
24+ :returns: An iterator of (section, extra_path, nb_parts) where nb is the
25+ number of path components in the section name, section is the section
26+ name and extra_path is the difference between location and the section
27+ name.
28+ """
29+ location_parts = location.rstrip('/').split('/')
30+
31+ for section in sections:
32+ # location is a local path if possible, so we need
33+ # to convert 'file://' urls to local paths if necessary.
34+
35+ # FIXME: I don't think the above comment is still up to date,
36+ # LocationConfig is always instantiated with an url -- vila 2011-04-07
37+
38+ # This also avoids having file:///path be a more exact
39+ # match than '/path'.
40+
41+ # FIXME: Not sure about the above either, but since the path components
42+ # are compared in sync, adding two empty components (//) is likely to
43+ # trick the comparison and also trick the check on the number of
44+ # components, so we *should* take only the relevant part of the url. On
45+ # the other hand, this means 'file://' urls *can't* be used in sections
46+ # so more work is probably needed -- vila 2011-04-07
47+
48+ if section.startswith('file://'):
49+ section_path = urlutils.local_path_from_url(section)
50+ else:
51+ section_path = section
52+ section_parts = section_path.rstrip('/').split('/')
53+
54+ matched = True
55+ if len(section_parts) > len(location_parts):
56+ # More path components in the section, they can't match
57+ matched = False
58+ else:
59+ # Rely on zip truncating in length to the length of the shortest
60+ # argument sequence.
61+ names = zip(location_parts, section_parts)
62+ for name in names:
63+ if not fnmatch.fnmatch(name[0], name[1]):
64+ matched = False
65+ break
66+ if not matched:
67+ continue
68+ # build the path difference between the section and the location
69+ extra_path = '/'.join(location_parts[len(section_parts):])
70+ yield section, extra_path, len(section_parts)
71+
72
73 class LocationConfig(LockableConfig):
74 """A configuration object that gives the policy for a location."""
75@@ -1001,49 +1056,20 @@
76
77 def _get_matching_sections(self):
78 """Return an ordered list of section names matching this location."""
79- sections = self._get_parser()
80- location_names = self.location.split('/')
81- if self.location.endswith('/'):
82- del location_names[-1]
83- matches=[]
84- for section in sections:
85- # location is a local path if possible, so we need
86- # to convert 'file://' urls to local paths if necessary.
87- # This also avoids having file:///path be a more exact
88- # match than '/path'.
89- if section.startswith('file://'):
90- section_path = urlutils.local_path_from_url(section)
91- else:
92- section_path = section
93- section_names = section_path.split('/')
94- if section.endswith('/'):
95- del section_names[-1]
96- names = zip(location_names, section_names)
97- matched = True
98- for name in names:
99- if not fnmatch.fnmatch(name[0], name[1]):
100- matched = False
101- break
102- if not matched:
103- continue
104- # so, for the common prefix they matched.
105- # if section is longer, no match.
106- if len(section_names) > len(location_names):
107- continue
108- matches.append((len(section_names), section,
109- '/'.join(location_names[len(section_names):])))
110+ matches = list(_iter_for_location_by_parts(self._get_parser(),
111+ self.location))
112 # put the longest (aka more specific) locations first
113- matches.sort(reverse=True)
114- sections = []
115- for (length, section, extra_path) in matches:
116- sections.append((section, extra_path))
117+ matches.sort(
118+ key=lambda (section, extra_path, length): (length, section),
119+ reverse=True)
120+ for (section, extra_path, length) in matches:
121+ yield section, extra_path
122 # should we stop looking for parent configs here?
123 try:
124 if self._get_parser()[section].as_bool('ignore_parents'):
125 break
126 except KeyError:
127 pass
128- return sections
129
130 def _get_sections(self, name=None):
131 """See IniBasedConfig._get_sections()."""
132@@ -2068,6 +2094,422 @@
133 self._transport.put_file(self._filename, out_file)
134
135
136+class ReadOnlySection(object):
137+ """A section defines a dict of options.
138+
139+ This is merely a read-only dict which can add some knowledge about the
140+ options.
141+ """
142+
143+ def __init__(self, section_id, options):
144+ self.id = section_id
145+ # We re-use the dict-like object received
146+ self.options = options
147+
148+ def get(self, name, default=None):
149+ return self.options.get(name, default)
150+
151+
152+_NewlyCreatedOption = object()
153+"""Was the option created during the MutableSection lifetime"""
154+
155+
156+class MutableSection(ReadOnlySection):
157+ """A section allowing changes and keeping track of the original values."""
158+
159+ def __init__(self, section_id, options):
160+ super(MutableSection, self).__init__(section_id, options)
161+ self.orig = {}
162+
163+ def set(self, name, value):
164+ if name not in self.options:
165+ # This is a new option
166+ self.orig[name] = _NewlyCreatedOption
167+ elif name not in self.orig:
168+ self.orig[name] = self.get(name, None)
169+ self.options[name] = value
170+
171+ def remove(self, name):
172+ if name not in self.orig:
173+ self.orig[name] = self.get(name, None)
174+ del self.options[name]
175+
176+
177+class Store(object):
178+ """Abstract interface to persistent storage for configuration options."""
179+
180+ readonly_section_class = ReadOnlySection
181+ mutable_section_class = MutableSection
182+
183+ @property
184+ def loaded(self):
185+ raise NotImplementedError(self.loaded)
186+
187+ def load(self):
188+ raise NotImplementedError(self.load)
189+
190+ def _load_from_string(self, str_or_unicode):
191+ """Create a store from a string in configobj syntax.
192+
193+ :param str_or_unicode: A string representing the file content. This will
194+ be encoded to suit store needs internally.
195+
196+ This is for tests and should not be used in production unless a
197+ convincing use case can be demonstrated :)
198+ """
199+ raise NotImplementedError(self._load_from_string)
200+
201+ def save(self):
202+ raise NotImplementedError(self.save)
203+
204+ def external_url(self):
205+ raise NotImplementedError(self.external_url)
206+
207+ def get_sections(self):
208+ """Returns an ordered iterable of existing sections.
209+
210+ :returns: An iterable of (name, dict).
211+ """
212+ raise NotImplementedError(self.get_sections)
213+
214+ def get_mutable_section(self, section_name=None):
215+ """Returns the specified mutable section.
216+
217+ :param section_name: The section identifier
218+ """
219+ raise NotImplementedError(self.get_mutable_section)
220+
221+
222+class ConfigObjStore(Store):
223+
224+ def __init__(self, transport, file_name):
225+ """A config Store using ConfigObj for storage.
226+
227+ :param transport: The transport object where the config file is located.
228+
229+ :param file_name: The config file basename in the transport directory.
230+ """
231+ super(ConfigObjStore, self).__init__()
232+ self.transport = transport
233+ self.file_name = file_name
234+ self._config_obj = None
235+
236+ @property
237+ def loaded(self):
238+ return self._config_obj != None
239+
240+ def load(self):
241+ """Load the store from the associated file."""
242+ if self.loaded:
243+ return
244+ content = self.transport.get_bytes(self.file_name)
245+ self._load_from_string(content)
246+
247+ def _load_from_string(self, str_or_unicode):
248+ """Create a config store from a string.
249+
250+ :param str_or_unicode: A string representing the file content. This will
251+ be utf-8 encoded internally.
252+
253+ This is for tests and should not be used in production unless a
254+ convincing use case can be demonstrated :)
255+ """
256+ if self.loaded:
257+ raise AssertionError('Already loaded: %r' % (self._config_obj,))
258+ co_input = StringIO(str_or_unicode.encode('utf-8'))
259+ try:
260+ # The config files are always stored utf8-encoded
261+ self._config_obj = ConfigObj(co_input, encoding='utf-8')
262+ except configobj.ConfigObjError, e:
263+ self._config_obj = None
264+ raise errors.ParseConfigError(e.errors, self.external_url())
265+
266+ def save(self):
267+ if not self.loaded:
268+ # Nothing to save
269+ return
270+ out = StringIO()
271+ self._config_obj.write(out)
272+ self.transport.put_bytes(self.file_name, out.getvalue())
273+
274+ def external_url(self):
275+ # FIXME: external_url should really accepts an optional relpath
276+ # parameter (bug #750169) :-/ -- vila 2011-04-04
277+ # The following will do in the interim but maybe we don't want to
278+ # expose a path here but rather a config ID and its associated
279+ # object </hand wawe>.
280+ return os.path.join(self.transport.external_url(), self.file_name)
281+
282+ def get_sections(self):
283+ """Get the configobj section in the file order.
284+
285+ :returns: An iterable of (name, dict).
286+ """
287+ # We need a loaded store
288+ self.load()
289+ cobj = self._config_obj
290+ if cobj.scalars:
291+ yield self.readonly_section_class(None, cobj)
292+ for section_name in cobj.sections:
293+ yield self.readonly_section_class(section_name, cobj[section_name])
294+
295+ def get_mutable_section(self, section_name=None):
296+ # We need a loaded store
297+ try:
298+ self.load()
299+ except errors.NoSuchFile:
300+ # The file doesn't exist, let's pretend it was empty
301+ self._load_from_string('')
302+ if section_name is None:
303+ section = self._config_obj
304+ else:
305+ section = self._config_obj.setdefault(section_name, {})
306+ return self.mutable_section_class(section_name, section)
307+
308+
309+# Note that LockableConfigObjStore inherits from ConfigObjStore because we need
310+# unlockable stores for use with objects that can already ensure the locking
311+# (think branches). If different stores (not based on ConfigObj) are created,
312+# they may face the same issue.
313+
314+
315+class LockableConfigObjStore(ConfigObjStore):
316+ """A ConfigObjStore using locks on save to ensure store integrity."""
317+
318+ def __init__(self, transport, file_name, lock_dir_name=None):
319+ """A config Store using ConfigObj for storage.
320+
321+ :param transport: The transport object where the config file is located.
322+
323+ :param file_name: The config file basename in the transport directory.
324+ """
325+ if lock_dir_name is None:
326+ lock_dir_name = 'lock'
327+ self.lock_dir_name = lock_dir_name
328+ super(LockableConfigObjStore, self).__init__(transport, file_name)
329+ self._lock = lockdir.LockDir(self.transport, self.lock_dir_name)
330+
331+ def lock_write(self, token=None):
332+ """Takes a write lock in the directory containing the config file.
333+
334+ If the directory doesn't exist it is created.
335+ """
336+ # FIXME: This doesn't check the ownership of the created directories as
337+ # ensure_config_dir_exists does. It should if the transport is local
338+ # -- vila 2011-04-06
339+ self.transport.create_prefix()
340+ return self._lock.lock_write(token)
341+
342+ def unlock(self):
343+ self._lock.unlock()
344+
345+ def break_lock(self):
346+ self._lock.break_lock()
347+
348+ @needs_write_lock
349+ def save(self):
350+ super(LockableConfigObjStore, self).save()
351+
352+
353+# FIXME: global, bazaar, shouldn't that be 'user' instead or even
354+# 'user_defaults' as opposed to 'user_overrides', 'system_defaults'
355+# (/etc/bzr/bazaar.conf) and 'system_overrides' ? -- vila 2011-04-05
356+class GlobalStore(LockableConfigObjStore):
357+
358+ def __init__(self, possible_transports=None):
359+ t = transport.get_transport(config_dir(),
360+ possible_transports=possible_transports)
361+ super(GlobalStore, self).__init__(t, 'bazaar.conf')
362+
363+
364+class LocationStore(LockableConfigObjStore):
365+
366+ def __init__(self, possible_transports=None):
367+ t = transport.get_transport(config_dir(),
368+ possible_transports=possible_transports)
369+ super(LocationStore, self).__init__(t, 'locations.conf')
370+
371+
372+class BranchStore(ConfigObjStore):
373+
374+ def __init__(self, branch):
375+ super(BranchStore, self).__init__(branch.control_transport,
376+ 'branch.conf')
377+
378+class SectionMatcher(object):
379+ """Select sections into a given Store.
380+
381+ This intended to be used to postpone getting an iterable of sections from a
382+ store.
383+ """
384+
385+ def __init__(self, store):
386+ self.store = store
387+
388+ def get_sections(self):
389+ # This is where we require loading the store so we can see all defined
390+ # sections.
391+ sections = self.store.get_sections()
392+ # Walk the revisions in the order provided
393+ for s in sections:
394+ if self.match(s):
395+ yield s
396+
397+ def match(self, secion):
398+ raise NotImplementedError(self.match)
399+
400+
401+class LocationSection(ReadOnlySection):
402+
403+ def __init__(self, section, length, extra_path):
404+ super(LocationSection, self).__init__(section.id, section.options)
405+ self.length = length
406+ self.extra_path = extra_path
407+
408+ def get(self, name, default=None):
409+ value = super(LocationSection, self).get(name, default)
410+ if value is not None:
411+ policy_name = self.get(name + ':policy', None)
412+ policy = _policy_value.get(policy_name, POLICY_NONE)
413+ if policy == POLICY_APPENDPATH:
414+ value = urlutils.join(value, self.extra_path)
415+ return value
416+
417+
418+class LocationMatcher(SectionMatcher):
419+
420+ def __init__(self, store, location):
421+ super(LocationMatcher, self).__init__(store)
422+ self.location = location
423+
424+ def get_sections(self):
425+ # Override the default implementation as we want to change the order
426+
427+ # The following is a bit hackish but ensures compatibility with
428+ # LocationConfig by reusing the same code
429+ sections = list(self.store.get_sections())
430+ filtered_sections = _iter_for_location_by_parts(
431+ [s.id for s in sections], self.location)
432+ iter_sections = iter(sections)
433+ matching_sections = []
434+ for section_id, extra_path, length in filtered_sections:
435+ # a section id is unique for a given store so it's safe to iterate
436+ # again
437+ section = iter_sections.next()
438+ if section_id == section.id:
439+ matching_sections.append(
440+ LocationSection(section, length, extra_path))
441+ # We want the longest (aka more specific) locations first
442+ sections = sorted(matching_sections,
443+ key=lambda section: (section.length, section.id),
444+ reverse=True)
445+ # Sections mentioning 'ignore_parents' restrict the selection
446+ for section in sections:
447+ # FIXME: We really want to use as_bool below -- vila 2011-04-07
448+ ignore = section.get('ignore_parents', None)
449+ if ignore is not None:
450+ ignore = ui.bool_from_string(ignore)
451+ if ignore:
452+ break
453+ # Finally, we have a valid section
454+ yield section
455+
456+
457+class ConfigStack(object):
458+ """A stack of configurations where an option can be defined"""
459+
460+ def __init__(self, sections=None, get_mutable_section=None):
461+ """Creates a stack of sections with an optional store for changes.
462+
463+ :param sections: A list of ReadOnlySection or callables that returns an
464+ iterable of ReadOnlySection.
465+
466+ :param get_mutable_section: A callable that returns a MutableSection
467+ where changes are recorded.
468+ """
469+ if sections is None:
470+ sections = []
471+ self.sections = sections
472+ self.get_mutable_section = get_mutable_section
473+
474+ def get(self, name):
475+ """Return the *first* option value found in the sections.
476+
477+ This is where we guarantee that sections coming from Store are loaded
478+ lazily: the loading is delayed until we need to either check that an
479+ option exists or get its value, which in turn may require to discover
480+ in which sections it can be defined. Both of these (section and option
481+ existence) require loading the store (even partially).
482+ """
483+ # Ensuring lazy loading is achieved by delaying section matching until
484+ # it can be avoided anymore by using callables to describe (possibly
485+ # empty) section lists.
486+ for section_or_callable in self.sections:
487+ # Each section can expand to multiple ones when a callable is used
488+ if callable(section_or_callable):
489+ sections = section_or_callable()
490+ else:
491+ sections = [section_or_callable]
492+ for section in sections:
493+ value = section.get(name)
494+ if value is not None:
495+ return value
496+ # No definition was found
497+ return None
498+
499+ def set(self, name, value):
500+ """Set a new value for the option.
501+
502+ This is where we guarantee that the mutable section is lazily loaded:
503+ this means we won't load the corresponding store before setting a value.
504+ """
505+ section = self.get_mutable_section()
506+ section.set(name, value)
507+
508+ def remove(self, name):
509+ """Remove an existing option.
510+
511+ This is where we guarantee that the mutable section is lazily loaded:
512+ this means we won't load the correspoding store before trying to delete
513+ an option. In practice the store will often be loaded but this allows
514+ catching some programming errors.
515+ """
516+ section = self.get_mutable_section()
517+ section.remove(name)
518+
519+
520+class GlobalStack(ConfigStack):
521+
522+ def __init__(self):
523+ # Get a GlobalStore
524+ gstore = GlobalStore()
525+ super(GlobalStack, self).__init__([gstore.get_sections],
526+ gstore.get_mutable_section)
527+
528+
529+class LocationStack(ConfigStack):
530+
531+ def __init__(self, location):
532+ lstore = LocationStore()
533+ matcher = LocationMatcher(lstore, location)
534+ gstore = GlobalStore()
535+ super(LocationStack, self).__init__(
536+ [matcher.get_sections, gstore.get_sections],
537+ lstore.get_mutable_section)
538+
539+
540+class BranchStack(ConfigStack):
541+
542+ def __init__(self, branch):
543+ bstore = BranchStore(branch)
544+ lstore = LocationStore()
545+ matcher = LocationMatcher(lstore, branch.base)
546+ gstore = GlobalStore()
547+ super(BranchStack, self).__init__(
548+ [matcher.get_sections, bstore.get_sections, gstore.get_sections],
549+ bstore.get_mutable_section)
550+
551+
552 class cmd_config(commands.Command):
553 __doc__ = """Display, set or remove a configuration option.
554
555
556=== modified file 'bzrlib/errors.py'
557--- bzrlib/errors.py 2011-03-12 23:58:55 +0000
558+++ bzrlib/errors.py 2011-04-12 12:36:46 +0000
559@@ -1766,12 +1766,12 @@
560
561 class ParseConfigError(BzrError):
562
563+ _fmt = "Error(s) parsing config file %(filename)s:\n%(errors)s"
564+
565 def __init__(self, errors, filename):
566- if filename is None:
567- filename = ""
568- message = "Error(s) parsing config file %s:\n%s" % \
569- (filename, ('\n'.join(e.msg for e in errors)))
570- BzrError.__init__(self, message)
571+ BzrError.__init__(self)
572+ self.filename = filename
573+ self.errors = '\n'.join(e.msg for e in errors)
574
575
576 class NoEmailInUsername(BzrError):
577
578=== modified file 'bzrlib/tests/test_config.py'
579--- bzrlib/tests/test_config.py 2011-04-05 12:11:26 +0000
580+++ bzrlib/tests/test_config.py 2011-04-12 12:36:46 +0000
581@@ -36,6 +36,7 @@
582 mergetools,
583 ui,
584 urlutils,
585+ registry,
586 tests,
587 trace,
588 transport,
589@@ -62,6 +63,23 @@
590
591 load_tests = scenarios.load_tests_apply_scenarios
592
593+# We need adpaters that can build a config store in a test context. Test
594+# classes, based on TestCaseWithTransport, can use the registry to parametrize
595+# themselves. The builder will receive a test instance and should return a
596+# ready-to-use store. Plugins that defines new stores can also register
597+# themselves here to be tested against the tests defined below.
598+test_store_builder_registry = registry.Registry()
599+test_store_builder_registry.register(
600+ 'configobj', lambda test: config.ConfigObjStore(test.get_transport(),
601+ 'configobj.conf'))
602+test_store_builder_registry.register(
603+ 'bazaar', lambda test: config.GlobalStore())
604+test_store_builder_registry.register(
605+ 'location', lambda test: config.LocationStore())
606+test_store_builder_registry.register(
607+ 'branch', lambda test: config.BranchStore(test.branch))
608+
609+
610
611 sample_long_alias="log -r-15..-1 --line"
612 sample_config_text = u"""
613@@ -832,7 +850,7 @@
614 def c1_write_config_file():
615 before_writing.set()
616 c1_orig()
617- # The lock is held we wait for the main thread to decide when to
618+ # The lock is held. We wait for the main thread to decide when to
619 # continue
620 after_writing.wait()
621 c1._write_config_file = c1_write_config_file
622@@ -865,7 +883,7 @@
623 c1_orig = c1._write_config_file
624 def c1_write_config_file():
625 ready_to_write.set()
626- # The lock is held we wait for the main thread to decide when to
627+ # The lock is held. We wait for the main thread to decide when to
628 # continue
629 do_writing.wait()
630 c1_orig()
631@@ -1264,55 +1282,52 @@
632 self.failUnless(isinstance(global_config, config.GlobalConfig))
633 self.failUnless(global_config is my_config._get_global_config())
634
635+ def assertLocationMatching(self, expected):
636+ self.assertEqual(expected,
637+ list(self.my_location_config._get_matching_sections()))
638+
639 def test__get_matching_sections_no_match(self):
640 self.get_branch_config('/')
641- self.assertEqual([], self.my_location_config._get_matching_sections())
642+ self.assertLocationMatching([])
643
644 def test__get_matching_sections_exact(self):
645 self.get_branch_config('http://www.example.com')
646- self.assertEqual([('http://www.example.com', '')],
647- self.my_location_config._get_matching_sections())
648+ self.assertLocationMatching([('http://www.example.com', '')])
649
650 def test__get_matching_sections_suffix_does_not(self):
651 self.get_branch_config('http://www.example.com-com')
652- self.assertEqual([], self.my_location_config._get_matching_sections())
653+ self.assertLocationMatching([])
654
655 def test__get_matching_sections_subdir_recursive(self):
656 self.get_branch_config('http://www.example.com/com')
657- self.assertEqual([('http://www.example.com', 'com')],
658- self.my_location_config._get_matching_sections())
659+ self.assertLocationMatching([('http://www.example.com', 'com')])
660
661 def test__get_matching_sections_ignoreparent(self):
662 self.get_branch_config('http://www.example.com/ignoreparent')
663- self.assertEqual([('http://www.example.com/ignoreparent', '')],
664- self.my_location_config._get_matching_sections())
665+ self.assertLocationMatching([('http://www.example.com/ignoreparent',
666+ '')])
667
668 def test__get_matching_sections_ignoreparent_subdir(self):
669 self.get_branch_config(
670 'http://www.example.com/ignoreparent/childbranch')
671- self.assertEqual([('http://www.example.com/ignoreparent',
672- 'childbranch')],
673- self.my_location_config._get_matching_sections())
674+ self.assertLocationMatching([('http://www.example.com/ignoreparent',
675+ 'childbranch')])
676
677 def test__get_matching_sections_subdir_trailing_slash(self):
678 self.get_branch_config('/b')
679- self.assertEqual([('/b/', '')],
680- self.my_location_config._get_matching_sections())
681+ self.assertLocationMatching([('/b/', '')])
682
683 def test__get_matching_sections_subdir_child(self):
684 self.get_branch_config('/a/foo')
685- self.assertEqual([('/a/*', ''), ('/a/', 'foo')],
686- self.my_location_config._get_matching_sections())
687+ self.assertLocationMatching([('/a/*', ''), ('/a/', 'foo')])
688
689 def test__get_matching_sections_subdir_child_child(self):
690 self.get_branch_config('/a/foo/bar')
691- self.assertEqual([('/a/*', 'bar'), ('/a/', 'foo/bar')],
692- self.my_location_config._get_matching_sections())
693+ self.assertLocationMatching([('/a/*', 'bar'), ('/a/', 'foo/bar')])
694
695 def test__get_matching_sections_trailing_slash_with_children(self):
696 self.get_branch_config('/a/')
697- self.assertEqual([('/a/', '')],
698- self.my_location_config._get_matching_sections())
699+ self.assertLocationMatching([('/a/', '')])
700
701 def test__get_matching_sections_explicit_over_glob(self):
702 # XXX: 2006-09-08 jamesh
703@@ -1320,8 +1335,7 @@
704 # was a config section for '/a/?', it would get precedence
705 # over '/a/c'.
706 self.get_branch_config('/a/c')
707- self.assertEqual([('/a/c', ''), ('/a/*', ''), ('/a/', 'c')],
708- self.my_location_config._get_matching_sections())
709+ self.assertLocationMatching([('/a/c', ''), ('/a/*', ''), ('/a/', 'c')])
710
711 def test__get_option_policy_normal(self):
712 self.get_branch_config('http://www.example.com')
713@@ -1818,13 +1832,443 @@
714 self.assertIs(None, bzrdir_config.get_default_stack_on())
715
716
717+class TestConfigReadOnlySection(tests.TestCase):
718+
719+ # FIXME: Parametrize so that all sections produced by Stores run these
720+ # tests -- vila 2011-04-01
721+
722+ def test_get_a_value(self):
723+ a_dict = dict(foo='bar')
724+ section = config.ReadOnlySection('myID', a_dict)
725+ self.assertEquals('bar', section.get('foo'))
726+
727+ def test_get_unkown_option(self):
728+ a_dict = dict()
729+ section = config.ReadOnlySection(None, a_dict)
730+ self.assertEquals('out of thin air',
731+ section.get('foo', 'out of thin air'))
732+
733+ def test_options_is_shared(self):
734+ a_dict = dict()
735+ section = config.ReadOnlySection(None, a_dict)
736+ self.assertIs(a_dict, section.options)
737+
738+
739+class TestConfigMutableSection(tests.TestCase):
740+
741+ # FIXME: Parametrize so that all sections (including os.environ and the
742+ # ones produced by Stores) run these tests -- vila 2011-04-01
743+
744+ def test_set(self):
745+ a_dict = dict(foo='bar')
746+ section = config.MutableSection('myID', a_dict)
747+ section.set('foo', 'new_value')
748+ self.assertEquals('new_value', section.get('foo'))
749+ # The change appears in the shared section
750+ self.assertEquals('new_value', a_dict.get('foo'))
751+ # We keep track of the change
752+ self.assertTrue('foo' in section.orig)
753+ self.assertEquals('bar', section.orig.get('foo'))
754+
755+ def test_set_preserve_original_once(self):
756+ a_dict = dict(foo='bar')
757+ section = config.MutableSection('myID', a_dict)
758+ section.set('foo', 'first_value')
759+ section.set('foo', 'second_value')
760+ # We keep track of the original value
761+ self.assertTrue('foo' in section.orig)
762+ self.assertEquals('bar', section.orig.get('foo'))
763+
764+ def test_remove(self):
765+ a_dict = dict(foo='bar')
766+ section = config.MutableSection('myID', a_dict)
767+ section.remove('foo')
768+ # We get None for unknown options via the default value
769+ self.assertEquals(None, section.get('foo'))
770+ # Or we just get the default value
771+ self.assertEquals('unknown', section.get('foo', 'unknown'))
772+ self.assertFalse('foo' in section.options)
773+ # We keep track of the deletion
774+ self.assertTrue('foo' in section.orig)
775+ self.assertEquals('bar', section.orig.get('foo'))
776+
777+ def test_remove_new_option(self):
778+ a_dict = dict()
779+ section = config.MutableSection('myID', a_dict)
780+ section.set('foo', 'bar')
781+ section.remove('foo')
782+ self.assertFalse('foo' in section.options)
783+ # The option didn't exist initially so it we need to keep track of it
784+ # with a special value
785+ self.assertTrue('foo' in section.orig)
786+ self.assertEquals(config._NewlyCreatedOption, section.orig['foo'])
787+
788+
789+class TestStore(tests.TestCaseWithTransport):
790+
791+ def assertSectionContent(self, expected, section):
792+ """Assert that some options have the proper values in a section."""
793+ expected_name, expected_options = expected
794+ self.assertEquals(expected_name, section.id)
795+ self.assertEquals(
796+ expected_options,
797+ dict([(k, section.get(k)) for k in expected_options.keys()]))
798+
799+
800+class TestReadonlyStore(TestStore):
801+
802+ scenarios = [(key, {'get_store': builder})
803+ for key, builder in test_store_builder_registry.iteritems()]
804+
805+ def setUp(self):
806+ super(TestReadonlyStore, self).setUp()
807+ self.branch = self.make_branch('branch')
808+
809+ def test_building_delays_load(self):
810+ store = self.get_store(self)
811+ self.assertEquals(False, store.loaded)
812+ store._load_from_string('')
813+ self.assertEquals(True, store.loaded)
814+
815+ def test_get_no_sections_for_empty(self):
816+ store = self.get_store(self)
817+ store._load_from_string('')
818+ self.assertEquals([], list(store.get_sections()))
819+
820+ def test_get_default_section(self):
821+ store = self.get_store(self)
822+ store._load_from_string('foo=bar')
823+ sections = list(store.get_sections())
824+ self.assertLength(1, sections)
825+ self.assertSectionContent((None, {'foo': 'bar'}), sections[0])
826+
827+ def test_get_named_section(self):
828+ store = self.get_store(self)
829+ store._load_from_string('[baz]\nfoo=bar')
830+ sections = list(store.get_sections())
831+ self.assertLength(1, sections)
832+ self.assertSectionContent(('baz', {'foo': 'bar'}), sections[0])
833+
834+ def test_load_from_string_fails_for_non_empty_store(self):
835+ store = self.get_store(self)
836+ store._load_from_string('foo=bar')
837+ self.assertRaises(AssertionError, store._load_from_string, 'bar=baz')
838+
839+
840+class TestMutableStore(TestStore):
841+
842+ scenarios = [(key, {'store_id': key, 'get_store': builder})
843+ for key, builder in test_store_builder_registry.iteritems()]
844+
845+ def setUp(self):
846+ super(TestMutableStore, self).setUp()
847+ self.transport = self.get_transport()
848+ self.branch = self.make_branch('branch')
849+
850+ def has_store(self, store):
851+ store_basename = urlutils.relative_url(self.transport.external_url(),
852+ store.external_url())
853+ return self.transport.has(store_basename)
854+
855+ def test_save_empty_creates_no_file(self):
856+ if self.store_id == 'branch':
857+ raise tests.TestNotApplicable(
858+ 'branch.conf is *always* created when a branch is initialized')
859+ store = self.get_store(self)
860+ store.save()
861+ self.assertEquals(False, self.has_store(store))
862+
863+ def test_save_emptied_succeeds(self):
864+ store = self.get_store(self)
865+ store._load_from_string('foo=bar\n')
866+ section = store.get_mutable_section(None)
867+ section.remove('foo')
868+ store.save()
869+ self.assertEquals(True, self.has_store(store))
870+ modified_store = self.get_store(self)
871+ sections = list(modified_store.get_sections())
872+ self.assertLength(0, sections)
873+
874+ def test_save_with_content_succeeds(self):
875+ if self.store_id == 'branch':
876+ raise tests.TestNotApplicable(
877+ 'branch.conf is *always* created when a branch is initialized')
878+ store = self.get_store(self)
879+ store._load_from_string('foo=bar\n')
880+ self.assertEquals(False, self.has_store(store))
881+ store.save()
882+ self.assertEquals(True, self.has_store(store))
883+ modified_store = self.get_store(self)
884+ sections = list(modified_store.get_sections())
885+ self.assertLength(1, sections)
886+ self.assertSectionContent((None, {'foo': 'bar'}), sections[0])
887+
888+ def test_set_option_in_empty_store(self):
889+ store = self.get_store(self)
890+ section = store.get_mutable_section(None)
891+ section.set('foo', 'bar')
892+ store.save()
893+ modified_store = self.get_store(self)
894+ sections = list(modified_store.get_sections())
895+ self.assertLength(1, sections)
896+ self.assertSectionContent((None, {'foo': 'bar'}), sections[0])
897+
898+ def test_set_option_in_default_section(self):
899+ store = self.get_store(self)
900+ store._load_from_string('')
901+ section = store.get_mutable_section(None)
902+ section.set('foo', 'bar')
903+ store.save()
904+ modified_store = self.get_store(self)
905+ sections = list(modified_store.get_sections())
906+ self.assertLength(1, sections)
907+ self.assertSectionContent((None, {'foo': 'bar'}), sections[0])
908+
909+ def test_set_option_in_named_section(self):
910+ store = self.get_store(self)
911+ store._load_from_string('')
912+ section = store.get_mutable_section('baz')
913+ section.set('foo', 'bar')
914+ store.save()
915+ modified_store = self.get_store(self)
916+ sections = list(modified_store.get_sections())
917+ self.assertLength(1, sections)
918+ self.assertSectionContent(('baz', {'foo': 'bar'}), sections[0])
919+
920+
921+class TestConfigObjStore(TestStore):
922+
923+ def test_loading_unknown_file_fails(self):
924+ store = config.ConfigObjStore(self.get_transport(), 'I-do-not-exist')
925+ self.assertRaises(errors.NoSuchFile, store.load)
926+
927+ def test_invalid_content(self):
928+ store = config.ConfigObjStore(self.get_transport(), 'foo.conf', )
929+ self.assertEquals(False, store.loaded)
930+ exc = self.assertRaises(
931+ errors.ParseConfigError, store._load_from_string,
932+ 'this is invalid !')
933+ self.assertEndsWith(exc.filename, 'foo.conf')
934+ # And the load failed
935+ self.assertEquals(False, store.loaded)
936+
937+ def test_get_embedded_sections(self):
938+ # A more complicated example (which also shows that section names and
939+ # option names share the same name space...)
940+ # FIXME: This should be fixed by forbidding dicts as values ?
941+ # -- vila 2011-04-05
942+ store = config.ConfigObjStore(self.get_transport(), 'foo.conf', )
943+ store._load_from_string('''
944+foo=bar
945+l=1,2
946+[DEFAULT]
947+foo_in_DEFAULT=foo_DEFAULT
948+[bar]
949+foo_in_bar=barbar
950+[baz]
951+foo_in_baz=barbaz
952+[[qux]]
953+foo_in_qux=quux
954+''')
955+ sections = list(store.get_sections())
956+ self.assertLength(4, sections)
957+ # The default section has no name.
958+ # List values are provided as lists
959+ self.assertSectionContent((None, {'foo': 'bar', 'l': ['1', '2']}),
960+ sections[0])
961+ self.assertSectionContent(
962+ ('DEFAULT', {'foo_in_DEFAULT': 'foo_DEFAULT'}), sections[1])
963+ self.assertSectionContent(
964+ ('bar', {'foo_in_bar': 'barbar'}), sections[2])
965+ # sub sections are provided as embedded dicts.
966+ self.assertSectionContent(
967+ ('baz', {'foo_in_baz': 'barbaz', 'qux': {'foo_in_qux': 'quux'}}),
968+ sections[3])
969+
970+
971+class TestLockableConfigObjStore(TestStore):
972+
973+ def test_create_store_in_created_dir(self):
974+ t = self.get_transport('dir/subdir')
975+ store = config.LockableConfigObjStore(t, 'foo.conf')
976+ store.get_mutable_section(None).set('foo', 'bar')
977+ store.save()
978+
979+ # FIXME: We should adapt the tests in TestLockableConfig about concurrent
980+ # writes. Since this requires a clearer rewrite, I'll just rely on using
981+ # the same code in LockableConfigObjStore (copied from LockableConfig, but
982+ # trivial enough, the main difference is that we add @needs_write_lock on
983+ # save() instead of set_user_option() and remove_user_option()). The intent
984+ # is to ensure that we always get a valid content for the store even when
985+ # concurrent accesses occur, read/write, write/write. It may be worth
986+ # looking into removing the lock dir when it;s not needed anymore and look
987+ # at possible fallouts for concurrent lockers -- vila 20110-04-06
988+
989+
990+class TestSectionMatcher(TestStore):
991+
992+ scenarios = [('location', {'matcher': config.LocationMatcher})]
993+
994+ def get_store(self, file_name):
995+ return config.ConfigObjStore(self.get_readonly_transport(), file_name)
996+
997+ def test_no_matches_for_empty_stores(self):
998+ store = self.get_store('foo.conf')
999+ store._load_from_string('')
1000+ matcher = self.matcher(store, '/bar')
1001+ self.assertEquals([], list(matcher.get_sections()))
1002+
1003+ def test_build_doesnt_load_store(self):
1004+ store = self.get_store('foo.conf')
1005+ matcher = self.matcher(store, '/bar')
1006+ self.assertFalse(store.loaded)
1007+
1008+
1009+class TestLocationSection(tests.TestCase):
1010+
1011+ def get_section(self, options, extra_path):
1012+ section = config.ReadOnlySection('foo', options)
1013+ # We don't care about the length so we use '0'
1014+ return config.LocationSection(section, 0, extra_path)
1015+
1016+ def test_simple_option(self):
1017+ section = self.get_section({'foo': 'bar'}, '')
1018+ self.assertEquals('bar', section.get('foo'))
1019+
1020+ def test_option_with_extra_path(self):
1021+ section = self.get_section({'foo': 'bar', 'foo:policy': 'appendpath'},
1022+ 'baz')
1023+ self.assertEquals('bar/baz', section.get('foo'))
1024+
1025+ def test_invalid_policy(self):
1026+ section = self.get_section({'foo': 'bar', 'foo:policy': 'die'},
1027+ 'baz')
1028+ # invalid policies are ignored
1029+ self.assertEquals('bar', section.get('foo'))
1030+
1031+
1032+class TestLocationMatcher(TestStore):
1033+
1034+ def get_store(self, file_name):
1035+ return config.ConfigObjStore(self.get_readonly_transport(), file_name)
1036+
1037+ def test_more_specific_sections_first(self):
1038+ store = self.get_store('foo.conf')
1039+ store._load_from_string('''
1040+[/foo]
1041+section=/foo
1042+[/foo/bar]
1043+section=/foo/bar
1044+''')
1045+ self.assertEquals(['/foo', '/foo/bar'],
1046+ [section.id for section in store.get_sections()])
1047+ matcher = config.LocationMatcher(store, '/foo/bar/baz')
1048+ sections = list(matcher.get_sections())
1049+ self.assertEquals([3, 2],
1050+ [section.length for section in sections])
1051+ self.assertEquals(['/foo/bar', '/foo'],
1052+ [section.id for section in sections])
1053+ self.assertEquals(['baz', 'bar/baz'],
1054+ [section.extra_path for section in sections])
1055+
1056+
1057+class TestConfigStackGet(tests.TestCase):
1058+
1059+ # FIXME: This should be parametrized for all known ConfigStack or dedicated
1060+ # paramerized tests created to avoid bloating -- vila 2011-03-31
1061+
1062+ def test_single_config_get(self):
1063+ conf = dict(foo='bar')
1064+ conf_stack = config.ConfigStack([conf])
1065+ self.assertEquals('bar', conf_stack.get('foo'))
1066+
1067+ def test_get_first_definition(self):
1068+ conf1 = dict(foo='bar')
1069+ conf2 = dict(foo='baz')
1070+ conf_stack = config.ConfigStack([conf1, conf2])
1071+ self.assertEquals('bar', conf_stack.get('foo'))
1072+
1073+ def test_get_embedded_definition(self):
1074+ conf1 = dict(yy='12')
1075+ conf2 = config.ConfigStack([dict(xx='42'), dict(foo='baz')])
1076+ conf_stack = config.ConfigStack([conf1, conf2])
1077+ self.assertEquals('baz', conf_stack.get('foo'))
1078+
1079+ def test_get_for_empty_stack(self):
1080+ conf_stack = config.ConfigStack()
1081+ self.assertEquals(None, conf_stack.get('foo'))
1082+
1083+ def test_get_for_empty_section_callable(self):
1084+ conf_stack = config.ConfigStack([lambda : []])
1085+ self.assertEquals(None, conf_stack.get('foo'))
1086+
1087+
1088+class TestConfigStackSet(tests.TestCaseWithTransport):
1089+
1090+ # FIXME: This should be parametrized for all known ConfigStack or dedicated
1091+ # paramerized tests created to avoid bloating -- vila 2011-04-05
1092+
1093+ def test_simple_set(self):
1094+ store = config.ConfigObjStore(self.get_transport(), 'test.conf')
1095+ store._load_from_string('foo=bar')
1096+ conf = config.ConfigStack(
1097+ [store.get_sections], store.get_mutable_section)
1098+ self.assertEquals('bar', conf.get('foo'))
1099+ conf.set('foo', 'baz')
1100+ # Did we get it back ?
1101+ self.assertEquals('baz', conf.get('foo'))
1102+
1103+ def test_set_creates_a_new_section(self):
1104+ store = config.ConfigObjStore(self.get_transport(), 'test.conf')
1105+ conf = config.ConfigStack(
1106+ [store.get_sections], store.get_mutable_section)
1107+ conf.set('foo', 'baz')
1108+ self.assertEquals, 'baz', conf.get('foo')
1109+
1110+
1111+class TestConfigStackRemove(tests.TestCaseWithTransport):
1112+
1113+ # FIXME: This should be parametrized for all known ConfigStack or dedicated
1114+ # paramerized tests created to avoid bloating -- vila 2011-04-06
1115+
1116+ def test_remove_existing(self):
1117+ store = config.ConfigObjStore(self.get_transport(), 'test.conf')
1118+ store._load_from_string('foo=bar')
1119+ conf = config.ConfigStack(
1120+ [store.get_sections], store.get_mutable_section)
1121+ self.assertEquals('bar', conf.get('foo'))
1122+ conf.remove('foo')
1123+ # Did we get it back ?
1124+ self.assertEquals(None, conf.get('foo'))
1125+
1126+ def test_remove_unknown(self):
1127+ store = config.ConfigObjStore(self.get_transport(), 'test.conf')
1128+ conf = config.ConfigStack(
1129+ [store.get_sections], store.get_mutable_section)
1130+ self.assertRaises(KeyError, conf.remove, 'I_do_not_exist')
1131+
1132+
1133+class TestConcreteStacks(tests.TestCaseWithTransport):
1134+
1135+ # basic smoke tests
1136+
1137+ def test_global_stack(self):
1138+ stack = config.GlobalStack()
1139+
1140+ def test_location_store(self):
1141+ stack = config.LocationStack('.')
1142+
1143+ def test_branch_store(self):
1144+ b = self.make_branch('.')
1145+ stack = config.BranchStack(b)
1146+
1147+
1148 class TestConfigGetOptions(tests.TestCaseWithTransport, TestOptionsMixin):
1149
1150 def setUp(self):
1151 super(TestConfigGetOptions, self).setUp()
1152 create_configs(self)
1153
1154- # One variable in none of the above
1155 def test_no_variable(self):
1156 # Using branch should query branch, locations and bazaar
1157 self.assertOptions([], self.branch_config)
1158
1159=== added file 'doc/developers/configuration.txt'
1160--- doc/developers/configuration.txt 1970-01-01 00:00:00 +0000
1161+++ doc/developers/configuration.txt 2011-04-12 12:36:46 +0000
1162@@ -0,0 +1,102 @@
1163+Configuring Bazaar
1164+==================
1165+
1166+A configuration option has:
1167+
1168+- a name: a valid python identifier (even if it's not used as an
1169+ identifier in python itself)
1170+
1171+- a value: a unicode string
1172+
1173+Sections
1174+--------
1175+
1176+Options are grouped into sections which share some properties with the well
1177+known dict objects:
1178+
1179+- the key is the name,
1180+- you can get, set and remove an option,
1181+- the value is a unicode string.
1182+
1183+MutableSection are needed to set or remove an option, ReadOnlySection should
1184+be used otherwise.
1185+
1186+Stores
1187+------
1188+
1189+Options can persistent in which case they are saved into Stores.
1190+
1191+``config.Store`` defines the abstract interface that all stores should
1192+implement.
1193+
1194+This object doesn't provide a direct access to the options, it only provides
1195+access to Sections. This is deliberate to ensure that sections can be properly
1196+shared by reusing the same underlying objects. Accessing options should be
1197+done via the ``Section`` objects.
1198+
1199+A ``Store`` can contain one or more sections, each section is uniquely
1200+identified by a unicode string.
1201+
1202+``config.ConfigObjStore`` is an implementation that use ``ConfigObj``.
1203+
1204+Depending on the object it is associated with (or not) a ``Store`` also needs
1205+to implement a locking mechanism. ``LockableConfigObjStore`` implements such a
1206+mechanism for ``ConfigObj`` based stores.
1207+
1208+Classes are provided for the usual Bazaar configuration files and could be
1209+used as examples to define new ones if needed. The associated tests provides a
1210+basis for new classes which only need to register themselves in the right
1211+places to inherit from the existing basic tests and add their own specific
1212+ones.
1213+
1214+Filtering sections
1215+------------------
1216+
1217+For some contexts, only some sections from a given store will apply. Defining
1218+which is what the ``SectionMatcher`` are about.
1219+
1220+The main constraint here is that a ``SectionMatcher`` should delay the loading
1221+of the associated store as long as possible. The constructor should collect
1222+all data needed for the selection and uses it while processing the sections in
1223+``get_sections``.
1224+
1225+Only ``ReadOnlySection`` objects are manipulated here but a ``SectionMatcher``
1226+can return dedicated ``Section`` to provide additional context (the
1227+``LocationSection`` add an ``extra_path`` attribute to implement the
1228+``appendpath`` policy for example).
1229+
1230+.. FIXME: Replace the appendpath example if/when it's deprecated ;)
1231+
1232+Stacks
1233+------
1234+
1235+An option can take different values depending on the context it is used. Such
1236+a context can involve configuration files, options from the command line,
1237+default values in bzrlib and then some.
1238+
1239+Such a context is implemented by creating a list of ``Section`` stacked upon
1240+each other. A ``Stack`` can then be asked for an option value and returns the
1241+first definition found.
1242+
1243+This provides a great flexibility to decide priorities between sections when
1244+the stack is defined without to worry about them in the code itself.
1245+
1246+A stack also defines a mutable section (which can be None) to handle
1247+modifications.
1248+
1249+Many sections (or even stores) are aimed at providing default values for an
1250+option but these sections shouldn't be modified lightly as modifying an option
1251+used for different contexts will indeed be seen by all these contexts.
1252+
1253+Default values in configuration files are defined by users, developers
1254+shouldn't have to modify them, as such, no mechanism nor heuristics are used
1255+to find which section (or sections) should be modified, a ``Stack`` defines a
1256+mutable section when there is no ambiguity, if there is one, then the *user*
1257+should be able to decide and this case a new ``Stack`` can be created cheaply.
1258+
1259+Different stacks can be created for different purposes, the existing
1260+``Global.Stack``, ``LocationStack`` and ``BranchStack`` can be used as basis
1261+or examples. These classes are the only ones that should be used in code,
1262+``Stores`` can be used to build them but shouldn't be used otherwise, ditto
1263+for sections. Again, the associated tests could and should be used against the
1264+created stacks.
1265
1266=== modified file 'doc/developers/index.txt'
1267--- doc/developers/index.txt 2011-01-06 06:26:23 +0000
1268+++ doc/developers/index.txt 2011-04-12 12:36:46 +0000
1269@@ -36,9 +36,10 @@
1270 .. toctree::
1271 :maxdepth: 1
1272
1273+ configuration
1274+ fetch
1275 transports
1276 ui
1277- fetch
1278
1279 Releasing and Packaging
1280 =======================
1281
1282=== modified file 'doc/en/user-guide/configuring_bazaar.txt'
1283--- doc/en/user-guide/configuring_bazaar.txt 2011-02-07 01:39:42 +0000
1284+++ doc/en/user-guide/configuring_bazaar.txt 2011-04-12 12:36:46 +0000
1285@@ -37,6 +37,24 @@
1286 which shouldn't be reached by the proxy. (See
1287 <http://docs.python.org/library/urllib.html> for more details.)
1288
1289+Various ways to configure
1290+-------------------------
1291+
1292+As shown in the example above, there are various ways to
1293+configure Bazaar, they all share some common properties though,
1294+an option has:
1295+
1296+- a name which is generally a valid python identifier,
1297+
1298+- a value which is a string. In some cases, Bazzar will be able
1299+ to recognize special values like 'True', 'False' to infer a
1300+ boolean type, but basically, as a user, you will always specify
1301+ a value as a string.
1302+
1303+Options are grouped in various contexts so their name uniquely
1304+identify them in this context. When needed, options can be made
1305+persistent by recording them in a configuration file.
1306+
1307
1308 Configuration files
1309 -------------------