##// END OF EJS Templates
config: track "source" along side value...
marmoute -
r47366:d3df397e default
parent child Browse files
Show More
@@ -1,326 +1,325 b''
1 # config.py - configuration parsing for Mercurial
1 # config.py - configuration parsing for Mercurial
2 #
2 #
3 # Copyright 2009 Matt Mackall <mpm@selenic.com> and others
3 # Copyright 2009 Matt Mackall <mpm@selenic.com> and others
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 import errno
10 import errno
11 import os
11 import os
12
12
13 from .i18n import _
13 from .i18n import _
14 from .pycompat import getattr
14 from .pycompat import getattr
15 from . import (
15 from . import (
16 encoding,
16 encoding,
17 error,
17 error,
18 pycompat,
18 pycompat,
19 util,
19 util,
20 )
20 )
21
21
22
22
23 class config(object):
23 class config(object):
24 def __init__(self, data=None):
24 def __init__(self, data=None):
25 self._data = {}
25 self._data = {}
26 self._unset = []
26 self._unset = []
27 if data:
27 if data:
28 for k in data._data:
28 for k in data._data:
29 self._data[k] = data[k].copy()
29 self._data[k] = data[k].copy()
30 self._source = data._source.copy()
31 else:
32 self._source = util.cowdict()
33
30
34 def copy(self):
31 def copy(self):
35 return config(self)
32 return config(self)
36
33
37 def __contains__(self, section):
34 def __contains__(self, section):
38 return section in self._data
35 return section in self._data
39
36
40 def hasitem(self, section, item):
37 def hasitem(self, section, item):
41 return item in self._data.get(section, {})
38 return item in self._data.get(section, {})
42
39
43 def __getitem__(self, section):
40 def __getitem__(self, section):
44 return self._data.get(section, {})
41 return self._data.get(section, {})
45
42
46 def __iter__(self):
43 def __iter__(self):
47 for d in self.sections():
44 for d in self.sections():
48 yield d
45 yield d
49
46
50 def update(self, src):
47 def update(self, src):
51 self._source = self._source.preparewrite()
52 for s, n in src._unset:
48 for s, n in src._unset:
53 ds = self._data.get(s, None)
49 ds = self._data.get(s, None)
54 if ds is not None and n in ds:
50 if ds is not None and n in ds:
55 self._data[s] = ds.preparewrite()
51 self._data[s] = ds.preparewrite()
56 del self._data[s][n]
52 del self._data[s][n]
57 del self._source[(s, n)]
58 for s in src:
53 for s in src:
59 ds = self._data.get(s, None)
54 ds = self._data.get(s, None)
60 if ds:
55 if ds:
61 self._data[s] = ds.preparewrite()
56 self._data[s] = ds.preparewrite()
62 else:
57 else:
63 self._data[s] = util.cowsortdict()
58 self._data[s] = util.cowsortdict()
64 self._data[s].update(src._data[s])
59 self._data[s].update(src._data[s])
65 self._source.update(src._source)
60
61 def _get(self, section, item):
62 return self._data.get(section, {}).get(item)
66
63
67 def get(self, section, item, default=None):
64 def get(self, section, item, default=None):
68 return self._data.get(section, {}).get(item, default)
65 result = self._get(section, item)
66 if result is None:
67 return default
68 return result[0]
69
69
70 def backup(self, section, item):
70 def backup(self, section, key):
71 """return a tuple allowing restore to reinstall a previous value
71 """return a tuple allowing restore to reinstall a previous value
72
72
73 The main reason we need it is because it handles the "no data" case.
73 The main reason we need it is because it handles the "no data" case.
74 """
74 """
75 try:
75 try:
76 value = self._data[section][item]
76 item = self._data[section][key]
77 source = self.source(section, item)
78 return (section, item, value, source)
79 except KeyError:
77 except KeyError:
80 return (section, item)
78 return (section, key)
79 else:
80 return (section, key) + item
81
81
82 def source(self, section, item):
82 def source(self, section, item):
83 return self._source.get((section, item), b"")
83 result = self._get(section, item)
84 if result is None:
85 return b""
86 return result[1]
84
87
85 def sections(self):
88 def sections(self):
86 return sorted(self._data.keys())
89 return sorted(self._data.keys())
87
90
88 def items(self, section):
91 def items(self, section):
89 return list(pycompat.iteritems(self._data.get(section, {})))
92 items = pycompat.iteritems(self._data.get(section, {}))
93 return [(k, v) for (k, (v, s)) in items]
90
94
91 def set(self, section, item, value, source=b""):
95 def set(self, section, item, value, source=b""):
92 if pycompat.ispy3:
96 if pycompat.ispy3:
93 assert not isinstance(
97 assert not isinstance(
94 section, str
98 section, str
95 ), b'config section may not be unicode strings on Python 3'
99 ), b'config section may not be unicode strings on Python 3'
96 assert not isinstance(
100 assert not isinstance(
97 item, str
101 item, str
98 ), b'config item may not be unicode strings on Python 3'
102 ), b'config item may not be unicode strings on Python 3'
99 assert not isinstance(
103 assert not isinstance(
100 value, str
104 value, str
101 ), b'config values may not be unicode strings on Python 3'
105 ), b'config values may not be unicode strings on Python 3'
102 if section not in self:
106 if section not in self:
103 self._data[section] = util.cowsortdict()
107 self._data[section] = util.cowsortdict()
104 else:
108 else:
105 self._data[section] = self._data[section].preparewrite()
109 self._data[section] = self._data[section].preparewrite()
106 self._data[section][item] = value
110 self._data[section][item] = (value, source)
107 if source:
108 self._source = self._source.preparewrite()
109 self._source[(section, item)] = source
110
111
111 def alter(self, section, key, new_value):
112 def alter(self, section, key, new_value):
112 """alter a value without altering its source or level
113 """alter a value without altering its source or level
113
114
114 This method is meant to be used by `ui.fixconfig` only."""
115 This method is meant to be used by `ui.fixconfig` only."""
115 item = self._data[section][key]
116 item = self._data[section][key]
116 size = len(item)
117 size = len(item)
117 new_item = (new_value,) + item[1:]
118 new_item = (new_value,) + item[1:]
118 assert len(new_item) == size
119 assert len(new_item) == size
119 self._data[section][key] = new_item
120 self._data[section][key] = new_item
120
121
121 def restore(self, data):
122 def restore(self, data):
122 """restore data returned by self.backup"""
123 """restore data returned by self.backup"""
123 self._source = self._source.preparewrite()
124 if len(data) != 2:
124 if len(data) == 4:
125 # restore old data
125 # restore old data
126 section, item, value, source = data
126 section, key = data[:2]
127 item = data[2:]
127 self._data[section] = self._data[section].preparewrite()
128 self._data[section] = self._data[section].preparewrite()
128 self._data[section][item] = value
129 self._data[section][key] = item
129 self._source[(section, item)] = source
130 else:
130 else:
131 # no data before, remove everything
131 # no data before, remove everything
132 section, item = data
132 section, item = data
133 if section in self._data:
133 if section in self._data:
134 self._data[section].pop(item, None)
134 self._data[section].pop(item, None)
135 self._source.pop((section, item), None)
136
135
137 def parse(self, src, data, sections=None, remap=None, include=None):
136 def parse(self, src, data, sections=None, remap=None, include=None):
138 sectionre = util.re.compile(br'\[([^\[]+)\]')
137 sectionre = util.re.compile(br'\[([^\[]+)\]')
139 itemre = util.re.compile(br'([^=\s][^=]*?)\s*=\s*(.*\S|)')
138 itemre = util.re.compile(br'([^=\s][^=]*?)\s*=\s*(.*\S|)')
140 contre = util.re.compile(br'\s+(\S|\S.*\S)\s*$')
139 contre = util.re.compile(br'\s+(\S|\S.*\S)\s*$')
141 emptyre = util.re.compile(br'(;|#|\s*$)')
140 emptyre = util.re.compile(br'(;|#|\s*$)')
142 commentre = util.re.compile(br'(;|#)')
141 commentre = util.re.compile(br'(;|#)')
143 unsetre = util.re.compile(br'%unset\s+(\S+)')
142 unsetre = util.re.compile(br'%unset\s+(\S+)')
144 includere = util.re.compile(br'%include\s+(\S|\S.*\S)\s*$')
143 includere = util.re.compile(br'%include\s+(\S|\S.*\S)\s*$')
145 section = b""
144 section = b""
146 item = None
145 item = None
147 line = 0
146 line = 0
148 cont = False
147 cont = False
149
148
150 if remap:
149 if remap:
151 section = remap.get(section, section)
150 section = remap.get(section, section)
152
151
153 for l in data.splitlines(True):
152 for l in data.splitlines(True):
154 line += 1
153 line += 1
155 if line == 1 and l.startswith(b'\xef\xbb\xbf'):
154 if line == 1 and l.startswith(b'\xef\xbb\xbf'):
156 # Someone set us up the BOM
155 # Someone set us up the BOM
157 l = l[3:]
156 l = l[3:]
158 if cont:
157 if cont:
159 if commentre.match(l):
158 if commentre.match(l):
160 continue
159 continue
161 m = contre.match(l)
160 m = contre.match(l)
162 if m:
161 if m:
163 if sections and section not in sections:
162 if sections and section not in sections:
164 continue
163 continue
165 v = self.get(section, item) + b"\n" + m.group(1)
164 v = self.get(section, item) + b"\n" + m.group(1)
166 self.set(section, item, v, b"%s:%d" % (src, line))
165 self.set(section, item, v, b"%s:%d" % (src, line))
167 continue
166 continue
168 item = None
167 item = None
169 cont = False
168 cont = False
170 m = includere.match(l)
169 m = includere.match(l)
171
170
172 if m and include:
171 if m and include:
173 expanded = util.expandpath(m.group(1))
172 expanded = util.expandpath(m.group(1))
174 try:
173 try:
175 include(expanded, remap=remap, sections=sections)
174 include(expanded, remap=remap, sections=sections)
176 except IOError as inst:
175 except IOError as inst:
177 if inst.errno != errno.ENOENT:
176 if inst.errno != errno.ENOENT:
178 raise error.ConfigError(
177 raise error.ConfigError(
179 _(b"cannot include %s (%s)")
178 _(b"cannot include %s (%s)")
180 % (expanded, encoding.strtolocal(inst.strerror)),
179 % (expanded, encoding.strtolocal(inst.strerror)),
181 b"%s:%d" % (src, line),
180 b"%s:%d" % (src, line),
182 )
181 )
183 continue
182 continue
184 if emptyre.match(l):
183 if emptyre.match(l):
185 continue
184 continue
186 m = sectionre.match(l)
185 m = sectionre.match(l)
187 if m:
186 if m:
188 section = m.group(1)
187 section = m.group(1)
189 if remap:
188 if remap:
190 section = remap.get(section, section)
189 section = remap.get(section, section)
191 if section not in self:
190 if section not in self:
192 self._data[section] = util.cowsortdict()
191 self._data[section] = util.cowsortdict()
193 continue
192 continue
194 m = itemre.match(l)
193 m = itemre.match(l)
195 if m:
194 if m:
196 item = m.group(1)
195 item = m.group(1)
197 cont = True
196 cont = True
198 if sections and section not in sections:
197 if sections and section not in sections:
199 continue
198 continue
200 self.set(section, item, m.group(2), b"%s:%d" % (src, line))
199 self.set(section, item, m.group(2), b"%s:%d" % (src, line))
201 continue
200 continue
202 m = unsetre.match(l)
201 m = unsetre.match(l)
203 if m:
202 if m:
204 name = m.group(1)
203 name = m.group(1)
205 if sections and section not in sections:
204 if sections and section not in sections:
206 continue
205 continue
207 if self.get(section, name) is not None:
206 if self.get(section, name) is not None:
208 self._data[section] = self._data[section].preparewrite()
207 self._data[section] = self._data[section].preparewrite()
209 del self._data[section][name]
208 del self._data[section][name]
210 self._unset.append((section, name))
209 self._unset.append((section, name))
211 continue
210 continue
212
211
213 message = l.rstrip()
212 message = l.rstrip()
214 if l.startswith(b' '):
213 if l.startswith(b' '):
215 message = b"unexpected leading whitespace: %s" % message
214 message = b"unexpected leading whitespace: %s" % message
216 raise error.ConfigError(message, (b"%s:%d" % (src, line)))
215 raise error.ConfigError(message, (b"%s:%d" % (src, line)))
217
216
218 def read(self, path, fp=None, sections=None, remap=None):
217 def read(self, path, fp=None, sections=None, remap=None):
219 if not fp:
218 if not fp:
220 fp = util.posixfile(path, b'rb')
219 fp = util.posixfile(path, b'rb')
221 assert (
220 assert (
222 getattr(fp, 'mode', 'rb') == 'rb'
221 getattr(fp, 'mode', 'rb') == 'rb'
223 ), b'config files must be opened in binary mode, got fp=%r mode=%r' % (
222 ), b'config files must be opened in binary mode, got fp=%r mode=%r' % (
224 fp,
223 fp,
225 fp.mode,
224 fp.mode,
226 )
225 )
227
226
228 dir = os.path.dirname(path)
227 dir = os.path.dirname(path)
229
228
230 def include(rel, remap, sections):
229 def include(rel, remap, sections):
231 abs = os.path.normpath(os.path.join(dir, rel))
230 abs = os.path.normpath(os.path.join(dir, rel))
232 self.read(abs, remap=remap, sections=sections)
231 self.read(abs, remap=remap, sections=sections)
233
232
234 self.parse(
233 self.parse(
235 path, fp.read(), sections=sections, remap=remap, include=include
234 path, fp.read(), sections=sections, remap=remap, include=include
236 )
235 )
237
236
238
237
239 def parselist(value):
238 def parselist(value):
240 """parse a configuration value as a list of comma/space separated strings
239 """parse a configuration value as a list of comma/space separated strings
241
240
242 >>> parselist(b'this,is "a small" ,test')
241 >>> parselist(b'this,is "a small" ,test')
243 ['this', 'is', 'a small', 'test']
242 ['this', 'is', 'a small', 'test']
244 """
243 """
245
244
246 def _parse_plain(parts, s, offset):
245 def _parse_plain(parts, s, offset):
247 whitespace = False
246 whitespace = False
248 while offset < len(s) and (
247 while offset < len(s) and (
249 s[offset : offset + 1].isspace() or s[offset : offset + 1] == b','
248 s[offset : offset + 1].isspace() or s[offset : offset + 1] == b','
250 ):
249 ):
251 whitespace = True
250 whitespace = True
252 offset += 1
251 offset += 1
253 if offset >= len(s):
252 if offset >= len(s):
254 return None, parts, offset
253 return None, parts, offset
255 if whitespace:
254 if whitespace:
256 parts.append(b'')
255 parts.append(b'')
257 if s[offset : offset + 1] == b'"' and not parts[-1]:
256 if s[offset : offset + 1] == b'"' and not parts[-1]:
258 return _parse_quote, parts, offset + 1
257 return _parse_quote, parts, offset + 1
259 elif s[offset : offset + 1] == b'"' and parts[-1][-1:] == b'\\':
258 elif s[offset : offset + 1] == b'"' and parts[-1][-1:] == b'\\':
260 parts[-1] = parts[-1][:-1] + s[offset : offset + 1]
259 parts[-1] = parts[-1][:-1] + s[offset : offset + 1]
261 return _parse_plain, parts, offset + 1
260 return _parse_plain, parts, offset + 1
262 parts[-1] += s[offset : offset + 1]
261 parts[-1] += s[offset : offset + 1]
263 return _parse_plain, parts, offset + 1
262 return _parse_plain, parts, offset + 1
264
263
265 def _parse_quote(parts, s, offset):
264 def _parse_quote(parts, s, offset):
266 if offset < len(s) and s[offset : offset + 1] == b'"': # ""
265 if offset < len(s) and s[offset : offset + 1] == b'"': # ""
267 parts.append(b'')
266 parts.append(b'')
268 offset += 1
267 offset += 1
269 while offset < len(s) and (
268 while offset < len(s) and (
270 s[offset : offset + 1].isspace()
269 s[offset : offset + 1].isspace()
271 or s[offset : offset + 1] == b','
270 or s[offset : offset + 1] == b','
272 ):
271 ):
273 offset += 1
272 offset += 1
274 return _parse_plain, parts, offset
273 return _parse_plain, parts, offset
275
274
276 while offset < len(s) and s[offset : offset + 1] != b'"':
275 while offset < len(s) and s[offset : offset + 1] != b'"':
277 if (
276 if (
278 s[offset : offset + 1] == b'\\'
277 s[offset : offset + 1] == b'\\'
279 and offset + 1 < len(s)
278 and offset + 1 < len(s)
280 and s[offset + 1 : offset + 2] == b'"'
279 and s[offset + 1 : offset + 2] == b'"'
281 ):
280 ):
282 offset += 1
281 offset += 1
283 parts[-1] += b'"'
282 parts[-1] += b'"'
284 else:
283 else:
285 parts[-1] += s[offset : offset + 1]
284 parts[-1] += s[offset : offset + 1]
286 offset += 1
285 offset += 1
287
286
288 if offset >= len(s):
287 if offset >= len(s):
289 real_parts = _configlist(parts[-1])
288 real_parts = _configlist(parts[-1])
290 if not real_parts:
289 if not real_parts:
291 parts[-1] = b'"'
290 parts[-1] = b'"'
292 else:
291 else:
293 real_parts[0] = b'"' + real_parts[0]
292 real_parts[0] = b'"' + real_parts[0]
294 parts = parts[:-1]
293 parts = parts[:-1]
295 parts.extend(real_parts)
294 parts.extend(real_parts)
296 return None, parts, offset
295 return None, parts, offset
297
296
298 offset += 1
297 offset += 1
299 while offset < len(s) and s[offset : offset + 1] in [b' ', b',']:
298 while offset < len(s) and s[offset : offset + 1] in [b' ', b',']:
300 offset += 1
299 offset += 1
301
300
302 if offset < len(s):
301 if offset < len(s):
303 if offset + 1 == len(s) and s[offset : offset + 1] == b'"':
302 if offset + 1 == len(s) and s[offset : offset + 1] == b'"':
304 parts[-1] += b'"'
303 parts[-1] += b'"'
305 offset += 1
304 offset += 1
306 else:
305 else:
307 parts.append(b'')
306 parts.append(b'')
308 else:
307 else:
309 return None, parts, offset
308 return None, parts, offset
310
309
311 return _parse_plain, parts, offset
310 return _parse_plain, parts, offset
312
311
313 def _configlist(s):
312 def _configlist(s):
314 s = s.rstrip(b' ,')
313 s = s.rstrip(b' ,')
315 if not s:
314 if not s:
316 return []
315 return []
317 parser, parts, offset = _parse_plain, [b''], 0
316 parser, parts, offset = _parse_plain, [b''], 0
318 while parser:
317 while parser:
319 parser, parts, offset = parser(parts, s, offset)
318 parser, parts, offset = parser(parts, s, offset)
320 return parts
319 return parts
321
320
322 if value is not None and isinstance(value, bytes):
321 if value is not None and isinstance(value, bytes):
323 result = _configlist(value.lstrip(b' ,\n'))
322 result = _configlist(value.lstrip(b' ,\n'))
324 else:
323 else:
325 result = value
324 result = value
326 return result or []
325 return result or []
General Comments 0
You need to be logged in to leave comments. Login now