Utilities/SyntaxCheck.py

branch
Py2 comp.
changeset 3456
96232974dcdb
parent 3178
f25fc1364c88
parent 3445
bf95eac5ce12
child 3484
645c12de6b0c
equal deleted inserted replaced
3178:f25fc1364c88 3456:96232974dcdb
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2011 - 2014 Detlev Offenbach <detlev@die-offenbachs.de>
4 #
5
6 """
7 Module implementing the syntax check for Python 2/3.
8 """
9
10 import sys
11 if sys.version_info[0] >= 3:
12 if __name__ == '__main__':
13 from py3flakes.checker import Checker
14 from py3flakes.messages import ImportStarUsed
15 else:
16 from .py3flakes.checker import Checker #__IGNORE_WARNING__
17 from .py3flakes.messages import ImportStarUsed #__IGNORE_WARNING__
18 else:
19 str = unicode #__IGNORE_WARNING__
20 if __name__ == '__main__':
21 from py2flakes.checker import Checker #__IGNORE_WARNING__
22 from py2flakes.messages import ImportStarUsed #__IGNORE_WARNING__
23 else:
24 from .py2flakes.checker import Checker #__IGNORE_WARNING__
25 from .py2flakes.messages import ImportStarUsed #__IGNORE_WARNING__
26
27 import re
28 import traceback
29 from codecs import BOM_UTF8, BOM_UTF16, BOM_UTF32
30
31 try:
32 import Preferences
33 except (ImportError):
34 pass
35
36 codingBytes_regexps = [
37 (2, re.compile(br'''coding[:=]\s*([-\w_.]+)''')),
38 (1, re.compile(br'''<\?xml.*\bencoding\s*=\s*['"]([-\w_.]+)['"]\?>''')),
39 ]
40
41
42 def get_codingBytes(text):
43 """
44 Function to get the coding of a bytes text.
45
46 @param text bytes text to inspect (bytes)
47 @return coding string
48 """
49 lines = text.splitlines()
50 for coding in codingBytes_regexps:
51 coding_re = coding[1]
52 head = lines[:coding[0]]
53 for l in head:
54 m = coding_re.search(l)
55 if m:
56 return str(m.group(1), "ascii").lower()
57 return None
58
59
60 def decode(text):
61 """
62 Function to decode some byte text into a string.
63
64 @param text byte text to decode (bytes)
65 @return tuple of decoded text and encoding (string, string)
66 """
67 try:
68 if text.startswith(BOM_UTF8):
69 # UTF-8 with BOM
70 return str(text[len(BOM_UTF8):], 'utf-8'), 'utf-8-bom'
71 elif text.startswith(BOM_UTF16):
72 # UTF-16 with BOM
73 return str(text[len(BOM_UTF16):], 'utf-16'), 'utf-16'
74 elif text.startswith(BOM_UTF32):
75 # UTF-32 with BOM
76 return str(text[len(BOM_UTF32):], 'utf-32'), 'utf-32'
77 coding = get_codingBytes(text)
78 if coding:
79 return str(text, coding), coding
80 except (UnicodeError, LookupError):
81 pass
82
83 # Assume UTF-8
84 try:
85 return str(text, 'utf-8'), 'utf-8-guessed'
86 except (UnicodeError, LookupError):
87 pass
88
89 try:
90 guess = None
91 if Preferences.getEditor("AdvancedEncodingDetection"):
92 # Try the universal character encoding detector
93 try:
94 import ThirdParty.CharDet.chardet
95 guess = ThirdParty.CharDet.chardet.detect(text)
96 if guess and guess['confidence'] > 0.95 \
97 and guess['encoding'] is not None:
98 codec = guess['encoding'].lower()
99 return str(text, codec), '{0}-guessed'.format(codec)
100 except (UnicodeError, LookupError, ImportError):
101 pass
102 except (NameError):
103 pass
104
105 # Try default encoding
106 try:
107 codec = Preferences.getEditor("DefaultEncoding")
108 return str(text, codec), '{0}-default'.format(codec)
109 except (UnicodeError, LookupError, NameError):
110 pass
111
112 try:
113 if Preferences.getEditor("AdvancedEncodingDetection"):
114 # Use the guessed one even if confifence level is low
115 if guess and guess['encoding'] is not None:
116 try:
117 codec = guess['encoding'].lower()
118 return str(text, codec), '{0}-guessed'.format(codec)
119 except (UnicodeError, LookupError):
120 pass
121 except (NameError):
122 pass
123
124 # Assume UTF-8 loosing information
125 return str(text, "utf-8", "ignore"), 'utf-8-ignore'
126
127
128 def readEncodedFile(filename):
129 """
130 Function to read a file and decode it's contents into proper text.
131
132 @param filename name of the file to read (string)
133 @return tuple of decoded text and encoding (string, string)
134 """
135 try:
136 filename = filename.encode(sys.getfilesystemencoding())
137 except (UnicodeDecodeError):
138 pass
139 f = open(filename, "rb")
140 text = f.read()
141 f.close()
142 return decode(text)
143
144
145 def normalizeCode(codestring):
146 """
147 Function to normalize the given code.
148
149 @param codestring code to be normalized (string)
150 @return normalized code (string)
151 """
152 codestring = codestring.replace("\r\n", "\n").replace("\r", "\n")
153
154 if codestring and codestring[-1] != '\n':
155 codestring = codestring + '\n'
156
157 # Check type for py2: if not str it's unicode
158 if sys.version_info[0] == 2:
159 try:
160 codestring = codestring.encode('utf-8')
161 except:
162 pass
163
164 return codestring
165
166
167 def extractLineFlags(line, startComment="#", endComment=""):
168 """
169 Function to extract flags starting and ending with '__' from a line
170 comment.
171
172 @param line line to extract flags from (string)
173 @keyparam startComment string identifying the start of the comment (string)
174 @keyparam endComment string identifying the end of a comment (string)
175 @return list containing the extracted flags (list of strings)
176 """
177 flags = []
178
179 pos = line.rfind(startComment)
180 if pos >= 0:
181 comment = line[pos + len(startComment):].strip()
182 if endComment:
183 comment = comment.replace("endComment", "")
184 flags = [f.strip() for f in comment.split()
185 if (f.startswith("__") and f.endswith("__"))]
186 return flags
187
188
189 def compile_and_check(file_, codestring="", checkFlakes=True,
190 ignoreStarImportWarnings=False):
191 """
192 Function to compile one Python source file to Python bytecode
193 and to perform a pyflakes check.
194
195 @param file_ source filename (string)
196 @param codestring string containing the code to compile (string)
197 @keyparam checkFlakes flag indicating to do a pyflakes check (boolean)
198 @keyparam ignoreStarImportWarnings flag indicating to
199 ignore 'star import' warnings (boolean)
200 @return A tuple indicating status (True = an error was found), the
201 file name, the line number, the index number, the code string
202 and the error message (boolean, string, string, string, string,
203 string). If checkFlakes is True, a list of strings containing the
204 warnings (marker, file name, line number, message)
205 The values are only valid, if the status is True.
206 """
207 try:
208 import builtins
209 except ImportError:
210 import __builtin__ as builtins #__IGNORE_WARNING__
211
212 try:
213 if sys.version_info[0] == 2:
214 file_enc = file_.encode(sys.getfilesystemencoding())
215 else:
216 file_enc = file_
217
218 if not codestring:
219 try:
220 codestring = readEncodedFile(file_)[0]
221 except (UnicodeDecodeError, IOError):
222 return (False, None, None, None, None, None, [])
223
224 codestring = normalizeCode(codestring)
225
226 if file_.endswith('.ptl'):
227 try:
228 import quixote.ptl_compile
229 except ImportError:
230 return (False, None, None, None, None, None, [])
231 template = quixote.ptl_compile.Template(codestring, file_enc)
232 template.compile()
233
234 # ast.PyCF_ONLY_AST = 1024, speed optimisation
235 module = builtins.compile(codestring, file_enc, 'exec', 1024)
236 except SyntaxError as detail:
237 index = 0
238 code = ""
239 error = ""
240 lines = traceback.format_exception_only(SyntaxError, detail)
241 match = re.match('\s*File "(.+)", line (\d+)',
242 lines[0].replace('<string>', '{0}'.format(file_)))
243 if match is not None:
244 fn, line = match.group(1, 2)
245 if lines[1].startswith('SyntaxError:'):
246 error = re.match('SyntaxError: (.+)', lines[1]).group(1)
247 else:
248 code = re.match('(.+)', lines[1]).group(1)
249 for seLine in lines[2:]:
250 if seLine.startswith('SyntaxError:'):
251 error = re.match('SyntaxError: (.+)', seLine).group(1)
252 elif seLine.rstrip().endswith('^'):
253 index = len(seLine.rstrip()) - 4
254 else:
255 fn = detail.filename
256 line = detail.lineno or 1
257 error = detail.msg
258 return (True, fn, int(line), index, code, error, [])
259 except ValueError as detail:
260 index = 0
261 code = ""
262 try:
263 fn = detail.filename
264 line = detail.lineno
265 error = detail.msg
266 except AttributeError:
267 fn = file_
268 line = 1
269 error = str(detail)
270 return (True, fn, line, index, code, error, [])
271 except Exception as detail:
272 try:
273 fn = detail.filename
274 line = detail.lineno
275 index = 0
276 code = ""
277 error = detail.msg
278 return (True, fn, line, index, code, error, [])
279 except: # this catchall is intentional
280 pass
281
282 # pyflakes
283 if not checkFlakes:
284 return (False, "", -1, -1, "", "", [])
285
286 strings = []
287 lines = codestring.splitlines()
288 try:
289 warnings = Checker(module, file_)
290 warnings.messages.sort(key=lambda a: a.lineno)
291 for warning in warnings.messages:
292 if ignoreStarImportWarnings and \
293 isinstance(warning, ImportStarUsed):
294 continue
295
296 _fn, lineno, message, msg_args = warning.getMessageData()
297 if "__IGNORE_WARNING__" not in extractLineFlags(
298 lines[lineno - 1].strip()):
299 strings.append([
300 "FLAKES_WARNING", _fn, lineno, message, msg_args])
301 except SyntaxError as err:
302 if err.text.strip():
303 msg = err.text.strip()
304 else:
305 msg = err.msg
306 strings.append(["FLAKES_ERROR", file_, err.lineno, msg, ()])
307
308 return (False, "", -1, -1, "", "", strings)
309
310
311 if __name__ == "__main__":
312 if len(sys.argv) < 2 or \
313 len(sys.argv) > 3 or \
314 (len(sys.argv) == 3 and sys.argv[1] not in ["-fi", "-fs"]):
315 print("ERROR")
316 print("")
317 print("")
318 print("")
319 print("")
320 print("No file name given.")
321 else:
322 filename = sys.argv[-1]
323 checkFlakes = len(sys.argv) == 3
324 # Setting is ignored if checkFlakes is False
325 ignoreStarImportWarnings = sys.argv[1] == "-fi"
326
327 try:
328 codestring = readEncodedFile(filename)[0]
329
330 syntaxerror, fname, line, index, code, error, warnings = \
331 compile_and_check(filename, codestring, checkFlakes,
332 ignoreStarImportWarnings)
333 except IOError as msg:
334 # fake a syntax error
335 syntaxerror, fname, line, index, code, error, warnings = \
336 True, filename, 1, 0, "", "I/O Error: %s" % str(msg), []
337
338 if syntaxerror:
339 print("ERROR")
340 else:
341 print("NO_ERROR")
342 print(fname)
343 print(line)
344 print(index)
345 print(code)
346 print(error)
347
348 if not syntaxerror:
349 for warningLine in warnings:
350 msg_args = warningLine.pop()
351 for warning in warningLine:
352 print(warning)
353 msg_args = [str(x) for x in msg_args]
354 print('#'.join(msg_args))
355
356 sys.exit(0)

eric ide

mercurial