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
=== modified file 'bzrlib/config.py'
--- bzrlib/config.py 2011-04-05 14:47:26 +0000
+++ bzrlib/config.py 2011-04-12 12:36:46 +0000
@@ -848,7 +848,7 @@
848 # LockableConfig for other kind of transports, we will need to reuse848 # LockableConfig for other kind of transports, we will need to reuse
849 # whatever connection is already established -- vila 20100929849 # whatever connection is already established -- vila 20100929
850 self.transport = transport.get_transport(self.dir)850 self.transport = transport.get_transport(self.dir)
851 self._lock = lockdir.LockDir(self.transport, 'lock')851 self._lock = lockdir.LockDir(self.transport, self.lock_name)
852852
853 def _create_from_string(self, unicode_bytes, save):853 def _create_from_string(self, unicode_bytes, save):
854 super(LockableConfig, self)._create_from_string(unicode_bytes, False)854 super(LockableConfig, self)._create_from_string(unicode_bytes, False)
@@ -967,6 +967,61 @@
967 super(LockableConfig, self).remove_user_option(option_name,967 super(LockableConfig, self).remove_user_option(option_name,
968 section_name)968 section_name)
969969
970def _iter_for_location_by_parts(sections, location):
971 """Keep only the sessions matching the specified location.
972
973 :param sections: An iterable of section names.
974
975 :param location: An url or a local path to match against.
976
977 :returns: An iterator of (section, extra_path, nb_parts) where nb is the
978 number of path components in the section name, section is the section
979 name and extra_path is the difference between location and the section
980 name.
981 """
982 location_parts = location.rstrip('/').split('/')
983
984 for section in sections:
985 # location is a local path if possible, so we need
986 # to convert 'file://' urls to local paths if necessary.
987
988 # FIXME: I don't think the above comment is still up to date,
989 # LocationConfig is always instantiated with an url -- vila 2011-04-07
990
991 # This also avoids having file:///path be a more exact
992 # match than '/path'.
993
994 # FIXME: Not sure about the above either, but since the path components
995 # are compared in sync, adding two empty components (//) is likely to
996 # trick the comparison and also trick the check on the number of
997 # components, so we *should* take only the relevant part of the url. On
998 # the other hand, this means 'file://' urls *can't* be used in sections
999 # so more work is probably needed -- vila 2011-04-07
1000
1001 if section.startswith('file://'):
1002 section_path = urlutils.local_path_from_url(section)
1003 else:
1004 section_path = section
1005 section_parts = section_path.rstrip('/').split('/')
1006
1007 matched = True
1008 if len(section_parts) > len(location_parts):
1009 # More path components in the section, they can't match
1010 matched = False
1011 else:
1012 # Rely on zip truncating in length to the length of the shortest
1013 # argument sequence.
1014 names = zip(location_parts, section_parts)
1015 for name in names:
1016 if not fnmatch.fnmatch(name[0], name[1]):
1017 matched = False
1018 break
1019 if not matched:
1020 continue
1021 # build the path difference between the section and the location
1022 extra_path = '/'.join(location_parts[len(section_parts):])
1023 yield section, extra_path, len(section_parts)
1024
9701025
971class LocationConfig(LockableConfig):1026class LocationConfig(LockableConfig):
972 """A configuration object that gives the policy for a location."""1027 """A configuration object that gives the policy for a location."""
@@ -1001,49 +1056,20 @@
10011056
1002 def _get_matching_sections(self):1057 def _get_matching_sections(self):
1003 """Return an ordered list of section names matching this location."""1058 """Return an ordered list of section names matching this location."""
1004 sections = self._get_parser()1059 matches = list(_iter_for_location_by_parts(self._get_parser(),
1005 location_names = self.location.split('/')1060 self.location))
1006 if self.location.endswith('/'):
1007 del location_names[-1]
1008 matches=[]
1009 for section in sections:
1010 # location is a local path if possible, so we need
1011 # to convert 'file://' urls to local paths if necessary.
1012 # This also avoids having file:///path be a more exact
1013 # match than '/path'.
1014 if section.startswith('file://'):
1015 section_path = urlutils.local_path_from_url(section)
1016 else:
1017 section_path = section
1018 section_names = section_path.split('/')
1019 if section.endswith('/'):
1020 del section_names[-1]
1021 names = zip(location_names, section_names)
1022 matched = True
1023 for name in names:
1024 if not fnmatch.fnmatch(name[0], name[1]):
1025 matched = False
1026 break
1027 if not matched:
1028 continue
1029 # so, for the common prefix they matched.
1030 # if section is longer, no match.
1031 if len(section_names) > len(location_names):
1032 continue
1033 matches.append((len(section_names), section,
1034 '/'.join(location_names[len(section_names):])))
1035 # put the longest (aka more specific) locations first1061 # put the longest (aka more specific) locations first
1036 matches.sort(reverse=True)1062 matches.sort(
1037 sections = []1063 key=lambda (section, extra_path, length): (length, section),
1038 for (length, section, extra_path) in matches:1064 reverse=True)
1039 sections.append((section, extra_path))1065 for (section, extra_path, length) in matches:
1066 yield section, extra_path
1040 # should we stop looking for parent configs here?1067 # should we stop looking for parent configs here?
1041 try:1068 try:
1042 if self._get_parser()[section].as_bool('ignore_parents'):1069 if self._get_parser()[section].as_bool('ignore_parents'):
1043 break1070 break
1044 except KeyError:1071 except KeyError:
1045 pass1072 pass
1046 return sections
10471073
1048 def _get_sections(self, name=None):1074 def _get_sections(self, name=None):
1049 """See IniBasedConfig._get_sections()."""1075 """See IniBasedConfig._get_sections()."""
@@ -2068,6 +2094,422 @@
2068 self._transport.put_file(self._filename, out_file)2094 self._transport.put_file(self._filename, out_file)
20692095
20702096
2097class ReadOnlySection(object):
2098 """A section defines a dict of options.
2099
2100 This is merely a read-only dict which can add some knowledge about the
2101 options.
2102 """
2103
2104 def __init__(self, section_id, options):
2105 self.id = section_id
2106 # We re-use the dict-like object received
2107 self.options = options
2108
2109 def get(self, name, default=None):
2110 return self.options.get(name, default)
2111
2112
2113_NewlyCreatedOption = object()
2114"""Was the option created during the MutableSection lifetime"""
2115
2116
2117class MutableSection(ReadOnlySection):
2118 """A section allowing changes and keeping track of the original values."""
2119
2120 def __init__(self, section_id, options):
2121 super(MutableSection, self).__init__(section_id, options)
2122 self.orig = {}
2123
2124 def set(self, name, value):
2125 if name not in self.options:
2126 # This is a new option
2127 self.orig[name] = _NewlyCreatedOption
2128 elif name not in self.orig:
2129 self.orig[name] = self.get(name, None)
2130 self.options[name] = value
2131
2132 def remove(self, name):
2133 if name not in self.orig:
2134 self.orig[name] = self.get(name, None)
2135 del self.options[name]
2136
2137
2138class Store(object):
2139 """Abstract interface to persistent storage for configuration options."""
2140
2141 readonly_section_class = ReadOnlySection
2142 mutable_section_class = MutableSection
2143
2144 @property
2145 def loaded(self):
2146 raise NotImplementedError(self.loaded)
2147
2148 def load(self):
2149 raise NotImplementedError(self.load)
2150
2151 def _load_from_string(self, str_or_unicode):
2152 """Create a store from a string in configobj syntax.
2153
2154 :param str_or_unicode: A string representing the file content. This will
2155 be encoded to suit store needs internally.
2156
2157 This is for tests and should not be used in production unless a
2158 convincing use case can be demonstrated :)
2159 """
2160 raise NotImplementedError(self._load_from_string)
2161
2162 def save(self):
2163 raise NotImplementedError(self.save)
2164
2165 def external_url(self):
2166 raise NotImplementedError(self.external_url)
2167
2168 def get_sections(self):
2169 """Returns an ordered iterable of existing sections.
2170
2171 :returns: An iterable of (name, dict).
2172 """
2173 raise NotImplementedError(self.get_sections)
2174
2175 def get_mutable_section(self, section_name=None):
2176 """Returns the specified mutable section.
2177
2178 :param section_name: The section identifier
2179 """
2180 raise NotImplementedError(self.get_mutable_section)
2181
2182
2183class ConfigObjStore(Store):
2184
2185 def __init__(self, transport, file_name):
2186 """A config Store using ConfigObj for storage.
2187
2188 :param transport: The transport object where the config file is located.
2189
2190 :param file_name: The config file basename in the transport directory.
2191 """
2192 super(ConfigObjStore, self).__init__()
2193 self.transport = transport
2194 self.file_name = file_name
2195 self._config_obj = None
2196
2197 @property
2198 def loaded(self):
2199 return self._config_obj != None
2200
2201 def load(self):
2202 """Load the store from the associated file."""
2203 if self.loaded:
2204 return
2205 content = self.transport.get_bytes(self.file_name)
2206 self._load_from_string(content)
2207
2208 def _load_from_string(self, str_or_unicode):
2209 """Create a config store from a string.
2210
2211 :param str_or_unicode: A string representing the file content. This will
2212 be utf-8 encoded internally.
2213
2214 This is for tests and should not be used in production unless a
2215 convincing use case can be demonstrated :)
2216 """
2217 if self.loaded:
2218 raise AssertionError('Already loaded: %r' % (self._config_obj,))
2219 co_input = StringIO(str_or_unicode.encode('utf-8'))
2220 try:
2221 # The config files are always stored utf8-encoded
2222 self._config_obj = ConfigObj(co_input, encoding='utf-8')
2223 except configobj.ConfigObjError, e:
2224 self._config_obj = None
2225 raise errors.ParseConfigError(e.errors, self.external_url())
2226
2227 def save(self):
2228 if not self.loaded:
2229 # Nothing to save
2230 return
2231 out = StringIO()
2232 self._config_obj.write(out)
2233 self.transport.put_bytes(self.file_name, out.getvalue())
2234
2235 def external_url(self):
2236 # FIXME: external_url should really accepts an optional relpath
2237 # parameter (bug #750169) :-/ -- vila 2011-04-04
2238 # The following will do in the interim but maybe we don't want to
2239 # expose a path here but rather a config ID and its associated
2240 # object </hand wawe>.
2241 return os.path.join(self.transport.external_url(), self.file_name)
2242
2243 def get_sections(self):
2244 """Get the configobj section in the file order.
2245
2246 :returns: An iterable of (name, dict).
2247 """
2248 # We need a loaded store
2249 self.load()
2250 cobj = self._config_obj
2251 if cobj.scalars:
2252 yield self.readonly_section_class(None, cobj)
2253 for section_name in cobj.sections:
2254 yield self.readonly_section_class(section_name, cobj[section_name])
2255
2256 def get_mutable_section(self, section_name=None):
2257 # We need a loaded store
2258 try:
2259 self.load()
2260 except errors.NoSuchFile:
2261 # The file doesn't exist, let's pretend it was empty
2262 self._load_from_string('')
2263 if section_name is None:
2264 section = self._config_obj
2265 else:
2266 section = self._config_obj.setdefault(section_name, {})
2267 return self.mutable_section_class(section_name, section)
2268
2269
2270# Note that LockableConfigObjStore inherits from ConfigObjStore because we need
2271# unlockable stores for use with objects that can already ensure the locking
2272# (think branches). If different stores (not based on ConfigObj) are created,
2273# they may face the same issue.
2274
2275
2276class LockableConfigObjStore(ConfigObjStore):
2277 """A ConfigObjStore using locks on save to ensure store integrity."""
2278
2279 def __init__(self, transport, file_name, lock_dir_name=None):
2280 """A config Store using ConfigObj for storage.
2281
2282 :param transport: The transport object where the config file is located.
2283
2284 :param file_name: The config file basename in the transport directory.
2285 """
2286 if lock_dir_name is None:
2287 lock_dir_name = 'lock'
2288 self.lock_dir_name = lock_dir_name
2289 super(LockableConfigObjStore, self).__init__(transport, file_name)
2290 self._lock = lockdir.LockDir(self.transport, self.lock_dir_name)
2291
2292 def lock_write(self, token=None):
2293 """Takes a write lock in the directory containing the config file.
2294
2295 If the directory doesn't exist it is created.
2296 """
2297 # FIXME: This doesn't check the ownership of the created directories as
2298 # ensure_config_dir_exists does. It should if the transport is local
2299 # -- vila 2011-04-06
2300 self.transport.create_prefix()
2301 return self._lock.lock_write(token)
2302
2303 def unlock(self):
2304 self._lock.unlock()
2305
2306 def break_lock(self):
2307 self._lock.break_lock()
2308
2309 @needs_write_lock
2310 def save(self):
2311 super(LockableConfigObjStore, self).save()
2312
2313
2314# FIXME: global, bazaar, shouldn't that be 'user' instead or even
2315# 'user_defaults' as opposed to 'user_overrides', 'system_defaults'
2316# (/etc/bzr/bazaar.conf) and 'system_overrides' ? -- vila 2011-04-05
2317class GlobalStore(LockableConfigObjStore):
2318
2319 def __init__(self, possible_transports=None):
2320 t = transport.get_transport(config_dir(),
2321 possible_transports=possible_transports)
2322 super(GlobalStore, self).__init__(t, 'bazaar.conf')
2323
2324
2325class LocationStore(LockableConfigObjStore):
2326
2327 def __init__(self, possible_transports=None):
2328 t = transport.get_transport(config_dir(),
2329 possible_transports=possible_transports)
2330 super(LocationStore, self).__init__(t, 'locations.conf')
2331
2332
2333class BranchStore(ConfigObjStore):
2334
2335 def __init__(self, branch):
2336 super(BranchStore, self).__init__(branch.control_transport,
2337 'branch.conf')
2338
2339class SectionMatcher(object):
2340 """Select sections into a given Store.
2341
2342 This intended to be used to postpone getting an iterable of sections from a
2343 store.
2344 """
2345
2346 def __init__(self, store):
2347 self.store = store
2348
2349 def get_sections(self):
2350 # This is where we require loading the store so we can see all defined
2351 # sections.
2352 sections = self.store.get_sections()
2353 # Walk the revisions in the order provided
2354 for s in sections:
2355 if self.match(s):
2356 yield s
2357
2358 def match(self, secion):
2359 raise NotImplementedError(self.match)
2360
2361
2362class LocationSection(ReadOnlySection):
2363
2364 def __init__(self, section, length, extra_path):
2365 super(LocationSection, self).__init__(section.id, section.options)
2366 self.length = length
2367 self.extra_path = extra_path
2368
2369 def get(self, name, default=None):
2370 value = super(LocationSection, self).get(name, default)
2371 if value is not None:
2372 policy_name = self.get(name + ':policy', None)
2373 policy = _policy_value.get(policy_name, POLICY_NONE)
2374 if policy == POLICY_APPENDPATH:
2375 value = urlutils.join(value, self.extra_path)
2376 return value
2377
2378
2379class LocationMatcher(SectionMatcher):
2380
2381 def __init__(self, store, location):
2382 super(LocationMatcher, self).__init__(store)
2383 self.location = location
2384
2385 def get_sections(self):
2386 # Override the default implementation as we want to change the order
2387
2388 # The following is a bit hackish but ensures compatibility with
2389 # LocationConfig by reusing the same code
2390 sections = list(self.store.get_sections())
2391 filtered_sections = _iter_for_location_by_parts(
2392 [s.id for s in sections], self.location)
2393 iter_sections = iter(sections)
2394 matching_sections = []
2395 for section_id, extra_path, length in filtered_sections:
2396 # a section id is unique for a given store so it's safe to iterate
2397 # again
2398 section = iter_sections.next()
2399 if section_id == section.id:
2400 matching_sections.append(
2401 LocationSection(section, length, extra_path))
2402 # We want the longest (aka more specific) locations first
2403 sections = sorted(matching_sections,
2404 key=lambda section: (section.length, section.id),
2405 reverse=True)
2406 # Sections mentioning 'ignore_parents' restrict the selection
2407 for section in sections:
2408 # FIXME: We really want to use as_bool below -- vila 2011-04-07
2409 ignore = section.get('ignore_parents', None)
2410 if ignore is not None:
2411 ignore = ui.bool_from_string(ignore)
2412 if ignore:
2413 break
2414 # Finally, we have a valid section
2415 yield section
2416
2417
2418class ConfigStack(object):
2419 """A stack of configurations where an option can be defined"""
2420
2421 def __init__(self, sections=None, get_mutable_section=None):
2422 """Creates a stack of sections with an optional store for changes.
2423
2424 :param sections: A list of ReadOnlySection or callables that returns an
2425 iterable of ReadOnlySection.
2426
2427 :param get_mutable_section: A callable that returns a MutableSection
2428 where changes are recorded.
2429 """
2430 if sections is None:
2431 sections = []
2432 self.sections = sections
2433 self.get_mutable_section = get_mutable_section
2434
2435 def get(self, name):
2436 """Return the *first* option value found in the sections.
2437
2438 This is where we guarantee that sections coming from Store are loaded
2439 lazily: the loading is delayed until we need to either check that an
2440 option exists or get its value, which in turn may require to discover
2441 in which sections it can be defined. Both of these (section and option
2442 existence) require loading the store (even partially).
2443 """
2444 # Ensuring lazy loading is achieved by delaying section matching until
2445 # it can be avoided anymore by using callables to describe (possibly
2446 # empty) section lists.
2447 for section_or_callable in self.sections:
2448 # Each section can expand to multiple ones when a callable is used
2449 if callable(section_or_callable):
2450 sections = section_or_callable()
2451 else:
2452 sections = [section_or_callable]
2453 for section in sections:
2454 value = section.get(name)
2455 if value is not None:
2456 return value
2457 # No definition was found
2458 return None
2459
2460 def set(self, name, value):
2461 """Set a new value for the option.
2462
2463 This is where we guarantee that the mutable section is lazily loaded:
2464 this means we won't load the corresponding store before setting a value.
2465 """
2466 section = self.get_mutable_section()
2467 section.set(name, value)
2468
2469 def remove(self, name):
2470 """Remove an existing option.
2471
2472 This is where we guarantee that the mutable section is lazily loaded:
2473 this means we won't load the correspoding store before trying to delete
2474 an option. In practice the store will often be loaded but this allows
2475 catching some programming errors.
2476 """
2477 section = self.get_mutable_section()
2478 section.remove(name)
2479
2480
2481class GlobalStack(ConfigStack):
2482
2483 def __init__(self):
2484 # Get a GlobalStore
2485 gstore = GlobalStore()
2486 super(GlobalStack, self).__init__([gstore.get_sections],
2487 gstore.get_mutable_section)
2488
2489
2490class LocationStack(ConfigStack):
2491
2492 def __init__(self, location):
2493 lstore = LocationStore()
2494 matcher = LocationMatcher(lstore, location)
2495 gstore = GlobalStore()
2496 super(LocationStack, self).__init__(
2497 [matcher.get_sections, gstore.get_sections],
2498 lstore.get_mutable_section)
2499
2500
2501class BranchStack(ConfigStack):
2502
2503 def __init__(self, branch):
2504 bstore = BranchStore(branch)
2505 lstore = LocationStore()
2506 matcher = LocationMatcher(lstore, branch.base)
2507 gstore = GlobalStore()
2508 super(BranchStack, self).__init__(
2509 [matcher.get_sections, bstore.get_sections, gstore.get_sections],
2510 bstore.get_mutable_section)
2511
2512
2071class cmd_config(commands.Command):2513class cmd_config(commands.Command):
2072 __doc__ = """Display, set or remove a configuration option.2514 __doc__ = """Display, set or remove a configuration option.
20732515
20742516
=== modified file 'bzrlib/errors.py'
--- bzrlib/errors.py 2011-03-12 23:58:55 +0000
+++ bzrlib/errors.py 2011-04-12 12:36:46 +0000
@@ -1766,12 +1766,12 @@
17661766
1767class ParseConfigError(BzrError):1767class ParseConfigError(BzrError):
17681768
1769 _fmt = "Error(s) parsing config file %(filename)s:\n%(errors)s"
1770
1769 def __init__(self, errors, filename):1771 def __init__(self, errors, filename):
1770 if filename is None:1772 BzrError.__init__(self)
1771 filename = ""1773 self.filename = filename
1772 message = "Error(s) parsing config file %s:\n%s" % \1774 self.errors = '\n'.join(e.msg for e in errors)
1773 (filename, ('\n'.join(e.msg for e in errors)))
1774 BzrError.__init__(self, message)
17751775
17761776
1777class NoEmailInUsername(BzrError):1777class NoEmailInUsername(BzrError):
17781778
=== modified file 'bzrlib/tests/test_config.py'
--- bzrlib/tests/test_config.py 2011-04-05 12:11:26 +0000
+++ bzrlib/tests/test_config.py 2011-04-12 12:36:46 +0000
@@ -36,6 +36,7 @@
36 mergetools,36 mergetools,
37 ui,37 ui,
38 urlutils,38 urlutils,
39 registry,
39 tests,40 tests,
40 trace,41 trace,
41 transport,42 transport,
@@ -62,6 +63,23 @@
6263
63load_tests = scenarios.load_tests_apply_scenarios64load_tests = scenarios.load_tests_apply_scenarios
6465
66# We need adpaters that can build a config store in a test context. Test
67# classes, based on TestCaseWithTransport, can use the registry to parametrize
68# themselves. The builder will receive a test instance and should return a
69# ready-to-use store. Plugins that defines new stores can also register
70# themselves here to be tested against the tests defined below.
71test_store_builder_registry = registry.Registry()
72test_store_builder_registry.register(
73 'configobj', lambda test: config.ConfigObjStore(test.get_transport(),
74 'configobj.conf'))
75test_store_builder_registry.register(
76 'bazaar', lambda test: config.GlobalStore())
77test_store_builder_registry.register(
78 'location', lambda test: config.LocationStore())
79test_store_builder_registry.register(
80 'branch', lambda test: config.BranchStore(test.branch))
81
82
6583
66sample_long_alias="log -r-15..-1 --line"84sample_long_alias="log -r-15..-1 --line"
67sample_config_text = u"""85sample_config_text = u"""
@@ -832,7 +850,7 @@
832 def c1_write_config_file():850 def c1_write_config_file():
833 before_writing.set()851 before_writing.set()
834 c1_orig()852 c1_orig()
835 # The lock is held we wait for the main thread to decide when to853 # The lock is held. We wait for the main thread to decide when to
836 # continue854 # continue
837 after_writing.wait()855 after_writing.wait()
838 c1._write_config_file = c1_write_config_file856 c1._write_config_file = c1_write_config_file
@@ -865,7 +883,7 @@
865 c1_orig = c1._write_config_file883 c1_orig = c1._write_config_file
866 def c1_write_config_file():884 def c1_write_config_file():
867 ready_to_write.set()885 ready_to_write.set()
868 # The lock is held we wait for the main thread to decide when to886 # The lock is held. We wait for the main thread to decide when to
869 # continue887 # continue
870 do_writing.wait()888 do_writing.wait()
871 c1_orig()889 c1_orig()
@@ -1264,55 +1282,52 @@
1264 self.failUnless(isinstance(global_config, config.GlobalConfig))1282 self.failUnless(isinstance(global_config, config.GlobalConfig))
1265 self.failUnless(global_config is my_config._get_global_config())1283 self.failUnless(global_config is my_config._get_global_config())
12661284
1285 def assertLocationMatching(self, expected):
1286 self.assertEqual(expected,
1287 list(self.my_location_config._get_matching_sections()))
1288
1267 def test__get_matching_sections_no_match(self):1289 def test__get_matching_sections_no_match(self):
1268 self.get_branch_config('/')1290 self.get_branch_config('/')
1269 self.assertEqual([], self.my_location_config._get_matching_sections())1291 self.assertLocationMatching([])
12701292
1271 def test__get_matching_sections_exact(self):1293 def test__get_matching_sections_exact(self):
1272 self.get_branch_config('http://www.example.com')1294 self.get_branch_config('http://www.example.com')
1273 self.assertEqual([('http://www.example.com', '')],1295 self.assertLocationMatching([('http://www.example.com', '')])
1274 self.my_location_config._get_matching_sections())
12751296
1276 def test__get_matching_sections_suffix_does_not(self):1297 def test__get_matching_sections_suffix_does_not(self):
1277 self.get_branch_config('http://www.example.com-com')1298 self.get_branch_config('http://www.example.com-com')
1278 self.assertEqual([], self.my_location_config._get_matching_sections())1299 self.assertLocationMatching([])
12791300
1280 def test__get_matching_sections_subdir_recursive(self):1301 def test__get_matching_sections_subdir_recursive(self):
1281 self.get_branch_config('http://www.example.com/com')1302 self.get_branch_config('http://www.example.com/com')
1282 self.assertEqual([('http://www.example.com', 'com')],1303 self.assertLocationMatching([('http://www.example.com', 'com')])
1283 self.my_location_config._get_matching_sections())
12841304
1285 def test__get_matching_sections_ignoreparent(self):1305 def test__get_matching_sections_ignoreparent(self):
1286 self.get_branch_config('http://www.example.com/ignoreparent')1306 self.get_branch_config('http://www.example.com/ignoreparent')
1287 self.assertEqual([('http://www.example.com/ignoreparent', '')],1307 self.assertLocationMatching([('http://www.example.com/ignoreparent',
1288 self.my_location_config._get_matching_sections())1308 '')])
12891309
1290 def test__get_matching_sections_ignoreparent_subdir(self):1310 def test__get_matching_sections_ignoreparent_subdir(self):
1291 self.get_branch_config(1311 self.get_branch_config(
1292 'http://www.example.com/ignoreparent/childbranch')1312 'http://www.example.com/ignoreparent/childbranch')
1293 self.assertEqual([('http://www.example.com/ignoreparent',1313 self.assertLocationMatching([('http://www.example.com/ignoreparent',
1294 'childbranch')],1314 'childbranch')])
1295 self.my_location_config._get_matching_sections())
12961315
1297 def test__get_matching_sections_subdir_trailing_slash(self):1316 def test__get_matching_sections_subdir_trailing_slash(self):
1298 self.get_branch_config('/b')1317 self.get_branch_config('/b')
1299 self.assertEqual([('/b/', '')],1318 self.assertLocationMatching([('/b/', '')])
1300 self.my_location_config._get_matching_sections())
13011319
1302 def test__get_matching_sections_subdir_child(self):1320 def test__get_matching_sections_subdir_child(self):
1303 self.get_branch_config('/a/foo')1321 self.get_branch_config('/a/foo')
1304 self.assertEqual([('/a/*', ''), ('/a/', 'foo')],1322 self.assertLocationMatching([('/a/*', ''), ('/a/', 'foo')])
1305 self.my_location_config._get_matching_sections())
13061323
1307 def test__get_matching_sections_subdir_child_child(self):1324 def test__get_matching_sections_subdir_child_child(self):
1308 self.get_branch_config('/a/foo/bar')1325 self.get_branch_config('/a/foo/bar')
1309 self.assertEqual([('/a/*', 'bar'), ('/a/', 'foo/bar')],1326 self.assertLocationMatching([('/a/*', 'bar'), ('/a/', 'foo/bar')])
1310 self.my_location_config._get_matching_sections())
13111327
1312 def test__get_matching_sections_trailing_slash_with_children(self):1328 def test__get_matching_sections_trailing_slash_with_children(self):
1313 self.get_branch_config('/a/')1329 self.get_branch_config('/a/')
1314 self.assertEqual([('/a/', '')],1330 self.assertLocationMatching([('/a/', '')])
1315 self.my_location_config._get_matching_sections())
13161331
1317 def test__get_matching_sections_explicit_over_glob(self):1332 def test__get_matching_sections_explicit_over_glob(self):
1318 # XXX: 2006-09-08 jamesh1333 # XXX: 2006-09-08 jamesh
@@ -1320,8 +1335,7 @@
1320 # was a config section for '/a/?', it would get precedence1335 # was a config section for '/a/?', it would get precedence
1321 # over '/a/c'.1336 # over '/a/c'.
1322 self.get_branch_config('/a/c')1337 self.get_branch_config('/a/c')
1323 self.assertEqual([('/a/c', ''), ('/a/*', ''), ('/a/', 'c')],1338 self.assertLocationMatching([('/a/c', ''), ('/a/*', ''), ('/a/', 'c')])
1324 self.my_location_config._get_matching_sections())
13251339
1326 def test__get_option_policy_normal(self):1340 def test__get_option_policy_normal(self):
1327 self.get_branch_config('http://www.example.com')1341 self.get_branch_config('http://www.example.com')
@@ -1818,13 +1832,443 @@
1818 self.assertIs(None, bzrdir_config.get_default_stack_on())1832 self.assertIs(None, bzrdir_config.get_default_stack_on())
18191833
18201834
1835class TestConfigReadOnlySection(tests.TestCase):
1836
1837 # FIXME: Parametrize so that all sections produced by Stores run these
1838 # tests -- vila 2011-04-01
1839
1840 def test_get_a_value(self):
1841 a_dict = dict(foo='bar')
1842 section = config.ReadOnlySection('myID', a_dict)
1843 self.assertEquals('bar', section.get('foo'))
1844
1845 def test_get_unkown_option(self):
1846 a_dict = dict()
1847 section = config.ReadOnlySection(None, a_dict)
1848 self.assertEquals('out of thin air',
1849 section.get('foo', 'out of thin air'))
1850
1851 def test_options_is_shared(self):
1852 a_dict = dict()
1853 section = config.ReadOnlySection(None, a_dict)
1854 self.assertIs(a_dict, section.options)
1855
1856
1857class TestConfigMutableSection(tests.TestCase):
1858
1859 # FIXME: Parametrize so that all sections (including os.environ and the
1860 # ones produced by Stores) run these tests -- vila 2011-04-01
1861
1862 def test_set(self):
1863 a_dict = dict(foo='bar')
1864 section = config.MutableSection('myID', a_dict)
1865 section.set('foo', 'new_value')
1866 self.assertEquals('new_value', section.get('foo'))
1867 # The change appears in the shared section
1868 self.assertEquals('new_value', a_dict.get('foo'))
1869 # We keep track of the change
1870 self.assertTrue('foo' in section.orig)
1871 self.assertEquals('bar', section.orig.get('foo'))
1872
1873 def test_set_preserve_original_once(self):
1874 a_dict = dict(foo='bar')
1875 section = config.MutableSection('myID', a_dict)
1876 section.set('foo', 'first_value')
1877 section.set('foo', 'second_value')
1878 # We keep track of the original value
1879 self.assertTrue('foo' in section.orig)
1880 self.assertEquals('bar', section.orig.get('foo'))
1881
1882 def test_remove(self):
1883 a_dict = dict(foo='bar')
1884 section = config.MutableSection('myID', a_dict)
1885 section.remove('foo')
1886 # We get None for unknown options via the default value
1887 self.assertEquals(None, section.get('foo'))
1888 # Or we just get the default value
1889 self.assertEquals('unknown', section.get('foo', 'unknown'))
1890 self.assertFalse('foo' in section.options)
1891 # We keep track of the deletion
1892 self.assertTrue('foo' in section.orig)
1893 self.assertEquals('bar', section.orig.get('foo'))
1894
1895 def test_remove_new_option(self):
1896 a_dict = dict()
1897 section = config.MutableSection('myID', a_dict)
1898 section.set('foo', 'bar')
1899 section.remove('foo')
1900 self.assertFalse('foo' in section.options)
1901 # The option didn't exist initially so it we need to keep track of it
1902 # with a special value
1903 self.assertTrue('foo' in section.orig)
1904 self.assertEquals(config._NewlyCreatedOption, section.orig['foo'])
1905
1906
1907class TestStore(tests.TestCaseWithTransport):
1908
1909 def assertSectionContent(self, expected, section):
1910 """Assert that some options have the proper values in a section."""
1911 expected_name, expected_options = expected
1912 self.assertEquals(expected_name, section.id)
1913 self.assertEquals(
1914 expected_options,
1915 dict([(k, section.get(k)) for k in expected_options.keys()]))
1916
1917
1918class TestReadonlyStore(TestStore):
1919
1920 scenarios = [(key, {'get_store': builder})
1921 for key, builder in test_store_builder_registry.iteritems()]
1922
1923 def setUp(self):
1924 super(TestReadonlyStore, self).setUp()
1925 self.branch = self.make_branch('branch')
1926
1927 def test_building_delays_load(self):
1928 store = self.get_store(self)
1929 self.assertEquals(False, store.loaded)
1930 store._load_from_string('')
1931 self.assertEquals(True, store.loaded)
1932
1933 def test_get_no_sections_for_empty(self):
1934 store = self.get_store(self)
1935 store._load_from_string('')
1936 self.assertEquals([], list(store.get_sections()))
1937
1938 def test_get_default_section(self):
1939 store = self.get_store(self)
1940 store._load_from_string('foo=bar')
1941 sections = list(store.get_sections())
1942 self.assertLength(1, sections)
1943 self.assertSectionContent((None, {'foo': 'bar'}), sections[0])
1944
1945 def test_get_named_section(self):
1946 store = self.get_store(self)
1947 store._load_from_string('[baz]\nfoo=bar')
1948 sections = list(store.get_sections())
1949 self.assertLength(1, sections)
1950 self.assertSectionContent(('baz', {'foo': 'bar'}), sections[0])
1951
1952 def test_load_from_string_fails_for_non_empty_store(self):
1953 store = self.get_store(self)
1954 store._load_from_string('foo=bar')
1955 self.assertRaises(AssertionError, store._load_from_string, 'bar=baz')
1956
1957
1958class TestMutableStore(TestStore):
1959
1960 scenarios = [(key, {'store_id': key, 'get_store': builder})
1961 for key, builder in test_store_builder_registry.iteritems()]
1962
1963 def setUp(self):
1964 super(TestMutableStore, self).setUp()
1965 self.transport = self.get_transport()
1966 self.branch = self.make_branch('branch')
1967
1968 def has_store(self, store):
1969 store_basename = urlutils.relative_url(self.transport.external_url(),
1970 store.external_url())
1971 return self.transport.has(store_basename)
1972
1973 def test_save_empty_creates_no_file(self):
1974 if self.store_id == 'branch':
1975 raise tests.TestNotApplicable(
1976 'branch.conf is *always* created when a branch is initialized')
1977 store = self.get_store(self)
1978 store.save()
1979 self.assertEquals(False, self.has_store(store))
1980
1981 def test_save_emptied_succeeds(self):
1982 store = self.get_store(self)
1983 store._load_from_string('foo=bar\n')
1984 section = store.get_mutable_section(None)
1985 section.remove('foo')
1986 store.save()
1987 self.assertEquals(True, self.has_store(store))
1988 modified_store = self.get_store(self)
1989 sections = list(modified_store.get_sections())
1990 self.assertLength(0, sections)
1991
1992 def test_save_with_content_succeeds(self):
1993 if self.store_id == 'branch':
1994 raise tests.TestNotApplicable(
1995 'branch.conf is *always* created when a branch is initialized')
1996 store = self.get_store(self)
1997 store._load_from_string('foo=bar\n')
1998 self.assertEquals(False, self.has_store(store))
1999 store.save()
2000 self.assertEquals(True, self.has_store(store))
2001 modified_store = self.get_store(self)
2002 sections = list(modified_store.get_sections())
2003 self.assertLength(1, sections)
2004 self.assertSectionContent((None, {'foo': 'bar'}), sections[0])
2005
2006 def test_set_option_in_empty_store(self):
2007 store = self.get_store(self)
2008 section = store.get_mutable_section(None)
2009 section.set('foo', 'bar')
2010 store.save()
2011 modified_store = self.get_store(self)
2012 sections = list(modified_store.get_sections())
2013 self.assertLength(1, sections)
2014 self.assertSectionContent((None, {'foo': 'bar'}), sections[0])
2015
2016 def test_set_option_in_default_section(self):
2017 store = self.get_store(self)
2018 store._load_from_string('')
2019 section = store.get_mutable_section(None)
2020 section.set('foo', 'bar')
2021 store.save()
2022 modified_store = self.get_store(self)
2023 sections = list(modified_store.get_sections())
2024 self.assertLength(1, sections)
2025 self.assertSectionContent((None, {'foo': 'bar'}), sections[0])
2026
2027 def test_set_option_in_named_section(self):
2028 store = self.get_store(self)
2029 store._load_from_string('')
2030 section = store.get_mutable_section('baz')
2031 section.set('foo', 'bar')
2032 store.save()
2033 modified_store = self.get_store(self)
2034 sections = list(modified_store.get_sections())
2035 self.assertLength(1, sections)
2036 self.assertSectionContent(('baz', {'foo': 'bar'}), sections[0])
2037
2038
2039class TestConfigObjStore(TestStore):
2040
2041 def test_loading_unknown_file_fails(self):
2042 store = config.ConfigObjStore(self.get_transport(), 'I-do-not-exist')
2043 self.assertRaises(errors.NoSuchFile, store.load)
2044
2045 def test_invalid_content(self):
2046 store = config.ConfigObjStore(self.get_transport(), 'foo.conf', )
2047 self.assertEquals(False, store.loaded)
2048 exc = self.assertRaises(
2049 errors.ParseConfigError, store._load_from_string,
2050 'this is invalid !')
2051 self.assertEndsWith(exc.filename, 'foo.conf')
2052 # And the load failed
2053 self.assertEquals(False, store.loaded)
2054
2055 def test_get_embedded_sections(self):
2056 # A more complicated example (which also shows that section names and
2057 # option names share the same name space...)
2058 # FIXME: This should be fixed by forbidding dicts as values ?
2059 # -- vila 2011-04-05
2060 store = config.ConfigObjStore(self.get_transport(), 'foo.conf', )
2061 store._load_from_string('''
2062foo=bar
2063l=1,2
2064[DEFAULT]
2065foo_in_DEFAULT=foo_DEFAULT
2066[bar]
2067foo_in_bar=barbar
2068[baz]
2069foo_in_baz=barbaz
2070[[qux]]
2071foo_in_qux=quux
2072''')
2073 sections = list(store.get_sections())
2074 self.assertLength(4, sections)
2075 # The default section has no name.
2076 # List values are provided as lists
2077 self.assertSectionContent((None, {'foo': 'bar', 'l': ['1', '2']}),
2078 sections[0])
2079 self.assertSectionContent(
2080 ('DEFAULT', {'foo_in_DEFAULT': 'foo_DEFAULT'}), sections[1])
2081 self.assertSectionContent(
2082 ('bar', {'foo_in_bar': 'barbar'}), sections[2])
2083 # sub sections are provided as embedded dicts.
2084 self.assertSectionContent(
2085 ('baz', {'foo_in_baz': 'barbaz', 'qux': {'foo_in_qux': 'quux'}}),
2086 sections[3])
2087
2088
2089class TestLockableConfigObjStore(TestStore):
2090
2091 def test_create_store_in_created_dir(self):
2092 t = self.get_transport('dir/subdir')
2093 store = config.LockableConfigObjStore(t, 'foo.conf')
2094 store.get_mutable_section(None).set('foo', 'bar')
2095 store.save()
2096
2097 # FIXME: We should adapt the tests in TestLockableConfig about concurrent
2098 # writes. Since this requires a clearer rewrite, I'll just rely on using
2099 # the same code in LockableConfigObjStore (copied from LockableConfig, but
2100 # trivial enough, the main difference is that we add @needs_write_lock on
2101 # save() instead of set_user_option() and remove_user_option()). The intent
2102 # is to ensure that we always get a valid content for the store even when
2103 # concurrent accesses occur, read/write, write/write. It may be worth
2104 # looking into removing the lock dir when it;s not needed anymore and look
2105 # at possible fallouts for concurrent lockers -- vila 20110-04-06
2106
2107
2108class TestSectionMatcher(TestStore):
2109
2110 scenarios = [('location', {'matcher': config.LocationMatcher})]
2111
2112 def get_store(self, file_name):
2113 return config.ConfigObjStore(self.get_readonly_transport(), file_name)
2114
2115 def test_no_matches_for_empty_stores(self):
2116 store = self.get_store('foo.conf')
2117 store._load_from_string('')
2118 matcher = self.matcher(store, '/bar')
2119 self.assertEquals([], list(matcher.get_sections()))
2120
2121 def test_build_doesnt_load_store(self):
2122 store = self.get_store('foo.conf')
2123 matcher = self.matcher(store, '/bar')
2124 self.assertFalse(store.loaded)
2125
2126
2127class TestLocationSection(tests.TestCase):
2128
2129 def get_section(self, options, extra_path):
2130 section = config.ReadOnlySection('foo', options)
2131 # We don't care about the length so we use '0'
2132 return config.LocationSection(section, 0, extra_path)
2133
2134 def test_simple_option(self):
2135 section = self.get_section({'foo': 'bar'}, '')
2136 self.assertEquals('bar', section.get('foo'))
2137
2138 def test_option_with_extra_path(self):
2139 section = self.get_section({'foo': 'bar', 'foo:policy': 'appendpath'},
2140 'baz')
2141 self.assertEquals('bar/baz', section.get('foo'))
2142
2143 def test_invalid_policy(self):
2144 section = self.get_section({'foo': 'bar', 'foo:policy': 'die'},
2145 'baz')
2146 # invalid policies are ignored
2147 self.assertEquals('bar', section.get('foo'))
2148
2149
2150class TestLocationMatcher(TestStore):
2151
2152 def get_store(self, file_name):
2153 return config.ConfigObjStore(self.get_readonly_transport(), file_name)
2154
2155 def test_more_specific_sections_first(self):
2156 store = self.get_store('foo.conf')
2157 store._load_from_string('''
2158[/foo]
2159section=/foo
2160[/foo/bar]
2161section=/foo/bar
2162''')
2163 self.assertEquals(['/foo', '/foo/bar'],
2164 [section.id for section in store.get_sections()])
2165 matcher = config.LocationMatcher(store, '/foo/bar/baz')
2166 sections = list(matcher.get_sections())
2167 self.assertEquals([3, 2],
2168 [section.length for section in sections])
2169 self.assertEquals(['/foo/bar', '/foo'],
2170 [section.id for section in sections])
2171 self.assertEquals(['baz', 'bar/baz'],
2172 [section.extra_path for section in sections])
2173
2174
2175class TestConfigStackGet(tests.TestCase):
2176
2177 # FIXME: This should be parametrized for all known ConfigStack or dedicated
2178 # paramerized tests created to avoid bloating -- vila 2011-03-31
2179
2180 def test_single_config_get(self):
2181 conf = dict(foo='bar')
2182 conf_stack = config.ConfigStack([conf])
2183 self.assertEquals('bar', conf_stack.get('foo'))
2184
2185 def test_get_first_definition(self):
2186 conf1 = dict(foo='bar')
2187 conf2 = dict(foo='baz')
2188 conf_stack = config.ConfigStack([conf1, conf2])
2189 self.assertEquals('bar', conf_stack.get('foo'))
2190
2191 def test_get_embedded_definition(self):
2192 conf1 = dict(yy='12')
2193 conf2 = config.ConfigStack([dict(xx='42'), dict(foo='baz')])
2194 conf_stack = config.ConfigStack([conf1, conf2])
2195 self.assertEquals('baz', conf_stack.get('foo'))
2196
2197 def test_get_for_empty_stack(self):
2198 conf_stack = config.ConfigStack()
2199 self.assertEquals(None, conf_stack.get('foo'))
2200
2201 def test_get_for_empty_section_callable(self):
2202 conf_stack = config.ConfigStack([lambda : []])
2203 self.assertEquals(None, conf_stack.get('foo'))
2204
2205
2206class TestConfigStackSet(tests.TestCaseWithTransport):
2207
2208 # FIXME: This should be parametrized for all known ConfigStack or dedicated
2209 # paramerized tests created to avoid bloating -- vila 2011-04-05
2210
2211 def test_simple_set(self):
2212 store = config.ConfigObjStore(self.get_transport(), 'test.conf')
2213 store._load_from_string('foo=bar')
2214 conf = config.ConfigStack(
2215 [store.get_sections], store.get_mutable_section)
2216 self.assertEquals('bar', conf.get('foo'))
2217 conf.set('foo', 'baz')
2218 # Did we get it back ?
2219 self.assertEquals('baz', conf.get('foo'))
2220
2221 def test_set_creates_a_new_section(self):
2222 store = config.ConfigObjStore(self.get_transport(), 'test.conf')
2223 conf = config.ConfigStack(
2224 [store.get_sections], store.get_mutable_section)
2225 conf.set('foo', 'baz')
2226 self.assertEquals, 'baz', conf.get('foo')
2227
2228
2229class TestConfigStackRemove(tests.TestCaseWithTransport):
2230
2231 # FIXME: This should be parametrized for all known ConfigStack or dedicated
2232 # paramerized tests created to avoid bloating -- vila 2011-04-06
2233
2234 def test_remove_existing(self):
2235 store = config.ConfigObjStore(self.get_transport(), 'test.conf')
2236 store._load_from_string('foo=bar')
2237 conf = config.ConfigStack(
2238 [store.get_sections], store.get_mutable_section)
2239 self.assertEquals('bar', conf.get('foo'))
2240 conf.remove('foo')
2241 # Did we get it back ?
2242 self.assertEquals(None, conf.get('foo'))
2243
2244 def test_remove_unknown(self):
2245 store = config.ConfigObjStore(self.get_transport(), 'test.conf')
2246 conf = config.ConfigStack(
2247 [store.get_sections], store.get_mutable_section)
2248 self.assertRaises(KeyError, conf.remove, 'I_do_not_exist')
2249
2250
2251class TestConcreteStacks(tests.TestCaseWithTransport):
2252
2253 # basic smoke tests
2254
2255 def test_global_stack(self):
2256 stack = config.GlobalStack()
2257
2258 def test_location_store(self):
2259 stack = config.LocationStack('.')
2260
2261 def test_branch_store(self):
2262 b = self.make_branch('.')
2263 stack = config.BranchStack(b)
2264
2265
1821class TestConfigGetOptions(tests.TestCaseWithTransport, TestOptionsMixin):2266class TestConfigGetOptions(tests.TestCaseWithTransport, TestOptionsMixin):
18222267
1823 def setUp(self):2268 def setUp(self):
1824 super(TestConfigGetOptions, self).setUp()2269 super(TestConfigGetOptions, self).setUp()
1825 create_configs(self)2270 create_configs(self)
18262271
1827 # One variable in none of the above
1828 def test_no_variable(self):2272 def test_no_variable(self):
1829 # Using branch should query branch, locations and bazaar2273 # Using branch should query branch, locations and bazaar
1830 self.assertOptions([], self.branch_config)2274 self.assertOptions([], self.branch_config)
18312275
=== added file 'doc/developers/configuration.txt'
--- doc/developers/configuration.txt 1970-01-01 00:00:00 +0000
+++ doc/developers/configuration.txt 2011-04-12 12:36:46 +0000
@@ -0,0 +1,102 @@
1Configuring Bazaar
2==================
3
4A configuration option has:
5
6- a name: a valid python identifier (even if it's not used as an
7 identifier in python itself)
8
9- a value: a unicode string
10
11Sections
12--------
13
14Options are grouped into sections which share some properties with the well
15known dict objects:
16
17- the key is the name,
18- you can get, set and remove an option,
19- the value is a unicode string.
20
21MutableSection are needed to set or remove an option, ReadOnlySection should
22be used otherwise.
23
24Stores
25------
26
27Options can persistent in which case they are saved into Stores.
28
29``config.Store`` defines the abstract interface that all stores should
30implement.
31
32This object doesn't provide a direct access to the options, it only provides
33access to Sections. This is deliberate to ensure that sections can be properly
34shared by reusing the same underlying objects. Accessing options should be
35done via the ``Section`` objects.
36
37A ``Store`` can contain one or more sections, each section is uniquely
38identified by a unicode string.
39
40``config.ConfigObjStore`` is an implementation that use ``ConfigObj``.
41
42Depending on the object it is associated with (or not) a ``Store`` also needs
43to implement a locking mechanism. ``LockableConfigObjStore`` implements such a
44mechanism for ``ConfigObj`` based stores.
45
46Classes are provided for the usual Bazaar configuration files and could be
47used as examples to define new ones if needed. The associated tests provides a
48basis for new classes which only need to register themselves in the right
49places to inherit from the existing basic tests and add their own specific
50ones.
51
52Filtering sections
53------------------
54
55For some contexts, only some sections from a given store will apply. Defining
56which is what the ``SectionMatcher`` are about.
57
58The main constraint here is that a ``SectionMatcher`` should delay the loading
59of the associated store as long as possible. The constructor should collect
60all data needed for the selection and uses it while processing the sections in
61``get_sections``.
62
63Only ``ReadOnlySection`` objects are manipulated here but a ``SectionMatcher``
64can return dedicated ``Section`` to provide additional context (the
65``LocationSection`` add an ``extra_path`` attribute to implement the
66``appendpath`` policy for example).
67
68.. FIXME: Replace the appendpath example if/when it's deprecated ;)
69
70Stacks
71------
72
73An option can take different values depending on the context it is used. Such
74a context can involve configuration files, options from the command line,
75default values in bzrlib and then some.
76
77Such a context is implemented by creating a list of ``Section`` stacked upon
78each other. A ``Stack`` can then be asked for an option value and returns the
79first definition found.
80
81This provides a great flexibility to decide priorities between sections when
82the stack is defined without to worry about them in the code itself.
83
84A stack also defines a mutable section (which can be None) to handle
85modifications.
86
87Many sections (or even stores) are aimed at providing default values for an
88option but these sections shouldn't be modified lightly as modifying an option
89used for different contexts will indeed be seen by all these contexts.
90
91Default values in configuration files are defined by users, developers
92shouldn't have to modify them, as such, no mechanism nor heuristics are used
93to find which section (or sections) should be modified, a ``Stack`` defines a
94mutable section when there is no ambiguity, if there is one, then the *user*
95should be able to decide and this case a new ``Stack`` can be created cheaply.
96
97Different stacks can be created for different purposes, the existing
98``Global.Stack``, ``LocationStack`` and ``BranchStack`` can be used as basis
99or examples. These classes are the only ones that should be used in code,
100``Stores`` can be used to build them but shouldn't be used otherwise, ditto
101for sections. Again, the associated tests could and should be used against the
102created stacks.
0103
=== modified file 'doc/developers/index.txt'
--- doc/developers/index.txt 2011-01-06 06:26:23 +0000
+++ doc/developers/index.txt 2011-04-12 12:36:46 +0000
@@ -36,9 +36,10 @@
36.. toctree::36.. toctree::
37 :maxdepth: 137 :maxdepth: 1
3838
39 configuration
40 fetch
39 transports41 transports
40 ui42 ui
41 fetch
4243
43Releasing and Packaging44Releasing and Packaging
44=======================45=======================
4546
=== modified file 'doc/en/user-guide/configuring_bazaar.txt'
--- doc/en/user-guide/configuring_bazaar.txt 2011-02-07 01:39:42 +0000
+++ doc/en/user-guide/configuring_bazaar.txt 2011-04-12 12:36:46 +0000
@@ -37,6 +37,24 @@
37which shouldn't be reached by the proxy. (See37which shouldn't be reached by the proxy. (See
38<http://docs.python.org/library/urllib.html> for more details.)38<http://docs.python.org/library/urllib.html> for more details.)
3939
40Various ways to configure
41-------------------------
42
43As shown in the example above, there are various ways to
44configure Bazaar, they all share some common properties though,
45an option has:
46
47- a name which is generally a valid python identifier,
48
49- a value which is a string. In some cases, Bazzar will be able
50 to recognize special values like 'True', 'False' to infer a
51 boolean type, but basically, as a user, you will always specify
52 a value as a string.
53
54Options are grouped in various contexts so their name uniquely
55identify them in this context. When needed, options can be made
56persistent by recording them in a configuration file.
57
4058
41Configuration files59Configuration files
42-------------------60-------------------