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 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
bzr-core | Pending | ||
Review via email: mp+56887@code.launchpad.net |
Commit message
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-
config-stack
config-
config-
config-
config-section
refactor-
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(
with
config.
or maybe even
config.
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.
Martin Pool (mbp) wrote : | # |
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_
> 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.
Martin Pool (mbp) 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.
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 ...
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.
>
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.
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_
- 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
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 | 848 | # LockableConfig for other kind of transports, we will need to reuse | 848 | # LockableConfig for other kind of transports, we will need to reuse |
6 | 849 | # whatever connection is already established -- vila 20100929 | 849 | # whatever connection is already established -- vila 20100929 |
7 | 850 | self.transport = transport.get_transport(self.dir) | 850 | self.transport = transport.get_transport(self.dir) |
9 | 851 | self._lock = lockdir.LockDir(self.transport, 'lock') | 851 | self._lock = lockdir.LockDir(self.transport, self.lock_name) |
10 | 852 | 852 | ||
11 | 853 | def _create_from_string(self, unicode_bytes, save): | 853 | def _create_from_string(self, unicode_bytes, save): |
12 | 854 | super(LockableConfig, self)._create_from_string(unicode_bytes, False) | 854 | super(LockableConfig, self)._create_from_string(unicode_bytes, False) |
13 | @@ -967,6 +967,61 @@ | |||
14 | 967 | super(LockableConfig, self).remove_user_option(option_name, | 967 | super(LockableConfig, self).remove_user_option(option_name, |
15 | 968 | section_name) | 968 | section_name) |
16 | 969 | 969 | ||
17 | 970 | def _iter_for_location_by_parts(sections, location): | ||
18 | 971 | """Keep only the sessions matching the specified location. | ||
19 | 972 | |||
20 | 973 | :param sections: An iterable of section names. | ||
21 | 974 | |||
22 | 975 | :param location: An url or a local path to match against. | ||
23 | 976 | |||
24 | 977 | :returns: An iterator of (section, extra_path, nb_parts) where nb is the | ||
25 | 978 | number of path components in the section name, section is the section | ||
26 | 979 | name and extra_path is the difference between location and the section | ||
27 | 980 | name. | ||
28 | 981 | """ | ||
29 | 982 | location_parts = location.rstrip('/').split('/') | ||
30 | 983 | |||
31 | 984 | for section in sections: | ||
32 | 985 | # location is a local path if possible, so we need | ||
33 | 986 | # to convert 'file://' urls to local paths if necessary. | ||
34 | 987 | |||
35 | 988 | # FIXME: I don't think the above comment is still up to date, | ||
36 | 989 | # LocationConfig is always instantiated with an url -- vila 2011-04-07 | ||
37 | 990 | |||
38 | 991 | # This also avoids having file:///path be a more exact | ||
39 | 992 | # match than '/path'. | ||
40 | 993 | |||
41 | 994 | # FIXME: Not sure about the above either, but since the path components | ||
42 | 995 | # are compared in sync, adding two empty components (//) is likely to | ||
43 | 996 | # trick the comparison and also trick the check on the number of | ||
44 | 997 | # components, so we *should* take only the relevant part of the url. On | ||
45 | 998 | # the other hand, this means 'file://' urls *can't* be used in sections | ||
46 | 999 | # so more work is probably needed -- vila 2011-04-07 | ||
47 | 1000 | |||
48 | 1001 | if section.startswith('file://'): | ||
49 | 1002 | section_path = urlutils.local_path_from_url(section) | ||
50 | 1003 | else: | ||
51 | 1004 | section_path = section | ||
52 | 1005 | section_parts = section_path.rstrip('/').split('/') | ||
53 | 1006 | |||
54 | 1007 | matched = True | ||
55 | 1008 | if len(section_parts) > len(location_parts): | ||
56 | 1009 | # More path components in the section, they can't match | ||
57 | 1010 | matched = False | ||
58 | 1011 | else: | ||
59 | 1012 | # Rely on zip truncating in length to the length of the shortest | ||
60 | 1013 | # argument sequence. | ||
61 | 1014 | names = zip(location_parts, section_parts) | ||
62 | 1015 | for name in names: | ||
63 | 1016 | if not fnmatch.fnmatch(name[0], name[1]): | ||
64 | 1017 | matched = False | ||
65 | 1018 | break | ||
66 | 1019 | if not matched: | ||
67 | 1020 | continue | ||
68 | 1021 | # build the path difference between the section and the location | ||
69 | 1022 | extra_path = '/'.join(location_parts[len(section_parts):]) | ||
70 | 1023 | yield section, extra_path, len(section_parts) | ||
71 | 1024 | |||
72 | 970 | 1025 | ||
73 | 971 | class LocationConfig(LockableConfig): | 1026 | class LocationConfig(LockableConfig): |
74 | 972 | """A configuration object that gives the policy for a location.""" | 1027 | """A configuration object that gives the policy for a location.""" |
75 | @@ -1001,49 +1056,20 @@ | |||
76 | 1001 | 1056 | ||
77 | 1002 | def _get_matching_sections(self): | 1057 | def _get_matching_sections(self): |
78 | 1003 | """Return an ordered list of section names matching this location.""" | 1058 | """Return an ordered list of section names matching this location.""" |
110 | 1004 | sections = self._get_parser() | 1059 | matches = list(_iter_for_location_by_parts(self._get_parser(), |
111 | 1005 | location_names = self.location.split('/') | 1060 | self.location)) |
81 | 1006 | if self.location.endswith('/'): | ||
82 | 1007 | del location_names[-1] | ||
83 | 1008 | matches=[] | ||
84 | 1009 | for section in sections: | ||
85 | 1010 | # location is a local path if possible, so we need | ||
86 | 1011 | # to convert 'file://' urls to local paths if necessary. | ||
87 | 1012 | # This also avoids having file:///path be a more exact | ||
88 | 1013 | # match than '/path'. | ||
89 | 1014 | if section.startswith('file://'): | ||
90 | 1015 | section_path = urlutils.local_path_from_url(section) | ||
91 | 1016 | else: | ||
92 | 1017 | section_path = section | ||
93 | 1018 | section_names = section_path.split('/') | ||
94 | 1019 | if section.endswith('/'): | ||
95 | 1020 | del section_names[-1] | ||
96 | 1021 | names = zip(location_names, section_names) | ||
97 | 1022 | matched = True | ||
98 | 1023 | for name in names: | ||
99 | 1024 | if not fnmatch.fnmatch(name[0], name[1]): | ||
100 | 1025 | matched = False | ||
101 | 1026 | break | ||
102 | 1027 | if not matched: | ||
103 | 1028 | continue | ||
104 | 1029 | # so, for the common prefix they matched. | ||
105 | 1030 | # if section is longer, no match. | ||
106 | 1031 | if len(section_names) > len(location_names): | ||
107 | 1032 | continue | ||
108 | 1033 | matches.append((len(section_names), section, | ||
109 | 1034 | '/'.join(location_names[len(section_names):]))) | ||
112 | 1035 | # put the longest (aka more specific) locations first | 1061 | # put the longest (aka more specific) locations first |
117 | 1036 | matches.sort(reverse=True) | 1062 | matches.sort( |
118 | 1037 | sections = [] | 1063 | key=lambda (section, extra_path, length): (length, section), |
119 | 1038 | for (length, section, extra_path) in matches: | 1064 | reverse=True) |
120 | 1039 | sections.append((section, extra_path)) | 1065 | for (section, extra_path, length) in matches: |
121 | 1066 | yield section, extra_path | ||
122 | 1040 | # should we stop looking for parent configs here? | 1067 | # should we stop looking for parent configs here? |
123 | 1041 | try: | 1068 | try: |
124 | 1042 | if self._get_parser()[section].as_bool('ignore_parents'): | 1069 | if self._get_parser()[section].as_bool('ignore_parents'): |
125 | 1043 | break | 1070 | break |
126 | 1044 | except KeyError: | 1071 | except KeyError: |
127 | 1045 | pass | 1072 | pass |
128 | 1046 | return sections | ||
129 | 1047 | 1073 | ||
130 | 1048 | def _get_sections(self, name=None): | 1074 | def _get_sections(self, name=None): |
131 | 1049 | """See IniBasedConfig._get_sections().""" | 1075 | """See IniBasedConfig._get_sections().""" |
132 | @@ -2068,6 +2094,422 @@ | |||
133 | 2068 | self._transport.put_file(self._filename, out_file) | 2094 | self._transport.put_file(self._filename, out_file) |
134 | 2069 | 2095 | ||
135 | 2070 | 2096 | ||
136 | 2097 | class ReadOnlySection(object): | ||
137 | 2098 | """A section defines a dict of options. | ||
138 | 2099 | |||
139 | 2100 | This is merely a read-only dict which can add some knowledge about the | ||
140 | 2101 | options. | ||
141 | 2102 | """ | ||
142 | 2103 | |||
143 | 2104 | def __init__(self, section_id, options): | ||
144 | 2105 | self.id = section_id | ||
145 | 2106 | # We re-use the dict-like object received | ||
146 | 2107 | self.options = options | ||
147 | 2108 | |||
148 | 2109 | def get(self, name, default=None): | ||
149 | 2110 | return self.options.get(name, default) | ||
150 | 2111 | |||
151 | 2112 | |||
152 | 2113 | _NewlyCreatedOption = object() | ||
153 | 2114 | """Was the option created during the MutableSection lifetime""" | ||
154 | 2115 | |||
155 | 2116 | |||
156 | 2117 | class MutableSection(ReadOnlySection): | ||
157 | 2118 | """A section allowing changes and keeping track of the original values.""" | ||
158 | 2119 | |||
159 | 2120 | def __init__(self, section_id, options): | ||
160 | 2121 | super(MutableSection, self).__init__(section_id, options) | ||
161 | 2122 | self.orig = {} | ||
162 | 2123 | |||
163 | 2124 | def set(self, name, value): | ||
164 | 2125 | if name not in self.options: | ||
165 | 2126 | # This is a new option | ||
166 | 2127 | self.orig[name] = _NewlyCreatedOption | ||
167 | 2128 | elif name not in self.orig: | ||
168 | 2129 | self.orig[name] = self.get(name, None) | ||
169 | 2130 | self.options[name] = value | ||
170 | 2131 | |||
171 | 2132 | def remove(self, name): | ||
172 | 2133 | if name not in self.orig: | ||
173 | 2134 | self.orig[name] = self.get(name, None) | ||
174 | 2135 | del self.options[name] | ||
175 | 2136 | |||
176 | 2137 | |||
177 | 2138 | class Store(object): | ||
178 | 2139 | """Abstract interface to persistent storage for configuration options.""" | ||
179 | 2140 | |||
180 | 2141 | readonly_section_class = ReadOnlySection | ||
181 | 2142 | mutable_section_class = MutableSection | ||
182 | 2143 | |||
183 | 2144 | @property | ||
184 | 2145 | def loaded(self): | ||
185 | 2146 | raise NotImplementedError(self.loaded) | ||
186 | 2147 | |||
187 | 2148 | def load(self): | ||
188 | 2149 | raise NotImplementedError(self.load) | ||
189 | 2150 | |||
190 | 2151 | def _load_from_string(self, str_or_unicode): | ||
191 | 2152 | """Create a store from a string in configobj syntax. | ||
192 | 2153 | |||
193 | 2154 | :param str_or_unicode: A string representing the file content. This will | ||
194 | 2155 | be encoded to suit store needs internally. | ||
195 | 2156 | |||
196 | 2157 | This is for tests and should not be used in production unless a | ||
197 | 2158 | convincing use case can be demonstrated :) | ||
198 | 2159 | """ | ||
199 | 2160 | raise NotImplementedError(self._load_from_string) | ||
200 | 2161 | |||
201 | 2162 | def save(self): | ||
202 | 2163 | raise NotImplementedError(self.save) | ||
203 | 2164 | |||
204 | 2165 | def external_url(self): | ||
205 | 2166 | raise NotImplementedError(self.external_url) | ||
206 | 2167 | |||
207 | 2168 | def get_sections(self): | ||
208 | 2169 | """Returns an ordered iterable of existing sections. | ||
209 | 2170 | |||
210 | 2171 | :returns: An iterable of (name, dict). | ||
211 | 2172 | """ | ||
212 | 2173 | raise NotImplementedError(self.get_sections) | ||
213 | 2174 | |||
214 | 2175 | def get_mutable_section(self, section_name=None): | ||
215 | 2176 | """Returns the specified mutable section. | ||
216 | 2177 | |||
217 | 2178 | :param section_name: The section identifier | ||
218 | 2179 | """ | ||
219 | 2180 | raise NotImplementedError(self.get_mutable_section) | ||
220 | 2181 | |||
221 | 2182 | |||
222 | 2183 | class ConfigObjStore(Store): | ||
223 | 2184 | |||
224 | 2185 | def __init__(self, transport, file_name): | ||
225 | 2186 | """A config Store using ConfigObj for storage. | ||
226 | 2187 | |||
227 | 2188 | :param transport: The transport object where the config file is located. | ||
228 | 2189 | |||
229 | 2190 | :param file_name: The config file basename in the transport directory. | ||
230 | 2191 | """ | ||
231 | 2192 | super(ConfigObjStore, self).__init__() | ||
232 | 2193 | self.transport = transport | ||
233 | 2194 | self.file_name = file_name | ||
234 | 2195 | self._config_obj = None | ||
235 | 2196 | |||
236 | 2197 | @property | ||
237 | 2198 | def loaded(self): | ||
238 | 2199 | return self._config_obj != None | ||
239 | 2200 | |||
240 | 2201 | def load(self): | ||
241 | 2202 | """Load the store from the associated file.""" | ||
242 | 2203 | if self.loaded: | ||
243 | 2204 | return | ||
244 | 2205 | content = self.transport.get_bytes(self.file_name) | ||
245 | 2206 | self._load_from_string(content) | ||
246 | 2207 | |||
247 | 2208 | def _load_from_string(self, str_or_unicode): | ||
248 | 2209 | """Create a config store from a string. | ||
249 | 2210 | |||
250 | 2211 | :param str_or_unicode: A string representing the file content. This will | ||
251 | 2212 | be utf-8 encoded internally. | ||
252 | 2213 | |||
253 | 2214 | This is for tests and should not be used in production unless a | ||
254 | 2215 | convincing use case can be demonstrated :) | ||
255 | 2216 | """ | ||
256 | 2217 | if self.loaded: | ||
257 | 2218 | raise AssertionError('Already loaded: %r' % (self._config_obj,)) | ||
258 | 2219 | co_input = StringIO(str_or_unicode.encode('utf-8')) | ||
259 | 2220 | try: | ||
260 | 2221 | # The config files are always stored utf8-encoded | ||
261 | 2222 | self._config_obj = ConfigObj(co_input, encoding='utf-8') | ||
262 | 2223 | except configobj.ConfigObjError, e: | ||
263 | 2224 | self._config_obj = None | ||
264 | 2225 | raise errors.ParseConfigError(e.errors, self.external_url()) | ||
265 | 2226 | |||
266 | 2227 | def save(self): | ||
267 | 2228 | if not self.loaded: | ||
268 | 2229 | # Nothing to save | ||
269 | 2230 | return | ||
270 | 2231 | out = StringIO() | ||
271 | 2232 | self._config_obj.write(out) | ||
272 | 2233 | self.transport.put_bytes(self.file_name, out.getvalue()) | ||
273 | 2234 | |||
274 | 2235 | def external_url(self): | ||
275 | 2236 | # FIXME: external_url should really accepts an optional relpath | ||
276 | 2237 | # parameter (bug #750169) :-/ -- vila 2011-04-04 | ||
277 | 2238 | # The following will do in the interim but maybe we don't want to | ||
278 | 2239 | # expose a path here but rather a config ID and its associated | ||
279 | 2240 | # object </hand wawe>. | ||
280 | 2241 | return os.path.join(self.transport.external_url(), self.file_name) | ||
281 | 2242 | |||
282 | 2243 | def get_sections(self): | ||
283 | 2244 | """Get the configobj section in the file order. | ||
284 | 2245 | |||
285 | 2246 | :returns: An iterable of (name, dict). | ||
286 | 2247 | """ | ||
287 | 2248 | # We need a loaded store | ||
288 | 2249 | self.load() | ||
289 | 2250 | cobj = self._config_obj | ||
290 | 2251 | if cobj.scalars: | ||
291 | 2252 | yield self.readonly_section_class(None, cobj) | ||
292 | 2253 | for section_name in cobj.sections: | ||
293 | 2254 | yield self.readonly_section_class(section_name, cobj[section_name]) | ||
294 | 2255 | |||
295 | 2256 | def get_mutable_section(self, section_name=None): | ||
296 | 2257 | # We need a loaded store | ||
297 | 2258 | try: | ||
298 | 2259 | self.load() | ||
299 | 2260 | except errors.NoSuchFile: | ||
300 | 2261 | # The file doesn't exist, let's pretend it was empty | ||
301 | 2262 | self._load_from_string('') | ||
302 | 2263 | if section_name is None: | ||
303 | 2264 | section = self._config_obj | ||
304 | 2265 | else: | ||
305 | 2266 | section = self._config_obj.setdefault(section_name, {}) | ||
306 | 2267 | return self.mutable_section_class(section_name, section) | ||
307 | 2268 | |||
308 | 2269 | |||
309 | 2270 | # Note that LockableConfigObjStore inherits from ConfigObjStore because we need | ||
310 | 2271 | # unlockable stores for use with objects that can already ensure the locking | ||
311 | 2272 | # (think branches). If different stores (not based on ConfigObj) are created, | ||
312 | 2273 | # they may face the same issue. | ||
313 | 2274 | |||
314 | 2275 | |||
315 | 2276 | class LockableConfigObjStore(ConfigObjStore): | ||
316 | 2277 | """A ConfigObjStore using locks on save to ensure store integrity.""" | ||
317 | 2278 | |||
318 | 2279 | def __init__(self, transport, file_name, lock_dir_name=None): | ||
319 | 2280 | """A config Store using ConfigObj for storage. | ||
320 | 2281 | |||
321 | 2282 | :param transport: The transport object where the config file is located. | ||
322 | 2283 | |||
323 | 2284 | :param file_name: The config file basename in the transport directory. | ||
324 | 2285 | """ | ||
325 | 2286 | if lock_dir_name is None: | ||
326 | 2287 | lock_dir_name = 'lock' | ||
327 | 2288 | self.lock_dir_name = lock_dir_name | ||
328 | 2289 | super(LockableConfigObjStore, self).__init__(transport, file_name) | ||
329 | 2290 | self._lock = lockdir.LockDir(self.transport, self.lock_dir_name) | ||
330 | 2291 | |||
331 | 2292 | def lock_write(self, token=None): | ||
332 | 2293 | """Takes a write lock in the directory containing the config file. | ||
333 | 2294 | |||
334 | 2295 | If the directory doesn't exist it is created. | ||
335 | 2296 | """ | ||
336 | 2297 | # FIXME: This doesn't check the ownership of the created directories as | ||
337 | 2298 | # ensure_config_dir_exists does. It should if the transport is local | ||
338 | 2299 | # -- vila 2011-04-06 | ||
339 | 2300 | self.transport.create_prefix() | ||
340 | 2301 | return self._lock.lock_write(token) | ||
341 | 2302 | |||
342 | 2303 | def unlock(self): | ||
343 | 2304 | self._lock.unlock() | ||
344 | 2305 | |||
345 | 2306 | def break_lock(self): | ||
346 | 2307 | self._lock.break_lock() | ||
347 | 2308 | |||
348 | 2309 | @needs_write_lock | ||
349 | 2310 | def save(self): | ||
350 | 2311 | super(LockableConfigObjStore, self).save() | ||
351 | 2312 | |||
352 | 2313 | |||
353 | 2314 | # FIXME: global, bazaar, shouldn't that be 'user' instead or even | ||
354 | 2315 | # 'user_defaults' as opposed to 'user_overrides', 'system_defaults' | ||
355 | 2316 | # (/etc/bzr/bazaar.conf) and 'system_overrides' ? -- vila 2011-04-05 | ||
356 | 2317 | class GlobalStore(LockableConfigObjStore): | ||
357 | 2318 | |||
358 | 2319 | def __init__(self, possible_transports=None): | ||
359 | 2320 | t = transport.get_transport(config_dir(), | ||
360 | 2321 | possible_transports=possible_transports) | ||
361 | 2322 | super(GlobalStore, self).__init__(t, 'bazaar.conf') | ||
362 | 2323 | |||
363 | 2324 | |||
364 | 2325 | class LocationStore(LockableConfigObjStore): | ||
365 | 2326 | |||
366 | 2327 | def __init__(self, possible_transports=None): | ||
367 | 2328 | t = transport.get_transport(config_dir(), | ||
368 | 2329 | possible_transports=possible_transports) | ||
369 | 2330 | super(LocationStore, self).__init__(t, 'locations.conf') | ||
370 | 2331 | |||
371 | 2332 | |||
372 | 2333 | class BranchStore(ConfigObjStore): | ||
373 | 2334 | |||
374 | 2335 | def __init__(self, branch): | ||
375 | 2336 | super(BranchStore, self).__init__(branch.control_transport, | ||
376 | 2337 | 'branch.conf') | ||
377 | 2338 | |||
378 | 2339 | class SectionMatcher(object): | ||
379 | 2340 | """Select sections into a given Store. | ||
380 | 2341 | |||
381 | 2342 | This intended to be used to postpone getting an iterable of sections from a | ||
382 | 2343 | store. | ||
383 | 2344 | """ | ||
384 | 2345 | |||
385 | 2346 | def __init__(self, store): | ||
386 | 2347 | self.store = store | ||
387 | 2348 | |||
388 | 2349 | def get_sections(self): | ||
389 | 2350 | # This is where we require loading the store so we can see all defined | ||
390 | 2351 | # sections. | ||
391 | 2352 | sections = self.store.get_sections() | ||
392 | 2353 | # Walk the revisions in the order provided | ||
393 | 2354 | for s in sections: | ||
394 | 2355 | if self.match(s): | ||
395 | 2356 | yield s | ||
396 | 2357 | |||
397 | 2358 | def match(self, secion): | ||
398 | 2359 | raise NotImplementedError(self.match) | ||
399 | 2360 | |||
400 | 2361 | |||
401 | 2362 | class LocationSection(ReadOnlySection): | ||
402 | 2363 | |||
403 | 2364 | def __init__(self, section, length, extra_path): | ||
404 | 2365 | super(LocationSection, self).__init__(section.id, section.options) | ||
405 | 2366 | self.length = length | ||
406 | 2367 | self.extra_path = extra_path | ||
407 | 2368 | |||
408 | 2369 | def get(self, name, default=None): | ||
409 | 2370 | value = super(LocationSection, self).get(name, default) | ||
410 | 2371 | if value is not None: | ||
411 | 2372 | policy_name = self.get(name + ':policy', None) | ||
412 | 2373 | policy = _policy_value.get(policy_name, POLICY_NONE) | ||
413 | 2374 | if policy == POLICY_APPENDPATH: | ||
414 | 2375 | value = urlutils.join(value, self.extra_path) | ||
415 | 2376 | return value | ||
416 | 2377 | |||
417 | 2378 | |||
418 | 2379 | class LocationMatcher(SectionMatcher): | ||
419 | 2380 | |||
420 | 2381 | def __init__(self, store, location): | ||
421 | 2382 | super(LocationMatcher, self).__init__(store) | ||
422 | 2383 | self.location = location | ||
423 | 2384 | |||
424 | 2385 | def get_sections(self): | ||
425 | 2386 | # Override the default implementation as we want to change the order | ||
426 | 2387 | |||
427 | 2388 | # The following is a bit hackish but ensures compatibility with | ||
428 | 2389 | # LocationConfig by reusing the same code | ||
429 | 2390 | sections = list(self.store.get_sections()) | ||
430 | 2391 | filtered_sections = _iter_for_location_by_parts( | ||
431 | 2392 | [s.id for s in sections], self.location) | ||
432 | 2393 | iter_sections = iter(sections) | ||
433 | 2394 | matching_sections = [] | ||
434 | 2395 | for section_id, extra_path, length in filtered_sections: | ||
435 | 2396 | # a section id is unique for a given store so it's safe to iterate | ||
436 | 2397 | # again | ||
437 | 2398 | section = iter_sections.next() | ||
438 | 2399 | if section_id == section.id: | ||
439 | 2400 | matching_sections.append( | ||
440 | 2401 | LocationSection(section, length, extra_path)) | ||
441 | 2402 | # We want the longest (aka more specific) locations first | ||
442 | 2403 | sections = sorted(matching_sections, | ||
443 | 2404 | key=lambda section: (section.length, section.id), | ||
444 | 2405 | reverse=True) | ||
445 | 2406 | # Sections mentioning 'ignore_parents' restrict the selection | ||
446 | 2407 | for section in sections: | ||
447 | 2408 | # FIXME: We really want to use as_bool below -- vila 2011-04-07 | ||
448 | 2409 | ignore = section.get('ignore_parents', None) | ||
449 | 2410 | if ignore is not None: | ||
450 | 2411 | ignore = ui.bool_from_string(ignore) | ||
451 | 2412 | if ignore: | ||
452 | 2413 | break | ||
453 | 2414 | # Finally, we have a valid section | ||
454 | 2415 | yield section | ||
455 | 2416 | |||
456 | 2417 | |||
457 | 2418 | class ConfigStack(object): | ||
458 | 2419 | """A stack of configurations where an option can be defined""" | ||
459 | 2420 | |||
460 | 2421 | def __init__(self, sections=None, get_mutable_section=None): | ||
461 | 2422 | """Creates a stack of sections with an optional store for changes. | ||
462 | 2423 | |||
463 | 2424 | :param sections: A list of ReadOnlySection or callables that returns an | ||
464 | 2425 | iterable of ReadOnlySection. | ||
465 | 2426 | |||
466 | 2427 | :param get_mutable_section: A callable that returns a MutableSection | ||
467 | 2428 | where changes are recorded. | ||
468 | 2429 | """ | ||
469 | 2430 | if sections is None: | ||
470 | 2431 | sections = [] | ||
471 | 2432 | self.sections = sections | ||
472 | 2433 | self.get_mutable_section = get_mutable_section | ||
473 | 2434 | |||
474 | 2435 | def get(self, name): | ||
475 | 2436 | """Return the *first* option value found in the sections. | ||
476 | 2437 | |||
477 | 2438 | This is where we guarantee that sections coming from Store are loaded | ||
478 | 2439 | lazily: the loading is delayed until we need to either check that an | ||
479 | 2440 | option exists or get its value, which in turn may require to discover | ||
480 | 2441 | in which sections it can be defined. Both of these (section and option | ||
481 | 2442 | existence) require loading the store (even partially). | ||
482 | 2443 | """ | ||
483 | 2444 | # Ensuring lazy loading is achieved by delaying section matching until | ||
484 | 2445 | # it can be avoided anymore by using callables to describe (possibly | ||
485 | 2446 | # empty) section lists. | ||
486 | 2447 | for section_or_callable in self.sections: | ||
487 | 2448 | # Each section can expand to multiple ones when a callable is used | ||
488 | 2449 | if callable(section_or_callable): | ||
489 | 2450 | sections = section_or_callable() | ||
490 | 2451 | else: | ||
491 | 2452 | sections = [section_or_callable] | ||
492 | 2453 | for section in sections: | ||
493 | 2454 | value = section.get(name) | ||
494 | 2455 | if value is not None: | ||
495 | 2456 | return value | ||
496 | 2457 | # No definition was found | ||
497 | 2458 | return None | ||
498 | 2459 | |||
499 | 2460 | def set(self, name, value): | ||
500 | 2461 | """Set a new value for the option. | ||
501 | 2462 | |||
502 | 2463 | This is where we guarantee that the mutable section is lazily loaded: | ||
503 | 2464 | this means we won't load the corresponding store before setting a value. | ||
504 | 2465 | """ | ||
505 | 2466 | section = self.get_mutable_section() | ||
506 | 2467 | section.set(name, value) | ||
507 | 2468 | |||
508 | 2469 | def remove(self, name): | ||
509 | 2470 | """Remove an existing option. | ||
510 | 2471 | |||
511 | 2472 | This is where we guarantee that the mutable section is lazily loaded: | ||
512 | 2473 | this means we won't load the correspoding store before trying to delete | ||
513 | 2474 | an option. In practice the store will often be loaded but this allows | ||
514 | 2475 | catching some programming errors. | ||
515 | 2476 | """ | ||
516 | 2477 | section = self.get_mutable_section() | ||
517 | 2478 | section.remove(name) | ||
518 | 2479 | |||
519 | 2480 | |||
520 | 2481 | class GlobalStack(ConfigStack): | ||
521 | 2482 | |||
522 | 2483 | def __init__(self): | ||
523 | 2484 | # Get a GlobalStore | ||
524 | 2485 | gstore = GlobalStore() | ||
525 | 2486 | super(GlobalStack, self).__init__([gstore.get_sections], | ||
526 | 2487 | gstore.get_mutable_section) | ||
527 | 2488 | |||
528 | 2489 | |||
529 | 2490 | class LocationStack(ConfigStack): | ||
530 | 2491 | |||
531 | 2492 | def __init__(self, location): | ||
532 | 2493 | lstore = LocationStore() | ||
533 | 2494 | matcher = LocationMatcher(lstore, location) | ||
534 | 2495 | gstore = GlobalStore() | ||
535 | 2496 | super(LocationStack, self).__init__( | ||
536 | 2497 | [matcher.get_sections, gstore.get_sections], | ||
537 | 2498 | lstore.get_mutable_section) | ||
538 | 2499 | |||
539 | 2500 | |||
540 | 2501 | class BranchStack(ConfigStack): | ||
541 | 2502 | |||
542 | 2503 | def __init__(self, branch): | ||
543 | 2504 | bstore = BranchStore(branch) | ||
544 | 2505 | lstore = LocationStore() | ||
545 | 2506 | matcher = LocationMatcher(lstore, branch.base) | ||
546 | 2507 | gstore = GlobalStore() | ||
547 | 2508 | super(BranchStack, self).__init__( | ||
548 | 2509 | [matcher.get_sections, bstore.get_sections, gstore.get_sections], | ||
549 | 2510 | bstore.get_mutable_section) | ||
550 | 2511 | |||
551 | 2512 | |||
552 | 2071 | class cmd_config(commands.Command): | 2513 | class cmd_config(commands.Command): |
553 | 2072 | __doc__ = """Display, set or remove a configuration option. | 2514 | __doc__ = """Display, set or remove a configuration option. |
554 | 2073 | 2515 | ||
555 | 2074 | 2516 | ||
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 | 1766 | 1766 | ||
561 | 1767 | class ParseConfigError(BzrError): | 1767 | class ParseConfigError(BzrError): |
562 | 1768 | 1768 | ||
563 | 1769 | _fmt = "Error(s) parsing config file %(filename)s:\n%(errors)s" | ||
564 | 1770 | |||
565 | 1769 | def __init__(self, errors, filename): | 1771 | def __init__(self, errors, filename): |
571 | 1770 | if filename is None: | 1772 | BzrError.__init__(self) |
572 | 1771 | filename = "" | 1773 | self.filename = filename |
573 | 1772 | message = "Error(s) parsing config file %s:\n%s" % \ | 1774 | self.errors = '\n'.join(e.msg for e in errors) |
569 | 1773 | (filename, ('\n'.join(e.msg for e in errors))) | ||
570 | 1774 | BzrError.__init__(self, message) | ||
574 | 1775 | 1775 | ||
575 | 1776 | 1776 | ||
576 | 1777 | class NoEmailInUsername(BzrError): | 1777 | class NoEmailInUsername(BzrError): |
577 | 1778 | 1778 | ||
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 | 36 | mergetools, | 36 | mergetools, |
583 | 37 | ui, | 37 | ui, |
584 | 38 | urlutils, | 38 | urlutils, |
585 | 39 | registry, | ||
586 | 39 | tests, | 40 | tests, |
587 | 40 | trace, | 41 | trace, |
588 | 41 | transport, | 42 | transport, |
589 | @@ -62,6 +63,23 @@ | |||
590 | 62 | 63 | ||
591 | 63 | load_tests = scenarios.load_tests_apply_scenarios | 64 | load_tests = scenarios.load_tests_apply_scenarios |
592 | 64 | 65 | ||
593 | 66 | # We need adpaters that can build a config store in a test context. Test | ||
594 | 67 | # classes, based on TestCaseWithTransport, can use the registry to parametrize | ||
595 | 68 | # themselves. The builder will receive a test instance and should return a | ||
596 | 69 | # ready-to-use store. Plugins that defines new stores can also register | ||
597 | 70 | # themselves here to be tested against the tests defined below. | ||
598 | 71 | test_store_builder_registry = registry.Registry() | ||
599 | 72 | test_store_builder_registry.register( | ||
600 | 73 | 'configobj', lambda test: config.ConfigObjStore(test.get_transport(), | ||
601 | 74 | 'configobj.conf')) | ||
602 | 75 | test_store_builder_registry.register( | ||
603 | 76 | 'bazaar', lambda test: config.GlobalStore()) | ||
604 | 77 | test_store_builder_registry.register( | ||
605 | 78 | 'location', lambda test: config.LocationStore()) | ||
606 | 79 | test_store_builder_registry.register( | ||
607 | 80 | 'branch', lambda test: config.BranchStore(test.branch)) | ||
608 | 81 | |||
609 | 82 | |||
610 | 65 | 83 | ||
611 | 66 | sample_long_alias="log -r-15..-1 --line" | 84 | sample_long_alias="log -r-15..-1 --line" |
612 | 67 | sample_config_text = u""" | 85 | sample_config_text = u""" |
613 | @@ -832,7 +850,7 @@ | |||
614 | 832 | def c1_write_config_file(): | 850 | def c1_write_config_file(): |
615 | 833 | before_writing.set() | 851 | before_writing.set() |
616 | 834 | c1_orig() | 852 | c1_orig() |
618 | 835 | # The lock is held we wait for the main thread to decide when to | 853 | # The lock is held. We wait for the main thread to decide when to |
619 | 836 | # continue | 854 | # continue |
620 | 837 | after_writing.wait() | 855 | after_writing.wait() |
621 | 838 | c1._write_config_file = c1_write_config_file | 856 | c1._write_config_file = c1_write_config_file |
622 | @@ -865,7 +883,7 @@ | |||
623 | 865 | c1_orig = c1._write_config_file | 883 | c1_orig = c1._write_config_file |
624 | 866 | def c1_write_config_file(): | 884 | def c1_write_config_file(): |
625 | 867 | ready_to_write.set() | 885 | ready_to_write.set() |
627 | 868 | # The lock is held we wait for the main thread to decide when to | 886 | # The lock is held. We wait for the main thread to decide when to |
628 | 869 | # continue | 887 | # continue |
629 | 870 | do_writing.wait() | 888 | do_writing.wait() |
630 | 871 | c1_orig() | 889 | c1_orig() |
631 | @@ -1264,55 +1282,52 @@ | |||
632 | 1264 | self.failUnless(isinstance(global_config, config.GlobalConfig)) | 1282 | self.failUnless(isinstance(global_config, config.GlobalConfig)) |
633 | 1265 | self.failUnless(global_config is my_config._get_global_config()) | 1283 | self.failUnless(global_config is my_config._get_global_config()) |
634 | 1266 | 1284 | ||
635 | 1285 | def assertLocationMatching(self, expected): | ||
636 | 1286 | self.assertEqual(expected, | ||
637 | 1287 | list(self.my_location_config._get_matching_sections())) | ||
638 | 1288 | |||
639 | 1267 | def test__get_matching_sections_no_match(self): | 1289 | def test__get_matching_sections_no_match(self): |
640 | 1268 | self.get_branch_config('/') | 1290 | self.get_branch_config('/') |
642 | 1269 | self.assertEqual([], self.my_location_config._get_matching_sections()) | 1291 | self.assertLocationMatching([]) |
643 | 1270 | 1292 | ||
644 | 1271 | def test__get_matching_sections_exact(self): | 1293 | def test__get_matching_sections_exact(self): |
645 | 1272 | self.get_branch_config('http://www.example.com') | 1294 | self.get_branch_config('http://www.example.com') |
648 | 1273 | self.assertEqual([('http://www.example.com', '')], | 1295 | self.assertLocationMatching([('http://www.example.com', '')]) |
647 | 1274 | self.my_location_config._get_matching_sections()) | ||
649 | 1275 | 1296 | ||
650 | 1276 | def test__get_matching_sections_suffix_does_not(self): | 1297 | def test__get_matching_sections_suffix_does_not(self): |
651 | 1277 | self.get_branch_config('http://www.example.com-com') | 1298 | self.get_branch_config('http://www.example.com-com') |
653 | 1278 | self.assertEqual([], self.my_location_config._get_matching_sections()) | 1299 | self.assertLocationMatching([]) |
654 | 1279 | 1300 | ||
655 | 1280 | def test__get_matching_sections_subdir_recursive(self): | 1301 | def test__get_matching_sections_subdir_recursive(self): |
656 | 1281 | self.get_branch_config('http://www.example.com/com') | 1302 | self.get_branch_config('http://www.example.com/com') |
659 | 1282 | self.assertEqual([('http://www.example.com', 'com')], | 1303 | self.assertLocationMatching([('http://www.example.com', 'com')]) |
658 | 1283 | self.my_location_config._get_matching_sections()) | ||
660 | 1284 | 1304 | ||
661 | 1285 | def test__get_matching_sections_ignoreparent(self): | 1305 | def test__get_matching_sections_ignoreparent(self): |
662 | 1286 | self.get_branch_config('http://www.example.com/ignoreparent') | 1306 | self.get_branch_config('http://www.example.com/ignoreparent') |
665 | 1287 | self.assertEqual([('http://www.example.com/ignoreparent', '')], | 1307 | self.assertLocationMatching([('http://www.example.com/ignoreparent', |
666 | 1288 | self.my_location_config._get_matching_sections()) | 1308 | '')]) |
667 | 1289 | 1309 | ||
668 | 1290 | def test__get_matching_sections_ignoreparent_subdir(self): | 1310 | def test__get_matching_sections_ignoreparent_subdir(self): |
669 | 1291 | self.get_branch_config( | 1311 | self.get_branch_config( |
670 | 1292 | 'http://www.example.com/ignoreparent/childbranch') | 1312 | 'http://www.example.com/ignoreparent/childbranch') |
674 | 1293 | self.assertEqual([('http://www.example.com/ignoreparent', | 1313 | self.assertLocationMatching([('http://www.example.com/ignoreparent', |
675 | 1294 | 'childbranch')], | 1314 | 'childbranch')]) |
673 | 1295 | self.my_location_config._get_matching_sections()) | ||
676 | 1296 | 1315 | ||
677 | 1297 | def test__get_matching_sections_subdir_trailing_slash(self): | 1316 | def test__get_matching_sections_subdir_trailing_slash(self): |
678 | 1298 | self.get_branch_config('/b') | 1317 | self.get_branch_config('/b') |
681 | 1299 | self.assertEqual([('/b/', '')], | 1318 | self.assertLocationMatching([('/b/', '')]) |
680 | 1300 | self.my_location_config._get_matching_sections()) | ||
682 | 1301 | 1319 | ||
683 | 1302 | def test__get_matching_sections_subdir_child(self): | 1320 | def test__get_matching_sections_subdir_child(self): |
684 | 1303 | self.get_branch_config('/a/foo') | 1321 | self.get_branch_config('/a/foo') |
687 | 1304 | self.assertEqual([('/a/*', ''), ('/a/', 'foo')], | 1322 | self.assertLocationMatching([('/a/*', ''), ('/a/', 'foo')]) |
686 | 1305 | self.my_location_config._get_matching_sections()) | ||
688 | 1306 | 1323 | ||
689 | 1307 | def test__get_matching_sections_subdir_child_child(self): | 1324 | def test__get_matching_sections_subdir_child_child(self): |
690 | 1308 | self.get_branch_config('/a/foo/bar') | 1325 | self.get_branch_config('/a/foo/bar') |
693 | 1309 | self.assertEqual([('/a/*', 'bar'), ('/a/', 'foo/bar')], | 1326 | self.assertLocationMatching([('/a/*', 'bar'), ('/a/', 'foo/bar')]) |
692 | 1310 | self.my_location_config._get_matching_sections()) | ||
694 | 1311 | 1327 | ||
695 | 1312 | def test__get_matching_sections_trailing_slash_with_children(self): | 1328 | def test__get_matching_sections_trailing_slash_with_children(self): |
696 | 1313 | self.get_branch_config('/a/') | 1329 | self.get_branch_config('/a/') |
699 | 1314 | self.assertEqual([('/a/', '')], | 1330 | self.assertLocationMatching([('/a/', '')]) |
698 | 1315 | self.my_location_config._get_matching_sections()) | ||
700 | 1316 | 1331 | ||
701 | 1317 | def test__get_matching_sections_explicit_over_glob(self): | 1332 | def test__get_matching_sections_explicit_over_glob(self): |
702 | 1318 | # XXX: 2006-09-08 jamesh | 1333 | # XXX: 2006-09-08 jamesh |
703 | @@ -1320,8 +1335,7 @@ | |||
704 | 1320 | # was a config section for '/a/?', it would get precedence | 1335 | # was a config section for '/a/?', it would get precedence |
705 | 1321 | # over '/a/c'. | 1336 | # over '/a/c'. |
706 | 1322 | self.get_branch_config('/a/c') | 1337 | self.get_branch_config('/a/c') |
709 | 1323 | self.assertEqual([('/a/c', ''), ('/a/*', ''), ('/a/', 'c')], | 1338 | self.assertLocationMatching([('/a/c', ''), ('/a/*', ''), ('/a/', 'c')]) |
708 | 1324 | self.my_location_config._get_matching_sections()) | ||
710 | 1325 | 1339 | ||
711 | 1326 | def test__get_option_policy_normal(self): | 1340 | def test__get_option_policy_normal(self): |
712 | 1327 | self.get_branch_config('http://www.example.com') | 1341 | self.get_branch_config('http://www.example.com') |
713 | @@ -1818,13 +1832,443 @@ | |||
714 | 1818 | self.assertIs(None, bzrdir_config.get_default_stack_on()) | 1832 | self.assertIs(None, bzrdir_config.get_default_stack_on()) |
715 | 1819 | 1833 | ||
716 | 1820 | 1834 | ||
717 | 1835 | class TestConfigReadOnlySection(tests.TestCase): | ||
718 | 1836 | |||
719 | 1837 | # FIXME: Parametrize so that all sections produced by Stores run these | ||
720 | 1838 | # tests -- vila 2011-04-01 | ||
721 | 1839 | |||
722 | 1840 | def test_get_a_value(self): | ||
723 | 1841 | a_dict = dict(foo='bar') | ||
724 | 1842 | section = config.ReadOnlySection('myID', a_dict) | ||
725 | 1843 | self.assertEquals('bar', section.get('foo')) | ||
726 | 1844 | |||
727 | 1845 | def test_get_unkown_option(self): | ||
728 | 1846 | a_dict = dict() | ||
729 | 1847 | section = config.ReadOnlySection(None, a_dict) | ||
730 | 1848 | self.assertEquals('out of thin air', | ||
731 | 1849 | section.get('foo', 'out of thin air')) | ||
732 | 1850 | |||
733 | 1851 | def test_options_is_shared(self): | ||
734 | 1852 | a_dict = dict() | ||
735 | 1853 | section = config.ReadOnlySection(None, a_dict) | ||
736 | 1854 | self.assertIs(a_dict, section.options) | ||
737 | 1855 | |||
738 | 1856 | |||
739 | 1857 | class TestConfigMutableSection(tests.TestCase): | ||
740 | 1858 | |||
741 | 1859 | # FIXME: Parametrize so that all sections (including os.environ and the | ||
742 | 1860 | # ones produced by Stores) run these tests -- vila 2011-04-01 | ||
743 | 1861 | |||
744 | 1862 | def test_set(self): | ||
745 | 1863 | a_dict = dict(foo='bar') | ||
746 | 1864 | section = config.MutableSection('myID', a_dict) | ||
747 | 1865 | section.set('foo', 'new_value') | ||
748 | 1866 | self.assertEquals('new_value', section.get('foo')) | ||
749 | 1867 | # The change appears in the shared section | ||
750 | 1868 | self.assertEquals('new_value', a_dict.get('foo')) | ||
751 | 1869 | # We keep track of the change | ||
752 | 1870 | self.assertTrue('foo' in section.orig) | ||
753 | 1871 | self.assertEquals('bar', section.orig.get('foo')) | ||
754 | 1872 | |||
755 | 1873 | def test_set_preserve_original_once(self): | ||
756 | 1874 | a_dict = dict(foo='bar') | ||
757 | 1875 | section = config.MutableSection('myID', a_dict) | ||
758 | 1876 | section.set('foo', 'first_value') | ||
759 | 1877 | section.set('foo', 'second_value') | ||
760 | 1878 | # We keep track of the original value | ||
761 | 1879 | self.assertTrue('foo' in section.orig) | ||
762 | 1880 | self.assertEquals('bar', section.orig.get('foo')) | ||
763 | 1881 | |||
764 | 1882 | def test_remove(self): | ||
765 | 1883 | a_dict = dict(foo='bar') | ||
766 | 1884 | section = config.MutableSection('myID', a_dict) | ||
767 | 1885 | section.remove('foo') | ||
768 | 1886 | # We get None for unknown options via the default value | ||
769 | 1887 | self.assertEquals(None, section.get('foo')) | ||
770 | 1888 | # Or we just get the default value | ||
771 | 1889 | self.assertEquals('unknown', section.get('foo', 'unknown')) | ||
772 | 1890 | self.assertFalse('foo' in section.options) | ||
773 | 1891 | # We keep track of the deletion | ||
774 | 1892 | self.assertTrue('foo' in section.orig) | ||
775 | 1893 | self.assertEquals('bar', section.orig.get('foo')) | ||
776 | 1894 | |||
777 | 1895 | def test_remove_new_option(self): | ||
778 | 1896 | a_dict = dict() | ||
779 | 1897 | section = config.MutableSection('myID', a_dict) | ||
780 | 1898 | section.set('foo', 'bar') | ||
781 | 1899 | section.remove('foo') | ||
782 | 1900 | self.assertFalse('foo' in section.options) | ||
783 | 1901 | # The option didn't exist initially so it we need to keep track of it | ||
784 | 1902 | # with a special value | ||
785 | 1903 | self.assertTrue('foo' in section.orig) | ||
786 | 1904 | self.assertEquals(config._NewlyCreatedOption, section.orig['foo']) | ||
787 | 1905 | |||
788 | 1906 | |||
789 | 1907 | class TestStore(tests.TestCaseWithTransport): | ||
790 | 1908 | |||
791 | 1909 | def assertSectionContent(self, expected, section): | ||
792 | 1910 | """Assert that some options have the proper values in a section.""" | ||
793 | 1911 | expected_name, expected_options = expected | ||
794 | 1912 | self.assertEquals(expected_name, section.id) | ||
795 | 1913 | self.assertEquals( | ||
796 | 1914 | expected_options, | ||
797 | 1915 | dict([(k, section.get(k)) for k in expected_options.keys()])) | ||
798 | 1916 | |||
799 | 1917 | |||
800 | 1918 | class TestReadonlyStore(TestStore): | ||
801 | 1919 | |||
802 | 1920 | scenarios = [(key, {'get_store': builder}) | ||
803 | 1921 | for key, builder in test_store_builder_registry.iteritems()] | ||
804 | 1922 | |||
805 | 1923 | def setUp(self): | ||
806 | 1924 | super(TestReadonlyStore, self).setUp() | ||
807 | 1925 | self.branch = self.make_branch('branch') | ||
808 | 1926 | |||
809 | 1927 | def test_building_delays_load(self): | ||
810 | 1928 | store = self.get_store(self) | ||
811 | 1929 | self.assertEquals(False, store.loaded) | ||
812 | 1930 | store._load_from_string('') | ||
813 | 1931 | self.assertEquals(True, store.loaded) | ||
814 | 1932 | |||
815 | 1933 | def test_get_no_sections_for_empty(self): | ||
816 | 1934 | store = self.get_store(self) | ||
817 | 1935 | store._load_from_string('') | ||
818 | 1936 | self.assertEquals([], list(store.get_sections())) | ||
819 | 1937 | |||
820 | 1938 | def test_get_default_section(self): | ||
821 | 1939 | store = self.get_store(self) | ||
822 | 1940 | store._load_from_string('foo=bar') | ||
823 | 1941 | sections = list(store.get_sections()) | ||
824 | 1942 | self.assertLength(1, sections) | ||
825 | 1943 | self.assertSectionContent((None, {'foo': 'bar'}), sections[0]) | ||
826 | 1944 | |||
827 | 1945 | def test_get_named_section(self): | ||
828 | 1946 | store = self.get_store(self) | ||
829 | 1947 | store._load_from_string('[baz]\nfoo=bar') | ||
830 | 1948 | sections = list(store.get_sections()) | ||
831 | 1949 | self.assertLength(1, sections) | ||
832 | 1950 | self.assertSectionContent(('baz', {'foo': 'bar'}), sections[0]) | ||
833 | 1951 | |||
834 | 1952 | def test_load_from_string_fails_for_non_empty_store(self): | ||
835 | 1953 | store = self.get_store(self) | ||
836 | 1954 | store._load_from_string('foo=bar') | ||
837 | 1955 | self.assertRaises(AssertionError, store._load_from_string, 'bar=baz') | ||
838 | 1956 | |||
839 | 1957 | |||
840 | 1958 | class TestMutableStore(TestStore): | ||
841 | 1959 | |||
842 | 1960 | scenarios = [(key, {'store_id': key, 'get_store': builder}) | ||
843 | 1961 | for key, builder in test_store_builder_registry.iteritems()] | ||
844 | 1962 | |||
845 | 1963 | def setUp(self): | ||
846 | 1964 | super(TestMutableStore, self).setUp() | ||
847 | 1965 | self.transport = self.get_transport() | ||
848 | 1966 | self.branch = self.make_branch('branch') | ||
849 | 1967 | |||
850 | 1968 | def has_store(self, store): | ||
851 | 1969 | store_basename = urlutils.relative_url(self.transport.external_url(), | ||
852 | 1970 | store.external_url()) | ||
853 | 1971 | return self.transport.has(store_basename) | ||
854 | 1972 | |||
855 | 1973 | def test_save_empty_creates_no_file(self): | ||
856 | 1974 | if self.store_id == 'branch': | ||
857 | 1975 | raise tests.TestNotApplicable( | ||
858 | 1976 | 'branch.conf is *always* created when a branch is initialized') | ||
859 | 1977 | store = self.get_store(self) | ||
860 | 1978 | store.save() | ||
861 | 1979 | self.assertEquals(False, self.has_store(store)) | ||
862 | 1980 | |||
863 | 1981 | def test_save_emptied_succeeds(self): | ||
864 | 1982 | store = self.get_store(self) | ||
865 | 1983 | store._load_from_string('foo=bar\n') | ||
866 | 1984 | section = store.get_mutable_section(None) | ||
867 | 1985 | section.remove('foo') | ||
868 | 1986 | store.save() | ||
869 | 1987 | self.assertEquals(True, self.has_store(store)) | ||
870 | 1988 | modified_store = self.get_store(self) | ||
871 | 1989 | sections = list(modified_store.get_sections()) | ||
872 | 1990 | self.assertLength(0, sections) | ||
873 | 1991 | |||
874 | 1992 | def test_save_with_content_succeeds(self): | ||
875 | 1993 | if self.store_id == 'branch': | ||
876 | 1994 | raise tests.TestNotApplicable( | ||
877 | 1995 | 'branch.conf is *always* created when a branch is initialized') | ||
878 | 1996 | store = self.get_store(self) | ||
879 | 1997 | store._load_from_string('foo=bar\n') | ||
880 | 1998 | self.assertEquals(False, self.has_store(store)) | ||
881 | 1999 | store.save() | ||
882 | 2000 | self.assertEquals(True, self.has_store(store)) | ||
883 | 2001 | modified_store = self.get_store(self) | ||
884 | 2002 | sections = list(modified_store.get_sections()) | ||
885 | 2003 | self.assertLength(1, sections) | ||
886 | 2004 | self.assertSectionContent((None, {'foo': 'bar'}), sections[0]) | ||
887 | 2005 | |||
888 | 2006 | def test_set_option_in_empty_store(self): | ||
889 | 2007 | store = self.get_store(self) | ||
890 | 2008 | section = store.get_mutable_section(None) | ||
891 | 2009 | section.set('foo', 'bar') | ||
892 | 2010 | store.save() | ||
893 | 2011 | modified_store = self.get_store(self) | ||
894 | 2012 | sections = list(modified_store.get_sections()) | ||
895 | 2013 | self.assertLength(1, sections) | ||
896 | 2014 | self.assertSectionContent((None, {'foo': 'bar'}), sections[0]) | ||
897 | 2015 | |||
898 | 2016 | def test_set_option_in_default_section(self): | ||
899 | 2017 | store = self.get_store(self) | ||
900 | 2018 | store._load_from_string('') | ||
901 | 2019 | section = store.get_mutable_section(None) | ||
902 | 2020 | section.set('foo', 'bar') | ||
903 | 2021 | store.save() | ||
904 | 2022 | modified_store = self.get_store(self) | ||
905 | 2023 | sections = list(modified_store.get_sections()) | ||
906 | 2024 | self.assertLength(1, sections) | ||
907 | 2025 | self.assertSectionContent((None, {'foo': 'bar'}), sections[0]) | ||
908 | 2026 | |||
909 | 2027 | def test_set_option_in_named_section(self): | ||
910 | 2028 | store = self.get_store(self) | ||
911 | 2029 | store._load_from_string('') | ||
912 | 2030 | section = store.get_mutable_section('baz') | ||
913 | 2031 | section.set('foo', 'bar') | ||
914 | 2032 | store.save() | ||
915 | 2033 | modified_store = self.get_store(self) | ||
916 | 2034 | sections = list(modified_store.get_sections()) | ||
917 | 2035 | self.assertLength(1, sections) | ||
918 | 2036 | self.assertSectionContent(('baz', {'foo': 'bar'}), sections[0]) | ||
919 | 2037 | |||
920 | 2038 | |||
921 | 2039 | class TestConfigObjStore(TestStore): | ||
922 | 2040 | |||
923 | 2041 | def test_loading_unknown_file_fails(self): | ||
924 | 2042 | store = config.ConfigObjStore(self.get_transport(), 'I-do-not-exist') | ||
925 | 2043 | self.assertRaises(errors.NoSuchFile, store.load) | ||
926 | 2044 | |||
927 | 2045 | def test_invalid_content(self): | ||
928 | 2046 | store = config.ConfigObjStore(self.get_transport(), 'foo.conf', ) | ||
929 | 2047 | self.assertEquals(False, store.loaded) | ||
930 | 2048 | exc = self.assertRaises( | ||
931 | 2049 | errors.ParseConfigError, store._load_from_string, | ||
932 | 2050 | 'this is invalid !') | ||
933 | 2051 | self.assertEndsWith(exc.filename, 'foo.conf') | ||
934 | 2052 | # And the load failed | ||
935 | 2053 | self.assertEquals(False, store.loaded) | ||
936 | 2054 | |||
937 | 2055 | def test_get_embedded_sections(self): | ||
938 | 2056 | # A more complicated example (which also shows that section names and | ||
939 | 2057 | # option names share the same name space...) | ||
940 | 2058 | # FIXME: This should be fixed by forbidding dicts as values ? | ||
941 | 2059 | # -- vila 2011-04-05 | ||
942 | 2060 | store = config.ConfigObjStore(self.get_transport(), 'foo.conf', ) | ||
943 | 2061 | store._load_from_string(''' | ||
944 | 2062 | foo=bar | ||
945 | 2063 | l=1,2 | ||
946 | 2064 | [DEFAULT] | ||
947 | 2065 | foo_in_DEFAULT=foo_DEFAULT | ||
948 | 2066 | [bar] | ||
949 | 2067 | foo_in_bar=barbar | ||
950 | 2068 | [baz] | ||
951 | 2069 | foo_in_baz=barbaz | ||
952 | 2070 | [[qux]] | ||
953 | 2071 | foo_in_qux=quux | ||
954 | 2072 | ''') | ||
955 | 2073 | sections = list(store.get_sections()) | ||
956 | 2074 | self.assertLength(4, sections) | ||
957 | 2075 | # The default section has no name. | ||
958 | 2076 | # List values are provided as lists | ||
959 | 2077 | self.assertSectionContent((None, {'foo': 'bar', 'l': ['1', '2']}), | ||
960 | 2078 | sections[0]) | ||
961 | 2079 | self.assertSectionContent( | ||
962 | 2080 | ('DEFAULT', {'foo_in_DEFAULT': 'foo_DEFAULT'}), sections[1]) | ||
963 | 2081 | self.assertSectionContent( | ||
964 | 2082 | ('bar', {'foo_in_bar': 'barbar'}), sections[2]) | ||
965 | 2083 | # sub sections are provided as embedded dicts. | ||
966 | 2084 | self.assertSectionContent( | ||
967 | 2085 | ('baz', {'foo_in_baz': 'barbaz', 'qux': {'foo_in_qux': 'quux'}}), | ||
968 | 2086 | sections[3]) | ||
969 | 2087 | |||
970 | 2088 | |||
971 | 2089 | class TestLockableConfigObjStore(TestStore): | ||
972 | 2090 | |||
973 | 2091 | def test_create_store_in_created_dir(self): | ||
974 | 2092 | t = self.get_transport('dir/subdir') | ||
975 | 2093 | store = config.LockableConfigObjStore(t, 'foo.conf') | ||
976 | 2094 | store.get_mutable_section(None).set('foo', 'bar') | ||
977 | 2095 | store.save() | ||
978 | 2096 | |||
979 | 2097 | # FIXME: We should adapt the tests in TestLockableConfig about concurrent | ||
980 | 2098 | # writes. Since this requires a clearer rewrite, I'll just rely on using | ||
981 | 2099 | # the same code in LockableConfigObjStore (copied from LockableConfig, but | ||
982 | 2100 | # trivial enough, the main difference is that we add @needs_write_lock on | ||
983 | 2101 | # save() instead of set_user_option() and remove_user_option()). The intent | ||
984 | 2102 | # is to ensure that we always get a valid content for the store even when | ||
985 | 2103 | # concurrent accesses occur, read/write, write/write. It may be worth | ||
986 | 2104 | # looking into removing the lock dir when it;s not needed anymore and look | ||
987 | 2105 | # at possible fallouts for concurrent lockers -- vila 20110-04-06 | ||
988 | 2106 | |||
989 | 2107 | |||
990 | 2108 | class TestSectionMatcher(TestStore): | ||
991 | 2109 | |||
992 | 2110 | scenarios = [('location', {'matcher': config.LocationMatcher})] | ||
993 | 2111 | |||
994 | 2112 | def get_store(self, file_name): | ||
995 | 2113 | return config.ConfigObjStore(self.get_readonly_transport(), file_name) | ||
996 | 2114 | |||
997 | 2115 | def test_no_matches_for_empty_stores(self): | ||
998 | 2116 | store = self.get_store('foo.conf') | ||
999 | 2117 | store._load_from_string('') | ||
1000 | 2118 | matcher = self.matcher(store, '/bar') | ||
1001 | 2119 | self.assertEquals([], list(matcher.get_sections())) | ||
1002 | 2120 | |||
1003 | 2121 | def test_build_doesnt_load_store(self): | ||
1004 | 2122 | store = self.get_store('foo.conf') | ||
1005 | 2123 | matcher = self.matcher(store, '/bar') | ||
1006 | 2124 | self.assertFalse(store.loaded) | ||
1007 | 2125 | |||
1008 | 2126 | |||
1009 | 2127 | class TestLocationSection(tests.TestCase): | ||
1010 | 2128 | |||
1011 | 2129 | def get_section(self, options, extra_path): | ||
1012 | 2130 | section = config.ReadOnlySection('foo', options) | ||
1013 | 2131 | # We don't care about the length so we use '0' | ||
1014 | 2132 | return config.LocationSection(section, 0, extra_path) | ||
1015 | 2133 | |||
1016 | 2134 | def test_simple_option(self): | ||
1017 | 2135 | section = self.get_section({'foo': 'bar'}, '') | ||
1018 | 2136 | self.assertEquals('bar', section.get('foo')) | ||
1019 | 2137 | |||
1020 | 2138 | def test_option_with_extra_path(self): | ||
1021 | 2139 | section = self.get_section({'foo': 'bar', 'foo:policy': 'appendpath'}, | ||
1022 | 2140 | 'baz') | ||
1023 | 2141 | self.assertEquals('bar/baz', section.get('foo')) | ||
1024 | 2142 | |||
1025 | 2143 | def test_invalid_policy(self): | ||
1026 | 2144 | section = self.get_section({'foo': 'bar', 'foo:policy': 'die'}, | ||
1027 | 2145 | 'baz') | ||
1028 | 2146 | # invalid policies are ignored | ||
1029 | 2147 | self.assertEquals('bar', section.get('foo')) | ||
1030 | 2148 | |||
1031 | 2149 | |||
1032 | 2150 | class TestLocationMatcher(TestStore): | ||
1033 | 2151 | |||
1034 | 2152 | def get_store(self, file_name): | ||
1035 | 2153 | return config.ConfigObjStore(self.get_readonly_transport(), file_name) | ||
1036 | 2154 | |||
1037 | 2155 | def test_more_specific_sections_first(self): | ||
1038 | 2156 | store = self.get_store('foo.conf') | ||
1039 | 2157 | store._load_from_string(''' | ||
1040 | 2158 | [/foo] | ||
1041 | 2159 | section=/foo | ||
1042 | 2160 | [/foo/bar] | ||
1043 | 2161 | section=/foo/bar | ||
1044 | 2162 | ''') | ||
1045 | 2163 | self.assertEquals(['/foo', '/foo/bar'], | ||
1046 | 2164 | [section.id for section in store.get_sections()]) | ||
1047 | 2165 | matcher = config.LocationMatcher(store, '/foo/bar/baz') | ||
1048 | 2166 | sections = list(matcher.get_sections()) | ||
1049 | 2167 | self.assertEquals([3, 2], | ||
1050 | 2168 | [section.length for section in sections]) | ||
1051 | 2169 | self.assertEquals(['/foo/bar', '/foo'], | ||
1052 | 2170 | [section.id for section in sections]) | ||
1053 | 2171 | self.assertEquals(['baz', 'bar/baz'], | ||
1054 | 2172 | [section.extra_path for section in sections]) | ||
1055 | 2173 | |||
1056 | 2174 | |||
1057 | 2175 | class TestConfigStackGet(tests.TestCase): | ||
1058 | 2176 | |||
1059 | 2177 | # FIXME: This should be parametrized for all known ConfigStack or dedicated | ||
1060 | 2178 | # paramerized tests created to avoid bloating -- vila 2011-03-31 | ||
1061 | 2179 | |||
1062 | 2180 | def test_single_config_get(self): | ||
1063 | 2181 | conf = dict(foo='bar') | ||
1064 | 2182 | conf_stack = config.ConfigStack([conf]) | ||
1065 | 2183 | self.assertEquals('bar', conf_stack.get('foo')) | ||
1066 | 2184 | |||
1067 | 2185 | def test_get_first_definition(self): | ||
1068 | 2186 | conf1 = dict(foo='bar') | ||
1069 | 2187 | conf2 = dict(foo='baz') | ||
1070 | 2188 | conf_stack = config.ConfigStack([conf1, conf2]) | ||
1071 | 2189 | self.assertEquals('bar', conf_stack.get('foo')) | ||
1072 | 2190 | |||
1073 | 2191 | def test_get_embedded_definition(self): | ||
1074 | 2192 | conf1 = dict(yy='12') | ||
1075 | 2193 | conf2 = config.ConfigStack([dict(xx='42'), dict(foo='baz')]) | ||
1076 | 2194 | conf_stack = config.ConfigStack([conf1, conf2]) | ||
1077 | 2195 | self.assertEquals('baz', conf_stack.get('foo')) | ||
1078 | 2196 | |||
1079 | 2197 | def test_get_for_empty_stack(self): | ||
1080 | 2198 | conf_stack = config.ConfigStack() | ||
1081 | 2199 | self.assertEquals(None, conf_stack.get('foo')) | ||
1082 | 2200 | |||
1083 | 2201 | def test_get_for_empty_section_callable(self): | ||
1084 | 2202 | conf_stack = config.ConfigStack([lambda : []]) | ||
1085 | 2203 | self.assertEquals(None, conf_stack.get('foo')) | ||
1086 | 2204 | |||
1087 | 2205 | |||
1088 | 2206 | class TestConfigStackSet(tests.TestCaseWithTransport): | ||
1089 | 2207 | |||
1090 | 2208 | # FIXME: This should be parametrized for all known ConfigStack or dedicated | ||
1091 | 2209 | # paramerized tests created to avoid bloating -- vila 2011-04-05 | ||
1092 | 2210 | |||
1093 | 2211 | def test_simple_set(self): | ||
1094 | 2212 | store = config.ConfigObjStore(self.get_transport(), 'test.conf') | ||
1095 | 2213 | store._load_from_string('foo=bar') | ||
1096 | 2214 | conf = config.ConfigStack( | ||
1097 | 2215 | [store.get_sections], store.get_mutable_section) | ||
1098 | 2216 | self.assertEquals('bar', conf.get('foo')) | ||
1099 | 2217 | conf.set('foo', 'baz') | ||
1100 | 2218 | # Did we get it back ? | ||
1101 | 2219 | self.assertEquals('baz', conf.get('foo')) | ||
1102 | 2220 | |||
1103 | 2221 | def test_set_creates_a_new_section(self): | ||
1104 | 2222 | store = config.ConfigObjStore(self.get_transport(), 'test.conf') | ||
1105 | 2223 | conf = config.ConfigStack( | ||
1106 | 2224 | [store.get_sections], store.get_mutable_section) | ||
1107 | 2225 | conf.set('foo', 'baz') | ||
1108 | 2226 | self.assertEquals, 'baz', conf.get('foo') | ||
1109 | 2227 | |||
1110 | 2228 | |||
1111 | 2229 | class TestConfigStackRemove(tests.TestCaseWithTransport): | ||
1112 | 2230 | |||
1113 | 2231 | # FIXME: This should be parametrized for all known ConfigStack or dedicated | ||
1114 | 2232 | # paramerized tests created to avoid bloating -- vila 2011-04-06 | ||
1115 | 2233 | |||
1116 | 2234 | def test_remove_existing(self): | ||
1117 | 2235 | store = config.ConfigObjStore(self.get_transport(), 'test.conf') | ||
1118 | 2236 | store._load_from_string('foo=bar') | ||
1119 | 2237 | conf = config.ConfigStack( | ||
1120 | 2238 | [store.get_sections], store.get_mutable_section) | ||
1121 | 2239 | self.assertEquals('bar', conf.get('foo')) | ||
1122 | 2240 | conf.remove('foo') | ||
1123 | 2241 | # Did we get it back ? | ||
1124 | 2242 | self.assertEquals(None, conf.get('foo')) | ||
1125 | 2243 | |||
1126 | 2244 | def test_remove_unknown(self): | ||
1127 | 2245 | store = config.ConfigObjStore(self.get_transport(), 'test.conf') | ||
1128 | 2246 | conf = config.ConfigStack( | ||
1129 | 2247 | [store.get_sections], store.get_mutable_section) | ||
1130 | 2248 | self.assertRaises(KeyError, conf.remove, 'I_do_not_exist') | ||
1131 | 2249 | |||
1132 | 2250 | |||
1133 | 2251 | class TestConcreteStacks(tests.TestCaseWithTransport): | ||
1134 | 2252 | |||
1135 | 2253 | # basic smoke tests | ||
1136 | 2254 | |||
1137 | 2255 | def test_global_stack(self): | ||
1138 | 2256 | stack = config.GlobalStack() | ||
1139 | 2257 | |||
1140 | 2258 | def test_location_store(self): | ||
1141 | 2259 | stack = config.LocationStack('.') | ||
1142 | 2260 | |||
1143 | 2261 | def test_branch_store(self): | ||
1144 | 2262 | b = self.make_branch('.') | ||
1145 | 2263 | stack = config.BranchStack(b) | ||
1146 | 2264 | |||
1147 | 2265 | |||
1148 | 1821 | class TestConfigGetOptions(tests.TestCaseWithTransport, TestOptionsMixin): | 2266 | class TestConfigGetOptions(tests.TestCaseWithTransport, TestOptionsMixin): |
1149 | 1822 | 2267 | ||
1150 | 1823 | def setUp(self): | 2268 | def setUp(self): |
1151 | 1824 | super(TestConfigGetOptions, self).setUp() | 2269 | super(TestConfigGetOptions, self).setUp() |
1152 | 1825 | create_configs(self) | 2270 | create_configs(self) |
1153 | 1826 | 2271 | ||
1154 | 1827 | # One variable in none of the above | ||
1155 | 1828 | def test_no_variable(self): | 2272 | def test_no_variable(self): |
1156 | 1829 | # Using branch should query branch, locations and bazaar | 2273 | # Using branch should query branch, locations and bazaar |
1157 | 1830 | self.assertOptions([], self.branch_config) | 2274 | self.assertOptions([], self.branch_config) |
1158 | 1831 | 2275 | ||
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 | 1 | Configuring Bazaar | ||
1164 | 2 | ================== | ||
1165 | 3 | |||
1166 | 4 | A configuration option has: | ||
1167 | 5 | |||
1168 | 6 | - a name: a valid python identifier (even if it's not used as an | ||
1169 | 7 | identifier in python itself) | ||
1170 | 8 | |||
1171 | 9 | - a value: a unicode string | ||
1172 | 10 | |||
1173 | 11 | Sections | ||
1174 | 12 | -------- | ||
1175 | 13 | |||
1176 | 14 | Options are grouped into sections which share some properties with the well | ||
1177 | 15 | known dict objects: | ||
1178 | 16 | |||
1179 | 17 | - the key is the name, | ||
1180 | 18 | - you can get, set and remove an option, | ||
1181 | 19 | - the value is a unicode string. | ||
1182 | 20 | |||
1183 | 21 | MutableSection are needed to set or remove an option, ReadOnlySection should | ||
1184 | 22 | be used otherwise. | ||
1185 | 23 | |||
1186 | 24 | Stores | ||
1187 | 25 | ------ | ||
1188 | 26 | |||
1189 | 27 | Options can persistent in which case they are saved into Stores. | ||
1190 | 28 | |||
1191 | 29 | ``config.Store`` defines the abstract interface that all stores should | ||
1192 | 30 | implement. | ||
1193 | 31 | |||
1194 | 32 | This object doesn't provide a direct access to the options, it only provides | ||
1195 | 33 | access to Sections. This is deliberate to ensure that sections can be properly | ||
1196 | 34 | shared by reusing the same underlying objects. Accessing options should be | ||
1197 | 35 | done via the ``Section`` objects. | ||
1198 | 36 | |||
1199 | 37 | A ``Store`` can contain one or more sections, each section is uniquely | ||
1200 | 38 | identified by a unicode string. | ||
1201 | 39 | |||
1202 | 40 | ``config.ConfigObjStore`` is an implementation that use ``ConfigObj``. | ||
1203 | 41 | |||
1204 | 42 | Depending on the object it is associated with (or not) a ``Store`` also needs | ||
1205 | 43 | to implement a locking mechanism. ``LockableConfigObjStore`` implements such a | ||
1206 | 44 | mechanism for ``ConfigObj`` based stores. | ||
1207 | 45 | |||
1208 | 46 | Classes are provided for the usual Bazaar configuration files and could be | ||
1209 | 47 | used as examples to define new ones if needed. The associated tests provides a | ||
1210 | 48 | basis for new classes which only need to register themselves in the right | ||
1211 | 49 | places to inherit from the existing basic tests and add their own specific | ||
1212 | 50 | ones. | ||
1213 | 51 | |||
1214 | 52 | Filtering sections | ||
1215 | 53 | ------------------ | ||
1216 | 54 | |||
1217 | 55 | For some contexts, only some sections from a given store will apply. Defining | ||
1218 | 56 | which is what the ``SectionMatcher`` are about. | ||
1219 | 57 | |||
1220 | 58 | The main constraint here is that a ``SectionMatcher`` should delay the loading | ||
1221 | 59 | of the associated store as long as possible. The constructor should collect | ||
1222 | 60 | all data needed for the selection and uses it while processing the sections in | ||
1223 | 61 | ``get_sections``. | ||
1224 | 62 | |||
1225 | 63 | Only ``ReadOnlySection`` objects are manipulated here but a ``SectionMatcher`` | ||
1226 | 64 | can return dedicated ``Section`` to provide additional context (the | ||
1227 | 65 | ``LocationSection`` add an ``extra_path`` attribute to implement the | ||
1228 | 66 | ``appendpath`` policy for example). | ||
1229 | 67 | |||
1230 | 68 | .. FIXME: Replace the appendpath example if/when it's deprecated ;) | ||
1231 | 69 | |||
1232 | 70 | Stacks | ||
1233 | 71 | ------ | ||
1234 | 72 | |||
1235 | 73 | An option can take different values depending on the context it is used. Such | ||
1236 | 74 | a context can involve configuration files, options from the command line, | ||
1237 | 75 | default values in bzrlib and then some. | ||
1238 | 76 | |||
1239 | 77 | Such a context is implemented by creating a list of ``Section`` stacked upon | ||
1240 | 78 | each other. A ``Stack`` can then be asked for an option value and returns the | ||
1241 | 79 | first definition found. | ||
1242 | 80 | |||
1243 | 81 | This provides a great flexibility to decide priorities between sections when | ||
1244 | 82 | the stack is defined without to worry about them in the code itself. | ||
1245 | 83 | |||
1246 | 84 | A stack also defines a mutable section (which can be None) to handle | ||
1247 | 85 | modifications. | ||
1248 | 86 | |||
1249 | 87 | Many sections (or even stores) are aimed at providing default values for an | ||
1250 | 88 | option but these sections shouldn't be modified lightly as modifying an option | ||
1251 | 89 | used for different contexts will indeed be seen by all these contexts. | ||
1252 | 90 | |||
1253 | 91 | Default values in configuration files are defined by users, developers | ||
1254 | 92 | shouldn't have to modify them, as such, no mechanism nor heuristics are used | ||
1255 | 93 | to find which section (or sections) should be modified, a ``Stack`` defines a | ||
1256 | 94 | mutable section when there is no ambiguity, if there is one, then the *user* | ||
1257 | 95 | should be able to decide and this case a new ``Stack`` can be created cheaply. | ||
1258 | 96 | |||
1259 | 97 | Different stacks can be created for different purposes, the existing | ||
1260 | 98 | ``Global.Stack``, ``LocationStack`` and ``BranchStack`` can be used as basis | ||
1261 | 99 | or examples. These classes are the only ones that should be used in code, | ||
1262 | 100 | ``Stores`` can be used to build them but shouldn't be used otherwise, ditto | ||
1263 | 101 | for sections. Again, the associated tests could and should be used against the | ||
1264 | 102 | created stacks. | ||
1265 | 0 | 103 | ||
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 | 36 | .. toctree:: | 36 | .. toctree:: |
1271 | 37 | :maxdepth: 1 | 37 | :maxdepth: 1 |
1272 | 38 | 38 | ||
1273 | 39 | configuration | ||
1274 | 40 | fetch | ||
1275 | 39 | transports | 41 | transports |
1276 | 40 | ui | 42 | ui |
1277 | 41 | fetch | ||
1278 | 42 | 43 | ||
1279 | 43 | Releasing and Packaging | 44 | Releasing and Packaging |
1280 | 44 | ======================= | 45 | ======================= |
1281 | 45 | 46 | ||
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 | 37 | which shouldn't be reached by the proxy. (See | 37 | which shouldn't be reached by the proxy. (See |
1287 | 38 | <http://docs.python.org/library/urllib.html> for more details.) | 38 | <http://docs.python.org/library/urllib.html> for more details.) |
1288 | 39 | 39 | ||
1289 | 40 | Various ways to configure | ||
1290 | 41 | ------------------------- | ||
1291 | 42 | |||
1292 | 43 | As shown in the example above, there are various ways to | ||
1293 | 44 | configure Bazaar, they all share some common properties though, | ||
1294 | 45 | an option has: | ||
1295 | 46 | |||
1296 | 47 | - a name which is generally a valid python identifier, | ||
1297 | 48 | |||
1298 | 49 | - a value which is a string. In some cases, Bazzar will be able | ||
1299 | 50 | to recognize special values like 'True', 'False' to infer a | ||
1300 | 51 | boolean type, but basically, as a user, you will always specify | ||
1301 | 52 | a value as a string. | ||
1302 | 53 | |||
1303 | 54 | Options are grouped in various contexts so their name uniquely | ||
1304 | 55 | identify them in this context. When needed, options can be made | ||
1305 | 56 | persistent by recording them in a configuration file. | ||
1306 | 57 | |||
1307 | 40 | 58 | ||
1308 | 41 | Configuration files | 59 | Configuration files |
1309 | 42 | ------------------- | 60 | ------------------- |
> config. GlobalStack( ).get(' blah') Global. get('blah' )
> or maybe even
> config.
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.