##// END OF EJS Templates
check-config: syntax to allow inconsistent config values...
Gregory Szorc -
r33192:5d8942db default
parent child Browse files
Show More
@@ -1,130 +1,139 b''
1 1 #!/usr/bin/env python
2 2 #
3 3 # check-config - a config flag documentation checker for Mercurial
4 4 #
5 5 # Copyright 2015 Matt Mackall <mpm@selenic.com>
6 6 #
7 7 # This software may be used and distributed according to the terms of the
8 8 # GNU General Public License version 2 or any later version.
9 9
10 10 from __future__ import absolute_import, print_function
11 11 import re
12 12 import sys
13 13
14 14 foundopts = {}
15 15 documented = {}
16 allowinconsistent = set()
16 17
17 18 configre = re.compile(r'''
18 19 # Function call
19 20 ui\.config(?P<ctype>|int|bool|list)\(
20 21 # First argument.
21 22 ['"](?P<section>\S+)['"],\s*
22 23 # Second argument
23 24 ['"](?P<option>\S+)['"](,\s+
24 25 (?:default=)?(?P<default>\S+?))?
25 26 \)''', re.VERBOSE | re.MULTILINE)
26 27
27 28 configwithre = re.compile('''
28 29 ui\.config(?P<ctype>with)\(
29 30 # First argument is callback function. This doesn't parse robustly
30 31 # if it is e.g. a function call.
31 32 [^,]+,\s*
32 33 ['"](?P<section>\S+)['"],\s*
33 34 ['"](?P<option>\S+)['"](,\s+
34 35 (?:default=)?(?P<default>\S+?))?
35 36 \)''', re.VERBOSE | re.MULTILINE)
36 37
37 38 configpartialre = (r"""ui\.config""")
38 39
40 ignorere = re.compile(r'''
41 \#\s(?P<reason>internal|experimental|deprecated|developer|inconsistent)\s
42 config:\s(?P<config>\S+\.\S+)$
43 ''', re.VERBOSE | re.MULTILINE)
44
39 45 def main(args):
40 46 for f in args:
41 47 sect = ''
42 48 prevname = ''
43 49 confsect = ''
44 50 carryover = ''
45 51 for l in open(f):
46 52
47 53 # check topic-like bits
48 54 m = re.match('\s*``(\S+)``', l)
49 55 if m:
50 56 prevname = m.group(1)
51 57 if re.match('^\s*-+$', l):
52 58 sect = prevname
53 59 prevname = ''
54 60
55 61 if sect and prevname:
56 62 name = sect + '.' + prevname
57 63 documented[name] = 1
58 64
59 65 # check docstring bits
60 66 m = re.match(r'^\s+\[(\S+)\]', l)
61 67 if m:
62 68 confsect = m.group(1)
63 69 continue
64 70 m = re.match(r'^\s+(?:#\s*)?(\S+) = ', l)
65 71 if m:
66 72 name = confsect + '.' + m.group(1)
67 73 documented[name] = 1
68 74
69 75 # like the bugzilla extension
70 76 m = re.match(r'^\s*(\S+\.\S+)$', l)
71 77 if m:
72 78 documented[m.group(1)] = 1
73 79
74 80 # like convert
75 81 m = re.match(r'^\s*:(\S+\.\S+):\s+', l)
76 82 if m:
77 83 documented[m.group(1)] = 1
78 84
79 85 # quoted in help or docstrings
80 86 m = re.match(r'.*?``(\S+\.\S+)``', l)
81 87 if m:
82 88 documented[m.group(1)] = 1
83 89
84 90 # look for ignore markers
85 m = re.search(r'# (?:internal|experimental|deprecated|developer)'
86 ' config: (\S+\.\S+)$', l)
91 m = ignorere.search(l)
87 92 if m:
88 documented[m.group(1)] = 1
93 if m.group('reason') == 'inconsistent':
94 allowinconsistent.add(m.group('config'))
95 else:
96 documented[m.group('config')] = 1
89 97
90 98 # look for code-like bits
91 99 line = carryover + l
92 100 m = configre.search(line) or configwithre.search(line)
93 101 if m:
94 102 ctype = m.group('ctype')
95 103 if not ctype:
96 104 ctype = 'str'
97 105 name = m.group('section') + "." + m.group('option')
98 106 default = m.group('default')
99 107 if default in (None, 'False', 'None', '0', '[]', '""', "''"):
100 108 default = ''
101 109 if re.match('[a-z.]+$', default):
102 110 default = '<variable>'
103 if name in foundopts and (ctype, default) != foundopts[name]:
111 if (name in foundopts and (ctype, default) != foundopts[name]
112 and name not in allowinconsistent):
104 113 print(l)
105 114 print("conflict on %s: %r != %r" % (name, (ctype, default),
106 115 foundopts[name]))
107 116 foundopts[name] = (ctype, default)
108 117 carryover = ''
109 118 else:
110 119 m = re.search(configpartialre, line)
111 120 if m:
112 121 carryover = line
113 122 else:
114 123 carryover = ''
115 124
116 125 for name in sorted(foundopts):
117 126 if name not in documented:
118 127 if not (name.startswith("devel.") or
119 128 name.startswith("experimental.") or
120 129 name.startswith("debug.")):
121 130 ctype, default = foundopts[name]
122 131 if default:
123 132 default = ' [%s]' % default
124 133 print("undocumented: %s (%s)%s" % (name, ctype, default))
125 134
126 135 if __name__ == "__main__":
127 136 if len(sys.argv) > 1:
128 137 sys.exit(main(sys.argv[1:]))
129 138 else:
130 139 sys.exit(main([l.rstrip() for l in sys.stdin]))
@@ -1,237 +1,238 b''
1 1 # profiling.py - profiling functions
2 2 #
3 3 # Copyright 2016 Gregory Szorc <gregory.szorc@gmail.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from __future__ import absolute_import, print_function
9 9
10 10 import contextlib
11 11
12 12 from .i18n import _
13 13 from . import (
14 14 encoding,
15 15 error,
16 16 extensions,
17 17 util,
18 18 )
19 19
20 20 def _loadprofiler(ui, profiler):
21 21 """load profiler extension. return profile method, or None on failure"""
22 22 extname = profiler
23 23 extensions.loadall(ui, whitelist=[extname])
24 24 try:
25 25 mod = extensions.find(extname)
26 26 except KeyError:
27 27 return None
28 28 else:
29 29 return getattr(mod, 'profile', None)
30 30
31 31 @contextlib.contextmanager
32 32 def lsprofile(ui, fp):
33 33 format = ui.config('profiling', 'format', default='text')
34 34 field = ui.config('profiling', 'sort', default='inlinetime')
35 35 limit = ui.configint('profiling', 'limit', default=30)
36 36 climit = ui.configint('profiling', 'nested', default=0)
37 37
38 38 if format not in ['text', 'kcachegrind']:
39 39 ui.warn(_("unrecognized profiling format '%s'"
40 40 " - Ignored\n") % format)
41 41 format = 'text'
42 42
43 43 try:
44 44 from . import lsprof
45 45 except ImportError:
46 46 raise error.Abort(_(
47 47 'lsprof not available - install from '
48 48 'http://codespeak.net/svn/user/arigo/hack/misc/lsprof/'))
49 49 p = lsprof.Profiler()
50 50 p.enable(subcalls=True)
51 51 try:
52 52 yield
53 53 finally:
54 54 p.disable()
55 55
56 56 if format == 'kcachegrind':
57 57 from . import lsprofcalltree
58 58 calltree = lsprofcalltree.KCacheGrind(p)
59 59 calltree.output(fp)
60 60 else:
61 61 # format == 'text'
62 62 stats = lsprof.Stats(p.getstats())
63 63 stats.sort(field)
64 64 stats.pprint(limit=limit, file=fp, climit=climit)
65 65
66 66 @contextlib.contextmanager
67 67 def flameprofile(ui, fp):
68 68 try:
69 69 from flamegraph import flamegraph
70 70 except ImportError:
71 71 raise error.Abort(_(
72 72 'flamegraph not available - install from '
73 73 'https://github.com/evanhempel/python-flamegraph'))
74 74 # developer config: profiling.freq
75 75 freq = ui.configint('profiling', 'freq', default=1000)
76 76 filter_ = None
77 77 collapse_recursion = True
78 78 thread = flamegraph.ProfileThread(fp, 1.0 / freq,
79 79 filter_, collapse_recursion)
80 80 start_time = util.timer()
81 81 try:
82 82 thread.start()
83 83 yield
84 84 finally:
85 85 thread.stop()
86 86 thread.join()
87 87 print('Collected %d stack frames (%d unique) in %2.2f seconds.' % (
88 88 util.timer() - start_time, thread.num_frames(),
89 89 thread.num_frames(unique=True)))
90 90
91 91 @contextlib.contextmanager
92 92 def statprofile(ui, fp):
93 93 from . import statprof
94 94
95 95 freq = ui.configint('profiling', 'freq', default=1000)
96 96 if freq > 0:
97 97 # Cannot reset when profiler is already active. So silently no-op.
98 98 if statprof.state.profile_level == 0:
99 99 statprof.reset(freq)
100 100 else:
101 101 ui.warn(_("invalid sampling frequency '%s' - ignoring\n") % freq)
102 102
103 103 statprof.start(mechanism='thread')
104 104
105 105 try:
106 106 yield
107 107 finally:
108 108 data = statprof.stop()
109 109
110 110 profformat = ui.config('profiling', 'statformat', 'hotpath')
111 111
112 112 formats = {
113 113 'byline': statprof.DisplayFormats.ByLine,
114 114 'bymethod': statprof.DisplayFormats.ByMethod,
115 115 'hotpath': statprof.DisplayFormats.Hotpath,
116 116 'json': statprof.DisplayFormats.Json,
117 117 'chrome': statprof.DisplayFormats.Chrome,
118 118 }
119 119
120 120 if profformat in formats:
121 121 displayformat = formats[profformat]
122 122 else:
123 123 ui.warn(_('unknown profiler output format: %s\n') % profformat)
124 124 displayformat = statprof.DisplayFormats.Hotpath
125 125
126 126 kwargs = {}
127 127
128 128 def fraction(s):
129 129 if isinstance(s, (float, int)):
130 130 return float(s)
131 131 if s.endswith('%'):
132 132 v = float(s[:-1]) / 100
133 133 else:
134 134 v = float(s)
135 135 if 0 <= v <= 1:
136 136 return v
137 137 raise ValueError(s)
138 138
139 139 if profformat == 'chrome':
140 140 showmin = ui.configwith(fraction, 'profiling', 'showmin', 0.005)
141 141 showmax = ui.configwith(fraction, 'profiling', 'showmax', 0.999)
142 142 kwargs.update(minthreshold=showmin, maxthreshold=showmax)
143 143 elif profformat == 'hotpath':
144 # inconsistent config: profiling.showmin
144 145 limit = ui.configwith(fraction, 'profiling', 'showmin', 0.05)
145 146 kwargs['limit'] = limit
146 147
147 148 statprof.display(fp, data=data, format=displayformat, **kwargs)
148 149
149 150 class profile(object):
150 151 """Start profiling.
151 152
152 153 Profiling is active when the context manager is active. When the context
153 154 manager exits, profiling results will be written to the configured output.
154 155 """
155 156 def __init__(self, ui, enabled=True):
156 157 self._ui = ui
157 158 self._output = None
158 159 self._fp = None
159 160 self._fpdoclose = True
160 161 self._profiler = None
161 162 self._enabled = enabled
162 163 self._entered = False
163 164 self._started = False
164 165
165 166 def __enter__(self):
166 167 self._entered = True
167 168 if self._enabled:
168 169 self.start()
169 170 return self
170 171
171 172 def start(self):
172 173 """Start profiling.
173 174
174 175 The profiling will stop at the context exit.
175 176
176 177 If the profiler was already started, this has no effect."""
177 178 if not self._entered:
178 179 raise error.ProgrammingError()
179 180 if self._started:
180 181 return
181 182 self._started = True
182 183 profiler = encoding.environ.get('HGPROF')
183 184 proffn = None
184 185 if profiler is None:
185 186 profiler = self._ui.config('profiling', 'type', default='stat')
186 187 if profiler not in ('ls', 'stat', 'flame'):
187 188 # try load profiler from extension with the same name
188 189 proffn = _loadprofiler(self._ui, profiler)
189 190 if proffn is None:
190 191 self._ui.warn(_("unrecognized profiler '%s' - ignored\n")
191 192 % profiler)
192 193 profiler = 'stat'
193 194
194 195 self._output = self._ui.config('profiling', 'output')
195 196
196 197 try:
197 198 if self._output == 'blackbox':
198 199 self._fp = util.stringio()
199 200 elif self._output:
200 201 path = self._ui.expandpath(self._output)
201 202 self._fp = open(path, 'wb')
202 203 else:
203 204 self._fpdoclose = False
204 205 self._fp = self._ui.ferr
205 206
206 207 if proffn is not None:
207 208 pass
208 209 elif profiler == 'ls':
209 210 proffn = lsprofile
210 211 elif profiler == 'flame':
211 212 proffn = flameprofile
212 213 else:
213 214 proffn = statprofile
214 215
215 216 self._profiler = proffn(self._ui, self._fp)
216 217 self._profiler.__enter__()
217 218 except: # re-raises
218 219 self._closefp()
219 220 raise
220 221
221 222 def __exit__(self, exception_type, exception_value, traceback):
222 223 propagate = None
223 224 if self._profiler is not None:
224 225 propagate = self._profiler.__exit__(exception_type, exception_value,
225 226 traceback)
226 227 if self._output == 'blackbox':
227 228 val = 'Profile:\n%s' % self._fp.getvalue()
228 229 # ui.log treats the input as a format string,
229 230 # so we need to escape any % signs.
230 231 val = val.replace('%', '%%')
231 232 self._ui.log('profile', val)
232 233 self._closefp()
233 234 return propagate
234 235
235 236 def _closefp(self):
236 237 if self._fpdoclose and self._fp is not None:
237 238 self._fp.close()
@@ -1,38 +1,47 b''
1 1 #require test-repo
2 2
3 3 $ . "$TESTDIR/helpers-testrepo.sh"
4 4
5 5 Sanity check check-config.py
6 6
7 7 $ cat > testfile.py << EOF
8 8 > # Good
9 9 > foo = ui.config('ui', 'username')
10 10 > # Missing
11 11 > foo = ui.config('ui', 'doesnotexist')
12 12 > # Missing different type
13 13 > foo = ui.configint('ui', 'missingint')
14 14 > # Missing with default value
15 15 > foo = ui.configbool('ui', 'missingbool1', default=True)
16 16 > foo = ui.configbool('ui', 'missingbool2', False)
17 > # Inconsistent values for defaults.
18 > foo = ui.configint('ui', 'intdefault', default=1)
19 > foo = ui.configint('ui', 'intdefault', default=42)
20 > # Can suppress inconsistent value error
21 > foo = ui.configint('ui', 'intdefault2', default=1)
22 > # inconsistent config: ui.intdefault2
23 > foo = ui.configint('ui', 'intdefault2', default=42)
17 24 > EOF
18 25
19 26 $ cat > files << EOF
20 27 > mercurial/help/config.txt
21 28 > $TESTTMP/testfile.py
22 29 > EOF
23 30
24 31 $ cd "$TESTDIR"/..
25 32
26 33 $ $PYTHON contrib/check-config.py < $TESTTMP/files
34 foo = ui.configint('ui', 'intdefault', default=42)
35
36 conflict on ui.intdefault: ('int', '42') != ('int', '1')
27 37 undocumented: ui.doesnotexist (str)
38 undocumented: ui.intdefault (int) [42]
39 undocumented: ui.intdefault2 (int) [42]
28 40 undocumented: ui.missingbool1 (bool) [True]
29 41 undocumented: ui.missingbool2 (bool)
30 42 undocumented: ui.missingint (int)
31 43
32 44 New errors are not allowed. Warnings are strongly discouraged.
33 45
34 46 $ syshg files "set:(**.py or **.txt) - tests/**" | sed 's|\\|/|g' |
35 47 > $PYTHON contrib/check-config.py
36 limit = ui.configwith(fraction, 'profiling', 'showmin', 0.05)
37
38 conflict on profiling.showmin: ('with', '0.05') != ('with', '0.005')
General Comments 0
You need to be logged in to leave comments. Login now