Plugins/CheckerPlugins/Pep8/Pep8Fixer.py

changeset 2875
1267f0663801
parent 2868
8d30ec21e9c7
child 2876
bfa39cf40277
equal deleted inserted replaced
2874:0d754c68e1db 2875:1267f0663801
14 14
15 from PyQt4.QtCore import QObject 15 from PyQt4.QtCore import QObject
16 16
17 from E5Gui import E5MessageBox 17 from E5Gui import E5MessageBox
18 18
19 from . import pep8
20
19 import Utilities 21 import Utilities
20 22
21 Pep8FixableIssues = ["E101", "E111", "W191", "E201", "E202", "E203", 23 Pep8FixableIssues = ["E101", "E111", "E121", "E122", "E123", "E124",
24 "E125", "E126", "E127", "E128", "E133", "W191", "E201", "E202", "E203",
22 "E211", "E221", "E222", "E223", "E224", "E225", 25 "E211", "E221", "E222", "E223", "E224", "E225",
23 "E226", "E227", "E228", "E231", "E241", "E242", 26 "E226", "E227", "E228", "E231", "E241", "E242",
24 "E251", "E261", "E262", "E271", "E272", "E273", 27 "E251", "E261", "E262", "E271", "E272", "E273",
25 "E274", "W291", "W292", "W293", "E301", "E302", 28 "E274", "W291", "W292", "W293", "E301", "E302",
26 "E303", "E304", "W391", "E401", "E502", "W603", 29 "E303", "E304", "W391", "E401", "E502", "W603",
63 "fixed_" + os.path.basename(self.__filename)) 66 "fixed_" + os.path.basename(self.__filename))
64 67
65 self.__fixes = { 68 self.__fixes = {
66 "E101": self.__fixE101, 69 "E101": self.__fixE101,
67 "E111": self.__fixE101, 70 "E111": self.__fixE101,
71 "E121": self.__fixE121,
72 "E122": self.__fixE122,
73 "E123": self.__fixE123,
74 "E124": self.__fixE121,
75 "E125": self.__fixE125,
76 "E126": self.__fixE126,
77 "E127": self.__fixE127,
78 "E128": self.__fixE127,
79 "E133": self.__fixE126,
68 "W191": self.__fixE101, 80 "W191": self.__fixE101,
69 "E201": self.__fixE201, 81 "E201": self.__fixE201,
70 "E202": self.__fixE201, 82 "E202": self.__fixE201,
71 "E203": self.__fixE201, 83 "E203": self.__fixE201,
72 "E211": self.__fixE201, 84 "E211": self.__fixE201,
104 "E703": self.__fixE702, 116 "E703": self.__fixE702,
105 "E711": self.__fixE711, 117 "E711": self.__fixE711,
106 "E712": self.__fixE711, 118 "E712": self.__fixE711,
107 } 119 }
108 self.__modified = False 120 self.__modified = False
109 self.__stack = [] # these need to be fixed before the file is saved 121 self.__stackLogical = [] # these need to be fixed before the file
110 # but after all inline fixes 122 # is saved but after all other inline
123 # fixes. These work with logical lines.
124 self.__stack = [] # these need to be fixed before the file
125 # is saved but after all inline fixes
111 126
112 def saveFile(self, encoding): 127 def saveFile(self, encoding):
113 """ 128 """
114 Public method to save the modified file. 129 Public method to save the modified file.
115 130
164 179
165 def __finalize(self): 180 def __finalize(self):
166 """ 181 """
167 Private method to apply all deferred fixes. 182 Private method to apply all deferred fixes.
168 """ 183 """
184 # step 1: do fixes operating on logical lines first
185 for code, line, pos in self.__stackLogical:
186 self.__fixes[code](code, line, pos, apply=True)
187
188 # step 2: do fixes that change the number of lines
169 for code, line, pos in reversed(self.__stack): 189 for code, line, pos in reversed(self.__stack):
170 self.__fixes[code](code, line, pos, apply=True) 190 self.__fixes[code](code, line, pos, apply=True)
171 191
172 def __getEol(self): 192 def __getEol(self):
173 """ 193 """
185 self.__eol = self.__project.getEolString() 205 self.__eol = self.__project.getEolString()
186 else: 206 else:
187 self.__eol = Utilities.linesep() 207 self.__eol = Utilities.linesep()
188 return self.__eol 208 return self.__eol
189 209
210 def __findLogical(self):
211 """
212 Private method to extract the index of all the starts and ends of lines.
213
214 @return tuple containing two lists of integer with start and end tuples
215 of lines
216 """
217 logical_start = []
218 logical_end = []
219 last_newline = True
220 sio = io.StringIO("".join(self.__source))
221 parens = 0
222 for t in tokenize.generate_tokens(sio.readline):
223 if t[0] in [tokenize.COMMENT, tokenize.DEDENT,
224 tokenize.INDENT, tokenize.NL,
225 tokenize.ENDMARKER]:
226 continue
227 if not parens and t[0] in [tokenize.NEWLINE, tokenize.SEMI]:
228 last_newline = True
229 logical_end.append((t[3][0] - 1, t[2][1]))
230 continue
231 if last_newline and not parens:
232 logical_start.append((t[2][0] - 1, t[2][1]))
233 last_newline = False
234 if t[0] == tokenize.OP:
235 if t[1] in '([{':
236 parens += 1
237 elif t[1] in '}])':
238 parens -= 1
239 return logical_start, logical_end
240
241 def __getLogical(self, line, pos):
242 """
243 Private method to get the logical line corresponding to the given
244 position.
245
246 @param line line number of the issue (integer)
247 @param pos position inside line (integer)
248 @return tuple of a tuple of two integers giving the start of the
249 logical line, another tuple of two integers giving the end
250 of the logical line and a list of strings with the original
251 source lines
252 """
253 try:
254 (logical_start, logical_end) = self.__findLogical()
255 except (SyntaxError, tokenize.TokenError):
256 return None
257
258 line = line - 1
259 ls = None
260 le = None
261 for i in range(0, len(logical_start)):
262 x = logical_end[i]
263 if x[0] > line or (x[0] == line and x[1] > pos):
264 le = x
265 ls = logical_start[i]
266 break
267 if ls is None:
268 return None
269
270 original = self.__source[ls[0]:le[0] + 1]
271 return ls, le, original
272
190 def __getIndentWord(self): 273 def __getIndentWord(self):
191 """ 274 """
192 Private method to determine the indentation type. 275 Private method to determine the indentation type.
193 276
194 @return string to be used for an indentation (string) 277 @return string to be used for an indentation (string)
210 293
211 @param line line to determine the indentation string from (string) 294 @param line line to determine the indentation string from (string)
212 @return indentation string (string) 295 @return indentation string (string)
213 """ 296 """
214 return line.replace(line.lstrip(), "") 297 return line.replace(line.lstrip(), "")
298
299 def __fixReindent(self, line, pos, logical):
300 """
301 Private method to fix a badly indented line.
302
303 This is done by adding or removing from its initial indent only.
304
305 @param line line number of the issue (integer)
306 @param pos position inside line (integer)
307 @return flag indicating a change was done (boolean)
308 """
309 assert logical
310 ls, _, original = logical
311
312 rewrapper = Pep8IndentationWrapper(original)
313 valid_indents = rewrapper.pep8Expected()
314 if not rewrapper.rel_indent:
315 return False
316
317 if line > ls[0]:
318 # got a valid continuation line number
319 row = line - ls[0] - 1
320 # always pick the first option for this
321 valid = valid_indents[row]
322 got = rewrapper.rel_indent[row]
323 else:
324 return False
325
326 line1 = ls[0] + row
327 # always pick the expected indent, for now.
328 indent_to = valid[0]
329
330 if got != indent_to:
331 orig_line = self.__source[line1]
332 new_line = ' ' * (indent_to) + orig_line.lstrip()
333 if new_line == orig_line:
334 return False
335 else:
336 self.__source[line1] = new_line
337 return True
338 else:
339 return False
215 340
216 def __fixWhitespace(self, line, offset, replacement): 341 def __fixWhitespace(self, line, offset, replacement):
217 """ 342 """
218 Private method to correct whitespace at the given offset. 343 Private method to correct whitespace at the given offset.
219 344
252 msg = self.trUtf8("Indentation adjusted to be a multiple of four.") 377 msg = self.trUtf8("Indentation adjusted to be a multiple of four.")
253 return (True, msg) 378 return (True, msg)
254 else: 379 else:
255 return (False, self.trUtf8("Fix for {0} failed.").format(code)) 380 return (False, self.trUtf8("Fix for {0} failed.").format(code))
256 381
382 def __fixE121(self, code, line, pos, apply=False):
383 """
384 Private method to fix the indentation of continuation lines and
385 closing brackets (E121,E124).
386
387 @param code code of the issue (string)
388 @param line line number of the issue (integer)
389 @param pos position inside line (integer)
390 @keyparam apply flag indicating, that the fix should be applied
391 (boolean)
392 @return flag indicating an applied fix (boolean) and a message for
393 the fix (string)
394 """
395 if apply:
396 logical = self.__getLogical(line, pos)
397 if logical:
398 # Fix by adjusting initial indent level.
399 self.__fixReindent(line, pos, logical)
400 else:
401 self.__stackLogical.append((code, line, pos))
402 if code == "E121":
403 msg = self.trUtf8("Indentation of continuation line corrected.")
404 elif code == "E124":
405 msg = self.trUtf8("Indentation of closing bracket corrected.")
406 return (True, msg)
407
408 def __fixE122(self, code, line, pos, apply=False):
409 """
410 Private method to fix a missing indentation of continuation lines (E122).
411
412 @param code code of the issue (string)
413 @param line line number of the issue (integer)
414 @param pos position inside line (integer)
415 @keyparam apply flag indicating, that the fix should be applied
416 (boolean)
417 @return flag indicating an applied fix (boolean) and a message for
418 the fix (string)
419 """
420 if apply:
421 logical = self.__getLogical(line, pos)
422 if logical:
423 # Fix by adding an initial indent.
424 modified = self.__fixReindent(line, pos, logical)
425 if not modified:
426 # fall back to simple method
427 line = line - 1
428 text = self.__source[line]
429 indentation = self.__getIndent(text)
430 self.__source[line] = indentation + \
431 self.__indentWord + text.lstrip()
432 else:
433 self.__stackLogical.append((code, line, pos))
434 return (True, self.trUtf8("Missing indentation of continuation line corrected."))
435
436 def __fixE123(self, code, line, pos, apply=False):
437 """
438 Private method to fix the indentation of a closing bracket lines (E123).
439
440 @param code code of the issue (string)
441 @param line line number of the issue (integer)
442 @param pos position inside line (integer)
443 @keyparam apply flag indicating, that the fix should be applied
444 (boolean)
445 @return flag indicating an applied fix (boolean) and a message for
446 the fix (string)
447 """
448 if apply:
449 logical = self.__getLogical(line, pos)
450 if logical:
451 # Fix by deleting whitespace to the correct level.
452 logicalLines = logical[2]
453 row = line - 1
454 text = self.__source[row]
455 newText = self.__getIndent(logicalLines[0]) + text.lstrip()
456 if newText == text:
457 # fall back to slower method
458 self.__fixReindent(line, pos, logical)
459 else:
460 self.__source[row] = newText
461 else:
462 self.__stackLogical.append((code, line, pos))
463 return (True, self.trUtf8("Closing bracket aligned to opening bracket."))
464
465 def __fixE125(self, code, line, pos, apply=False):
466 """
467 Private method to fix the indentation of continuation lines not
468 distinguishable from next logical line (E125).
469
470 @param code code of the issue (string)
471 @param line line number of the issue (integer)
472 @param pos position inside line (integer)
473 @keyparam apply flag indicating, that the fix should be applied
474 (boolean)
475 @return flag indicating an applied fix (boolean) and a message for
476 the fix (string)
477 """
478 if apply:
479 logical = self.__getLogical(line, pos)
480 if logical:
481 # Fix by adjusting initial indent level.
482 modified = self.__fixReindent(line, pos, logical)
483 if not modified:
484 row = line - 1
485 text = self.__source[row]
486 self.__source[row] = self.__getIndent(text) + \
487 self.__indentWord + text.lstrip()
488 else:
489 self.__stackLogical.append((code, line, pos))
490 return (True, self.trUtf8("Indentation level changed."))
491
492 def __fixE126(self, code, line, pos, apply=False):
493 """
494 Private method to fix over-indented/under-indented hanging
495 indentation (E126, E133).
496
497 @param code code of the issue (string)
498 @param line line number of the issue (integer)
499 @param pos position inside line (integer)
500 @keyparam apply flag indicating, that the fix should be applied
501 (boolean)
502 @return flag indicating an applied fix (boolean) and a message for
503 the fix (string)
504 """
505 if apply:
506 logical = self.__getLogical(line, pos)
507 if logical:
508 # Fix by deleting whitespace to the left.
509 logicalLines = logical[2]
510 row = line - 1
511 text = self.__source[row]
512 newText = self.__getIndent(logicalLines[0]) + \
513 self.__indentWord + text.lstrip()
514 if newText == text:
515 # fall back to slower method
516 self.__fixReindent(line, pos, logical)
517 else:
518 self.__source[row] = newText
519 else:
520 self.__stackLogical.append((code, line, pos))
521 return (True, self.trUtf8("Indentation level of hanging indentation changed."))
522
523 def __fixE127(self, code, line, pos, apply=False):
524 """
525 Private method to fix over/under indented lines (E127, E128).
526
527 @param code code of the issue (string)
528 @param line line number of the issue (integer)
529 @param pos position inside line (integer)
530 @keyparam apply flag indicating, that the fix should be applied
531 (boolean)
532 @return flag indicating an applied fix (boolean) and a message for
533 the fix (string)
534 """
535 if apply:
536 logical = self.__getLogical(line, pos)
537 if logical:
538 # Fix by inserting/deleting whitespace to the correct level.
539 logicalLines = logical[2]
540 row = line - 1
541 text = self.__source[row]
542 newText = text
543
544 if logicalLines[0].rstrip().endswith('\\'):
545 newText = self.__getIndent(logicalLines[0]) + \
546 self.__indentWord + text.lstrip()
547 else:
548 startIndex = None
549 for symbol in '([{':
550 if symbol in logicalLines[0]:
551 foundIndex = logicalLines[0].find(symbol) + 1
552 if startIndex is None:
553 startIndex = foundIndex
554 else:
555 startIndex = min(startIndex, foundIndex)
556
557 if startIndex is not None:
558 newText = startIndex * ' ' + text.lstrip()
559
560 if newText == text:
561 # fall back to slower method
562 self.__fixReindent(line, pos, logical)
563 else:
564 self.__source[row] = newText
565 else:
566 self.__stackLogical.append((code, line, pos))
567 return (True, self.trUtf8("Visual indentation corrected."))
568
257 def __fixE201(self, code, line, pos): 569 def __fixE201(self, code, line, pos):
258 """ 570 """
259 Private method to fix extraneous whitespace (E201, E202, 571 Private method to fix extraneous whitespace (E201, E202,
260 E203, E211). 572 E203, E211).
261 573
395 return (True, self.trUtf8("One blank line inserted.")) 707 return (True, self.trUtf8("One blank line inserted."))
396 708
397 def __fixE302(self, code, line, pos, apply=False): 709 def __fixE302(self, code, line, pos, apply=False):
398 """ 710 """
399 Private method to fix the need for two blank lines (E302). 711 Private method to fix the need for two blank lines (E302).
712
713 @param code code of the issue (string)
714 @param line line number of the issue (integer)
715 @param pos position inside line (integer)
716 @keyparam apply flag indicating, that the fix should be applied
717 (boolean)
718 @return flag indicating an applied fix (boolean) and a message for
719 the fix (string)
400 """ 720 """
401 # count blank lines 721 # count blank lines
402 index = line - 1 722 index = line - 1
403 blanks = 0 723 blanks = 0
404 while index: 724 while index:
866 i = 0 1186 i = 0
867 n = len(line) 1187 n = len(line)
868 while i < n and line[i] == " ": 1188 while i < n and line[i] == " ":
869 i += 1 1189 i += 1
870 return i 1190 return i
1191
1192
1193 class Pep8IndentationWrapper(object):
1194 """
1195 Class used by fixers dealing with indentation.
1196
1197 Each instance operates on a single logical line.
1198 """
1199
1200 SKIP_TOKENS = frozenset([
1201 tokenize.COMMENT, tokenize.NL, tokenize.INDENT,
1202 tokenize.DEDENT, tokenize.NEWLINE, tokenize.ENDMARKER
1203 ])
1204
1205 def __init__(self, physical_lines):
1206 """
1207 Constructor
1208
1209 @param physical_lines list of physical lines to operate on
1210 (list of strings)
1211 """
1212 self.lines = physical_lines
1213 self.tokens = []
1214 self.rel_indent = None
1215 sio = io.StringIO(''.join(physical_lines))
1216 for t in tokenize.generate_tokens(sio.readline):
1217 if not len(self.tokens) and t[0] in self.SKIP_TOKENS:
1218 continue
1219 if t[0] != tokenize.ENDMARKER:
1220 self.tokens.append(t)
1221
1222 self.logical_line = self.__buildTokensLogical(self.tokens)
1223
1224 def __buildTokensLogical(self, tokens):
1225 """
1226 Private method to build a logical line from a list of tokens.
1227
1228 @param tokens list of tokens as generated by tokenize.generate_tokens
1229 @return logical line (string)
1230 """
1231 # from pep8.py with minor modifications
1232 logical = []
1233 previous = None
1234 for t in tokens:
1235 token_type, text = t[0:2]
1236 if token_type in self.SKIP_TOKENS:
1237 continue
1238 if previous:
1239 end_line, end = previous[3]
1240 start_line, start = t[2]
1241 if end_line != start_line: # different row
1242 prev_text = self.lines[end_line - 1][end - 1]
1243 if prev_text == ',' or (prev_text not in '{[('
1244 and text not in '}])'):
1245 logical.append(' ')
1246 elif end != start: # different column
1247 fill = self.lines[end_line - 1][end:start]
1248 logical.append(fill)
1249 logical.append(text)
1250 previous = t
1251 logical_line = ''.join(logical)
1252 assert logical_line.lstrip() == logical_line
1253 assert logical_line.rstrip() == logical_line
1254 return logical_line
1255
1256 def pep8Expected(self):
1257 """
1258 Public method to replicate logic in pep8.py, to know what level to
1259 indent things to.
1260
1261 @return list of lists, where each list represents valid indent levels for
1262 the line in question, relative from the initial indent. However, the
1263 first entry is the indent level which was expected.
1264 """
1265 # What follows is an adjusted version of
1266 # pep8.py:continuation_line_indentation. All of the comments have been
1267 # stripped and the 'yield' statements replaced with 'pass'.
1268 if not self.tokens:
1269 return
1270
1271 first_row = self.tokens[0][2][0]
1272 nrows = 1 + self.tokens[-1][2][0] - first_row
1273
1274 # here are the return values
1275 valid_indents = [list()] * nrows
1276 indent_level = self.tokens[0][2][1]
1277 valid_indents[0].append(indent_level)
1278
1279 if nrows == 1:
1280 # bug, really.
1281 return valid_indents
1282
1283 indent_next = self.logical_line.endswith(':')
1284
1285 row = depth = 0
1286 parens = [0] * nrows
1287 self.rel_indent = rel_indent = [0] * nrows
1288 indent = [indent_level]
1289 indent_chances = {}
1290 last_indent = (0, 0)
1291 last_token_multiline = None
1292
1293 for token_type, text, start, end, line in self.tokens:
1294 newline = row < start[0] - first_row
1295 if newline:
1296 row = start[0] - first_row
1297 newline = (not last_token_multiline and
1298 token_type not in (tokenize.NL, tokenize.NEWLINE))
1299
1300 if newline:
1301 # This is where the differences start. Instead of looking at
1302 # the line and determining whether the observed indent matches
1303 # our expectations, we decide which type of indentation is in
1304 # use at the given indent level, and return the offset. This
1305 # algorithm is susceptible to "carried errors", but should
1306 # through repeated runs eventually solve indentation for
1307 # multiline expressions.
1308
1309 if depth:
1310 for open_row in range(row - 1, -1, -1):
1311 if parens[open_row]:
1312 break
1313 else:
1314 open_row = 0
1315
1316 # That's all we get to work with. This code attempts to
1317 # "reverse" the below logic, and place into the valid indents
1318 # list
1319 vi = []
1320 add_second_chances = False
1321 if token_type == tokenize.OP and text in ']})':
1322 # this line starts with a closing bracket, so it needs to
1323 # be closed at the same indent as the opening one.
1324 if indent[depth]:
1325 # hanging indent
1326 vi.append(indent[depth])
1327 else:
1328 # visual indent
1329 vi.append(indent_level + rel_indent[open_row])
1330 elif depth and indent[depth]:
1331 # visual indent was previously confirmed.
1332 vi.append(indent[depth])
1333 add_second_chances = True
1334 elif depth and True in indent_chances.values():
1335 # visual indent happened before, so stick to
1336 # visual indent this time.
1337 if depth > 1 and indent[depth - 1]:
1338 vi.append(indent[depth - 1])
1339 else:
1340 # stupid fallback
1341 vi.append(indent_level + 4)
1342 add_second_chances = True
1343 elif not depth:
1344 vi.append(indent_level + 4)
1345 else:
1346 # must be in hanging indent
1347 hang = rel_indent[open_row] + 4
1348 vi.append(indent_level + hang)
1349
1350 # about the best we can do without look-ahead
1351 if (indent_next and vi[0] == indent_level + 4 and
1352 nrows == row + 1):
1353 vi[0] += 4
1354
1355 if add_second_chances:
1356 # visual indenters like to line things up.
1357 min_indent = vi[0]
1358 for col, what in indent_chances.items():
1359 if col > min_indent and (
1360 what is True or
1361 (what == str and token_type == tokenize.STRING) or
1362 (what == text and token_type == tokenize.OP)
1363 ):
1364 vi.append(col)
1365 vi = sorted(vi)
1366
1367 valid_indents[row] = vi
1368
1369 # Returning to original continuation_line_indentation() from
1370 # pep8.
1371 visual_indent = indent_chances.get(start[1])
1372 last_indent = start
1373 rel_indent[row] = pep8.expand_indent(line) - indent_level
1374 hang = rel_indent[row] - rel_indent[open_row]
1375
1376 if token_type == tokenize.OP and text in ']})':
1377 pass
1378 elif visual_indent is True:
1379 if not indent[depth]:
1380 indent[depth] = start[1]
1381
1382 # line altered: comments shouldn't define a visual indent
1383 if parens[row] and not indent[depth] and token_type not in (
1384 tokenize.NL, tokenize.COMMENT
1385 ):
1386 indent[depth] = start[1]
1387 indent_chances[start[1]] = True
1388 elif token_type == tokenize.STRING or text in (
1389 'u', 'ur', 'b', 'br'
1390 ):
1391 indent_chances[start[1]] = str
1392
1393 if token_type == tokenize.OP:
1394 if text in '([{':
1395 depth += 1
1396 indent.append(0)
1397 parens[row] += 1
1398 elif text in ')]}' and depth > 0:
1399 prev_indent = indent.pop() or last_indent[1]
1400 for d in range(depth):
1401 if indent[d] > prev_indent:
1402 indent[d] = 0
1403 for ind in list(indent_chances):
1404 if ind >= prev_indent:
1405 del indent_chances[ind]
1406 depth -= 1
1407 if depth and indent[depth]: # modified
1408 indent_chances[indent[depth]] = True
1409 for idx in range(row, -1, -1):
1410 if parens[idx]:
1411 parens[idx] -= 1
1412 break
1413 assert len(indent) == depth + 1
1414 if start[1] not in indent_chances:
1415 indent_chances[start[1]] = text
1416
1417 last_token_multiline = (start[0] != end[0])
1418
1419 return valid_indents

eric ide

mercurial