Plugins/CheckerPlugins/Pep8/Pep8Fixer.py

branch
Py2 comp.
changeset 2911
ce77f0b1ee67
parent 2525
8b507a9a2d40
parent 2883
3c99a1db1506
child 3056
9986ec0e559a
equal deleted inserted replaced
2847:1843ef6e2656 2911:ce77f0b1ee67
9 9
10 from __future__ import unicode_literals # __IGNORE_WARNING__ 10 from __future__ import unicode_literals # __IGNORE_WARNING__
11 11
12 import os 12 import os
13 import re 13 import re
14 import tokenize
15 import io
14 16
15 from PyQt4.QtCore import QObject 17 from PyQt4.QtCore import QObject
16 18
17 from E5Gui import E5MessageBox 19 from E5Gui import E5MessageBox
18 20
21 from . import pep8
22
19 import Utilities 23 import Utilities
20 24
21 Pep8FixableIssues = ["E101", "W191", "E201", "E202", "E203", "E211", "E221", 25 Pep8FixableIssues = ["E101", "E111", "E121", "E122", "E123", "E124",
22 "E222", "E225", "E231", "E241", "E251", "E261", "E262", 26 "E125", "E126", "E127", "E128", "E133", "W191",
23 "W291", "W292", "W293", "E301", "E302", "E303", "E304", 27 "E201", "E202", "E203", "E211", "E221", "E222",
24 "W391", "W603"] 28 "E223", "E224", "E225", "E226", "E227", "E228",
29 "E231", "E241", "E242", "E251", "E261", "E262",
30 "E271", "E272", "E273", "E274", "W291", "W292",
31 "W293", "E301", "E302", "E303", "E304", "W391",
32 "E401", "E501", "E502", "W603", "E701", "E702",
33 "E703", "E711", "E712"
34 ]
25 35
26 36
27 class Pep8Fixer(QObject): 37 class Pep8Fixer(QObject):
28 """ 38 """
29 Class implementing a fixer for certain PEP 8 issues. 39 Class implementing a fixer for certain PEP 8 issues.
30 """ 40 """
31 def __init__(self, project, filename, sourceLines, fixCodes, inPlace): 41 def __init__(self, project, filename, sourceLines, fixCodes, noFixCodes,
42 maxLineLength, inPlace):
32 """ 43 """
33 Constructor 44 Constructor
34 45
35 @param project reference to the project object (Project) 46 @param project reference to the project object (Project)
36 @param filename name of the file to be fixed (string) 47 @param filename name of the file to be fixed (string)
37 @param sourceLines list of source lines including eol marker 48 @param sourceLines list of source lines including eol marker
38 (list of string) 49 (list of string)
39 @param fixCodes list of codes to be fixed as a comma separated 50 @param fixCodes list of codes to be fixed as a comma separated
40 string (string) 51 string (string)
52 @param noFixCodes list of codes not to be fixed as a comma
53 separated string (string)
54 @param maxLineLength maximum allowed line length (integer)
41 @param inPlace flag indicating to modify the file in place (boolean) 55 @param inPlace flag indicating to modify the file in place (boolean)
42 """ 56 """
43 super(Pep8Fixer, self).__init__() 57 super(Pep8Fixer, self).__init__()
44 58
45 self.__project = project 59 self.__project = project
46 self.__filename = filename 60 self.__filename = filename
47 self.__origName = "" 61 self.__origName = ""
48 self.__source = sourceLines[:] # save a copy 62 self.__source = sourceLines[:] # save a copy
49 self.__fixCodes = [c.strip() for c in fixCodes.split(",") if c.strip()] 63 self.__fixCodes = [c.strip() for c in fixCodes.split(",") if c.strip()]
64 self.__noFixCodes = [
65 c.strip() for c in noFixCodes.split(",") if c.strip()]
66 self.__maxLineLength = maxLineLength
50 self.fixed = 0 67 self.fixed = 0
68
69 self.__reindenter = None
70 self.__eol = ""
71 self.__indentWord = self.__getIndentWord()
51 72
52 if not inPlace: 73 if not inPlace:
53 self.__origName = self.__filename 74 self.__origName = self.__filename
54 self.__filename = os.path.join(os.path.dirname(self.__filename), 75 self.__filename = os.path.join(os.path.dirname(self.__filename),
55 "fixed_" + os.path.basename(self.__filename)) 76 "fixed_" + os.path.basename(self.__filename))
56 77
57 self.__fixes = { 78 self.__fixes = {
58 "E101": self.__fixTabs, 79 "E101": self.__fixE101,
59 "W191": self.__fixTabs, 80 "E111": self.__fixE101,
60 "E201": self.__fixWhitespaceAfter, 81 "E121": self.__fixE121,
61 "E202": self.__fixWhitespaceBefore, 82 "E122": self.__fixE122,
62 "E203": self.__fixWhitespaceBefore, 83 "E123": self.__fixE123,
63 "E211": self.__fixWhitespaceBefore, 84 "E124": self.__fixE121,
64 "E221": self.__fixWhitespaceAroundOperator, 85 "E125": self.__fixE125,
65 "E222": self.__fixWhitespaceAroundOperator, 86 "E126": self.__fixE126,
66 "E225": self.__fixMissingWhitespaceAroundOperator, 87 "E127": self.__fixE127,
67 "E231": self.__fixMissingWhitespaceAfter, 88 "E128": self.__fixE127,
68 "E241": self.__fixWhitespaceAroundOperator, 89 "E133": self.__fixE126,
69 "E251": self.__fixWhitespaceAroundEquals, 90 "W191": self.__fixE101,
70 "E261": self.__fixWhitespaceBeforeInline, 91 "E201": self.__fixE201,
71 "E262": self.__fixWhitespaceAfterInline, 92 "E202": self.__fixE201,
72 "W291": self.__fixWhitespace, 93 "E203": self.__fixE201,
73 "W292": self.__fixNewline, 94 "E211": self.__fixE201,
74 "W293": self.__fixWhitespace, 95 "E221": self.__fixE221,
75 "E301": self.__fixOneBlankLine, 96 "E222": self.__fixE221,
76 "E302": self.__fixTwoBlankLines, 97 "E223": self.__fixE221,
77 "E303": self.__fixTooManyBlankLines, 98 "E224": self.__fixE221,
78 "E304": self.__fixBlankLinesAfterDecorator, 99 "E225": self.__fixE221,
79 "W391": self.__fixTrailingBlankLines, 100 "E226": self.__fixE221,
80 "W603": self.__fixNotEqual, 101 "E227": self.__fixE221,
102 "E228": self.__fixE221,
103 "E231": self.__fixE231,
104 "E241": self.__fixE221,
105 "E242": self.__fixE221,
106 "E251": self.__fixE251,
107 "E261": self.__fixE261,
108 "E262": self.__fixE261,
109 "E271": self.__fixE221,
110 "E272": self.__fixE221,
111 "E273": self.__fixE221,
112 "E274": self.__fixE221,
113 "W291": self.__fixW291,
114 "W292": self.__fixW292,
115 "W293": self.__fixW291,
116 "E301": self.__fixE301,
117 "E302": self.__fixE302,
118 "E303": self.__fixE303,
119 "E304": self.__fixE304,
120 "W391": self.__fixW391,
121 "E401": self.__fixE401,
122 "E501": self.__fixE501,
123 "E502": self.__fixE502,
124 "W603": self.__fixW603,
125 "E701": self.__fixE701,
126 "E702": self.__fixE702,
127 "E703": self.__fixE702,
128 "E711": self.__fixE711,
129 "E712": self.__fixE711,
81 } 130 }
82 self.__modified = False 131 self.__modified = False
83 self.__stack = [] # these need to be fixed before the file is saved 132 self.__stackLogical = [] # these need to be fixed before the file
84 # but after all inline fixes 133 # is saved but after all other inline
134 # fixes. These work with logical lines.
135 self.__stack = [] # these need to be fixed before the file
136 # is saved but after all inline fixes
137
138 self.__multiLineNumbers = None
139 self.__docLineNumbers = None
85 140
86 def saveFile(self, encoding): 141 def saveFile(self, encoding):
87 """ 142 """
88 Public method to save the modified file. 143 Public method to save the modified file.
89 144
103 except (IOError, Utilities.CodingError, UnicodeError) as err: 158 except (IOError, Utilities.CodingError, UnicodeError) as err:
104 E5MessageBox.critical(self, 159 E5MessageBox.critical(self,
105 self.trUtf8("Fix PEP 8 issues"), 160 self.trUtf8("Fix PEP 8 issues"),
106 self.trUtf8( 161 self.trUtf8(
107 """<p>Could not save the file <b>{0}</b>.""" 162 """<p>Could not save the file <b>{0}</b>."""
108 """ Skipping it.</p><p>Reason: {1}</p>""")\ 163 """ Skipping it.</p><p>Reason: {1}</p>""")
109 .format(self.__filename, str(err)) 164 .format(self.__filename, str(err))
110 ) 165 )
111 return False 166 return False
112 167
113 return True 168 return True
114 169
170 def __codeMatch(self, code):
171 """
172 Private method to check, if the code should be fixed.
173
174 @param code to check (string)
175 @return flag indicating it should be fixed (boolean)
176 """
177 def mutualStartswith(a, b):
178 """
179 Local helper method to compare the beginnings of two strings
180 against each other.
181
182 @return flag indicating that one string starts with the other
183 (boolean)
184 """
185 return b.startswith(a) or a.startswith(b)
186
187 if self.__noFixCodes:
188 for noFixCode in [c.strip() for c in self.__noFixCodes]:
189 if mutualStartswith(code.lower(), noFixCode.lower()):
190 return False
191
192 if self.__fixCodes:
193 for fixCode in [c.strip() for c in self.__fixCodes]:
194 if mutualStartswith(code.lower(), fixCode.lower()):
195 return True
196 return False
197
198 return True
199
115 def fixIssue(self, line, pos, message): 200 def fixIssue(self, line, pos, message):
116 """ 201 """
117 Public method to fix the fixable issues. 202 Public method to fix the fixable issues.
118 203
119 @param line line number of issue (integer) 204 @param line line number of issue (integer)
123 the fix (string) 208 the fix (string)
124 """ 209 """
125 code = message.split(None, 1)[0].strip() 210 code = message.split(None, 1)[0].strip()
126 211
127 if line <= len(self.__source) and \ 212 if line <= len(self.__source) and \
128 (code in self.__fixCodes or len(self.__fixCodes) == 0) and \ 213 self.__codeMatch(code) and \
129 code in self.__fixes: 214 code in self.__fixes:
130 res = self.__fixes[code](code, line, pos) 215 res = self.__fixes[code](code, line, pos)
131 if res[0]: 216 if res[0]:
132 self.__modified = True 217 self.__modified = True
133 self.fixed += 1 218 self.fixed += 1
138 223
139 def __finalize(self): 224 def __finalize(self):
140 """ 225 """
141 Private method to apply all deferred fixes. 226 Private method to apply all deferred fixes.
142 """ 227 """
228 # step 1: do fixes operating on logical lines first
229 for code, line, pos in self.__stackLogical:
230 self.__fixes[code](code, line, pos, apply=True)
231
232 # step 2: do fixes that change the number of lines
143 for code, line, pos in reversed(self.__stack): 233 for code, line, pos in reversed(self.__stack):
144 self.__fixes[code](code, line, pos, apply=True) 234 self.__fixes[code](code, line, pos, apply=True)
145 235
146 def __getEol(self): 236 def __getEol(self):
147 """ 237 """
148 Private method to get the applicable eol string. 238 Private method to get the applicable eol string.
149 239
150 @return eol string (string) 240 @return eol string (string)
151 """ 241 """
152 if self.__origName: 242 if not self.__eol:
153 fn = self.__origName 243 if self.__origName:
154 else: 244 fn = self.__origName
155 fn = self.__filename
156
157 if self.__project.isOpen() and self.__project.isProjectFile(fn):
158 eol = self.__project.getEolString()
159 else:
160 eol = Utilities.linesep()
161 return eol
162
163 def __fixTabs(self, code, line, pos):
164 """
165 Private method to fix obsolete tab usage.
166
167 @param code code of the issue (string)
168 @param line line number of the issue (integer)
169 @param pos position inside line (integer)
170 @return flag indicating an applied fix (boolean) and a message for
171 the fix (string)
172 """
173 self.__source[line - 1] = self.__source[line - 1].replace("\t", " ")
174 return (True, self.trUtf8("Tab converted to 4 spaces."))
175
176 def __fixWhitespace(self, code, line, pos):
177 """
178 Private method to fix trailing whitespace.
179
180 @param code code of the issue (string)
181 @param line line number of the issue (integer)
182 @param pos position inside line (integer)
183 @return flag indicating an applied fix (boolean) and a message for
184 the fix (string)
185 """
186 self.__source[line - 1] = re.sub(r'[\t ]+(\r?)$', r"\1",
187 self.__source[line - 1])
188 return (True, self.trUtf8("Whitespace stripped from end of line."))
189
190 def __fixNewline(self, code, line, pos):
191 """
192 Private method to fix a missing newline at the end of file.
193
194 @param code code of the issue (string)
195 @param line line number of the issue (integer)
196 @param pos position inside line (integer)
197 @return flag indicating an applied fix (boolean) and a message for
198 the fix (string)
199 """
200 self.__source[line - 1] += self.__getEol()
201 return (True, self.trUtf8("newline added to end of file."))
202
203 def __fixTrailingBlankLines(self, code, line, pos):
204 """
205 Private method to fix trailing blank lines.
206
207 @param code code of the issue (string)
208 @param line line number of the issue (integer)
209 @param pos position inside line (integer)
210 @return flag indicating an applied fix (boolean) and a message for
211 the fix (string)
212 """
213 index = line - 1
214 while index:
215 if self.__source[index].strip() == "":
216 del self.__source[index]
217 index -= 1
218 else: 245 else:
246 fn = self.__filename
247
248 if self.__project.isOpen() and self.__project.isProjectFile(fn):
249 self.__eol = self.__project.getEolString()
250 else:
251 self.__eol = Utilities.linesep()
252 return self.__eol
253
254 def __findLogical(self):
255 """
256 Private method to extract the index of all the starts and ends of
257 lines.
258
259 @return tuple containing two lists of integer with start and end tuples
260 of lines
261 """
262 logical_start = []
263 logical_end = []
264 last_newline = True
265 sio = io.StringIO("".join(self.__source))
266 parens = 0
267 for t in tokenize.generate_tokens(sio.readline):
268 if t[0] in [tokenize.COMMENT, tokenize.DEDENT,
269 tokenize.INDENT, tokenize.NL,
270 tokenize.ENDMARKER]:
271 continue
272 if not parens and t[0] in [tokenize.NEWLINE, tokenize.SEMI]:
273 last_newline = True
274 logical_end.append((t[3][0] - 1, t[2][1]))
275 continue
276 if last_newline and not parens:
277 logical_start.append((t[2][0] - 1, t[2][1]))
278 last_newline = False
279 if t[0] == tokenize.OP:
280 if t[1] in '([{':
281 parens += 1
282 elif t[1] in '}])':
283 parens -= 1
284 return logical_start, logical_end
285
286 def __getLogical(self, line, pos):
287 """
288 Private method to get the logical line corresponding to the given
289 position.
290
291 @param line line number of the issue (integer)
292 @param pos position inside line (integer)
293 @return tuple of a tuple of two integers giving the start of the
294 logical line, another tuple of two integers giving the end
295 of the logical line and a list of strings with the original
296 source lines
297 """
298 try:
299 (logical_start, logical_end) = self.__findLogical()
300 except (SyntaxError, tokenize.TokenError):
301 return None
302
303 line = line - 1
304 ls = None
305 le = None
306 for i in range(0, len(logical_start)):
307 x = logical_end[i]
308 if x[0] > line or (x[0] == line and x[1] > pos):
309 le = x
310 ls = logical_start[i]
219 break 311 break
220 return (True, self.trUtf8( 312 if ls is None:
221 "Superfluous trailing blank lines removed from end of file.")) 313 return None
222 314
223 def __fixNotEqual(self, code, line, pos): 315 original = self.__source[ls[0]:le[0] + 1]
224 """ 316 return ls, le, original
225 Private method to fix the not equal notation. 317
226 318 def __getIndentWord(self):
227 @param code code of the issue (string) 319 """
228 @param line line number of the issue (integer) 320 Private method to determine the indentation type.
229 @param pos position inside line (integer) 321
230 @return flag indicating an applied fix (boolean) and a message for 322 @return string to be used for an indentation (string)
231 the fix (string) 323 """
232 """ 324 sio = io.StringIO("".join(self.__source))
233 self.__source[line - 1] = self.__source[line - 1].replace("<>", "!=") 325 indentWord = " " # default in case of failure
234 return (True, self.trUtf8("'<>' replaced by '!='.")) 326 try:
235 327 for token in tokenize.generate_tokens(sio.readline):
236 def __fixBlankLinesAfterDecorator(self, code, line, pos, apply=False): 328 if token[0] == tokenize.INDENT:
237 """ 329 indentWord = token[1]
238 Private method to fix superfluous blank lines after a function 330 break
239 decorator. 331 except (SyntaxError, tokenize.TokenError):
332 pass
333 return indentWord
334
335 def __getIndent(self, line):
336 """
337 Private method to get the indentation string.
338
339 @param line line to determine the indentation string from (string)
340 @return indentation string (string)
341 """
342 return line.replace(line.lstrip(), "")
343
344 def __multilineStringLines(self):
345 """
346 Private method to determine the line numbers that are within multi line
347 strings and these which are part of a documentation string.
348
349 @return tuple of a set of line numbers belonging to a multi line
350 string and a set of line numbers belonging to a multi line
351 documentation string (tuple of two set of integer)
352 """
353 if self.__multiLineNumbers is None:
354 source = "".join(self.__source)
355 sio = io.StringIO(source)
356 self.__multiLineNumbers = set()
357 self.__docLineNumbers = set()
358 previousTokenType = ''
359 try:
360 for t in tokenize.generate_tokens(sio.readline):
361 tokenType = t[0]
362 startRow = t[2][0]
363 endRow = t[3][0]
364
365 if (tokenType == tokenize.STRING and startRow != endRow):
366 if previousTokenType != tokenize.INDENT:
367 self.__multiLineNumbers |= set(
368 range(startRow, 1 + endRow))
369 else:
370 self.__docLineNumbers |= set(
371 range(startRow, 1 + endRow))
372
373 previousTokenType = tokenType
374 except (SyntaxError, tokenize.TokenError):
375 pass
376
377 return self.__multiLineNumbers, self.__docLineNumbers
378
379 def __fixReindent(self, line, pos, logical):
380 """
381 Private method to fix a badly indented line.
382
383 This is done by adding or removing from its initial indent only.
384
385 @param line line number of the issue (integer)
386 @param pos position inside line (integer)
387 @return flag indicating a change was done (boolean)
388 """
389 assert logical
390 ls, _, original = logical
391
392 rewrapper = Pep8IndentationWrapper(original)
393 valid_indents = rewrapper.pep8Expected()
394 if not rewrapper.rel_indent:
395 return False
396
397 if line > ls[0]:
398 # got a valid continuation line number
399 row = line - ls[0] - 1
400 # always pick the first option for this
401 valid = valid_indents[row]
402 got = rewrapper.rel_indent[row]
403 else:
404 return False
405
406 line1 = ls[0] + row
407 # always pick the expected indent, for now.
408 indent_to = valid[0]
409
410 if got != indent_to:
411 orig_line = self.__source[line1]
412 new_line = ' ' * (indent_to) + orig_line.lstrip()
413 if new_line == orig_line:
414 return False
415 else:
416 self.__source[line1] = new_line
417 return True
418 else:
419 return False
420
421 def __fixWhitespace(self, line, offset, replacement):
422 """
423 Private method to correct whitespace at the given offset.
424
425 @param line line to be corrected (string)
426 @param offset offset within line (integer)
427 @param replacement replacement string (string)
428 @return corrected line
429 """
430 left = line[:offset].rstrip(" \t")
431 right = line[offset:].lstrip(" \t")
432 if right.startswith("#"):
433 return line
434 else:
435 return left + replacement + right
436
437 def __fixE101(self, code, line, pos):
438 """
439 Private method to fix obsolete tab usage and indentation errors
440 (E101, E111, W191).
441
442 @param code code of the issue (string)
443 @param line line number of the issue (integer)
444 @param pos position inside line (integer)
445 @return flag indicating an applied fix (boolean) and a message for
446 the fix (string)
447 """
448 if self.__reindenter is None:
449 self.__reindenter = Pep8Reindenter(self.__source)
450 self.__reindenter.run()
451 fixedLine = self.__reindenter.fixedLine(line - 1)
452 if fixedLine is not None:
453 self.__source[line - 1] = fixedLine
454 if code in ["E101", "W191"]:
455 msg = self.trUtf8("Tab converted to 4 spaces.")
456 else:
457 msg = self.trUtf8(
458 "Indentation adjusted to be a multiple of four.")
459 return (True, msg)
460 else:
461 return (False, self.trUtf8("Fix for {0} failed.").format(code))
462
463 def __fixE121(self, code, line, pos, apply=False):
464 """
465 Private method to fix the indentation of continuation lines and
466 closing brackets (E121,E124).
240 467
241 @param code code of the issue (string) 468 @param code code of the issue (string)
242 @param line line number of the issue (integer) 469 @param line line number of the issue (integer)
243 @param pos position inside line (integer) 470 @param pos position inside line (integer)
244 @keyparam apply flag indicating, that the fix should be applied 471 @keyparam apply flag indicating, that the fix should be applied
245 (boolean) 472 (boolean)
246 @return flag indicating an applied fix (boolean) and a message for 473 @return flag indicating an applied fix (boolean) and a message for
247 the fix (string) 474 the fix (string)
248 """ 475 """
249 if apply: 476 if apply:
250 index = line - 2 477 logical = self.__getLogical(line, pos)
251 while index: 478 if logical:
252 if self.__source[index].strip() == "": 479 # Fix by adjusting initial indent level.
253 del self.__source[index] 480 self.__fixReindent(line, pos, logical)
254 index -= 1 481 else:
255 else: 482 self.__stackLogical.append((code, line, pos))
256 break 483 if code == "E121":
257 else: 484 msg = self.trUtf8("Indentation of continuation line corrected.")
258 self.__stack.append((code, line, pos)) 485 elif code == "E124":
259 return (True, self.trUtf8( 486 msg = self.trUtf8("Indentation of closing bracket corrected.")
260 "Superfluous blank lines after function decorator removed.")) 487 return (True, msg)
261 488
262 def __fixTooManyBlankLines(self, code, line, pos, apply=False): 489 def __fixE122(self, code, line, pos, apply=False):
263 """ 490 """
264 Private method to fix superfluous blank lines. 491 Private method to fix a missing indentation of continuation lines
492 (E122).
265 493
266 @param code code of the issue (string) 494 @param code code of the issue (string)
267 @param line line number of the issue (integer) 495 @param line line number of the issue (integer)
268 @param pos position inside line (integer) 496 @param pos position inside line (integer)
269 @keyparam apply flag indicating, that the fix should be applied 497 @keyparam apply flag indicating, that the fix should be applied
270 (boolean) 498 (boolean)
271 @return flag indicating an applied fix (boolean) and a message for 499 @return flag indicating an applied fix (boolean) and a message for
272 the fix (string) 500 the fix (string)
273 """ 501 """
274 if apply: 502 if apply:
275 index = line - 3 503 logical = self.__getLogical(line, pos)
276 while index: 504 if logical:
277 if self.__source[index].strip() == "": 505 # Fix by adding an initial indent.
278 del self.__source[index] 506 modified = self.__fixReindent(line, pos, logical)
279 index -= 1 507 if not modified:
280 else: 508 # fall back to simple method
281 break 509 line = line - 1
282 else: 510 text = self.__source[line]
283 self.__stack.append((code, line, pos)) 511 indentation = self.__getIndent(text)
284 return (True, self.trUtf8("Superfluous blank lines removed.")) 512 self.__source[line] = indentation + \
285 513 self.__indentWord + text.lstrip()
286 def __fixOneBlankLine(self, code, line, pos, apply=False): 514 else:
287 """ 515 self.__stackLogical.append((code, line, pos))
288 Private method to fix the need for one blank line. 516 return (
517 True,
518 self.trUtf8("Missing indentation of continuation line corrected."))
519
520 def __fixE123(self, code, line, pos, apply=False):
521 """
522 Private method to fix the indentation of a closing bracket lines
523 (E123).
289 524
290 @param code code of the issue (string) 525 @param code code of the issue (string)
291 @param line line number of the issue (integer) 526 @param line line number of the issue (integer)
292 @param pos position inside line (integer) 527 @param pos position inside line (integer)
293 @keyparam apply flag indicating, that the fix should be applied 528 @keyparam apply flag indicating, that the fix should be applied
294 (boolean) 529 (boolean)
295 @return flag indicating an applied fix (boolean) and a message for 530 @return flag indicating an applied fix (boolean) and a message for
296 the fix (string) 531 the fix (string)
297 """ 532 """
298 if apply: 533 if apply:
534 logical = self.__getLogical(line, pos)
535 if logical:
536 # Fix by deleting whitespace to the correct level.
537 logicalLines = logical[2]
538 row = line - 1
539 text = self.__source[row]
540 newText = self.__getIndent(logicalLines[0]) + text.lstrip()
541 if newText == text:
542 # fall back to slower method
543 self.__fixReindent(line, pos, logical)
544 else:
545 self.__source[row] = newText
546 else:
547 self.__stackLogical.append((code, line, pos))
548 return (
549 True, self.trUtf8("Closing bracket aligned to opening bracket."))
550
551 def __fixE125(self, code, line, pos, apply=False):
552 """
553 Private method to fix the indentation of continuation lines not
554 distinguishable from next logical line (E125).
555
556 @param code code of the issue (string)
557 @param line line number of the issue (integer)
558 @param pos position inside line (integer)
559 @keyparam apply flag indicating, that the fix should be applied
560 (boolean)
561 @return flag indicating an applied fix (boolean) and a message for
562 the fix (string)
563 """
564 if apply:
565 logical = self.__getLogical(line, pos)
566 if logical:
567 # Fix by adjusting initial indent level.
568 modified = self.__fixReindent(line, pos, logical)
569 if not modified:
570 row = line - 1
571 text = self.__source[row]
572 self.__source[row] = self.__getIndent(text) + \
573 self.__indentWord + text.lstrip()
574 else:
575 self.__stackLogical.append((code, line, pos))
576 return (True, self.trUtf8("Indentation level changed."))
577
578 def __fixE126(self, code, line, pos, apply=False):
579 """
580 Private method to fix over-indented/under-indented hanging
581 indentation (E126, E133).
582
583 @param code code of the issue (string)
584 @param line line number of the issue (integer)
585 @param pos position inside line (integer)
586 @keyparam apply flag indicating, that the fix should be applied
587 (boolean)
588 @return flag indicating an applied fix (boolean) and a message for
589 the fix (string)
590 """
591 if apply:
592 logical = self.__getLogical(line, pos)
593 if logical:
594 # Fix by deleting whitespace to the left.
595 logicalLines = logical[2]
596 row = line - 1
597 text = self.__source[row]
598 newText = self.__getIndent(logicalLines[0]) + \
599 self.__indentWord + text.lstrip()
600 if newText == text:
601 # fall back to slower method
602 self.__fixReindent(line, pos, logical)
603 else:
604 self.__source[row] = newText
605 else:
606 self.__stackLogical.append((code, line, pos))
607 return (
608 True,
609 self.trUtf8("Indentation level of hanging indentation changed."))
610
611 def __fixE127(self, code, line, pos, apply=False):
612 """
613 Private method to fix over/under indented lines (E127, E128).
614
615 @param code code of the issue (string)
616 @param line line number of the issue (integer)
617 @param pos position inside line (integer)
618 @keyparam apply flag indicating, that the fix should be applied
619 (boolean)
620 @return flag indicating an applied fix (boolean) and a message for
621 the fix (string)
622 """
623 if apply:
624 logical = self.__getLogical(line, pos)
625 if logical:
626 # Fix by inserting/deleting whitespace to the correct level.
627 logicalLines = logical[2]
628 row = line - 1
629 text = self.__source[row]
630 newText = text
631
632 if logicalLines[0].rstrip().endswith('\\'):
633 newText = self.__getIndent(logicalLines[0]) + \
634 self.__indentWord + text.lstrip()
635 else:
636 startIndex = None
637 for symbol in '([{':
638 if symbol in logicalLines[0]:
639 foundIndex = logicalLines[0].find(symbol) + 1
640 if startIndex is None:
641 startIndex = foundIndex
642 else:
643 startIndex = min(startIndex, foundIndex)
644
645 if startIndex is not None:
646 newText = startIndex * ' ' + text.lstrip()
647
648 if newText == text:
649 # fall back to slower method
650 self.__fixReindent(line, pos, logical)
651 else:
652 self.__source[row] = newText
653 else:
654 self.__stackLogical.append((code, line, pos))
655 return (True, self.trUtf8("Visual indentation corrected."))
656
657 def __fixE201(self, code, line, pos):
658 """
659 Private method to fix extraneous whitespace (E201, E202,
660 E203, E211).
661
662 @param code code of the issue (string)
663 @param line line number of the issue (integer)
664 @param pos position inside line (integer)
665 @return flag indicating an applied fix (boolean) and a message for
666 the fix (string)
667 """
668 line = line - 1
669 text = self.__source[line]
670
671 if '"""' in text or "'''" in text or text.rstrip().endswith('\\'):
672 return (
673 False, self.trUtf8("Extraneous whitespace cannot be removed."))
674
675 newText = self.__fixWhitespace(text, pos, '')
676 if newText == text:
677 return (False, "")
678
679 self.__source[line] = newText
680 return (True, self.trUtf8("Extraneous whitespace removed."))
681
682 def __fixE221(self, code, line, pos):
683 """
684 Private method to fix extraneous whitespace around operator or
685 keyword (E221, E222, E223, E224, E225, E226, E227, E228, E241,
686 E242, E271, E272, E273, E274).
687
688 @param code code of the issue (string)
689 @param line line number of the issue (integer)
690 @param pos position inside line (integer)
691 @return flag indicating an applied fix (boolean) and a message for
692 the fix (string)
693 """
694 line = line - 1
695 text = self.__source[line]
696
697 if '"""' in text or "'''" in text or text.rstrip().endswith('\\'):
698 return (
699 False, self.trUtf8("Extraneous whitespace cannot be removed."))
700
701 newText = self.__fixWhitespace(text, pos, ' ')
702 if newText == text:
703 return (False, "")
704
705 self.__source[line] = newText
706 if code in ["E225", "E226", "E227", "E228"]:
707 return (True, self.trUtf8("Missing whitespace added."))
708 else:
709 return (True, self.trUtf8("Extraneous whitespace removed."))
710
711 def __fixE231(self, code, line, pos):
712 """
713 Private method to fix missing whitespace after ',;:'.
714
715 @param code code of the issue (string)
716 @param line line number of the issue (integer)
717 @param pos position inside line (integer)
718 @return flag indicating an applied fix (boolean) and a message for
719 the fix (string)
720 """
721 line = line - 1
722 pos = pos + 1
723 self.__source[line] = self.__source[line][:pos] + \
724 " " + \
725 self.__source[line][pos:]
726 return (True, self.trUtf8("Missing whitespace added."))
727
728 def __fixE251(self, code, line, pos):
729 """
730 Private method to fix extraneous whitespace around keyword and
731 default parameter equals (E251).
732
733 @param code code of the issue (string)
734 @param line line number of the issue (integer)
735 @param pos position inside line (integer)
736 @return flag indicating an applied fix (boolean) and a message for
737 the fix (string)
738 """
739 line = line - 1
740 text = self.__source[line]
741
742 # This is necessary since pep8 sometimes reports columns that goes
743 # past the end of the physical line. This happens in cases like,
744 # foo(bar\n=None)
745 col = min(pos, len(text) - 1)
746 if text[col].strip():
747 newText = text
748 else:
749 newText = text[:col].rstrip() + text[col:].lstrip()
750
751 # There could be an escaped newline
752 #
753 # def foo(a=\
754 # 1)
755 if newText.endswith(('=\\\n', '=\\\r\n', '=\\\r')):
756 self.__source[line] = newText.rstrip("\n\r \t\\")
757 self.__source[line + 1] = self.__source[line + 1].lstrip()
758 else:
759 self.__source[line] = newText
760 return (True, self.trUtf8("Extraneous whitespace removed."))
761
762 def __fixE261(self, code, line, pos):
763 """
764 Private method to fix whitespace before or after inline comment
765 (E261, E262).
766
767 @param code code of the issue (string)
768 @param line line number of the issue (integer)
769 @param pos position inside line (integer)
770 @return flag indicating an applied fix (boolean) and a message for
771 the fix (string)
772 """
773 line = line - 1
774 text = self.__source[line]
775 left = text[:pos].rstrip(' \t#')
776 right = text[pos:].lstrip(' \t#')
777 newText = left + (" # " + right if right.strip() else right)
778 self.__source[line] = newText
779 return (True, self.trUtf8("Whitespace around comment sign corrected."))
780
781 def __fixE301(self, code, line, pos, apply=False):
782 """
783 Private method to fix the need for one blank line (E301).
784
785 @param code code of the issue (string)
786 @param line line number of the issue (integer)
787 @param pos position inside line (integer)
788 @keyparam apply flag indicating, that the fix should be applied
789 (boolean)
790 @return flag indicating an applied fix (boolean) and a message for
791 the fix (string)
792 """
793 if apply:
299 self.__source.insert(line - 1, self.__getEol()) 794 self.__source.insert(line - 1, self.__getEol())
300 else: 795 else:
301 self.__stack.append((code, line, pos)) 796 self.__stack.append((code, line, pos))
302 return (True, self.trUtf8("One blank line inserted.")) 797 return (True, self.trUtf8("One blank line inserted."))
303 798
304 def __fixTwoBlankLines(self, code, line, pos, apply=False): 799 def __fixE302(self, code, line, pos, apply=False):
305 """ 800 """
306 Private method to fix the need for two blank lines. 801 Private method to fix the need for two blank lines (E302).
802
803 @param code code of the issue (string)
804 @param line line number of the issue (integer)
805 @param pos position inside line (integer)
806 @keyparam apply flag indicating, that the fix should be applied
807 (boolean)
808 @return flag indicating an applied fix (boolean) and a message for
809 the fix (string)
307 """ 810 """
308 # count blank lines 811 # count blank lines
309 index = line - 1 812 index = line - 1
310 blanks = 0 813 blanks = 0
311 while index: 814 while index:
338 msg = self.trUtf8("%n superfluous lines removed", "", delta) 841 msg = self.trUtf8("%n superfluous lines removed", "", delta)
339 else: 842 else:
340 msg = "" 843 msg = ""
341 return (True, msg) 844 return (True, msg)
342 845
343 def __fixWhitespaceAfter(self, code, line, pos, apply=False): 846 def __fixE303(self, code, line, pos, apply=False):
344 """ 847 """
345 Private method to fix superfluous whitespace after '([{'. 848 Private method to fix superfluous blank lines (E303).
346 849
347 @param code code of the issue (string) 850 @param code code of the issue (string)
348 @param line line number of the issue (integer) 851 @param line line number of the issue (integer)
349 @param pos position inside line (integer) 852 @param pos position inside line (integer)
350 @keyparam apply flag indicating, that the fix should be applied 853 @keyparam apply flag indicating, that the fix should be applied
351 (boolean) 854 (boolean)
352 @return flag indicating an applied fix (boolean) and a message for 855 @return flag indicating an applied fix (boolean) and a message for
353 the fix (string) 856 the fix (string)
354 """ 857 """
355 line = line - 1 858 if apply:
356 pos = pos - 1 859 index = line - 3
357 while self.__source[line][pos] in [" ", "\t"]: 860 while index:
358 self.__source[line] = self.__source[line][:pos] + \ 861 if self.__source[index].strip() == "":
359 self.__source[line][pos + 1:] 862 del self.__source[index]
360 return (True, self.trUtf8("Superfluous whitespace removed.")) 863 index -= 1
361 864 else:
362 def __fixWhitespaceBefore(self, code, line, pos, apply=False): 865 break
363 """ 866 else:
364 Private method to fix superfluous whitespace before '}])', 867 self.__stack.append((code, line, pos))
365 ',;:' and '(['. 868 return (True, self.trUtf8("Superfluous blank lines removed."))
869
870 def __fixE304(self, code, line, pos, apply=False):
871 """
872 Private method to fix superfluous blank lines after a function
873 decorator (E304).
366 874
367 @param code code of the issue (string) 875 @param code code of the issue (string)
368 @param line line number of the issue (integer) 876 @param line line number of the issue (integer)
369 @param pos position inside line (integer) 877 @param pos position inside line (integer)
370 @keyparam apply flag indicating, that the fix should be applied 878 @keyparam apply flag indicating, that the fix should be applied
371 (boolean) 879 (boolean)
372 @return flag indicating an applied fix (boolean) and a message for 880 @return flag indicating an applied fix (boolean) and a message for
373 the fix (string) 881 the fix (string)
374 """ 882 """
375 line = line - 1 883 if apply:
376 pos = pos - 1 884 index = line - 2
377 while self.__source[line][pos] in [" ", "\t"]: 885 while index:
378 self.__source[line] = self.__source[line][:pos] + \ 886 if self.__source[index].strip() == "":
379 self.__source[line][pos + 1:] 887 del self.__source[index]
380 pos -= 1 888 index -= 1
381 return (True, self.trUtf8("Superfluous whitespace removed.")) 889 else:
382 890 break
383 def __fixMissingWhitespaceAfter(self, code, line, pos, apply=False): 891 else:
384 """ 892 self.__stack.append((code, line, pos))
385 Private method to fix missing whitespace after ',;:'. 893 return (True, self.trUtf8(
894 "Superfluous blank lines after function decorator removed."))
895
896 def __fixE401(self, code, line, pos, apply=False):
897 """
898 Private method to fix multiple imports on one line (E401).
386 899
387 @param code code of the issue (string) 900 @param code code of the issue (string)
388 @param line line number of the issue (integer) 901 @param line line number of the issue (integer)
389 @param pos position inside line (integer) 902 @param pos position inside line (integer)
390 @keyparam apply flag indicating, that the fix should be applied 903 @keyparam apply flag indicating, that the fix should be applied
391 (boolean) 904 (boolean)
392 @return flag indicating an applied fix (boolean) and a message for 905 @return flag indicating an applied fix (boolean) and a message for
393 the fix (string) 906 the fix (string)
394 """ 907 """
395 line = line - 1 908 if apply:
396 self.__source[line] = self.__source[line][:pos] + \ 909 line = line - 1
397 " " + \ 910 text = self.__source[line]
398 self.__source[line][pos:] 911 if not text.lstrip().startswith("import"):
399 return (True, self.trUtf8("Missing whitespace added.")) 912 return (False, "")
400 913
401 def __fixWhitespaceAroundOperator(self, code, line, pos, apply=False): 914 # pep8 (1.3.1) reports false positive if there is an import
402 """ 915 # statement followed by a semicolon and some unrelated
403 Private method to fix extraneous whitespace around operator. 916 # statement with commas in it.
917 if ';' in text:
918 return (False, "")
919
920 newText = text[:pos].rstrip("\t ,") + self.__getEol() + \
921 self.__getIndent(text) + "import " + text[pos:].lstrip("\t ,")
922 self.__source[line] = newText
923 else:
924 self.__stack.append((code, line, pos))
925 return (True, self.trUtf8("Imports were put on separate lines."))
926
927 def __fixE501(self, code, line, pos, apply=False):
928 """
929 Private method to fix the long lines by breaking them (E501).
404 930
405 @param code code of the issue (string) 931 @param code code of the issue (string)
406 @param line line number of the issue (integer) 932 @param line line number of the issue (integer)
407 @param pos position inside line (integer) 933 @param pos position inside line (integer)
408 @keyparam apply flag indicating, that the fix should be applied 934 @keyparam apply flag indicating, that the fix should be applied
409 (boolean) 935 (boolean)
410 @return flag indicating an applied fix (boolean) and a message for 936 @return flag indicating an applied fix (boolean) and a message for
411 the fix (string) 937 the fix (string)
412 """ 938 """
413 line = line - 1 939 multilineStringLines, docStringLines = self.__multilineStringLines()
414 while self.__source[line][pos - 1] in [" ", "\t"]: 940 if apply:
415 self.__source[line] = self.__source[line][:pos - 1] + \ 941 isDocString = line in docStringLines
416 self.__source[line][pos:] 942 line = line - 1
417 pos -= 1 943 text = self.__source[line]
418 return (True, self.trUtf8("Extraneous whitespace removed.")) 944 if line > 0:
419 945 prevText = self.__source[line - 1]
420 def __fixMissingWhitespaceAroundOperator(self, code, line, pos, 946 else:
421 apply=False): 947 prevText = ""
422 """ 948 if line < len(self.__source) - 1:
423 Private method to fix missing whitespace after ',;:'. 949 nextText = self.__source[line + 1]
950 else:
951 nextText = ""
952 shortener = Pep8LineShortener(text, prevText, nextText,
953 maxLength=self.__maxLineLength, eol=self.__getEol(),
954 indentWord=self.__indentWord, isDocString=isDocString)
955 changed, newText, newNextText = shortener.shorten()
956 if changed:
957 if newText != text:
958 self.__source[line] = newText
959 if newNextText and newNextText != nextText:
960 self.__source[line + 1] = newNextText
961 else:
962 self.__stack.append((code, line, pos))
963 return (True, self.trUtf8("Long lines have been shortened."))
964
965 def __fixE502(self, code, line, pos):
966 """
967 Private method to fix redundant backslash within brackets (E502).
968
969 @param code code of the issue (string)
970 @param line line number of the issue (integer)
971 @param pos position inside line (integer)
972 @return flag indicating an applied fix (boolean) and a message for
973 the fix (string)
974 """
975 self.__source[line - 1] = \
976 self.__source[line - 1].rstrip("\n\r \t\\") + self.__getEol()
977 return (True, self.trUtf8("Redundant backslash in brackets removed."))
978
979 def __fixE701(self, code, line, pos, apply=False):
980 """
981 Private method to fix colon-separated compund statements (E701).
424 982
425 @param code code of the issue (string) 983 @param code code of the issue (string)
426 @param line line number of the issue (integer) 984 @param line line number of the issue (integer)
427 @param pos position inside line (integer) 985 @param pos position inside line (integer)
428 @keyparam apply flag indicating, that the fix should be applied 986 @keyparam apply flag indicating, that the fix should be applied
429 (boolean) 987 (boolean)
430 @return flag indicating an applied fix (boolean) and a message for 988 @return flag indicating an applied fix (boolean) and a message for
431 the fix (string) 989 the fix (string)
432 """ 990 """
433 line = line - 1 991 if apply:
434 pos = pos - 1 992 line = line - 1
435 self.__source[line] = self.__source[line][:pos] + \ 993 text = self.__source[line]
436 " " + \ 994 pos = pos + 1
437 self.__source[line][pos:] 995
438 return (True, self.trUtf8("Missing whitespace added.")) 996 newText = text[:pos] + self.__getEol() + self.__getIndent(text) + \
439 997 self.__indentWord + text[pos:].lstrip("\n\r \t\\") + \
440 def __fixWhitespaceAroundEquals(self, code, line, pos, apply=False): 998 self.__getEol()
441 """ 999 self.__source[line] = newText
442 Private method to fix extraneous whitespace around keyword and 1000 else:
443 default parameter equals. 1001 self.__stack.append((code, line, pos))
1002 return (True, self.trUtf8("Compound statement corrected."))
1003
1004 def __fixE702(self, code, line, pos, apply=False):
1005 """
1006 Private method to fix semicolon-separated compound statements
1007 (E702, E703).
444 1008
445 @param code code of the issue (string) 1009 @param code code of the issue (string)
446 @param line line number of the issue (integer) 1010 @param line line number of the issue (integer)
447 @param pos position inside line (integer) 1011 @param pos position inside line (integer)
448 @keyparam apply flag indicating, that the fix should be applied 1012 @keyparam apply flag indicating, that the fix should be applied
449 (boolean) 1013 (boolean)
450 @return flag indicating an applied fix (boolean) and a message for 1014 @return flag indicating an applied fix (boolean) and a message for
451 the fix (string) 1015 the fix (string)
452 """ 1016 """
1017 if apply:
1018 line = line - 1
1019 text = self.__source[line]
1020
1021 if text.rstrip().endswith("\\"):
1022 # normalize '1; \\\n2' into '1; 2'
1023 self.__source[line] = text.rstrip("\n\r \t\\")
1024 self.__source[line + 1] = self.__source[line + 1].lstrip()
1025 elif text.rstrip().endswith(";"):
1026 self.__source[line] = text.rstrip("\n\r \t;") + self.__getEol()
1027 else:
1028 first = text[:pos].rstrip("\n\r \t;") + self.__getEol()
1029 second = text[pos:].lstrip("\n\r \t;")
1030 self.__source[line] = first + self.__getIndent(text) + second
1031 else:
1032 self.__stack.append((code, line, pos))
1033 return (True, self.trUtf8("Compound statement corrected."))
1034
1035 def __fixE711(self, code, line, pos):
1036 """
1037 Private method to fix comparison with None (E711, E712).
1038
1039 @param code code of the issue (string)
1040 @param line line number of the issue (integer)
1041 @param pos position inside line (integer)
1042 @return flag indicating an applied fix (boolean) and a message for
1043 the fix (string)
1044 """
453 line = line - 1 1045 line = line - 1
454 if self.__source[line][pos + 1] == " ": 1046 text = self.__source[line]
455 self.__source[line] = self.__source[line][:pos + 1] + \ 1047
456 self.__source[line][pos + 2:] 1048 rightPos = pos + 2
457 if self.__source[line][pos - 1] == " ": 1049 if rightPos >= len(text):
458 self.__source[line] = self.__source[line][:pos - 1] + \ 1050 return (False, "")
459 self.__source[line][pos:] 1051
460 return (True, self.trUtf8("Extraneous whitespace removed.")) 1052 left = text[:pos].rstrip()
461 1053 center = text[pos:rightPos]
462 def __fixWhitespaceBeforeInline(self, code, line, pos, apply=False): 1054 right = text[rightPos:].lstrip()
463 """ 1055
464 Private method to fix missing whitespace before inline comment. 1056 if not right.startswith(("None", "True", "False")):
465 1057 return (False, "")
466 @param code code of the issue (string) 1058
467 @param line line number of the issue (integer) 1059 if center.strip() == "==":
468 @param pos position inside line (integer) 1060 center = "is"
469 @keyparam apply flag indicating, that the fix should be applied 1061 elif center.strip() == "!=":
1062 center = "is not"
1063 else:
1064 return (False, "")
1065
1066 self.__source[line] = " ".join([left, center, right])
1067 return (True, self.trUtf8("Comparison to None/True/False corrected."))
1068
1069 def __fixW291(self, code, line, pos):
1070 """
1071 Private method to fix trailing whitespace (W291, W293).
1072
1073 @param code code of the issue (string)
1074 @param line line number of the issue (integer)
1075 @param pos position inside line (integer)
1076 @return flag indicating an applied fix (boolean) and a message for
1077 the fix (string)
1078 """
1079 self.__source[line - 1] = re.sub(r'[\t ]+(\r?)$', r"\1",
1080 self.__source[line - 1])
1081 return (True, self.trUtf8("Whitespace stripped from end of line."))
1082
1083 def __fixW292(self, code, line, pos):
1084 """
1085 Private method to fix a missing newline at the end of file (W292).
1086
1087 @param code code of the issue (string)
1088 @param line line number of the issue (integer)
1089 @param pos position inside line (integer)
1090 @return flag indicating an applied fix (boolean) and a message for
1091 the fix (string)
1092 """
1093 self.__source[line - 1] += self.__getEol()
1094 return (True, self.trUtf8("newline added to end of file."))
1095
1096 def __fixW391(self, code, line, pos):
1097 """
1098 Private method to fix trailing blank lines (W391).
1099
1100 @param code code of the issue (string)
1101 @param line line number of the issue (integer)
1102 @param pos position inside line (integer)
1103 @return flag indicating an applied fix (boolean) and a message for
1104 the fix (string)
1105 """
1106 index = line - 1
1107 while index:
1108 if self.__source[index].strip() == "":
1109 del self.__source[index]
1110 index -= 1
1111 else:
1112 break
1113 return (True, self.trUtf8(
1114 "Superfluous trailing blank lines removed from end of file."))
1115
1116 def __fixW603(self, code, line, pos):
1117 """
1118 Private method to fix the not equal notation (W603).
1119
1120 @param code code of the issue (string)
1121 @param line line number of the issue (integer)
1122 @param pos position inside line (integer)
1123 @return flag indicating an applied fix (boolean) and a message for
1124 the fix (string)
1125 """
1126 self.__source[line - 1] = self.__source[line - 1].replace("<>", "!=")
1127 return (True, self.trUtf8("'<>' replaced by '!='."))
1128
1129
1130 class Pep8Reindenter(object):
1131 """
1132 Class to reindent badly-indented code to uniformly use four-space
1133 indentation.
1134
1135 Released to the public domain, by Tim Peters, 03 October 2000.
1136 """
1137 def __init__(self, sourceLines):
1138 """
1139 Constructor
1140
1141 @param sourceLines list of source lines including eol marker
1142 (list of string)
1143 """
1144 # Raw file lines.
1145 self.raw = sourceLines
1146 self.after = []
1147
1148 # File lines, rstripped & tab-expanded. Dummy at start is so
1149 # that we can use tokenize's 1-based line numbering easily.
1150 # Note that a line is all-blank iff it's "\n".
1151 self.lines = [line.rstrip().expandtabs() + "\n"
1152 for line in self.raw]
1153 self.lines.insert(0, None)
1154 self.index = 1 # index into self.lines of next line
1155
1156 # List of (lineno, indentlevel) pairs, one for each stmt and
1157 # comment line. indentlevel is -1 for comment lines, as a
1158 # signal that tokenize doesn't know what to do about them;
1159 # indeed, they're our headache!
1160 self.stats = []
1161
1162 def run(self):
1163 """
1164 Public method to run the re-indenter.
1165 """
1166 try:
1167 stats = self.__genStats(tokenize.generate_tokens(self.getline))
1168 except (SyntaxError, tokenize.TokenError):
1169 return False
1170
1171 # Remove trailing empty lines.
1172 lines = self.lines
1173 while lines and lines[-1] == "\n":
1174 lines.pop()
1175 # Sentinel.
1176 stats.append((len(lines), 0))
1177 # Map count of leading spaces to # we want.
1178 have2want = {}
1179 # Program after transformation.
1180 after = self.after = []
1181 # Copy over initial empty lines -- there's nothing to do until
1182 # we see a line with *something* on it.
1183 i = stats[0][0]
1184 after.extend(lines[1:i])
1185 for i in range(len(stats) - 1):
1186 thisstmt, thislevel = stats[i]
1187 nextstmt = stats[i + 1][0]
1188 have = self.__getlspace(lines[thisstmt])
1189 want = thislevel * 4
1190 if want < 0:
1191 # A comment line.
1192 if have:
1193 # An indented comment line. If we saw the same
1194 # indentation before, reuse what it most recently
1195 # mapped to.
1196 want = have2want.get(have, -1)
1197 if want < 0:
1198 # Then it probably belongs to the next real stmt.
1199 for j in range(i + 1, len(stats) - 1):
1200 jline, jlevel = stats[j]
1201 if jlevel >= 0:
1202 if have == self.__getlspace(lines[jline]):
1203 want = jlevel * 4
1204 break
1205 if want < 0: # Maybe it's a hanging
1206 # comment like this one,
1207 # in which case we should shift it like its base
1208 # line got shifted.
1209 for j in range(i - 1, -1, -1):
1210 jline, jlevel = stats[j]
1211 if jlevel >= 0:
1212 want = \
1213 have + \
1214 self.__getlspace(after[jline - 1]) - \
1215 self.__getlspace(lines[jline])
1216 break
1217 if want < 0:
1218 # Still no luck -- leave it alone.
1219 want = have
1220 else:
1221 want = 0
1222 assert want >= 0
1223 have2want[have] = want
1224 diff = want - have
1225 if diff == 0 or have == 0:
1226 after.extend(lines[thisstmt:nextstmt])
1227 else:
1228 for line in lines[thisstmt:nextstmt]:
1229 if diff > 0:
1230 if line == "\n":
1231 after.append(line)
1232 else:
1233 after.append(" " * diff + line)
1234 else:
1235 remove = min(self.__getlspace(line), -diff)
1236 after.append(line[remove:])
1237 return self.raw != self.after
1238
1239 def fixedLine(self, line):
1240 """
1241 Public method to get a fixed line.
1242
1243 @param line number of the line to retrieve (integer)
1244 @return fixed line (string)
1245 """
1246 if line < len(self.after):
1247 return self.after[line]
1248
1249 def getline(self):
1250 """
1251 Public method to get a line of text for tokenize.
1252
1253 @return line of text (string)
1254 """
1255 if self.index >= len(self.lines):
1256 line = ""
1257 else:
1258 line = self.lines[self.index]
1259 self.index += 1
1260 return line
1261
1262 def __genStats(self, tokens):
1263 """
1264 Private method to generate the re-indent statistics.
1265
1266 @param tokens tokens generator (tokenize._tokenize)
1267 """
1268 find_stmt = True # next token begins a fresh stmt?
1269 level = 0 # current indent level
1270 stats = []
1271
1272 for t in tokens:
1273 token_type = t[0]
1274 sline = t[2][0]
1275 line = t[4]
1276
1277 if token_type == tokenize.NEWLINE:
1278 # A program statement, or ENDMARKER, will eventually follow,
1279 # after some (possibly empty) run of tokens of the form
1280 # (NL | COMMENT)* (INDENT | DEDENT+)?
1281 self.find_stmt = True
1282
1283 elif token_type == tokenize.INDENT:
1284 find_stmt = True
1285 level += 1
1286
1287 elif token_type == tokenize.DEDENT:
1288 find_stmt = True
1289 level -= 1
1290
1291 elif token_type == tokenize.COMMENT:
1292 if find_stmt:
1293 stats.append((sline, -1))
1294 # but we're still looking for a new stmt, so leave
1295 # find_stmt alone
1296
1297 elif token_type == tokenize.NL:
1298 pass
1299
1300 elif find_stmt:
1301 # This is the first "real token" following a NEWLINE, so it
1302 # must be the first token of the next program statement, or an
1303 # ENDMARKER.
1304 find_stmt = False
1305 if line: # not endmarker
1306 stats.append((sline, level))
1307
1308 return stats
1309
1310 def __getlspace(self, line):
1311 """
1312 Private method to count number of leading blanks.
1313
1314 @param line line to check (string)
1315 @return number of leading blanks (integer)
1316 """
1317 i = 0
1318 n = len(line)
1319 while i < n and line[i] == " ":
1320 i += 1
1321 return i
1322
1323
1324 class Pep8IndentationWrapper(object):
1325 """
1326 Class used by fixers dealing with indentation.
1327
1328 Each instance operates on a single logical line.
1329 """
1330
1331 SKIP_TOKENS = frozenset([
1332 tokenize.COMMENT, tokenize.NL, tokenize.INDENT,
1333 tokenize.DEDENT, tokenize.NEWLINE, tokenize.ENDMARKER
1334 ])
1335
1336 def __init__(self, physical_lines):
1337 """
1338 Constructor
1339
1340 @param physical_lines list of physical lines to operate on
1341 (list of strings)
1342 """
1343 self.lines = physical_lines
1344 self.tokens = []
1345 self.rel_indent = None
1346 sio = io.StringIO(''.join(physical_lines))
1347 for t in tokenize.generate_tokens(sio.readline):
1348 if not len(self.tokens) and t[0] in self.SKIP_TOKENS:
1349 continue
1350 if t[0] != tokenize.ENDMARKER:
1351 self.tokens.append(t)
1352
1353 self.logical_line = self.__buildTokensLogical(self.tokens)
1354
1355 def __buildTokensLogical(self, tokens):
1356 """
1357 Private method to build a logical line from a list of tokens.
1358
1359 @param tokens list of tokens as generated by tokenize.generate_tokens
1360 @return logical line (string)
1361 """
1362 # from pep8.py with minor modifications
1363 logical = []
1364 previous = None
1365 for t in tokens:
1366 token_type, text = t[0:2]
1367 if token_type in self.SKIP_TOKENS:
1368 continue
1369 if previous:
1370 end_line, end = previous[3]
1371 start_line, start = t[2]
1372 if end_line != start_line: # different row
1373 prev_text = self.lines[end_line - 1][end - 1]
1374 if prev_text == ',' or (prev_text not in '{[('
1375 and text not in '}])'):
1376 logical.append(' ')
1377 elif end != start: # different column
1378 fill = self.lines[end_line - 1][end:start]
1379 logical.append(fill)
1380 logical.append(text)
1381 previous = t
1382 logical_line = ''.join(logical)
1383 assert logical_line.lstrip() == logical_line
1384 assert logical_line.rstrip() == logical_line
1385 return logical_line
1386
1387 def pep8Expected(self):
1388 """
1389 Public method to replicate logic in pep8.py, to know what level to
1390 indent things to.
1391
1392 @return list of lists, where each list represents valid indent levels
1393 for the line in question, relative from the initial indent. However,
1394 the first entry is the indent level which was expected.
1395 """
1396 # What follows is an adjusted version of
1397 # pep8.py:continuation_line_indentation. All of the comments have been
1398 # stripped and the 'yield' statements replaced with 'pass'.
1399 if not self.tokens:
1400 return
1401
1402 first_row = self.tokens[0][2][0]
1403 nrows = 1 + self.tokens[-1][2][0] - first_row
1404
1405 # here are the return values
1406 valid_indents = [list()] * nrows
1407 indent_level = self.tokens[0][2][1]
1408 valid_indents[0].append(indent_level)
1409
1410 if nrows == 1:
1411 # bug, really.
1412 return valid_indents
1413
1414 indent_next = self.logical_line.endswith(':')
1415
1416 row = depth = 0
1417 parens = [0] * nrows
1418 self.rel_indent = rel_indent = [0] * nrows
1419 indent = [indent_level]
1420 indent_chances = {}
1421 last_indent = (0, 0)
1422 last_token_multiline = None
1423
1424 for token_type, text, start, end, line in self.tokens:
1425 newline = row < start[0] - first_row
1426 if newline:
1427 row = start[0] - first_row
1428 newline = (not last_token_multiline and
1429 token_type not in (tokenize.NL, tokenize.NEWLINE))
1430
1431 if newline:
1432 # This is where the differences start. Instead of looking at
1433 # the line and determining whether the observed indent matches
1434 # our expectations, we decide which type of indentation is in
1435 # use at the given indent level, and return the offset. This
1436 # algorithm is susceptible to "carried errors", but should
1437 # through repeated runs eventually solve indentation for
1438 # multiline expressions.
1439
1440 if depth:
1441 for open_row in range(row - 1, -1, -1):
1442 if parens[open_row]:
1443 break
1444 else:
1445 open_row = 0
1446
1447 # That's all we get to work with. This code attempts to
1448 # "reverse" the below logic, and place into the valid indents
1449 # list
1450 vi = []
1451 add_second_chances = False
1452 if token_type == tokenize.OP and text in ']})':
1453 # this line starts with a closing bracket, so it needs to
1454 # be closed at the same indent as the opening one.
1455 if indent[depth]:
1456 # hanging indent
1457 vi.append(indent[depth])
1458 else:
1459 # visual indent
1460 vi.append(indent_level + rel_indent[open_row])
1461 elif depth and indent[depth]:
1462 # visual indent was previously confirmed.
1463 vi.append(indent[depth])
1464 add_second_chances = True
1465 elif depth and True in indent_chances.values():
1466 # visual indent happened before, so stick to
1467 # visual indent this time.
1468 if depth > 1 and indent[depth - 1]:
1469 vi.append(indent[depth - 1])
1470 else:
1471 # stupid fallback
1472 vi.append(indent_level + 4)
1473 add_second_chances = True
1474 elif not depth:
1475 vi.append(indent_level + 4)
1476 else:
1477 # must be in hanging indent
1478 hang = rel_indent[open_row] + 4
1479 vi.append(indent_level + hang)
1480
1481 # about the best we can do without look-ahead
1482 if (indent_next and vi[0] == indent_level + 4 and
1483 nrows == row + 1):
1484 vi[0] += 4
1485
1486 if add_second_chances:
1487 # visual indenters like to line things up.
1488 min_indent = vi[0]
1489 for col, what in indent_chances.items():
1490 if col > min_indent and (
1491 what is True or
1492 (what == str and token_type == tokenize.STRING) or
1493 (what == text and token_type == tokenize.OP)
1494 ):
1495 vi.append(col)
1496 vi = sorted(vi)
1497
1498 valid_indents[row] = vi
1499
1500 # Returning to original continuation_line_indentation() from
1501 # pep8.
1502 visual_indent = indent_chances.get(start[1])
1503 last_indent = start
1504 rel_indent[row] = pep8.expand_indent(line) - indent_level
1505 hang = rel_indent[row] - rel_indent[open_row]
1506
1507 if token_type == tokenize.OP and text in ']})':
1508 pass
1509 elif visual_indent is True:
1510 if not indent[depth]:
1511 indent[depth] = start[1]
1512
1513 # line altered: comments shouldn't define a visual indent
1514 if parens[row] and not indent[depth] and token_type not in (
1515 tokenize.NL, tokenize.COMMENT
1516 ):
1517 indent[depth] = start[1]
1518 indent_chances[start[1]] = True
1519 elif token_type == tokenize.STRING or text in (
1520 'u', 'ur', 'b', 'br'
1521 ):
1522 indent_chances[start[1]] = str
1523
1524 if token_type == tokenize.OP:
1525 if text in '([{':
1526 depth += 1
1527 indent.append(0)
1528 parens[row] += 1
1529 elif text in ')]}' and depth > 0:
1530 prev_indent = indent.pop() or last_indent[1]
1531 for d in range(depth):
1532 if indent[d] > prev_indent:
1533 indent[d] = 0
1534 for ind in list(indent_chances):
1535 if ind >= prev_indent:
1536 del indent_chances[ind]
1537 depth -= 1
1538 if depth and indent[depth]: # modified
1539 indent_chances[indent[depth]] = True
1540 for idx in range(row, -1, -1):
1541 if parens[idx]:
1542 parens[idx] -= 1
1543 break
1544 assert len(indent) == depth + 1
1545 if start[1] not in indent_chances:
1546 indent_chances[start[1]] = text
1547
1548 last_token_multiline = (start[0] != end[0])
1549
1550 return valid_indents
1551
1552
1553 class Pep8LineShortener(object):
1554 """
1555 Class used to shorten lines to a given maximum of characters.
1556 """
1557 def __init__(self, curLine, prevLine, nextLine, maxLength=79, eol="\n",
1558 indentWord=" ", isDocString=False):
1559 """
1560 Constructor
1561
1562 @param curLine text to work on (string)
1563 @param prevLine line before the text to work on (string)
1564 @param nextLine line after the text to work on (string)
1565 @keyparam maxLength maximum allowed line length (integer)
1566 @keyparam eol eond-of-line marker (string)
1567 @keyparam indentWord string used for indentation (string)
1568 @keyparam isDocString flag indicating that the line belongs to
1569 a documentation string (boolean)
1570 """
1571 self.__text = curLine
1572 self.__prevText = prevLine
1573 self.__nextText = nextLine
1574 self.__maxLength = maxLength
1575 self.__eol = eol
1576 self.__indentWord = indentWord
1577 self.__isDocString = isDocString
1578
1579 def shorten(self):
1580 """
1581 Public method to shorten the line wrapped by the class instance.
1582
1583 @return tuple of a flag indicating successful shortening, the
1584 shortened line and the changed next line (boolean, string, string)
1585 """
1586 # 1. check for comment
1587 if self.__text.lstrip().startswith('#'):
1588 lastComment = True
1589 if self.__nextText.lstrip().startswith('#'):
1590 lastComment = False
1591
1592 # Wrap commented lines.
1593 newText = self.__shortenComment(lastComment)
1594 if newText == self.__text:
1595 return False, "", ""
1596 else:
1597 return True, newText, ""
1598 elif '#' in self.__text:
1599 pos = self.__text.rfind("#")
1600 newText = self.__text[:pos].rstrip() + self.__eol + \
1601 self.__getIndent(self.__text) + self.__text[pos:]
1602 if newText == self.__text:
1603 return False, "", ""
1604 else:
1605 return True, newText, ""
1606
1607 # Do multi line doc strings
1608 if self.__isDocString:
1609 source = self.__text.rstrip()
1610 blank = source.rfind(" ")
1611 while blank > self.__maxLength and blank != -1:
1612 blank = source.rfind(" ", 0, blank)
1613 if blank == -1:
1614 # Cannot break
1615 return False, "", ""
1616 else:
1617 first = self.__text[:blank]
1618 second = self.__text[blank:].lstrip()
1619 if self.__nextText.strip():
1620 if self.__nextText.lstrip().startswith("@"):
1621 # eric doc comment
1622 # create a new line and indent it
1623 newText = first + self.__eol + \
1624 self.__getIndent(first) + self.__indentWord + \
1625 second
1626 newNext = ""
1627 else:
1628 newText = first + self.__eol
1629 newNext = self.__getIndent(self.__nextText) + \
1630 second.rstrip() + " " + self.__nextText.lstrip()
1631 else:
1632 # empty line, add a new line
1633 newText = first + self.__eol + self.__getIndent(first) + \
1634 second
1635 newNext = ""
1636 return True, newText, newNext
1637
1638 indent = self.__getIndent(self.__text)
1639 source = self.__text[len(indent):]
1640 assert source.lstrip() == source
1641 sio = io.StringIO(source)
1642
1643 # Check for multi line string.
1644 try:
1645 tokens = list(tokenize.generate_tokens(sio.readline))
1646 except (SyntaxError, tokenize.TokenError):
1647 if source.rstrip().endswith("\\"):
1648 # just join the continuation line and let the next run
1649 # handle it once it tokenizes ok
1650 newText = indent + source.rstrip()[:-1].rstrip() + " " + \
1651 self.__nextText.lstrip()
1652 if indent:
1653 newNext = indent
1654 else:
1655 newNext = " "
1656 return True, newText, newNext
1657 else:
1658 multilineCandidate = self.__breakMultiline()
1659 if multilineCandidate:
1660 return True, multilineCandidate[0], multilineCandidate[1]
1661 else:
1662 return False, "", ""
1663
1664 # Handle statements by putting the right hand side on a line by itself.
1665 # This should let the next pass shorten it.
1666 if source.startswith('return '):
1667 newText = (
1668 indent +
1669 'return (' +
1670 self.__eol +
1671 indent + self.__indentWord + re.sub('^return ', '', source) +
1672 indent + ')' + self.__eol
1673 )
1674 return True, newText, ""
1675
1676 candidates = self.__shortenLine(tokens, source, indent)
1677 if candidates:
1678 candidates = list(sorted(
1679 set(candidates).union([self.__text]),
1680 key=lambda x: self.__lineShorteningRank(x)))
1681 return True, candidates[0], ""
1682
1683 source = self.__text
1684 rs = source.rstrip()
1685 if rs.endswith(("'", '"')) and " " in source:
1686 if rs.endswith(('"""', "'''")):
1687 quote = rs[-3:]
1688 else:
1689 quote = rs[-1]
1690 blank = source.rfind(" ")
1691 maxLen = self.__maxLength - 2 - len(quote)
1692 while blank > maxLen and blank != -1:
1693 blank = source.rfind(" ", 0, blank)
1694 if blank != -1:
1695 if source[blank + 1:].startswith(quote):
1696 first = source[:maxLen]
1697 second = source[maxLen:]
1698 else:
1699 first = source[:blank]
1700 second = source[blank + 1:]
1701 return (True,
1702 first + quote + " \\" + self.__eol +
1703 indent + self.__indentWord + quote + second,
1704 "")
1705 else:
1706 # Cannot break
1707 return False, "", ""
1708
1709 return False, "", ""
1710
1711 def __shortenComment(self, isLast):
1712 """
1713 Private method to shorten a comment line.
1714
1715 @param isLast flag indicating, that the line is the last comment line
470 (boolean) 1716 (boolean)
471 @return flag indicating an applied fix (boolean) and a message for 1717 @return shortened comment line (string)
472 the fix (string) 1718 """
473 """ 1719 if len(self.__text) <= self.__maxLength:
474 line = line - 1 1720 return self.__text
475 pos = pos - 1 1721
476 if self.__source[line][pos] == " ": 1722 newText = self.__text.rstrip()
477 count = 1 1723
478 else: 1724 # PEP 8 recommends 72 characters for comment text.
479 count = 2 1725 indentation = self.__getIndent(newText) + '# '
480 self.__source[line] = self.__source[line][:pos] + \ 1726 maxLength = min(self.__maxLength,
481 count * " " + \ 1727 len(indentation) + 72)
482 self.__source[line][pos:] 1728
483 return (True, self.trUtf8("Missing whitespace added.")) 1729 MIN_CHARACTER_REPEAT = 5
484 1730 if (len(newText) - len(newText.rstrip(newText[-1])) >= \
485 def __fixWhitespaceAfterInline(self, code, line, pos, apply=False): 1731 MIN_CHARACTER_REPEAT and
486 """ 1732 not newText[-1].isalnum()):
487 Private method to fix whitespace after inline comment. 1733 # Trim comments that end with things like ---------
488 1734 return newText[:maxLength] + self.__eol
489 @param code code of the issue (string) 1735 elif isLast and re.match(r"\s*#+\s*\w+", newText):
490 @param line line number of the issue (integer) 1736 import textwrap
491 @param pos position inside line (integer) 1737 splitLines = textwrap.wrap(newText.lstrip(" \t#"),
492 @keyparam apply flag indicating, that the fix should be applied 1738 initial_indent=indentation,
493 (boolean) 1739 subsequent_indent=indentation,
494 @return flag indicating an applied fix (boolean) and a message for 1740 width=maxLength,
495 the fix (string) 1741 break_long_words=False,
496 """ 1742 break_on_hyphens=False)
497 line = line - 1 1743 return self.__eol.join(splitLines) + self.__eol
498 if self.__source[line][pos] == " ": 1744 else:
499 pos += 1 1745 return newText + self.__eol
500 while self.__source[line][pos] == " ": 1746
501 self.__source[line] = self.__source[line][:pos] + \ 1747 def __breakMultiline(self):
502 self.__source[line][pos + 1:] 1748 """
503 else: 1749 Private method to break multi line strings.
504 self.__source[line] = self.__source[line][:pos] + \ 1750
505 " " + \ 1751 @return tuple of the shortened line and the changed next line
506 self.__source[line][pos:] 1752 (string, string)
507 return (True, self.trUtf8( 1753 """
508 "Whitespace after inline comment sign corrected.")) 1754 indentation = self.__getIndent(self.__text)
1755
1756 # Handle special case.
1757 for symbol in '([{':
1758 # Only valid if symbol is not on a line by itself.
1759 if (
1760 symbol in self.__text and
1761 self.__text.strip() != symbol and
1762 self.__text.rstrip().endswith((',', '%'))
1763 ):
1764 index = 1 + self.__text.find(symbol)
1765
1766 if index <= len(self.__indentWord) + len(indentation):
1767 continue
1768
1769 if self.__isProbablyInsideStringOrComment(
1770 self.__text, index - 1):
1771 continue
1772
1773 return (self.__text[:index].rstrip() + self.__eol +
1774 indentation + self.__indentWord +
1775 self.__text[index:].lstrip(), "")
1776
1777 newText = self.__text
1778 newNext = self.__nextText
1779 blank = newText.rfind(" ")
1780 while blank > self.__maxLength and blank != -1:
1781 blank = newText.rfind(" ", 0, blank)
1782 if blank != -1:
1783 first = self.__text[:blank]
1784 second = self.__text[blank:].strip()
1785 if newNext.strip():
1786 newText = first + self.__eol
1787 newNext = self.__getIndent(newNext) + \
1788 second + " " + newNext.lstrip()
1789 else:
1790 # empty line, add a new line
1791 newText = first + self.__eol
1792 newNext = self.__getIndent(newNext) + \
1793 second + self.__eol + newNext.lstrip()
1794 return newText, newNext
1795
1796 def __isProbablyInsideStringOrComment(self, line, index):
1797 """
1798 Private method to check, if the given string might be inside a string
1799 or comment.
1800
1801 @param line line to check (string)
1802 @param index position inside line to check (integer)
1803 @return flag indicating the possibility of being inside a string
1804 or comment
1805 """
1806 # Check against being in a string.
1807 for quote in ['"', "'"]:
1808 pos = line.find(quote)
1809 if pos != -1 and pos <= index:
1810 return True
1811
1812 # Check against being in a comment.
1813 pos = line.find('#')
1814 if pos != -1 and pos <= index:
1815 return True
1816
1817 return False
1818
1819 def __shortenLine(self, tokens, source, indent):
1820 """
1821 Private method to shorten a line of code at an operator.
1822
1823 @param tokens tokens of the line as generated by tokenize
1824 (list of token)
1825 @param source code string to work at (string)
1826 @param indent indentation string of the code line (string)
1827 @return list of candidates (list of string)
1828 """
1829 candidates = []
1830
1831 for tkn in tokens:
1832 tokenType = tkn[0]
1833 tokenString = tkn[1]
1834
1835 if (
1836 tokenType == tokenize.COMMENT and
1837 not self.__prevText.rstrip().endswith('\\')
1838 ):
1839 # Move inline comments to previous line.
1840 offset = tkn[2][1]
1841 first = source[:offset]
1842 second = source[offset:]
1843 candidates.append(indent + second.strip() + self.__eol +
1844 indent + first.strip() + self.__eol)
1845 elif tokenType == tokenize.OP and tokenString != '=':
1846 # Don't break on '=' after keyword as this violates PEP 8.
1847
1848 assert tokenType != tokenize.INDENT
1849
1850 offset = tkn[2][1] + 1
1851 first = source[:offset]
1852
1853 secondIndent = indent
1854 if first.rstrip().endswith('('):
1855 secondIndent += self.__indentWord
1856 elif '(' in first:
1857 secondIndent += ' ' * (1 + first.find('('))
1858 else:
1859 secondIndent += self.__indentWord
1860
1861 second = (secondIndent + source[offset:].lstrip())
1862 if not second.strip():
1863 continue
1864
1865 # Do not begin a line with a comma
1866 if second.lstrip().startswith(','):
1867 continue
1868
1869 # Do end a line with a dot
1870 if first.rstrip().endswith('.'):
1871 continue
1872
1873 if tokenString in '+-*/':
1874 newText = first + ' \\' + self.__eol + second
1875 else:
1876 newText = first + self.__eol + second
1877
1878 # Only fix if syntax is okay.
1879 if self.__checkSyntax(self.__normalizeMultiline(newText)):
1880 candidates.append(indent + newText)
1881
1882 return candidates
1883
1884 def __normalizeMultiline(self, text):
1885 """
1886 Private method to remove multiline-related code that will cause syntax
1887 error.
1888
1889 @param line code line to work on (string)
1890 @return normalized code line (string)
1891 """
1892 for quote in '\'"':
1893 dictPattern = r"^{q}[^{q}]*{q} *: *".format(q=quote)
1894 if re.match(dictPattern, text):
1895 if not text.strip().endswith('}'):
1896 text += '}'
1897 return '{' + text
1898
1899 if text.startswith('def ') and text.rstrip().endswith(':'):
1900 # Do not allow ':' to be alone. That is invalid.
1901 splitText = [item.strip() for item in text.split(self.__eol)]
1902 if ':' not in splitText and 'def' not in splitText:
1903 return text[len('def'):].strip().rstrip(':')
1904
1905 return text
1906
1907 def __lineShorteningRank(self, candidate):
1908 """
1909 Private method to rank a candidate.
1910
1911 @param candidate candidate line to rank (string)
1912 @return rank of the candidate (integer)
1913 """
1914 rank = 0
1915 if candidate.strip():
1916 lines = candidate.split(self.__eol)
1917
1918 offset = 0
1919 if lines[0].rstrip()[-1] not in '([{':
1920 for symbol in '([{':
1921 offset = max(offset, 1 + lines[0].find(symbol))
1922
1923 maxLength = max([offset + len(x.strip()) for x in lines])
1924 rank += maxLength
1925 rank += len(lines)
1926
1927 badStartingSymbol = {
1928 '(': ')',
1929 '[': ']',
1930 '{': '}'}.get(lines[0][-1], None)
1931
1932 if len(lines) > 1:
1933 if (badStartingSymbol and
1934 lines[1].lstrip().startswith(badStartingSymbol)):
1935 rank += 20
1936
1937 if re.match(r".*[+\-\*/] \($", lines[0]):
1938 # "1 * (\n" is ugly as hell.
1939 rank += 100
1940
1941 for currentLine in lines:
1942 for badStart in ['.', '%', '+', '-', '/']:
1943 if currentLine.startswith(badStart):
1944 rank += 100
1945
1946 for ending in '([{':
1947 # Avoid lonely opening. They result in longer lines.
1948 if (currentLine.endswith(ending) and
1949 len(currentLine.strip()) <= \
1950 len(self.__indentWord)):
1951 rank += 100
1952
1953 if currentLine.endswith('%'):
1954 rank -= 20
1955
1956 # Try to break list comprehensions at the "for".
1957 if currentLine.lstrip().startswith('for'):
1958 rank -= 50
1959
1960 rank += 10 * self.__countUnbalancedBrackets(currentLine)
1961 else:
1962 rank = 100000
1963
1964 return max(0, rank)
1965
1966 def __countUnbalancedBrackets(self, line):
1967 """
1968 Private method to determine the number of unmatched open/close
1969 brackets.
1970
1971 @param line line to work at (string)
1972 @return number of unmatched open/close brackets (integer)
1973 """
1974 count = 0
1975 for opening, closing in ['()', '[]', '{}']:
1976 count += abs(line.count(opening) - line.count(closing))
1977
1978 return count
1979
1980 def __getIndent(self, line):
1981 """
1982 Private method to get the indentation string.
1983
1984 @param line line to determine the indentation string from (string)
1985 @return indentation string (string)
1986 """
1987 # copied from Pep8Fixer
1988 return line.replace(line.lstrip(), "")
1989
1990 def __checkSyntax(self, code):
1991 """
1992 Private method to check the syntax of the given code fragment.
1993
1994 @param code code fragment to check (string)
1995 @return flag indicating syntax is ok (boolean)
1996 """
1997 code = code.replace("\r\n", "\n").replace("\r", "\n")
1998 try:
1999 return compile(code, '<string>', 'exec')
2000 except (SyntaxError, TypeError, UnicodeDecodeError):
2001 return False

eric ide

mercurial