##// END OF EJS Templates
check-code: detect legacy octal syntax...
Gregory Szorc -
r25659:d60678a5 default
parent child Browse files
Show More
@@ -1,570 +1,571 b''
1 #!/usr/bin/env python
1 #!/usr/bin/env python
2 #
2 #
3 # check-code - a style and portability checker for Mercurial
3 # check-code - a style and portability checker for Mercurial
4 #
4 #
5 # Copyright 2010 Matt Mackall <mpm@selenic.com>
5 # Copyright 2010 Matt Mackall <mpm@selenic.com>
6 #
6 #
7 # This software may be used and distributed according to the terms of the
7 # This software may be used and distributed according to the terms of the
8 # GNU General Public License version 2 or any later version.
8 # GNU General Public License version 2 or any later version.
9
9
10 """style and portability checker for Mercurial
10 """style and portability checker for Mercurial
11
11
12 when a rule triggers wrong, do one of the following (prefer one from top):
12 when a rule triggers wrong, do one of the following (prefer one from top):
13 * do the work-around the rule suggests
13 * do the work-around the rule suggests
14 * doublecheck that it is a false match
14 * doublecheck that it is a false match
15 * improve the rule pattern
15 * improve the rule pattern
16 * add an ignore pattern to the rule (3rd arg) which matches your good line
16 * add an ignore pattern to the rule (3rd arg) which matches your good line
17 (you can append a short comment and match this, like: #re-raises, # no-py24)
17 (you can append a short comment and match this, like: #re-raises, # no-py24)
18 * change the pattern to a warning and list the exception in test-check-code-hg
18 * change the pattern to a warning and list the exception in test-check-code-hg
19 * ONLY use no--check-code for skipping entire files from external sources
19 * ONLY use no--check-code for skipping entire files from external sources
20 """
20 """
21
21
22 import re, glob, os, sys
22 import re, glob, os, sys
23 import keyword
23 import keyword
24 import optparse
24 import optparse
25 try:
25 try:
26 import re2
26 import re2
27 except ImportError:
27 except ImportError:
28 re2 = None
28 re2 = None
29
29
30 def compilere(pat, multiline=False):
30 def compilere(pat, multiline=False):
31 if multiline:
31 if multiline:
32 pat = '(?m)' + pat
32 pat = '(?m)' + pat
33 if re2:
33 if re2:
34 try:
34 try:
35 return re2.compile(pat)
35 return re2.compile(pat)
36 except re2.error:
36 except re2.error:
37 pass
37 pass
38 return re.compile(pat)
38 return re.compile(pat)
39
39
40 def repquote(m):
40 def repquote(m):
41 fromc = '.:'
41 fromc = '.:'
42 tochr = 'pq'
42 tochr = 'pq'
43 def encodechr(i):
43 def encodechr(i):
44 if i > 255:
44 if i > 255:
45 return 'u'
45 return 'u'
46 c = chr(i)
46 c = chr(i)
47 if c in ' \n':
47 if c in ' \n':
48 return c
48 return c
49 if c.isalpha():
49 if c.isalpha():
50 return 'x'
50 return 'x'
51 if c.isdigit():
51 if c.isdigit():
52 return 'n'
52 return 'n'
53 try:
53 try:
54 return tochr[fromc.find(c)]
54 return tochr[fromc.find(c)]
55 except (ValueError, IndexError):
55 except (ValueError, IndexError):
56 return 'o'
56 return 'o'
57 t = m.group('text')
57 t = m.group('text')
58 tt = ''.join(encodechr(i) for i in xrange(256))
58 tt = ''.join(encodechr(i) for i in xrange(256))
59 t = t.translate(tt)
59 t = t.translate(tt)
60 return m.group('quote') + t + m.group('quote')
60 return m.group('quote') + t + m.group('quote')
61
61
62 def reppython(m):
62 def reppython(m):
63 comment = m.group('comment')
63 comment = m.group('comment')
64 if comment:
64 if comment:
65 l = len(comment.rstrip())
65 l = len(comment.rstrip())
66 return "#" * l + comment[l:]
66 return "#" * l + comment[l:]
67 return repquote(m)
67 return repquote(m)
68
68
69 def repcomment(m):
69 def repcomment(m):
70 return m.group(1) + "#" * len(m.group(2))
70 return m.group(1) + "#" * len(m.group(2))
71
71
72 def repccomment(m):
72 def repccomment(m):
73 t = re.sub(r"((?<=\n) )|\S", "x", m.group(2))
73 t = re.sub(r"((?<=\n) )|\S", "x", m.group(2))
74 return m.group(1) + t + "*/"
74 return m.group(1) + t + "*/"
75
75
76 def repcallspaces(m):
76 def repcallspaces(m):
77 t = re.sub(r"\n\s+", "\n", m.group(2))
77 t = re.sub(r"\n\s+", "\n", m.group(2))
78 return m.group(1) + t
78 return m.group(1) + t
79
79
80 def repinclude(m):
80 def repinclude(m):
81 return m.group(1) + "<foo>"
81 return m.group(1) + "<foo>"
82
82
83 def rephere(m):
83 def rephere(m):
84 t = re.sub(r"\S", "x", m.group(2))
84 t = re.sub(r"\S", "x", m.group(2))
85 return m.group(1) + t
85 return m.group(1) + t
86
86
87
87
88 testpats = [
88 testpats = [
89 [
89 [
90 (r'pushd|popd', "don't use 'pushd' or 'popd', use 'cd'"),
90 (r'pushd|popd', "don't use 'pushd' or 'popd', use 'cd'"),
91 (r'\W\$?\(\([^\)\n]*\)\)', "don't use (()) or $(()), use 'expr'"),
91 (r'\W\$?\(\([^\)\n]*\)\)', "don't use (()) or $(()), use 'expr'"),
92 (r'grep.*-q', "don't use 'grep -q', redirect to /dev/null"),
92 (r'grep.*-q', "don't use 'grep -q', redirect to /dev/null"),
93 (r'(?<!hg )grep.*-a', "don't use 'grep -a', use in-line python"),
93 (r'(?<!hg )grep.*-a', "don't use 'grep -a', use in-line python"),
94 (r'sed.*-i', "don't use 'sed -i', use a temporary file"),
94 (r'sed.*-i', "don't use 'sed -i', use a temporary file"),
95 (r'\becho\b.*\\n', "don't use 'echo \\n', use printf"),
95 (r'\becho\b.*\\n', "don't use 'echo \\n', use printf"),
96 (r'echo -n', "don't use 'echo -n', use printf"),
96 (r'echo -n', "don't use 'echo -n', use printf"),
97 (r'(^|\|\s*)\bwc\b[^|]*$\n(?!.*\(re\))', "filter wc output"),
97 (r'(^|\|\s*)\bwc\b[^|]*$\n(?!.*\(re\))', "filter wc output"),
98 (r'head -c', "don't use 'head -c', use 'dd'"),
98 (r'head -c', "don't use 'head -c', use 'dd'"),
99 (r'tail -n', "don't use the '-n' option to tail, just use '-<num>'"),
99 (r'tail -n', "don't use the '-n' option to tail, just use '-<num>'"),
100 (r'sha1sum', "don't use sha1sum, use $TESTDIR/md5sum.py"),
100 (r'sha1sum', "don't use sha1sum, use $TESTDIR/md5sum.py"),
101 (r'ls.*-\w*R', "don't use 'ls -R', use 'find'"),
101 (r'ls.*-\w*R', "don't use 'ls -R', use 'find'"),
102 (r'printf.*[^\\]\\([1-9]|0\d)', "don't use 'printf \NNN', use Python"),
102 (r'printf.*[^\\]\\([1-9]|0\d)', "don't use 'printf \NNN', use Python"),
103 (r'printf.*[^\\]\\x', "don't use printf \\x, use Python"),
103 (r'printf.*[^\\]\\x', "don't use printf \\x, use Python"),
104 (r'\$\(.*\)', "don't use $(expr), use `expr`"),
104 (r'\$\(.*\)', "don't use $(expr), use `expr`"),
105 (r'rm -rf \*', "don't use naked rm -rf, target a directory"),
105 (r'rm -rf \*', "don't use naked rm -rf, target a directory"),
106 (r'(^|\|\s*)grep (-\w\s+)*[^|]*[(|]\w',
106 (r'(^|\|\s*)grep (-\w\s+)*[^|]*[(|]\w',
107 "use egrep for extended grep syntax"),
107 "use egrep for extended grep syntax"),
108 (r'/bin/', "don't use explicit paths for tools"),
108 (r'/bin/', "don't use explicit paths for tools"),
109 (r'[^\n]\Z', "no trailing newline"),
109 (r'[^\n]\Z', "no trailing newline"),
110 (r'export.*=', "don't export and assign at once"),
110 (r'export.*=', "don't export and assign at once"),
111 (r'^source\b', "don't use 'source', use '.'"),
111 (r'^source\b', "don't use 'source', use '.'"),
112 (r'touch -d', "don't use 'touch -d', use 'touch -t' instead"),
112 (r'touch -d', "don't use 'touch -d', use 'touch -t' instead"),
113 (r'ls +[^|\n-]+ +-', "options to 'ls' must come before filenames"),
113 (r'ls +[^|\n-]+ +-', "options to 'ls' must come before filenames"),
114 (r'[^>\n]>\s*\$HGRCPATH', "don't overwrite $HGRCPATH, append to it"),
114 (r'[^>\n]>\s*\$HGRCPATH', "don't overwrite $HGRCPATH, append to it"),
115 (r'^stop\(\)', "don't use 'stop' as a shell function name"),
115 (r'^stop\(\)', "don't use 'stop' as a shell function name"),
116 (r'(\[|\btest\b).*-e ', "don't use 'test -e', use 'test -f'"),
116 (r'(\[|\btest\b).*-e ', "don't use 'test -e', use 'test -f'"),
117 (r'\[\[\s+[^\]]*\]\]', "don't use '[[ ]]', use '[ ]'"),
117 (r'\[\[\s+[^\]]*\]\]', "don't use '[[ ]]', use '[ ]'"),
118 (r'^alias\b.*=', "don't use alias, use a function"),
118 (r'^alias\b.*=', "don't use alias, use a function"),
119 (r'if\s*!', "don't use '!' to negate exit status"),
119 (r'if\s*!', "don't use '!' to negate exit status"),
120 (r'/dev/u?random', "don't use entropy, use /dev/zero"),
120 (r'/dev/u?random', "don't use entropy, use /dev/zero"),
121 (r'do\s*true;\s*done', "don't use true as loop body, use sleep 0"),
121 (r'do\s*true;\s*done', "don't use true as loop body, use sleep 0"),
122 (r'^( *)\t', "don't use tabs to indent"),
122 (r'^( *)\t', "don't use tabs to indent"),
123 (r'sed (-e )?\'(\d+|/[^/]*/)i(?!\\\n)',
123 (r'sed (-e )?\'(\d+|/[^/]*/)i(?!\\\n)',
124 "put a backslash-escaped newline after sed 'i' command"),
124 "put a backslash-escaped newline after sed 'i' command"),
125 (r'^diff *-\w*u.*$\n(^ \$ |^$)', "prefix diff -u with cmp"),
125 (r'^diff *-\w*u.*$\n(^ \$ |^$)', "prefix diff -u with cmp"),
126 (r'seq ', "don't use 'seq', use $TESTDIR/seq.py")
126 (r'seq ', "don't use 'seq', use $TESTDIR/seq.py")
127 ],
127 ],
128 # warnings
128 # warnings
129 [
129 [
130 (r'^function', "don't use 'function', use old style"),
130 (r'^function', "don't use 'function', use old style"),
131 (r'^diff.*-\w*N', "don't use 'diff -N'"),
131 (r'^diff.*-\w*N', "don't use 'diff -N'"),
132 (r'\$PWD|\${PWD}', "don't use $PWD, use `pwd`"),
132 (r'\$PWD|\${PWD}', "don't use $PWD, use `pwd`"),
133 (r'^([^"\'\n]|("[^"\n]*")|(\'[^\'\n]*\'))*\^', "^ must be quoted"),
133 (r'^([^"\'\n]|("[^"\n]*")|(\'[^\'\n]*\'))*\^', "^ must be quoted"),
134 (r'kill (`|\$\()', "don't use kill, use killdaemons.py")
134 (r'kill (`|\$\()', "don't use kill, use killdaemons.py")
135 ]
135 ]
136 ]
136 ]
137
137
138 testfilters = [
138 testfilters = [
139 (r"( *)(#([^\n]*\S)?)", repcomment),
139 (r"( *)(#([^\n]*\S)?)", repcomment),
140 (r"<<(\S+)((.|\n)*?\n\1)", rephere),
140 (r"<<(\S+)((.|\n)*?\n\1)", rephere),
141 ]
141 ]
142
142
143 winglobmsg = "use (glob) to match Windows paths too"
143 winglobmsg = "use (glob) to match Windows paths too"
144 uprefix = r"^ \$ "
144 uprefix = r"^ \$ "
145 utestpats = [
145 utestpats = [
146 [
146 [
147 (r'^(\S.*|| [$>] .*)[ \t]\n', "trailing whitespace on non-output"),
147 (r'^(\S.*|| [$>] .*)[ \t]\n', "trailing whitespace on non-output"),
148 (uprefix + r'.*\|\s*sed[^|>\n]*\n',
148 (uprefix + r'.*\|\s*sed[^|>\n]*\n',
149 "use regex test output patterns instead of sed"),
149 "use regex test output patterns instead of sed"),
150 (uprefix + r'(true|exit 0)', "explicit zero exit unnecessary"),
150 (uprefix + r'(true|exit 0)', "explicit zero exit unnecessary"),
151 (uprefix + r'.*(?<!\[)\$\?', "explicit exit code checks unnecessary"),
151 (uprefix + r'.*(?<!\[)\$\?', "explicit exit code checks unnecessary"),
152 (uprefix + r'.*\|\| echo.*(fail|error)',
152 (uprefix + r'.*\|\| echo.*(fail|error)',
153 "explicit exit code checks unnecessary"),
153 "explicit exit code checks unnecessary"),
154 (uprefix + r'set -e', "don't use set -e"),
154 (uprefix + r'set -e', "don't use set -e"),
155 (uprefix + r'(\s|fi\b|done\b)', "use > for continued lines"),
155 (uprefix + r'(\s|fi\b|done\b)', "use > for continued lines"),
156 (uprefix + r'.*:\.\S*/', "x:.y in a path does not work on msys, rewrite "
156 (uprefix + r'.*:\.\S*/', "x:.y in a path does not work on msys, rewrite "
157 "as x://.y, or see `hg log -k msys` for alternatives", r'-\S+:\.|' #-Rxxx
157 "as x://.y, or see `hg log -k msys` for alternatives", r'-\S+:\.|' #-Rxxx
158 '# no-msys'), # in test-pull.t which is skipped on windows
158 '# no-msys'), # in test-pull.t which is skipped on windows
159 (r'^ saved backup bundle to \$TESTTMP.*\.hg$', winglobmsg),
159 (r'^ saved backup bundle to \$TESTTMP.*\.hg$', winglobmsg),
160 (r'^ changeset .* references (corrupted|missing) \$TESTTMP/.*[^)]$',
160 (r'^ changeset .* references (corrupted|missing) \$TESTTMP/.*[^)]$',
161 winglobmsg),
161 winglobmsg),
162 (r'^ pulling from \$TESTTMP/.*[^)]$', winglobmsg,
162 (r'^ pulling from \$TESTTMP/.*[^)]$', winglobmsg,
163 '\$TESTTMP/unix-repo$'), # in test-issue1802.t which skipped on windows
163 '\$TESTTMP/unix-repo$'), # in test-issue1802.t which skipped on windows
164 (r'^ reverting (?!subrepo ).*/.*[^)]$', winglobmsg),
164 (r'^ reverting (?!subrepo ).*/.*[^)]$', winglobmsg),
165 (r'^ cloning subrepo \S+/.*[^)]$', winglobmsg),
165 (r'^ cloning subrepo \S+/.*[^)]$', winglobmsg),
166 (r'^ pushing to \$TESTTMP/.*[^)]$', winglobmsg),
166 (r'^ pushing to \$TESTTMP/.*[^)]$', winglobmsg),
167 (r'^ pushing subrepo \S+/\S+ to.*[^)]$', winglobmsg),
167 (r'^ pushing subrepo \S+/\S+ to.*[^)]$', winglobmsg),
168 (r'^ moving \S+/.*[^)]$', winglobmsg),
168 (r'^ moving \S+/.*[^)]$', winglobmsg),
169 (r'^ no changes made to subrepo since.*/.*[^)]$', winglobmsg),
169 (r'^ no changes made to subrepo since.*/.*[^)]$', winglobmsg),
170 (r'^ .*: largefile \S+ not available from file:.*/.*[^)]$', winglobmsg),
170 (r'^ .*: largefile \S+ not available from file:.*/.*[^)]$', winglobmsg),
171 (r'^ .*file://\$TESTTMP',
171 (r'^ .*file://\$TESTTMP',
172 'write "file:/*/$TESTTMP" + (glob) to match on windows too'),
172 'write "file:/*/$TESTTMP" + (glob) to match on windows too'),
173 (r'^ (cat|find): .*: No such file or directory',
173 (r'^ (cat|find): .*: No such file or directory',
174 'use test -f to test for file existence'),
174 'use test -f to test for file existence'),
175 ],
175 ],
176 # warnings
176 # warnings
177 [
177 [
178 (r'^ [^*?/\n]* \(glob\)$',
178 (r'^ [^*?/\n]* \(glob\)$',
179 "glob match with no glob character (?*/)"),
179 "glob match with no glob character (?*/)"),
180 ]
180 ]
181 ]
181 ]
182
182
183 for i in [0, 1]:
183 for i in [0, 1]:
184 for tp in testpats[i]:
184 for tp in testpats[i]:
185 p = tp[0]
185 p = tp[0]
186 m = tp[1]
186 m = tp[1]
187 if p.startswith(r'^'):
187 if p.startswith(r'^'):
188 p = r"^ [$>] (%s)" % p[1:]
188 p = r"^ [$>] (%s)" % p[1:]
189 else:
189 else:
190 p = r"^ [$>] .*(%s)" % p
190 p = r"^ [$>] .*(%s)" % p
191 utestpats[i].append((p, m) + tp[2:])
191 utestpats[i].append((p, m) + tp[2:])
192
192
193 utestfilters = [
193 utestfilters = [
194 (r"<<(\S+)((.|\n)*?\n > \1)", rephere),
194 (r"<<(\S+)((.|\n)*?\n > \1)", rephere),
195 (r"( *)(#([^\n]*\S)?)", repcomment),
195 (r"( *)(#([^\n]*\S)?)", repcomment),
196 ]
196 ]
197
197
198 pypats = [
198 pypats = [
199 [
199 [
200 (r'\([^)]*\*\w[^()]+\w+=', "can't pass varargs with keyword in Py2.5"),
200 (r'\([^)]*\*\w[^()]+\w+=', "can't pass varargs with keyword in Py2.5"),
201 (r'^\s*def\s*\w+\s*\(.*,\s*\(',
201 (r'^\s*def\s*\w+\s*\(.*,\s*\(',
202 "tuple parameter unpacking not available in Python 3+"),
202 "tuple parameter unpacking not available in Python 3+"),
203 (r'lambda\s*\(.*,.*\)',
203 (r'lambda\s*\(.*,.*\)',
204 "tuple parameter unpacking not available in Python 3+"),
204 "tuple parameter unpacking not available in Python 3+"),
205 (r'import (.+,[^.]+\.[^.]+|[^.]+\.[^.]+,)',
205 (r'import (.+,[^.]+\.[^.]+|[^.]+\.[^.]+,)',
206 '2to3 can\'t always rewrite "import qux, foo.bar", '
206 '2to3 can\'t always rewrite "import qux, foo.bar", '
207 'use "import foo.bar" on its own line instead.'),
207 'use "import foo.bar" on its own line instead.'),
208 (r'(?<!def)\s+(cmp)\(', "cmp is not available in Python 3+"),
208 (r'(?<!def)\s+(cmp)\(', "cmp is not available in Python 3+"),
209 (r'\breduce\s*\(.*', "reduce is not available in Python 3+"),
209 (r'\breduce\s*\(.*', "reduce is not available in Python 3+"),
210 (r'dict\(.*=', 'dict() is different in Py2 and 3 and is slower than {}',
210 (r'dict\(.*=', 'dict() is different in Py2 and 3 and is slower than {}',
211 'dict-from-generator'),
211 'dict-from-generator'),
212 (r'\.has_key\b', "dict.has_key is not available in Python 3+"),
212 (r'\.has_key\b', "dict.has_key is not available in Python 3+"),
213 (r'\s<>\s', '<> operator is not available in Python 3+, use !='),
213 (r'\s<>\s', '<> operator is not available in Python 3+, use !='),
214 (r'^\s*\t', "don't use tabs"),
214 (r'^\s*\t', "don't use tabs"),
215 (r'\S;\s*\n', "semicolon"),
215 (r'\S;\s*\n', "semicolon"),
216 (r'[^_]_\([ \t\n]*(?:"[^"]+"[ \t\n+]*)+%', "don't use % inside _()"),
216 (r'[^_]_\([ \t\n]*(?:"[^"]+"[ \t\n+]*)+%', "don't use % inside _()"),
217 (r"[^_]_\([ \t\n]*(?:'[^']+'[ \t\n+]*)+%", "don't use % inside _()"),
217 (r"[^_]_\([ \t\n]*(?:'[^']+'[ \t\n+]*)+%", "don't use % inside _()"),
218 (r'(\w|\)),\w', "missing whitespace after ,"),
218 (r'(\w|\)),\w', "missing whitespace after ,"),
219 (r'(\w|\))[+/*\-<>]\w', "missing whitespace in expression"),
219 (r'(\w|\))[+/*\-<>]\w', "missing whitespace in expression"),
220 (r'^\s+(\w|\.)+=\w[^,()\n]*$', "missing whitespace in assignment"),
220 (r'^\s+(\w|\.)+=\w[^,()\n]*$', "missing whitespace in assignment"),
221 (r'.{81}', "line too long"),
221 (r'.{81}', "line too long"),
222 (r' x+[xo][\'"]\n\s+[\'"]x', 'string join across lines with no space'),
222 (r' x+[xo][\'"]\n\s+[\'"]x', 'string join across lines with no space'),
223 (r'[^\n]\Z', "no trailing newline"),
223 (r'[^\n]\Z', "no trailing newline"),
224 (r'(\S[ \t]+|^[ \t]+)\n', "trailing whitespace"),
224 (r'(\S[ \t]+|^[ \t]+)\n', "trailing whitespace"),
225 # (r'^\s+[^_ \n][^_. \n]+_[^_\n]+\s*=',
225 # (r'^\s+[^_ \n][^_. \n]+_[^_\n]+\s*=',
226 # "don't use underbars in identifiers"),
226 # "don't use underbars in identifiers"),
227 (r'^\s+(self\.)?[A-za-z][a-z0-9]+[A-Z]\w* = ',
227 (r'^\s+(self\.)?[A-za-z][a-z0-9]+[A-Z]\w* = ',
228 "don't use camelcase in identifiers"),
228 "don't use camelcase in identifiers"),
229 (r'^\s*(if|while|def|class|except|try)\s[^[\n]*:\s*[^\\n]#\s]+',
229 (r'^\s*(if|while|def|class|except|try)\s[^[\n]*:\s*[^\\n]#\s]+',
230 "linebreak after :"),
230 "linebreak after :"),
231 (r'class\s[^( \n]+:', "old-style class, use class foo(object)"),
231 (r'class\s[^( \n]+:', "old-style class, use class foo(object)"),
232 (r'class\s[^( \n]+\(\):',
232 (r'class\s[^( \n]+\(\):',
233 "class foo() creates old style object, use class foo(object)"),
233 "class foo() creates old style object, use class foo(object)"),
234 (r'\b(%s)\(' % '|'.join(k for k in keyword.kwlist
234 (r'\b(%s)\(' % '|'.join(k for k in keyword.kwlist
235 if k not in ('print', 'exec')),
235 if k not in ('print', 'exec')),
236 "Python keyword is not a function"),
236 "Python keyword is not a function"),
237 (r',]', "unneeded trailing ',' in list"),
237 (r',]', "unneeded trailing ',' in list"),
238 # (r'class\s[A-Z][^\(]*\((?!Exception)',
238 # (r'class\s[A-Z][^\(]*\((?!Exception)',
239 # "don't capitalize non-exception classes"),
239 # "don't capitalize non-exception classes"),
240 # (r'in range\(', "use xrange"),
240 # (r'in range\(', "use xrange"),
241 # (r'^\s*print\s+', "avoid using print in core and extensions"),
241 # (r'^\s*print\s+', "avoid using print in core and extensions"),
242 (r'[\x80-\xff]', "non-ASCII character literal"),
242 (r'[\x80-\xff]', "non-ASCII character literal"),
243 (r'("\')\.format\(', "str.format() has no bytes counterpart, use %"),
243 (r'("\')\.format\(', "str.format() has no bytes counterpart, use %"),
244 (r'^\s*(%s)\s\s' % '|'.join(keyword.kwlist),
244 (r'^\s*(%s)\s\s' % '|'.join(keyword.kwlist),
245 "gratuitous whitespace after Python keyword"),
245 "gratuitous whitespace after Python keyword"),
246 (r'([\(\[][ \t]\S)|(\S[ \t][\)\]])', "gratuitous whitespace in () or []"),
246 (r'([\(\[][ \t]\S)|(\S[ \t][\)\]])', "gratuitous whitespace in () or []"),
247 # (r'\s\s=', "gratuitous whitespace before ="),
247 # (r'\s\s=', "gratuitous whitespace before ="),
248 (r'[^>< ](\+=|-=|!=|<>|<=|>=|<<=|>>=|%=)\S',
248 (r'[^>< ](\+=|-=|!=|<>|<=|>=|<<=|>>=|%=)\S',
249 "missing whitespace around operator"),
249 "missing whitespace around operator"),
250 (r'[^>< ](\+=|-=|!=|<>|<=|>=|<<=|>>=|%=)\s',
250 (r'[^>< ](\+=|-=|!=|<>|<=|>=|<<=|>>=|%=)\s',
251 "missing whitespace around operator"),
251 "missing whitespace around operator"),
252 (r'\s(\+=|-=|!=|<>|<=|>=|<<=|>>=|%=)\S',
252 (r'\s(\+=|-=|!=|<>|<=|>=|<<=|>>=|%=)\S',
253 "missing whitespace around operator"),
253 "missing whitespace around operator"),
254 (r'[^^+=*/!<>&| %-](\s=|=\s)[^= ]',
254 (r'[^^+=*/!<>&| %-](\s=|=\s)[^= ]',
255 "wrong whitespace around ="),
255 "wrong whitespace around ="),
256 (r'\([^()]*( =[^=]|[^<>!=]= )',
256 (r'\([^()]*( =[^=]|[^<>!=]= )',
257 "no whitespace around = for named parameters"),
257 "no whitespace around = for named parameters"),
258 (r'raise Exception', "don't raise generic exceptions"),
258 (r'raise Exception', "don't raise generic exceptions"),
259 (r'raise [^,(]+, (\([^\)]+\)|[^,\(\)]+)$',
259 (r'raise [^,(]+, (\([^\)]+\)|[^,\(\)]+)$',
260 "don't use old-style two-argument raise, use Exception(message)"),
260 "don't use old-style two-argument raise, use Exception(message)"),
261 (r' is\s+(not\s+)?["\'0-9-]', "object comparison with literal"),
261 (r' is\s+(not\s+)?["\'0-9-]', "object comparison with literal"),
262 (r' [=!]=\s+(True|False|None)',
262 (r' [=!]=\s+(True|False|None)',
263 "comparison with singleton, use 'is' or 'is not' instead"),
263 "comparison with singleton, use 'is' or 'is not' instead"),
264 (r'^\s*(while|if) [01]:',
264 (r'^\s*(while|if) [01]:',
265 "use True/False for constant Boolean expression"),
265 "use True/False for constant Boolean expression"),
266 (r'(?:(?<!def)\s+|\()hasattr',
266 (r'(?:(?<!def)\s+|\()hasattr',
267 'hasattr(foo, bar) is broken, use util.safehasattr(foo, bar) instead'),
267 'hasattr(foo, bar) is broken, use util.safehasattr(foo, bar) instead'),
268 (r'opener\([^)]*\).read\(',
268 (r'opener\([^)]*\).read\(',
269 "use opener.read() instead"),
269 "use opener.read() instead"),
270 (r'opener\([^)]*\).write\(',
270 (r'opener\([^)]*\).write\(',
271 "use opener.write() instead"),
271 "use opener.write() instead"),
272 (r'[\s\(](open|file)\([^)]*\)\.read\(',
272 (r'[\s\(](open|file)\([^)]*\)\.read\(',
273 "use util.readfile() instead"),
273 "use util.readfile() instead"),
274 (r'[\s\(](open|file)\([^)]*\)\.write\(',
274 (r'[\s\(](open|file)\([^)]*\)\.write\(',
275 "use util.writefile() instead"),
275 "use util.writefile() instead"),
276 (r'^[\s\(]*(open(er)?|file)\([^)]*\)',
276 (r'^[\s\(]*(open(er)?|file)\([^)]*\)',
277 "always assign an opened file to a variable, and close it afterwards"),
277 "always assign an opened file to a variable, and close it afterwards"),
278 (r'[\s\(](open|file)\([^)]*\)\.',
278 (r'[\s\(](open|file)\([^)]*\)\.',
279 "always assign an opened file to a variable, and close it afterwards"),
279 "always assign an opened file to a variable, and close it afterwards"),
280 (r'(?i)descend[e]nt', "the proper spelling is descendAnt"),
280 (r'(?i)descend[e]nt', "the proper spelling is descendAnt"),
281 (r'\.debug\(\_', "don't mark debug messages for translation"),
281 (r'\.debug\(\_', "don't mark debug messages for translation"),
282 (r'\.strip\(\)\.split\(\)', "no need to strip before splitting"),
282 (r'\.strip\(\)\.split\(\)', "no need to strip before splitting"),
283 (r'^\s*except\s*:', "naked except clause", r'#.*re-raises'),
283 (r'^\s*except\s*:', "naked except clause", r'#.*re-raises'),
284 (r':\n( )*( ){1,3}[^ ]', "must indent 4 spaces"),
284 (r':\n( )*( ){1,3}[^ ]', "must indent 4 spaces"),
285 (r'ui\.(status|progress|write|note|warn)\([\'\"]x',
285 (r'ui\.(status|progress|write|note|warn)\([\'\"]x',
286 "missing _() in ui message (use () to hide false-positives)"),
286 "missing _() in ui message (use () to hide false-positives)"),
287 (r'release\(.*wlock, .*lock\)', "wrong lock release order"),
287 (r'release\(.*wlock, .*lock\)', "wrong lock release order"),
288 (r'\b__bool__\b', "__bool__ should be __nonzero__ in Python 2"),
288 (r'\b__bool__\b', "__bool__ should be __nonzero__ in Python 2"),
289 (r'os\.path\.join\(.*, *(""|\'\')\)',
289 (r'os\.path\.join\(.*, *(""|\'\')\)',
290 "use pathutil.normasprefix(path) instead of os.path.join(path, '')"),
290 "use pathutil.normasprefix(path) instead of os.path.join(path, '')"),
291 (r'\s0[0-7]+\b', 'legacy octal syntax; use "0o" prefix instead of "0"'),
291 ],
292 ],
292 # warnings
293 # warnings
293 [
294 [
294 (r'(^| )pp +xxxxqq[ \n][^\n]', "add two newlines after '.. note::'"),
295 (r'(^| )pp +xxxxqq[ \n][^\n]', "add two newlines after '.. note::'"),
295 ]
296 ]
296 ]
297 ]
297
298
298 pyfilters = [
299 pyfilters = [
299 (r"""(?msx)(?P<comment>\#.*?$)|
300 (r"""(?msx)(?P<comment>\#.*?$)|
300 ((?P<quote>('''|\"\"\"|(?<!')'(?!')|(?<!")"(?!")))
301 ((?P<quote>('''|\"\"\"|(?<!')'(?!')|(?<!")"(?!")))
301 (?P<text>(([^\\]|\\.)*?))
302 (?P<text>(([^\\]|\\.)*?))
302 (?P=quote))""", reppython),
303 (?P=quote))""", reppython),
303 ]
304 ]
304
305
305 txtfilters = []
306 txtfilters = []
306
307
307 txtpats = [
308 txtpats = [
308 [
309 [
309 ('\s$', 'trailing whitespace'),
310 ('\s$', 'trailing whitespace'),
310 ('.. note::[ \n][^\n]', 'add two newlines after note::')
311 ('.. note::[ \n][^\n]', 'add two newlines after note::')
311 ],
312 ],
312 []
313 []
313 ]
314 ]
314
315
315 cpats = [
316 cpats = [
316 [
317 [
317 (r'//', "don't use //-style comments"),
318 (r'//', "don't use //-style comments"),
318 (r'^ ', "don't use spaces to indent"),
319 (r'^ ', "don't use spaces to indent"),
319 (r'\S\t', "don't use tabs except for indent"),
320 (r'\S\t', "don't use tabs except for indent"),
320 (r'(\S[ \t]+|^[ \t]+)\n', "trailing whitespace"),
321 (r'(\S[ \t]+|^[ \t]+)\n', "trailing whitespace"),
321 (r'.{81}', "line too long"),
322 (r'.{81}', "line too long"),
322 (r'(while|if|do|for)\(', "use space after while/if/do/for"),
323 (r'(while|if|do|for)\(', "use space after while/if/do/for"),
323 (r'return\(', "return is not a function"),
324 (r'return\(', "return is not a function"),
324 (r' ;', "no space before ;"),
325 (r' ;', "no space before ;"),
325 (r'[^;] \)', "no space before )"),
326 (r'[^;] \)', "no space before )"),
326 (r'[)][{]', "space between ) and {"),
327 (r'[)][{]', "space between ) and {"),
327 (r'\w+\* \w+', "use int *foo, not int* foo"),
328 (r'\w+\* \w+', "use int *foo, not int* foo"),
328 (r'\W\([^\)]+\) \w+', "use (int)foo, not (int) foo"),
329 (r'\W\([^\)]+\) \w+', "use (int)foo, not (int) foo"),
329 (r'\w+ (\+\+|--)', "use foo++, not foo ++"),
330 (r'\w+ (\+\+|--)', "use foo++, not foo ++"),
330 (r'\w,\w', "missing whitespace after ,"),
331 (r'\w,\w', "missing whitespace after ,"),
331 (r'^[^#]\w[+/*]\w', "missing whitespace in expression"),
332 (r'^[^#]\w[+/*]\w', "missing whitespace in expression"),
332 (r'^#\s+\w', "use #foo, not # foo"),
333 (r'^#\s+\w', "use #foo, not # foo"),
333 (r'[^\n]\Z', "no trailing newline"),
334 (r'[^\n]\Z', "no trailing newline"),
334 (r'^\s*#import\b', "use only #include in standard C code"),
335 (r'^\s*#import\b', "use only #include in standard C code"),
335 ],
336 ],
336 # warnings
337 # warnings
337 []
338 []
338 ]
339 ]
339
340
340 cfilters = [
341 cfilters = [
341 (r'(/\*)(((\*(?!/))|[^*])*)\*/', repccomment),
342 (r'(/\*)(((\*(?!/))|[^*])*)\*/', repccomment),
342 (r'''(?P<quote>(?<!")")(?P<text>([^"]|\\")+)"(?!")''', repquote),
343 (r'''(?P<quote>(?<!")")(?P<text>([^"]|\\")+)"(?!")''', repquote),
343 (r'''(#\s*include\s+<)([^>]+)>''', repinclude),
344 (r'''(#\s*include\s+<)([^>]+)>''', repinclude),
344 (r'(\()([^)]+\))', repcallspaces),
345 (r'(\()([^)]+\))', repcallspaces),
345 ]
346 ]
346
347
347 inutilpats = [
348 inutilpats = [
348 [
349 [
349 (r'\bui\.', "don't use ui in util"),
350 (r'\bui\.', "don't use ui in util"),
350 ],
351 ],
351 # warnings
352 # warnings
352 []
353 []
353 ]
354 ]
354
355
355 inrevlogpats = [
356 inrevlogpats = [
356 [
357 [
357 (r'\brepo\.', "don't use repo in revlog"),
358 (r'\brepo\.', "don't use repo in revlog"),
358 ],
359 ],
359 # warnings
360 # warnings
360 []
361 []
361 ]
362 ]
362
363
363 webtemplatefilters = []
364 webtemplatefilters = []
364
365
365 webtemplatepats = [
366 webtemplatepats = [
366 [],
367 [],
367 [
368 [
368 (r'{desc(\|(?!websub|firstline)[^\|]*)+}',
369 (r'{desc(\|(?!websub|firstline)[^\|]*)+}',
369 'follow desc keyword with either firstline or websub'),
370 'follow desc keyword with either firstline or websub'),
370 ]
371 ]
371 ]
372 ]
372
373
373 checks = [
374 checks = [
374 ('python', r'.*\.(py|cgi)$', r'^#!.*python', pyfilters, pypats),
375 ('python', r'.*\.(py|cgi)$', r'^#!.*python', pyfilters, pypats),
375 ('test script', r'(.*/)?test-[^.~]*$', '', testfilters, testpats),
376 ('test script', r'(.*/)?test-[^.~]*$', '', testfilters, testpats),
376 ('c', r'.*\.[ch]$', '', cfilters, cpats),
377 ('c', r'.*\.[ch]$', '', cfilters, cpats),
377 ('unified test', r'.*\.t$', '', utestfilters, utestpats),
378 ('unified test', r'.*\.t$', '', utestfilters, utestpats),
378 ('layering violation repo in revlog', r'mercurial/revlog\.py', '',
379 ('layering violation repo in revlog', r'mercurial/revlog\.py', '',
379 pyfilters, inrevlogpats),
380 pyfilters, inrevlogpats),
380 ('layering violation ui in util', r'mercurial/util\.py', '', pyfilters,
381 ('layering violation ui in util', r'mercurial/util\.py', '', pyfilters,
381 inutilpats),
382 inutilpats),
382 ('txt', r'.*\.txt$', '', txtfilters, txtpats),
383 ('txt', r'.*\.txt$', '', txtfilters, txtpats),
383 ('web template', r'mercurial/templates/.*\.tmpl', '',
384 ('web template', r'mercurial/templates/.*\.tmpl', '',
384 webtemplatefilters, webtemplatepats),
385 webtemplatefilters, webtemplatepats),
385 ]
386 ]
386
387
387 def _preparepats():
388 def _preparepats():
388 for c in checks:
389 for c in checks:
389 failandwarn = c[-1]
390 failandwarn = c[-1]
390 for pats in failandwarn:
391 for pats in failandwarn:
391 for i, pseq in enumerate(pats):
392 for i, pseq in enumerate(pats):
392 # fix-up regexes for multi-line searches
393 # fix-up regexes for multi-line searches
393 p = pseq[0]
394 p = pseq[0]
394 # \s doesn't match \n
395 # \s doesn't match \n
395 p = re.sub(r'(?<!\\)\\s', r'[ \\t]', p)
396 p = re.sub(r'(?<!\\)\\s', r'[ \\t]', p)
396 # [^...] doesn't match newline
397 # [^...] doesn't match newline
397 p = re.sub(r'(?<!\\)\[\^', r'[^\\n', p)
398 p = re.sub(r'(?<!\\)\[\^', r'[^\\n', p)
398
399
399 pats[i] = (re.compile(p, re.MULTILINE),) + pseq[1:]
400 pats[i] = (re.compile(p, re.MULTILINE),) + pseq[1:]
400 filters = c[3]
401 filters = c[3]
401 for i, flt in enumerate(filters):
402 for i, flt in enumerate(filters):
402 filters[i] = re.compile(flt[0]), flt[1]
403 filters[i] = re.compile(flt[0]), flt[1]
403 _preparepats()
404 _preparepats()
404
405
405 class norepeatlogger(object):
406 class norepeatlogger(object):
406 def __init__(self):
407 def __init__(self):
407 self._lastseen = None
408 self._lastseen = None
408
409
409 def log(self, fname, lineno, line, msg, blame):
410 def log(self, fname, lineno, line, msg, blame):
410 """print error related a to given line of a given file.
411 """print error related a to given line of a given file.
411
412
412 The faulty line will also be printed but only once in the case
413 The faulty line will also be printed but only once in the case
413 of multiple errors.
414 of multiple errors.
414
415
415 :fname: filename
416 :fname: filename
416 :lineno: line number
417 :lineno: line number
417 :line: actual content of the line
418 :line: actual content of the line
418 :msg: error message
419 :msg: error message
419 """
420 """
420 msgid = fname, lineno, line
421 msgid = fname, lineno, line
421 if msgid != self._lastseen:
422 if msgid != self._lastseen:
422 if blame:
423 if blame:
423 print "%s:%d (%s):" % (fname, lineno, blame)
424 print "%s:%d (%s):" % (fname, lineno, blame)
424 else:
425 else:
425 print "%s:%d:" % (fname, lineno)
426 print "%s:%d:" % (fname, lineno)
426 print " > %s" % line
427 print " > %s" % line
427 self._lastseen = msgid
428 self._lastseen = msgid
428 print " " + msg
429 print " " + msg
429
430
430 _defaultlogger = norepeatlogger()
431 _defaultlogger = norepeatlogger()
431
432
432 def getblame(f):
433 def getblame(f):
433 lines = []
434 lines = []
434 for l in os.popen('hg annotate -un %s' % f):
435 for l in os.popen('hg annotate -un %s' % f):
435 start, line = l.split(':', 1)
436 start, line = l.split(':', 1)
436 user, rev = start.split()
437 user, rev = start.split()
437 lines.append((line[1:-1], user, rev))
438 lines.append((line[1:-1], user, rev))
438 return lines
439 return lines
439
440
440 def checkfile(f, logfunc=_defaultlogger.log, maxerr=None, warnings=False,
441 def checkfile(f, logfunc=_defaultlogger.log, maxerr=None, warnings=False,
441 blame=False, debug=False, lineno=True):
442 blame=False, debug=False, lineno=True):
442 """checks style and portability of a given file
443 """checks style and portability of a given file
443
444
444 :f: filepath
445 :f: filepath
445 :logfunc: function used to report error
446 :logfunc: function used to report error
446 logfunc(filename, linenumber, linecontent, errormessage)
447 logfunc(filename, linenumber, linecontent, errormessage)
447 :maxerr: number of error to display before aborting.
448 :maxerr: number of error to display before aborting.
448 Set to false (default) to report all errors
449 Set to false (default) to report all errors
449
450
450 return True if no error is found, False otherwise.
451 return True if no error is found, False otherwise.
451 """
452 """
452 blamecache = None
453 blamecache = None
453 result = True
454 result = True
454
455
455 try:
456 try:
456 fp = open(f)
457 fp = open(f)
457 except IOError, e:
458 except IOError, e:
458 print "Skipping %s, %s" % (f, str(e).split(':', 1)[0])
459 print "Skipping %s, %s" % (f, str(e).split(':', 1)[0])
459 return result
460 return result
460 pre = post = fp.read()
461 pre = post = fp.read()
461 fp.close()
462 fp.close()
462
463
463 for name, match, magic, filters, pats in checks:
464 for name, match, magic, filters, pats in checks:
464 if debug:
465 if debug:
465 print name, f
466 print name, f
466 fc = 0
467 fc = 0
467 if not (re.match(match, f) or (magic and re.search(magic, f))):
468 if not (re.match(match, f) or (magic and re.search(magic, f))):
468 if debug:
469 if debug:
469 print "Skipping %s for %s it doesn't match %s" % (
470 print "Skipping %s for %s it doesn't match %s" % (
470 name, match, f)
471 name, match, f)
471 continue
472 continue
472 if "no-" "check-code" in pre:
473 if "no-" "check-code" in pre:
473 print "Skipping %s it has no-" "check-code" % f
474 print "Skipping %s it has no-" "check-code" % f
474 return "Skip" # skip checking this file
475 return "Skip" # skip checking this file
475 for p, r in filters:
476 for p, r in filters:
476 post = re.sub(p, r, post)
477 post = re.sub(p, r, post)
477 nerrs = len(pats[0]) # nerr elements are errors
478 nerrs = len(pats[0]) # nerr elements are errors
478 if warnings:
479 if warnings:
479 pats = pats[0] + pats[1]
480 pats = pats[0] + pats[1]
480 else:
481 else:
481 pats = pats[0]
482 pats = pats[0]
482 # print post # uncomment to show filtered version
483 # print post # uncomment to show filtered version
483
484
484 if debug:
485 if debug:
485 print "Checking %s for %s" % (name, f)
486 print "Checking %s for %s" % (name, f)
486
487
487 prelines = None
488 prelines = None
488 errors = []
489 errors = []
489 for i, pat in enumerate(pats):
490 for i, pat in enumerate(pats):
490 if len(pat) == 3:
491 if len(pat) == 3:
491 p, msg, ignore = pat
492 p, msg, ignore = pat
492 else:
493 else:
493 p, msg = pat
494 p, msg = pat
494 ignore = None
495 ignore = None
495 if i >= nerrs:
496 if i >= nerrs:
496 msg = "warning: " + msg
497 msg = "warning: " + msg
497
498
498 pos = 0
499 pos = 0
499 n = 0
500 n = 0
500 for m in p.finditer(post):
501 for m in p.finditer(post):
501 if prelines is None:
502 if prelines is None:
502 prelines = pre.splitlines()
503 prelines = pre.splitlines()
503 postlines = post.splitlines(True)
504 postlines = post.splitlines(True)
504
505
505 start = m.start()
506 start = m.start()
506 while n < len(postlines):
507 while n < len(postlines):
507 step = len(postlines[n])
508 step = len(postlines[n])
508 if pos + step > start:
509 if pos + step > start:
509 break
510 break
510 pos += step
511 pos += step
511 n += 1
512 n += 1
512 l = prelines[n]
513 l = prelines[n]
513
514
514 if ignore and re.search(ignore, l, re.MULTILINE):
515 if ignore and re.search(ignore, l, re.MULTILINE):
515 if debug:
516 if debug:
516 print "Skipping %s for %s:%s (ignore pattern)" % (
517 print "Skipping %s for %s:%s (ignore pattern)" % (
517 name, f, n)
518 name, f, n)
518 continue
519 continue
519 bd = ""
520 bd = ""
520 if blame:
521 if blame:
521 bd = 'working directory'
522 bd = 'working directory'
522 if not blamecache:
523 if not blamecache:
523 blamecache = getblame(f)
524 blamecache = getblame(f)
524 if n < len(blamecache):
525 if n < len(blamecache):
525 bl, bu, br = blamecache[n]
526 bl, bu, br = blamecache[n]
526 if bl == l:
527 if bl == l:
527 bd = '%s@%s' % (bu, br)
528 bd = '%s@%s' % (bu, br)
528
529
529 errors.append((f, lineno and n + 1, l, msg, bd))
530 errors.append((f, lineno and n + 1, l, msg, bd))
530 result = False
531 result = False
531
532
532 errors.sort()
533 errors.sort()
533 for e in errors:
534 for e in errors:
534 logfunc(*e)
535 logfunc(*e)
535 fc += 1
536 fc += 1
536 if maxerr and fc >= maxerr:
537 if maxerr and fc >= maxerr:
537 print " (too many errors, giving up)"
538 print " (too many errors, giving up)"
538 break
539 break
539
540
540 return result
541 return result
541
542
542 if __name__ == "__main__":
543 if __name__ == "__main__":
543 parser = optparse.OptionParser("%prog [options] [files]")
544 parser = optparse.OptionParser("%prog [options] [files]")
544 parser.add_option("-w", "--warnings", action="store_true",
545 parser.add_option("-w", "--warnings", action="store_true",
545 help="include warning-level checks")
546 help="include warning-level checks")
546 parser.add_option("-p", "--per-file", type="int",
547 parser.add_option("-p", "--per-file", type="int",
547 help="max warnings per file")
548 help="max warnings per file")
548 parser.add_option("-b", "--blame", action="store_true",
549 parser.add_option("-b", "--blame", action="store_true",
549 help="use annotate to generate blame info")
550 help="use annotate to generate blame info")
550 parser.add_option("", "--debug", action="store_true",
551 parser.add_option("", "--debug", action="store_true",
551 help="show debug information")
552 help="show debug information")
552 parser.add_option("", "--nolineno", action="store_false",
553 parser.add_option("", "--nolineno", action="store_false",
553 dest='lineno', help="don't show line numbers")
554 dest='lineno', help="don't show line numbers")
554
555
555 parser.set_defaults(per_file=15, warnings=False, blame=False, debug=False,
556 parser.set_defaults(per_file=15, warnings=False, blame=False, debug=False,
556 lineno=True)
557 lineno=True)
557 (options, args) = parser.parse_args()
558 (options, args) = parser.parse_args()
558
559
559 if len(args) == 0:
560 if len(args) == 0:
560 check = glob.glob("*")
561 check = glob.glob("*")
561 else:
562 else:
562 check = args
563 check = args
563
564
564 ret = 0
565 ret = 0
565 for f in check:
566 for f in check:
566 if not checkfile(f, maxerr=options.per_file, warnings=options.warnings,
567 if not checkfile(f, maxerr=options.per_file, warnings=options.warnings,
567 blame=options.blame, debug=options.debug,
568 blame=options.blame, debug=options.debug,
568 lineno=options.lineno):
569 lineno=options.lineno):
569 ret = 1
570 ret = 1
570 sys.exit(ret)
571 sys.exit(ret)
General Comments 0
You need to be logged in to leave comments. Login now