Plugins/CheckerPlugins/CodeStyleChecker/CodeStyleFixer.py

changeset 2982
556adfe76ba7
parent 2981
0c5205aa7a5a
child 3057
10516539f238
child 3160
209a07d7e401
equal deleted inserted replaced
2981:0c5205aa7a5a 2982:556adfe76ba7
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2011 - 2013 Detlev Offenbach <detlev@die-offenbachs.de>
4 #
5
6 """
7 Module implementing a class to fix certain code style issues.
8 """
9
10 import os
11 import re
12 import tokenize
13 import io
14
15 from PyQt4.QtCore import QObject
16
17 from E5Gui import E5MessageBox
18
19 from . import pep8
20
21 import Utilities
22
23 FixableCodeStyleIssues = [
24 "D111", "D112", "D113", "D121", "D131", "D141",
25 "D142", "D143", "D144", "D145",
26 "D221", "D222", "D231", "D242", "D243", "D244",
27 "D245", "D246", "D247",
28 "E101", "E111", "E121", "E122", "E123", "E124",
29 "E125", "E126", "E127", "E128", "E133", "E201",
30 "E202", "E203", "E211", "E221", "E222", "E223",
31 "E224", "E225", "E226", "E227", "E228", "E231",
32 "E241", "E242", "E251", "E261", "E262", "E271",
33 "E272", "E273", "E274", "E301", "E302", "E303",
34 "E304", "E401", "E501", "E502", "E701", "E702",
35 "E703", "E711", "E712",
36 "N804", "N805", "N806",
37 "W191", "W291", "W292", "W293", "W391", "W603",
38 ]
39
40
41 class CodeStyleFixer(QObject):
42 """
43 Class implementing a fixer for certain code style issues.
44 """
45 def __init__(self, project, filename, sourceLines, fixCodes, noFixCodes,
46 maxLineLength, inPlace):
47 """
48 Constructor
49
50 @param project reference to the project object (Project)
51 @param filename name of the file to be fixed (string)
52 @param sourceLines list of source lines including eol marker
53 (list of string)
54 @param fixCodes list of codes to be fixed as a comma separated
55 string (string)
56 @param noFixCodes list of codes not to be fixed as a comma
57 separated string (string)
58 @param maxLineLength maximum allowed line length (integer)
59 @param inPlace flag indicating to modify the file in place (boolean)
60 """
61 super().__init__()
62
63 self.__project = project
64 self.__filename = filename
65 self.__origName = ""
66 self.__source = sourceLines[:] # save a copy
67 self.__fixCodes = [c.strip() for c in fixCodes.split(",") if c.strip()]
68 self.__noFixCodes = [
69 c.strip() for c in noFixCodes.split(",") if c.strip()]
70 self.__maxLineLength = maxLineLength
71 self.fixed = 0
72
73 self.__reindenter = None
74 self.__eol = ""
75 self.__indentWord = self.__getIndentWord()
76
77 if not inPlace:
78 self.__origName = self.__filename
79 self.__filename = os.path.join(
80 os.path.dirname(self.__filename),
81 "fixed_" + os.path.basename(self.__filename))
82
83 self.__fixes = {
84 "D111": self.__fixD111,
85 "D112": self.__fixD112,
86 "D113": self.__fixD112,
87 "D121": self.__fixD121,
88 "D131": self.__fixD131,
89 "D141": self.__fixD141,
90 "D142": self.__fixD142,
91 "D143": self.__fixD143,
92 "D144": self.__fixD144,
93 "D145": self.__fixD145,
94 "D221": self.__fixD221,
95 "D222": self.__fixD221,
96 "D231": self.__fixD131,
97 "D242": self.__fixD242,
98 "D243": self.__fixD243,
99 "D244": self.__fixD242,
100 "D245": self.__fixD243,
101 "D246": self.__fixD144,
102 "D247": self.__fixD247,
103 "E101": self.__fixE101,
104 "E111": self.__fixE101,
105 "E121": self.__fixE121,
106 "E122": self.__fixE122,
107 "E123": self.__fixE123,
108 "E124": self.__fixE121,
109 "E125": self.__fixE125,
110 "E126": self.__fixE126,
111 "E127": self.__fixE127,
112 "E128": self.__fixE127,
113 "E133": self.__fixE126,
114 "E201": self.__fixE201,
115 "E202": self.__fixE201,
116 "E203": self.__fixE201,
117 "E211": self.__fixE201,
118 "E221": self.__fixE221,
119 "E222": self.__fixE221,
120 "E223": self.__fixE221,
121 "E224": self.__fixE221,
122 "E225": self.__fixE221,
123 "E226": self.__fixE221,
124 "E227": self.__fixE221,
125 "E228": self.__fixE221,
126 "E231": self.__fixE231,
127 "E241": self.__fixE221,
128 "E242": self.__fixE221,
129 "E251": self.__fixE251,
130 "E261": self.__fixE261,
131 "E262": self.__fixE261,
132 "E271": self.__fixE221,
133 "E272": self.__fixE221,
134 "E273": self.__fixE221,
135 "E274": self.__fixE221,
136 "E301": self.__fixE301,
137 "E302": self.__fixE302,
138 "E303": self.__fixE303,
139 "E304": self.__fixE304,
140 "E401": self.__fixE401,
141 "E501": self.__fixE501,
142 "E502": self.__fixE502,
143 "E701": self.__fixE701,
144 "E702": self.__fixE702,
145 "E703": self.__fixE702,
146 "E711": self.__fixE711,
147 "E712": self.__fixE711,
148 "N804": self.__fixN804,
149 "N805": self.__fixN804,
150 "N806": self.__fixN806,
151 "W191": self.__fixE101,
152 "W291": self.__fixW291,
153 "W292": self.__fixW292,
154 "W293": self.__fixW291,
155 "W391": self.__fixW391,
156 "W603": self.__fixW603,
157 }
158 self.__modified = False
159 self.__stackLogical = [] # these need to be fixed before the file
160 # is saved but after all other inline
161 # fixes. These work with logical lines.
162 self.__stack = [] # these need to be fixed before the file
163 # is saved but after all inline fixes
164
165 self.__multiLineNumbers = None
166 self.__docLineNumbers = None
167
168 self.__lastID = 0
169
170 def saveFile(self, encoding):
171 """
172 Public method to save the modified file.
173
174 @param encoding encoding of the source file (string)
175 @return flag indicating success (boolean)
176 """
177 if not self.__modified:
178 # no need to write
179 return True
180
181 txt = "".join(self.__source)
182 try:
183 Utilities.writeEncodedFile(self.__filename, txt, encoding)
184 except (IOError, Utilities.CodingError, UnicodeError) as err:
185 E5MessageBox.critical(
186 self,
187 self.trUtf8("Fix Code Style Issues"),
188 self.trUtf8(
189 """<p>Could not save the file <b>{0}</b>."""
190 """ Skipping it.</p><p>Reason: {1}</p>""")
191 .format(self.__filename, str(err))
192 )
193 return False
194
195 return True
196
197 def __codeMatch(self, code):
198 """
199 Private method to check, if the code should be fixed.
200
201 @param code to check (string)
202 @return flag indicating it should be fixed (boolean)
203 """
204 def mutualStartswith(a, b):
205 """
206 Local helper method to compare the beginnings of two strings
207 against each other.
208
209 @return flag indicating that one string starts with the other
210 (boolean)
211 """
212 return b.startswith(a) or a.startswith(b)
213
214 if self.__noFixCodes:
215 for noFixCode in [c.strip() for c in self.__noFixCodes]:
216 if mutualStartswith(code.lower(), noFixCode.lower()):
217 return False
218
219 if self.__fixCodes:
220 for fixCode in [c.strip() for c in self.__fixCodes]:
221 if mutualStartswith(code.lower(), fixCode.lower()):
222 return True
223 return False
224
225 return True
226
227 def fixIssue(self, line, pos, message):
228 """
229 Public method to fix the fixable issues.
230
231 @param line line number of issue (integer)
232 @param pos character position of issue (integer)
233 @param message message text (string)
234 @return value indicating an applied/deferred fix (-1, 0, 1),
235 a message for the fix (string) and an ID for a deferred
236 fix (integer)
237 """
238 code = message.split(None, 1)[0].strip()
239
240 if line <= len(self.__source) and \
241 self.__codeMatch(code) and \
242 code in self.__fixes:
243 res = self.__fixes[code](code, line, pos)
244 if res[0] == 1:
245 self.__modified = True
246 self.fixed += 1
247 else:
248 res = (0, "", 0)
249
250 return res
251
252 def finalize(self):
253 """
254 Public method to apply all deferred fixes.
255
256 @return dictionary containing the fix results
257 """
258 results = {}
259
260 # step 1: do fixes operating on logical lines first
261 for id_, code, line, pos in self.__stackLogical:
262 res, msg, _ = self.__fixes[code](code, line, pos, apply=True)
263 if res == 1:
264 self.__modified = True
265 self.fixed += 1
266 results[id_] = (res, msg)
267
268 # step 2: do fixes that change the number of lines
269 for id_, code, line, pos in reversed(self.__stack):
270 res, msg, _ = self.__fixes[code](code, line, pos, apply=True)
271 if res == 1:
272 self.__modified = True
273 self.fixed += 1
274 results[id_] = (res, msg)
275
276 return results
277
278 def __getID(self):
279 """
280 Private method to get the ID for a deferred fix.
281
282 @return ID for a deferred fix (integer)
283 """
284 self.__lastID += 1
285 return self.__lastID
286
287 def __getEol(self):
288 """
289 Private method to get the applicable eol string.
290
291 @return eol string (string)
292 """
293 if not self.__eol:
294 if self.__origName:
295 fn = self.__origName
296 else:
297 fn = self.__filename
298
299 if self.__project.isOpen() and self.__project.isProjectFile(fn):
300 self.__eol = self.__project.getEolString()
301 else:
302 self.__eol = Utilities.linesep()
303 return self.__eol
304
305 def __findLogical(self):
306 """
307 Private method to extract the index of all the starts and ends of
308 lines.
309
310 @return tuple containing two lists of integer with start and end tuples
311 of lines
312 """
313 logical_start = []
314 logical_end = []
315 last_newline = True
316 sio = io.StringIO("".join(self.__source))
317 parens = 0
318 for t in tokenize.generate_tokens(sio.readline):
319 if t[0] in [tokenize.COMMENT, tokenize.DEDENT,
320 tokenize.INDENT, tokenize.NL,
321 tokenize.ENDMARKER]:
322 continue
323 if not parens and t[0] in [tokenize.NEWLINE, tokenize.SEMI]:
324 last_newline = True
325 logical_end.append((t[3][0] - 1, t[2][1]))
326 continue
327 if last_newline and not parens:
328 logical_start.append((t[2][0] - 1, t[2][1]))
329 last_newline = False
330 if t[0] == tokenize.OP:
331 if t[1] in '([{':
332 parens += 1
333 elif t[1] in '}])':
334 parens -= 1
335 return logical_start, logical_end
336
337 def __getLogical(self, line, pos):
338 """
339 Private method to get the logical line corresponding to the given
340 position.
341
342 @param line line number of the issue (integer)
343 @param pos position inside line (integer)
344 @return tuple of a tuple of two integers giving the start of the
345 logical line, another tuple of two integers giving the end
346 of the logical line and a list of strings with the original
347 source lines
348 """
349 try:
350 (logical_start, logical_end) = self.__findLogical()
351 except (SyntaxError, tokenize.TokenError):
352 return None
353
354 line = line - 1
355 ls = None
356 le = None
357 for i in range(0, len(logical_start)):
358 x = logical_end[i]
359 if x[0] > line or (x[0] == line and x[1] > pos):
360 le = x
361 ls = logical_start[i]
362 break
363 if ls is None:
364 return None
365
366 original = self.__source[ls[0]:le[0] + 1]
367 return ls, le, original
368
369 def __getIndentWord(self):
370 """
371 Private method to determine the indentation type.
372
373 @return string to be used for an indentation (string)
374 """
375 sio = io.StringIO("".join(self.__source))
376 indentWord = " " # default in case of failure
377 try:
378 for token in tokenize.generate_tokens(sio.readline):
379 if token[0] == tokenize.INDENT:
380 indentWord = token[1]
381 break
382 except (SyntaxError, tokenize.TokenError):
383 pass
384 return indentWord
385
386 def __getIndent(self, line):
387 """
388 Private method to get the indentation string.
389
390 @param line line to determine the indentation string from (string)
391 @return indentation string (string)
392 """
393 return line.replace(line.lstrip(), "")
394
395 def __multilineStringLines(self):
396 """
397 Private method to determine the line numbers that are within multi line
398 strings and these which are part of a documentation string.
399
400 @return tuple of a set of line numbers belonging to a multi line
401 string and a set of line numbers belonging to a multi line
402 documentation string (tuple of two set of integer)
403 """
404 if self.__multiLineNumbers is None:
405 source = "".join(self.__source)
406 sio = io.StringIO(source)
407 self.__multiLineNumbers = set()
408 self.__docLineNumbers = set()
409 previousTokenType = ''
410 try:
411 for t in tokenize.generate_tokens(sio.readline):
412 tokenType = t[0]
413 startRow = t[2][0]
414 endRow = t[3][0]
415
416 if (tokenType == tokenize.STRING and startRow != endRow):
417 if previousTokenType != tokenize.INDENT:
418 self.__multiLineNumbers |= set(
419 range(startRow, 1 + endRow))
420 else:
421 self.__docLineNumbers |= set(
422 range(startRow, 1 + endRow))
423
424 previousTokenType = tokenType
425 except (SyntaxError, tokenize.TokenError):
426 pass
427
428 return self.__multiLineNumbers, self.__docLineNumbers
429
430 def __fixReindent(self, line, pos, logical):
431 """
432 Private method to fix a badly indented line.
433
434 This is done by adding or removing from its initial indent only.
435
436 @param line line number of the issue (integer)
437 @param pos position inside line (integer)
438 @param logical logical line structure
439 @return flag indicating a change was done (boolean)
440 """
441 assert logical
442 ls, _, original = logical
443
444 rewrapper = IndentationWrapper(original)
445 valid_indents = rewrapper.pep8Expected()
446 if not rewrapper.rel_indent:
447 return False
448
449 if line > ls[0]:
450 # got a valid continuation line number
451 row = line - ls[0] - 1
452 # always pick the first option for this
453 valid = valid_indents[row]
454 got = rewrapper.rel_indent[row]
455 else:
456 return False
457
458 line1 = ls[0] + row
459 # always pick the expected indent, for now.
460 indent_to = valid[0]
461
462 if got != indent_to:
463 orig_line = self.__source[line1]
464 new_line = ' ' * (indent_to) + orig_line.lstrip()
465 if new_line == orig_line:
466 return False
467 else:
468 self.__source[line1] = new_line
469 return True
470 else:
471 return False
472
473 def __fixWhitespace(self, line, offset, replacement):
474 """
475 Private method to correct whitespace at the given offset.
476
477 @param line line to be corrected (string)
478 @param offset offset within line (integer)
479 @param replacement replacement string (string)
480 @return corrected line
481 """
482 left = line[:offset].rstrip(" \t")
483 right = line[offset:].lstrip(" \t")
484 if right.startswith("#"):
485 return line
486 else:
487 return left + replacement + right
488
489 def __fixD111(self, code, line, pos):
490 """
491 Private method to fix docstring enclosed in wrong quotes.
492
493 Codes: D111
494
495 @param code code of the issue (string)
496 @param line line number of the issue (integer)
497 @param pos position inside line (integer)
498 @return value indicating an applied/deferred fix (-1, 0, 1),
499 a message for the fix (string) and an ID for a deferred
500 fix (integer)
501 """
502 line = line - 1
503 left, right = self.__source[line].split("'''", 1)
504 self.__source[line] = left + '"""' + right
505 while line < len(self.__source):
506 if self.__source[line].rstrip().endswith("'''"):
507 left, right = self.__source[line].rsplit("'''", 1)
508 self.__source[line] = left + '"""' + right
509 break
510 line += 1
511
512 return (
513 1,
514 self.trUtf8(
515 "Triple single quotes converted to triple double quotes."),
516 0)
517
518 def __fixD112(self, code, line, pos):
519 """
520 Private method to fix docstring 'r' or 'u' in leading quotes.
521
522 Codes: D112, D113
523
524 @param code code of the issue (string)
525 @param line line number of the issue (integer)
526 @param pos position inside line (integer)
527 @return value indicating an applied/deferred fix (-1, 0, 1),
528 a message for the fix (string) and an ID for a deferred
529 fix (integer)
530 """
531 line = line - 1
532 if code == "D112":
533 insertChar = "r"
534 elif code == "D113":
535 insertChar = "u"
536 else:
537 return (0, "", 0)
538
539 newText = self.__getIndent(self.__source[line]) + \
540 insertChar + self.__source[line].lstrip()
541 self.__source[line] = newText
542 return (
543 1,
544 self.trUtf8('Introductory quotes corrected to be {0}"""')
545 .format(insertChar),
546 0)
547
548 def __fixD121(self, code, line, pos, apply=False):
549 """
550 Private method to fix a single line docstring on multiple lines.
551
552 Codes: D121
553
554 @param code code of the issue (string)
555 @param line line number of the issue (integer)
556 @param pos position inside line (integer)
557 @keyparam apply flag indicating, that the fix should be applied
558 (boolean)
559 @return value indicating an applied/deferred fix (-1, 0, 1),
560 a message for the fix (string) and an ID for a deferred
561 fix (integer)
562 """
563 if apply:
564 line = line - 1
565 if not self.__source[line].lstrip().startswith(
566 ('"""', 'r"""', 'u"""')):
567 # only correctly formatted docstrings will be fixed
568 return (0, "", 0)
569
570 docstring = self.__source[line].rstrip() + \
571 self.__source[line + 1].strip()
572 if docstring.endswith('"""'):
573 docstring += self.__getEol()
574 else:
575 docstring += self.__source[line + 2].lstrip()
576 self.__source[line + 2] = ""
577
578 self.__source[line] = docstring
579 self.__source[line + 1] = ""
580 return (
581 1,
582 self.trUtf8("Single line docstring put on one line."),
583 0)
584 else:
585 id = self.__getID()
586 self.__stack.append((id, code, line, pos))
587 return (-1, "", id)
588
589 def __fixD131(self, code, line, pos):
590 """
591 Private method to fix a docstring summary not ending with a
592 period.
593
594 Codes: D131
595
596 @param code code of the issue (string)
597 @param line line number of the issue (integer)
598 @param pos position inside line (integer)
599 @return value indicating an applied/deferred fix (-1, 0, 1),
600 a message for the fix (string) and an ID for a deferred
601 fix (integer)
602 """
603 line = line - 1
604 newText = ""
605 if self.__source[line].rstrip().endswith(('"""', "'''")) and \
606 self.__source[line].lstrip().startswith(('"""', 'r"""', 'u"""')):
607 # it is a one-liner
608 newText = self.__source[line].rstrip()[:-3].rstrip() + "." + \
609 self.__source[line].rstrip()[-3:] + self.__getEol()
610 else:
611 if line < len(self.__source) - 1 and \
612 (not self.__source[line + 1].strip() or
613 self.__source[line + 1].lstrip().startswith("@") or
614 (self.__source[line + 1].strip() in ('"""', "'''") and
615 not self.__source[line].lstrip().startswith("@"))):
616 newText = self.__source[line].rstrip() + "." + self.__getEol()
617
618 if newText:
619 self.__source[line] = newText
620 return (1, self.trUtf8("Period added to summary line."), 0)
621 else:
622 return (0, "", 0)
623
624 def __fixD141(self, code, line, pos, apply=False):
625 """
626 Private method to fix a function/method docstring preceded by a
627 blank line.
628
629 Codes: D141
630
631 @param code code of the issue (string)
632 @param line line number of the issue (integer)
633 @param pos position inside line (integer)
634 @keyparam apply flag indicating, that the fix should be applied
635 (boolean)
636 @return value indicating an applied/deferred fix (-1, 0, 1),
637 a message for the fix (string) and an ID for a deferred
638 fix (integer)
639 """
640 if apply:
641 line = line - 1
642 self.__source[line - 1] = ""
643 return (
644 1,
645 self.trUtf8(
646 "Blank line before function/method docstring removed."),
647 0)
648 else:
649 id = self.__getID()
650 self.__stack.append((id, code, line, pos))
651 return (-1, "", id)
652
653 def __fixD142(self, code, line, pos, apply=False):
654 """
655 Private method to fix a class docstring not preceded by a
656 blank line.
657
658 Codes: D142
659
660 @param code code of the issue (string)
661 @param line line number of the issue (integer)
662 @param pos position inside line (integer)
663 @keyparam apply flag indicating, that the fix should be applied
664 (boolean)
665 @return value indicating an applied/deferred fix (-1, 0, 1),
666 a message for the fix (string) and an ID for a deferred
667 fix (integer)
668 """
669 if apply:
670 line = line - 1
671 self.__source[line] = self.__getEol() + self.__source[line]
672 return (
673 1,
674 self.trUtf8("Blank line inserted before class docstring."),
675 0)
676 else:
677 id = self.__getID()
678 self.__stack.append((id, code, line, pos))
679 return (-1, "", id)
680
681 def __fixD143(self, code, line, pos, apply=False):
682 """
683 Private method to fix a class docstring not followed by a
684 blank line.
685
686 Codes: D143
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 @keyparam apply flag indicating, that the fix should be applied
692 (boolean)
693 @return value indicating an applied/deferred fix (-1, 0, 1),
694 a message for the fix (string) and an ID for a deferred
695 fix (integer)
696 """
697 if apply:
698 line = line - 1
699 self.__source[line] += self.__getEol()
700 return (
701 1,
702 self.trUtf8("Blank line inserted after class docstring."),
703 0)
704 else:
705 id = self.__getID()
706 self.__stack.append((id, code, line, pos))
707 return (-1, "", id)
708
709 def __fixD144(self, code, line, pos, apply=False):
710 """
711 Private method to fix a docstring summary not followed by a
712 blank line.
713
714 Codes: D144
715
716 @param code code of the issue (string)
717 @param line line number of the issue (integer)
718 @param pos position inside line (integer)
719 @keyparam apply flag indicating, that the fix should be applied
720 (boolean)
721 @return value indicating an applied/deferred fix (-1, 0, 1),
722 a message for the fix (string) and an ID for a deferred
723 fix (integer)
724 """
725 if apply:
726 line = line - 1
727 if not self.__source[line].rstrip().endswith("."):
728 # only correct summary lines can be fixed here
729 return (0, "", 0)
730
731 self.__source[line] += self.__getEol()
732 return (
733 1,
734 self.trUtf8("Blank line inserted after docstring summary."),
735 0)
736 else:
737 id = self.__getID()
738 self.__stack.append((id, code, line, pos))
739 return (-1, "", id)
740
741 def __fixD145(self, code, line, pos, apply=False):
742 """
743 Private method to fix the last paragraph of a multi-line docstring
744 not followed by a blank line.
745
746 Codes: D143
747
748 @param code code of the issue (string)
749 @param line line number of the issue (integer)
750 @param pos position inside line (integer)
751 @keyparam apply flag indicating, that the fix should be applied
752 (boolean)
753 @return value indicating an applied/deferred fix (-1, 0, 1),
754 a message for the fix (string) and an ID for a deferred
755 fix (integer)
756 """
757 if apply:
758 line = line - 1
759 self.__source[line] = self.__getEol() + self.__source[line]
760 return (
761 1,
762 self.trUtf8("Blank line inserted after last paragraph"
763 " of docstring."),
764 0)
765 else:
766 id = self.__getID()
767 self.__stack.append((id, code, line, pos))
768 return (-1, "", id)
769
770 def __fixD221(self, code, line, pos, apply=False):
771 """
772 Private method to fix leading and trailing quotes of docstring
773 not on separate lines.
774
775 Codes: D221, D222
776
777 @param code code of the issue (string)
778 @param line line number of the issue (integer)
779 @param pos position inside line (integer)
780 @keyparam apply flag indicating, that the fix should be applied
781 (boolean)
782 @return value indicating an applied/deferred fix (-1, 0, 1),
783 a message for the fix (string) and an ID for a deferred
784 fix (integer)
785 """
786 if apply:
787 line = line - 1
788 indent = self.__getIndent(self.__source[line])
789 source = self.__source[line].strip()
790 if code == "D221":
791 # leading
792 if source.startswith(("r", "u")):
793 first, second = source[:4], source[4:].strip()
794 else:
795 first, second = source[:3], source[3:].strip()
796 else:
797 # trailing
798 first, second = source[:-3].strip(), source[-3:]
799 newText = indent + first + self.__getEol() + \
800 indent + second + self.__getEol()
801 self.__source[line] = newText
802 if code == "D221":
803 msg = self.trUtf8("Leading quotes put on separate line.")
804 else:
805 msg = self.trUtf8("Trailing quotes put on separate line.")
806 return (1, msg, 0)
807 else:
808 id = self.__getID()
809 self.__stack.append((id, code, line, pos))
810 return (-1, "", id)
811
812 def __fixD242(self, code, line, pos, apply=False):
813 """
814 Private method to fix a class or function/method docstring preceded
815 by a blank line.
816
817 Codes: D242, D244
818
819 @param code code of the issue (string)
820 @param line line number of the issue (integer)
821 @param pos position inside line (integer)
822 @keyparam apply flag indicating, that the fix should be applied
823 (boolean)
824 @return value indicating an applied/deferred fix (-1, 0, 1),
825 a message for the fix (string) and an ID for a deferred
826 fix (integer)
827 """
828 if apply:
829 line = line - 1
830 self.__source[line - 1] = ""
831 if code == "D242":
832 msg = self.trUtf8("Blank line before class docstring removed.")
833 else:
834 msg = self.trUtf8(
835 "Blank line before function/method docstring removed.")
836 return (1, msg, 0)
837 else:
838 id = self.__getID()
839 self.__stack.append((id, code, line, pos))
840 return (-1, "", id)
841
842 def __fixD243(self, code, line, pos, apply=False):
843 """
844 Private method to fix a class or function/method docstring followed
845 by a blank line.
846
847 Codes: D243, D245
848
849 @param code code of the issue (string)
850 @param line line number of the issue (integer)
851 @param pos position inside line (integer)
852 @keyparam apply flag indicating, that the fix should be applied
853 (boolean)
854 @return value indicating an applied/deferred fix (-1, 0, 1),
855 a message for the fix (string) and an ID for a deferred
856 fix (integer)
857 """
858 if apply:
859 line = line - 1
860 self.__source[line + 1] = ""
861 if code == "D243":
862 msg = self.trUtf8("Blank line after class docstring removed.")
863 else:
864 msg = self.trUtf8(
865 "Blank line after function/method docstring removed.")
866 return (1, msg, 0)
867 else:
868 id = self.__getID()
869 self.__stack.append((id, code, line, pos))
870 return (-1, "", id)
871
872 def __fixD247(self, code, line, pos, apply=False):
873 """
874 Private method to fix a last paragraph of a docstring followed
875 by a blank line.
876
877 Codes: D247
878
879 @param code code of the issue (string)
880 @param line line number of the issue (integer)
881 @param pos position inside line (integer)
882 @keyparam apply flag indicating, that the fix should be applied
883 (boolean)
884 @return value indicating an applied/deferred fix (-1, 0, 1),
885 a message for the fix (string) and an ID for a deferred
886 fix (integer)
887 """
888 if apply:
889 line = line - 1
890 self.__source[line - 1] = ""
891 return (
892 1,
893 self.trUtf8("Blank line after last paragraph removed."),
894 0)
895 else:
896 id = self.__getID()
897 self.__stack.append((id, code, line, pos))
898 return (-1, "", id)
899
900 def __fixE101(self, code, line, pos):
901 """
902 Private method to fix obsolete tab usage and indentation errors.
903
904 Codes: E101, E111, W191
905
906 @param code code of the issue (string)
907 @param line line number of the issue (integer)
908 @param pos position inside line (integer)
909 @return value indicating an applied/deferred fix (-1, 0, 1),
910 a message for the fix (string) and an ID for a deferred
911 fix (integer)
912 """
913 if self.__reindenter is None:
914 self.__reindenter = Reindenter(self.__source)
915 self.__reindenter.run()
916 fixedLine = self.__reindenter.fixedLine(line - 1)
917 if fixedLine is not None and fixedLine != self.__source[line - 1]:
918 self.__source[line - 1] = fixedLine
919 if code in ["E101", "W191"]:
920 msg = self.trUtf8("Tab converted to 4 spaces.")
921 else:
922 msg = self.trUtf8(
923 "Indentation adjusted to be a multiple of four.")
924 return (1, msg, 0)
925 else:
926 return (0, "", 0)
927
928 def __fixE121(self, code, line, pos, apply=False):
929 """
930 Private method to fix the indentation of continuation lines and
931 closing brackets.
932
933 Codes: E121, E124
934
935 @param code code of the issue (string)
936 @param line line number of the issue (integer)
937 @param pos position inside line (integer)
938 @keyparam apply flag indicating, that the fix should be applied
939 (boolean)
940 @return value indicating an applied/deferred fix (-1, 0, 1),
941 a message for the fix (string) and an ID for a deferred
942 fix (integer)
943 """
944 if apply:
945 logical = self.__getLogical(line, pos)
946 if logical:
947 # Fix by adjusting initial indent level.
948 changed = self.__fixReindent(line, pos, logical)
949 if changed:
950 if code == "E121":
951 msg = self.trUtf8(
952 "Indentation of continuation line corrected.")
953 elif code == "E124":
954 msg = self.trUtf8(
955 "Indentation of closing bracket corrected.")
956 return (1, msg, 0)
957 return (0, "", 0)
958 else:
959 id = self.__getID()
960 self.__stackLogical.append((id, code, line, pos))
961 return (-1, "", id)
962
963 def __fixE122(self, code, line, pos, apply=False):
964 """
965 Private method to fix a missing indentation of continuation lines.
966
967 Codes: E122
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 @keyparam apply flag indicating, that the fix should be applied
973 (boolean)
974 @return value indicating an applied/deferred fix (-1, 0, 1),
975 a message for the fix (string) and an ID for a deferred
976 fix (integer)
977 """
978 if apply:
979 logical = self.__getLogical(line, pos)
980 if logical:
981 # Fix by adding an initial indent.
982 modified = self.__fixReindent(line, pos, logical)
983 if not modified:
984 # fall back to simple method
985 line = line - 1
986 text = self.__source[line]
987 indentation = self.__getIndent(text)
988 self.__source[line] = indentation + \
989 self.__indentWord + text.lstrip()
990 return (
991 1,
992 self.trUtf8(
993 "Missing indentation of continuation line corrected."),
994 0)
995 return (0, "", 0)
996 else:
997 id = self.__getID()
998 self.__stackLogical.append((id, code, line, pos))
999 return (-1, "", id)
1000
1001 def __fixE123(self, code, line, pos, apply=False):
1002 """
1003 Private method to fix the indentation of a closing bracket lines.
1004
1005 Codes: E123
1006
1007 @param code code of the issue (string)
1008 @param line line number of the issue (integer)
1009 @param pos position inside line (integer)
1010 @keyparam apply flag indicating, that the fix should be applied
1011 (boolean)
1012 @return value indicating an applied/deferred fix (-1, 0, 1),
1013 a message for the fix (string) and an ID for a deferred
1014 fix (integer)
1015 """
1016 if apply:
1017 logical = self.__getLogical(line, pos)
1018 if logical:
1019 # Fix by deleting whitespace to the correct level.
1020 logicalLines = logical[2]
1021 row = line - 1
1022 text = self.__source[row]
1023 newText = self.__getIndent(logicalLines[0]) + text.lstrip()
1024 if newText == text:
1025 # fall back to slower method
1026 changed = self.__fixReindent(line, pos, logical)
1027 else:
1028 self.__source[row] = newText
1029 changed = True
1030 if changed:
1031 return (1, self.trUtf8(
1032 "Closing bracket aligned to opening bracket."),
1033 0)
1034 return (0, "", 0)
1035 else:
1036 id = self.__getID()
1037 self.__stackLogical.append((id, code, line, pos))
1038 return (-1, "", id)
1039
1040 def __fixE125(self, code, line, pos, apply=False):
1041 """
1042 Private method to fix the indentation of continuation lines not
1043 distinguishable from next logical line.
1044
1045 Codes: E125
1046
1047 @param code code of the issue (string)
1048 @param line line number of the issue (integer)
1049 @param pos position inside line (integer)
1050 @keyparam apply flag indicating, that the fix should be applied
1051 (boolean)
1052 @return value indicating an applied/deferred fix (-1, 0, 1),
1053 a message for the fix (string) and an ID for a deferred
1054 fix (integer)
1055 """
1056 if apply:
1057 logical = self.__getLogical(line, pos)
1058 if logical:
1059 # Fix by adjusting initial indent level.
1060 modified = self.__fixReindent(line, pos, logical)
1061 if not modified:
1062 row = line - 1
1063 text = self.__source[row]
1064 self.__source[row] = self.__getIndent(text) + \
1065 self.__indentWord + text.lstrip()
1066 return (1, self.trUtf8("Indentation level changed."), 0)
1067 return (0, "", 0)
1068 else:
1069 id = self.__getID()
1070 self.__stackLogical.append((id, code, line, pos))
1071 return (-1, "", id)
1072
1073 def __fixE126(self, code, line, pos, apply=False):
1074 """
1075 Private method to fix over-indented/under-indented hanging
1076 indentation.
1077
1078 Codes: E126, E133
1079
1080 @param code code of the issue (string)
1081 @param line line number of the issue (integer)
1082 @param pos position inside line (integer)
1083 @keyparam apply flag indicating, that the fix should be applied
1084 (boolean)
1085 @return value indicating an applied/deferred fix (-1, 0, 1),
1086 a message for the fix (string) and an ID for a deferred
1087 fix (integer)
1088 """
1089 if apply:
1090 logical = self.__getLogical(line, pos)
1091 if logical:
1092 # Fix by deleting whitespace to the left.
1093 logicalLines = logical[2]
1094 row = line - 1
1095 text = self.__source[row]
1096 newText = self.__getIndent(logicalLines[0]) + \
1097 self.__indentWord + text.lstrip()
1098 if newText == text:
1099 # fall back to slower method
1100 changed = self.__fixReindent(line, pos, logical)
1101 else:
1102 self.__source[row] = newText
1103 changed = True
1104 if changed:
1105 return (1, self.trUtf8(
1106 "Indentation level of hanging indentation changed."),
1107 0)
1108 return (0, "", 0)
1109 else:
1110 id = self.__getID()
1111 self.__stackLogical.append((id, code, line, pos))
1112 return (-1, "", id)
1113
1114 def __fixE127(self, code, line, pos, apply=False):
1115 """
1116 Private method to fix over/under indented lines.
1117
1118 Codes: E127, E128
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 @keyparam apply flag indicating, that the fix should be applied
1124 (boolean)
1125 @return value indicating an applied/deferred fix (-1, 0, 1),
1126 a message for the fix (string) and an ID for a deferred
1127 fix (integer)
1128 """
1129 if apply:
1130 logical = self.__getLogical(line, pos)
1131 if logical:
1132 # Fix by inserting/deleting whitespace to the correct level.
1133 logicalLines = logical[2]
1134 row = line - 1
1135 text = self.__source[row]
1136 newText = text
1137
1138 if logicalLines[0].rstrip().endswith('\\'):
1139 newText = self.__getIndent(logicalLines[0]) + \
1140 self.__indentWord + text.lstrip()
1141 else:
1142 startIndex = None
1143 for symbol in '([{':
1144 if symbol in logicalLines[0]:
1145 foundIndex = logicalLines[0].find(symbol) + 1
1146 if startIndex is None:
1147 startIndex = foundIndex
1148 else:
1149 startIndex = min(startIndex, foundIndex)
1150
1151 if startIndex is not None:
1152 newText = startIndex * ' ' + text.lstrip()
1153
1154 if newText == text:
1155 # fall back to slower method
1156 changed = self.__fixReindent(line, pos, logical)
1157 else:
1158 self.__source[row] = newText
1159 changed = True
1160 if changed:
1161 return (1, self.trUtf8("Visual indentation corrected."), 0)
1162 return (0, "", 0)
1163 else:
1164 id = self.__getID()
1165 self.__stackLogical.append((id, code, line, pos))
1166 return (-1, "", id)
1167
1168 def __fixE201(self, code, line, pos):
1169 """
1170 Private method to fix extraneous whitespace.
1171
1172 Codes: E201, E202, E203, E211
1173
1174 @param code code of the issue (string)
1175 @param line line number of the issue (integer)
1176 @param pos position inside line (integer)
1177 @return value indicating an applied/deferred fix (-1, 0, 1),
1178 a message for the fix (string) and an ID for a deferred
1179 fix (integer)
1180 """
1181 line = line - 1
1182 text = self.__source[line]
1183
1184 if '"""' in text or "'''" in text or text.rstrip().endswith('\\'):
1185 return (0, "", 0)
1186
1187 newText = self.__fixWhitespace(text, pos, '')
1188 if newText == text:
1189 return (0, "", 0)
1190
1191 self.__source[line] = newText
1192 return (1, self.trUtf8("Extraneous whitespace removed."), 0)
1193
1194 def __fixE221(self, code, line, pos):
1195 """
1196 Private method to fix extraneous whitespace around operator or
1197 keyword.
1198
1199 Codes: E221, E222, E223, E224, E225, E226, E227, E228, E241,
1200 E242, E271, E272, E273, E274).
1201
1202 @param code code of the issue (string)
1203 @param line line number of the issue (integer)
1204 @param pos position inside line (integer)
1205 @return value indicating an applied/deferred fix (-1, 0, 1),
1206 a message for the fix (string) and an ID for a deferred
1207 fix (integer)
1208 """
1209 line = line - 1
1210 text = self.__source[line]
1211
1212 if '"""' in text or "'''" in text or text.rstrip().endswith('\\'):
1213 return (0, "", 0)
1214
1215 newText = self.__fixWhitespace(text, pos, ' ')
1216 if newText == text:
1217 return (0, "", 0)
1218
1219 self.__source[line] = newText
1220 if code in ["E225", "E226", "E227", "E228"]:
1221 return (1, self.trUtf8("Missing whitespace added."), 0)
1222 else:
1223 return (1, self.trUtf8("Extraneous whitespace removed."), 0)
1224
1225 def __fixE231(self, code, line, pos):
1226 """
1227 Private method to fix missing whitespace after ',;:'.
1228
1229 Codes: E231
1230
1231 @param code code of the issue (string)
1232 @param line line number of the issue (integer)
1233 @param pos position inside line (integer)
1234 @return value indicating an applied/deferred fix (-1, 0, 1),
1235 a message for the fix (string) and an ID for a deferred
1236 fix (integer)
1237 """
1238 line = line - 1
1239 pos = pos + 1
1240 self.__source[line] = self.__source[line][:pos] + \
1241 " " + self.__source[line][pos:]
1242 return (1, self.trUtf8("Missing whitespace added."), 0)
1243
1244 def __fixE251(self, code, line, pos):
1245 """
1246 Private method to fix extraneous whitespace around keyword and
1247 default parameter equals.
1248
1249 Codes: E251
1250
1251 @param code code of the issue (string)
1252 @param line line number of the issue (integer)
1253 @param pos position inside line (integer)
1254 @return value indicating an applied/deferred fix (-1, 0, 1),
1255 a message for the fix (string) and an ID for a deferred
1256 fix (integer)
1257 """
1258 line = line - 1
1259 text = self.__source[line]
1260
1261 # This is necessary since pep8 sometimes reports columns that goes
1262 # past the end of the physical line. This happens in cases like,
1263 # foo(bar\n=None)
1264 col = min(pos, len(text) - 1)
1265 if text[col].strip():
1266 newText = text
1267 else:
1268 newText = text[:col].rstrip() + text[col:].lstrip()
1269
1270 # There could be an escaped newline
1271 #
1272 # def foo(a=\
1273 # 1)
1274 if newText.endswith(('=\\\n', '=\\\r\n', '=\\\r')):
1275 self.__source[line] = newText.rstrip("\n\r \t\\")
1276 self.__source[line + 1] = self.__source[line + 1].lstrip()
1277 else:
1278 self.__source[line] = newText
1279 return (1, self.trUtf8("Extraneous whitespace removed."), 0)
1280
1281 def __fixE261(self, code, line, pos):
1282 """
1283 Private method to fix whitespace before or after inline comment.
1284
1285 Codes: E261, E262
1286
1287 @param code code of the issue (string)
1288 @param line line number of the issue (integer)
1289 @param pos position inside line (integer)
1290 @return value indicating an applied/deferred fix (-1, 0, 1),
1291 a message for the fix (string) and an ID for a deferred
1292 fix (integer)
1293 """
1294 line = line - 1
1295 text = self.__source[line]
1296 left = text[:pos].rstrip(' \t#')
1297 right = text[pos:].lstrip(' \t#')
1298 newText = left + (" # " + right if right.strip() else right)
1299 self.__source[line] = newText
1300 return (1, self.trUtf8("Whitespace around comment sign corrected."), 0)
1301
1302 def __fixE301(self, code, line, pos, apply=False):
1303 """
1304 Private method to fix the need for one blank line.
1305
1306 Codes: E301
1307
1308 @param code code of the issue (string)
1309 @param line line number of the issue (integer)
1310 @param pos position inside line (integer)
1311 @keyparam apply flag indicating, that the fix should be applied
1312 (boolean)
1313 @return value indicating an applied/deferred fix (-1, 0, 1),
1314 a message for the fix (string) and an ID for a deferred
1315 fix (integer)
1316 """
1317 if apply:
1318 self.__source.insert(line - 1, self.__getEol())
1319 return (1, self.trUtf8("One blank line inserted."), 0)
1320 else:
1321 id = self.__getID()
1322 self.__stack.append((id, code, line, pos))
1323 return (-1, "", id)
1324
1325 def __fixE302(self, code, line, pos, apply=False):
1326 """
1327 Private method to fix the need for two blank lines.
1328
1329 Codes: E302
1330
1331 @param code code of the issue (string)
1332 @param line line number of the issue (integer)
1333 @param pos position inside line (integer)
1334 @keyparam apply flag indicating, that the fix should be applied
1335 (boolean)
1336 @return value indicating an applied/deferred fix (-1, 0, 1),
1337 a message for the fix (string) and an ID for a deferred
1338 fix (integer)
1339 """
1340 if apply:
1341 # count blank lines
1342 index = line - 1
1343 blanks = 0
1344 while index:
1345 if self.__source[index - 1].strip() == "":
1346 blanks += 1
1347 index -= 1
1348 else:
1349 break
1350 delta = blanks - 2
1351
1352 line -= 1
1353 if delta < 0:
1354 # insert blank lines (one or two)
1355 while delta < 0:
1356 self.__source.insert(line, self.__getEol())
1357 delta += 1
1358 changed = True
1359 elif delta > 0:
1360 # delete superfluous blank lines
1361 while delta > 0:
1362 del self.__source[line - 1]
1363 line -= 1
1364 delta -= 1
1365 changed = True
1366 else:
1367 changed = False
1368
1369 if changed:
1370 if delta < 0:
1371 msg = self.trUtf8(
1372 "%n blank line(s) inserted.", "", -delta)
1373 elif delta > 0:
1374 msg = self.trUtf8(
1375 "%n superfluous lines removed", "", delta)
1376 else:
1377 msg = ""
1378 return (1, msg, 0)
1379 return (0, "", 0)
1380 else:
1381 id = self.__getID()
1382 self.__stack.append((id, code, line, pos))
1383 return (-1, "", id)
1384
1385 def __fixE303(self, code, line, pos, apply=False):
1386 """
1387 Private method to fix superfluous blank lines.
1388
1389 Codes: E303
1390
1391 @param code code of the issue (string)
1392 @param line line number of the issue (integer)
1393 @param pos position inside line (integer)
1394 @keyparam apply flag indicating, that the fix should be applied
1395 (boolean)
1396 @return value indicating an applied/deferred fix (-1, 0, 1),
1397 a message for the fix (string) and an ID for a deferred
1398 fix (integer)
1399 """
1400 if apply:
1401 index = line - 3
1402 while index:
1403 if self.__source[index].strip() == "":
1404 del self.__source[index]
1405 index -= 1
1406 else:
1407 break
1408 return (1, self.trUtf8("Superfluous blank lines removed."), 0)
1409 else:
1410 id = self.__getID()
1411 self.__stack.append((id, code, line, pos))
1412 return (-1, "", id)
1413
1414 def __fixE304(self, code, line, pos, apply=False):
1415 """
1416 Private method to fix superfluous blank lines after a function
1417 decorator.
1418
1419 Codes: E304
1420
1421 @param code code of the issue (string)
1422 @param line line number of the issue (integer)
1423 @param pos position inside line (integer)
1424 @keyparam apply flag indicating, that the fix should be applied
1425 (boolean)
1426 @return value indicating an applied/deferred fix (-1, 0, 1),
1427 a message for the fix (string) and an ID for a deferred
1428 fix (integer)
1429 """
1430 if apply:
1431 index = line - 2
1432 while index:
1433 if self.__source[index].strip() == "":
1434 del self.__source[index]
1435 index -= 1
1436 else:
1437 break
1438 return (1, self.trUtf8(
1439 "Superfluous blank lines after function decorator removed."),
1440 0)
1441 else:
1442 id = self.__getID()
1443 self.__stack.append((id, code, line, pos))
1444 return (-1, "", id)
1445
1446 def __fixE401(self, code, line, pos, apply=False):
1447 """
1448 Private method to fix multiple imports on one line.
1449
1450 Codes: E401
1451
1452 @param code code of the issue (string)
1453 @param line line number of the issue (integer)
1454 @param pos position inside line (integer)
1455 @keyparam apply flag indicating, that the fix should be applied
1456 (boolean)
1457 @return value indicating an applied/deferred fix (-1, 0, 1),
1458 a message for the fix (string) and an ID for a deferred
1459 fix (integer)
1460 """
1461 if apply:
1462 line = line - 1
1463 text = self.__source[line]
1464 if not text.lstrip().startswith("import"):
1465 return (0, "", 0)
1466
1467 # pep8 (1.3.1) reports false positive if there is an import
1468 # statement followed by a semicolon and some unrelated
1469 # statement with commas in it.
1470 if ';' in text:
1471 return (0, "", 0)
1472
1473 newText = text[:pos].rstrip("\t ,") + self.__getEol() + \
1474 self.__getIndent(text) + "import " + text[pos:].lstrip("\t ,")
1475 self.__source[line] = newText
1476 return (1, self.trUtf8("Imports were put on separate lines."), 0)
1477 else:
1478 id = self.__getID()
1479 self.__stack.append((id, code, line, pos))
1480 return (-1, "", id)
1481
1482 def __fixE501(self, code, line, pos, apply=False):
1483 """
1484 Private method to fix the long lines by breaking them.
1485
1486 Codes: E501
1487
1488 @param code code of the issue (string)
1489 @param line line number of the issue (integer)
1490 @param pos position inside line (integer)
1491 @keyparam apply flag indicating, that the fix should be applied
1492 (boolean)
1493 @return value indicating an applied/deferred fix (-1, 0, 1),
1494 a message for the fix (string) and an ID for a deferred
1495 fix (integer)
1496 """
1497 if apply:
1498 multilineStringLines, docStringLines = \
1499 self.__multilineStringLines()
1500 isDocString = line in docStringLines
1501 line = line - 1
1502 text = self.__source[line]
1503 if line > 0:
1504 prevText = self.__source[line - 1]
1505 else:
1506 prevText = ""
1507 if line < len(self.__source) - 1:
1508 nextText = self.__source[line + 1]
1509 else:
1510 nextText = ""
1511 shortener = LineShortener(
1512 text, prevText, nextText,
1513 maxLength=self.__maxLineLength, eol=self.__getEol(),
1514 indentWord=self.__indentWord, isDocString=isDocString)
1515 changed, newText, newNextText = shortener.shorten()
1516 if changed:
1517 if newText != text:
1518 self.__source[line] = newText
1519 if newNextText and newNextText != nextText:
1520 if newNextText == " ":
1521 newNextText = ""
1522 self.__source[line + 1] = newNextText
1523 return (1, self.trUtf8("Long lines have been shortened."), 0)
1524 else:
1525 return (0, "", 0)
1526 else:
1527 id = self.__getID()
1528 self.__stack.append((id, code, line, pos))
1529 return (-1, "", id)
1530
1531 def __fixE502(self, code, line, pos):
1532 """
1533 Private method to fix redundant backslash within brackets.
1534
1535 Codes: E502
1536
1537 @param code code of the issue (string)
1538 @param line line number of the issue (integer)
1539 @param pos position inside line (integer)
1540 @return value indicating an applied/deferred fix (-1, 0, 1),
1541 a message for the fix (string) and an ID for a deferred
1542 fix (integer)
1543 """
1544 self.__source[line - 1] = \
1545 self.__source[line - 1].rstrip("\n\r \t\\") + self.__getEol()
1546 return (1, self.trUtf8("Redundant backslash in brackets removed."), 0)
1547
1548 def __fixE701(self, code, line, pos, apply=False):
1549 """
1550 Private method to fix colon-separated compound statements.
1551
1552 Codes: E701
1553
1554 @param code code of the issue (string)
1555 @param line line number of the issue (integer)
1556 @param pos position inside line (integer)
1557 @keyparam apply flag indicating, that the fix should be applied
1558 (boolean)
1559 @return value indicating an applied/deferred fix (-1, 0, 1),
1560 a message for the fix (string) and an ID for a deferred
1561 fix (integer)
1562 """
1563 if apply:
1564 line = line - 1
1565 text = self.__source[line]
1566 pos = pos + 1
1567
1568 newText = text[:pos] + self.__getEol() + self.__getIndent(text) + \
1569 self.__indentWord + text[pos:].lstrip("\n\r \t\\") + \
1570 self.__getEol()
1571 self.__source[line] = newText
1572 return (1, self.trUtf8("Compound statement corrected."), 0)
1573 else:
1574 id = self.__getID()
1575 self.__stack.append((id, code, line, pos))
1576 return (-1, "", id)
1577
1578 def __fixE702(self, code, line, pos, apply=False):
1579 """
1580 Private method to fix semicolon-separated compound statements.
1581
1582 Codes: E702, E703
1583
1584 @param code code of the issue (string)
1585 @param line line number of the issue (integer)
1586 @param pos position inside line (integer)
1587 @keyparam apply flag indicating, that the fix should be applied
1588 (boolean)
1589 @return value indicating an applied/deferred fix (-1, 0, 1),
1590 a message for the fix (string) and an ID for a deferred
1591 fix (integer)
1592 """
1593 if apply:
1594 line = line - 1
1595 text = self.__source[line]
1596
1597 if text.rstrip().endswith("\\"):
1598 # normalize '1; \\\n2' into '1; 2'
1599 self.__source[line] = text.rstrip("\n\r \t\\")
1600 self.__source[line + 1] = self.__source[line + 1].lstrip()
1601 elif text.rstrip().endswith(";"):
1602 self.__source[line] = text.rstrip("\n\r \t;") + self.__getEol()
1603 else:
1604 first = text[:pos].rstrip("\n\r \t;") + self.__getEol()
1605 second = text[pos:].lstrip("\n\r \t;")
1606 self.__source[line] = first + self.__getIndent(text) + second
1607 return (1, self.trUtf8("Compound statement corrected."), 0)
1608 else:
1609 id = self.__getID()
1610 self.__stack.append((id, code, line, pos))
1611 return (-1, "", id)
1612
1613 def __fixE711(self, code, line, pos):
1614 """
1615 Private method to fix comparison with None.
1616
1617 Codes: E711, E712
1618
1619 @param code code of the issue (string)
1620 @param line line number of the issue (integer)
1621 @param pos position inside line (integer)
1622 @return value indicating an applied/deferred fix (-1, 0, 1),
1623 a message for the fix (string) and an ID for a deferred
1624 fix (integer)
1625 """
1626 line = line - 1
1627 text = self.__source[line]
1628
1629 rightPos = pos + 2
1630 if rightPos >= len(text):
1631 return (0, "", 0)
1632
1633 left = text[:pos].rstrip()
1634 center = text[pos:rightPos]
1635 right = text[rightPos:].lstrip()
1636
1637 if not right.startswith(("None", "True", "False")):
1638 return (0, "", 0)
1639
1640 if center.strip() == "==":
1641 center = "is"
1642 elif center.strip() == "!=":
1643 center = "is not"
1644 else:
1645 return (0, "", 0)
1646
1647 self.__source[line] = " ".join([left, center, right])
1648 return (1, self.trUtf8("Comparison to None/True/False corrected."), 0)
1649
1650 def __fixN804(self, code, line, pos, apply=False):
1651 """
1652 Private method to fix a wrong first argument of normal and
1653 class methods.
1654
1655 Codes: N804, N805
1656
1657 @param code code of the issue (string)
1658 @param line line number of the issue (integer)
1659 @param pos position inside line (integer)
1660 @keyparam apply flag indicating, that the fix should be applied
1661 (boolean)
1662 @return value indicating an applied/deferred fix (-1, 0, 1),
1663 a message for the fix (string) and an ID for a deferred
1664 fix (integer)
1665 """
1666 if apply:
1667 line = line - 1
1668 text = self.__source[line]
1669 if code == "N804":
1670 arg = "cls"
1671 else:
1672 arg = "self"
1673
1674 if text.rstrip().endswith("("):
1675 newText = text + self.__getIndent(text) + \
1676 self.__indentWord + arg + "," + self.__getEol()
1677 else:
1678 index = text.find("(") + 1
1679 left = text[:index]
1680 right = text[index:]
1681 if right.startswith(")"):
1682 center = arg
1683 else:
1684 center = arg + ", "
1685 newText = left + center + right
1686 self.__source[line] = newText
1687 return (1, self.trUtf8("'{0}' argument added.").format(arg), 0)
1688 else:
1689 id = self.__getID()
1690 self.__stack.append((id, code, line, pos))
1691 return (-1, "", id)
1692
1693 def __fixN806(self, code, line, pos, apply=False):
1694 """
1695 Private method to fix a wrong first argument of static methods.
1696
1697 Codes: N806
1698
1699 @param code code of the issue (string)
1700 @param line line number of the issue (integer)
1701 @param pos position inside line (integer)
1702 @keyparam apply flag indicating, that the fix should be applied
1703 (boolean)
1704 @return value indicating an applied/deferred fix (-1, 0, 1),
1705 a message for the fix (string) and an ID for a deferred
1706 fix (integer)
1707 """
1708 if apply:
1709 line = line - 1
1710 text = self.__source[line]
1711 index = text.find("(") + 1
1712 left = text[:index]
1713 right = text[index:]
1714
1715 if right.startswith(("cls", "self")):
1716 # cls or self are on the definition line
1717 if right.startswith("cls"):
1718 right = right[3:]
1719 arg = "cls"
1720 else:
1721 right = right[4:]
1722 arg = "self"
1723 right = right.lstrip(", ")
1724 newText = left + right
1725 self.__source[line] = newText
1726 else:
1727 # they are on the next line
1728 line = line + 1
1729 text = self.__source[line]
1730 indent = self.__getIndent(text)
1731 right = text.lstrip()
1732 if right.startswith("cls"):
1733 right = right[3:]
1734 arg = "cls"
1735 else:
1736 right = right[4:]
1737 arg = "self"
1738 right = right.lstrip(", ")
1739 if right.startswith("):"):
1740 # merge with previous line
1741 self.__source[line - 1] = \
1742 self.__source[line - 1].rstrip() + right
1743 self.__source[line] = ""
1744 else:
1745 self.__source[line] = indent + right
1746
1747 return (1, self.trUtf8("'{0}' argument removed.").format(arg), 0)
1748 else:
1749 id = self.__getID()
1750 self.__stack.append((id, code, line, pos))
1751 return (-1, "", id)
1752
1753 def __fixW291(self, code, line, pos):
1754 """
1755 Private method to fix trailing whitespace.
1756
1757 Codes: W291, W293
1758
1759 @param code code of the issue (string)
1760 @param line line number of the issue (integer)
1761 @param pos position inside line (integer)
1762 @return value indicating an applied/deferred fix (-1, 0, 1),
1763 a message for the fix (string) and an ID for a deferred
1764 fix (integer)
1765 """
1766 self.__source[line - 1] = re.sub(r'[\t ]+(\r?)$', r"\1",
1767 self.__source[line - 1])
1768 return (1, self.trUtf8("Whitespace stripped from end of line."), 0)
1769
1770 def __fixW292(self, code, line, pos):
1771 """
1772 Private method to fix a missing newline at the end of file.
1773
1774 Codes: W292
1775
1776 @param code code of the issue (string)
1777 @param line line number of the issue (integer)
1778 @param pos position inside line (integer)
1779 @return value indicating an applied/deferred fix (-1, 0, 1),
1780 a message for the fix (string) and an ID for a deferred
1781 fix (integer)
1782 """
1783 self.__source[line - 1] += self.__getEol()
1784 return (1, self.trUtf8("newline added to end of file."), 0)
1785
1786 def __fixW391(self, code, line, pos):
1787 """
1788 Private method to fix trailing blank lines.
1789
1790 Codes: W391
1791
1792 @param code code of the issue (string)
1793 @param line line number of the issue (integer)
1794 @param pos position inside line (integer)
1795 @return value indicating an applied/deferred fix (-1, 0, 1),
1796 a message for the fix (string) and an ID for a deferred
1797 fix (integer)
1798 """
1799 index = line - 1
1800 while index:
1801 if self.__source[index].strip() == "":
1802 del self.__source[index]
1803 index -= 1
1804 else:
1805 break
1806 return (1, self.trUtf8(
1807 "Superfluous trailing blank lines removed from end of file."), 0)
1808
1809 def __fixW603(self, code, line, pos):
1810 """
1811 Private method to fix the not equal notation.
1812
1813 Codes: W603
1814
1815 @param code code of the issue (string)
1816 @param line line number of the issue (integer)
1817 @param pos position inside line (integer)
1818 @return value indicating an applied/deferred fix (-1, 0, 1),
1819 a message for the fix (string) and an ID for a deferred
1820 fix (integer)
1821 """
1822 self.__source[line - 1] = self.__source[line - 1].replace("<>", "!=")
1823 return (1, self.trUtf8("'<>' replaced by '!='."), 0)
1824
1825
1826 class Reindenter(object):
1827 """
1828 Class to reindent badly-indented code to uniformly use four-space
1829 indentation.
1830
1831 Released to the public domain, by Tim Peters, 03 October 2000.
1832 """
1833 def __init__(self, sourceLines):
1834 """
1835 Constructor
1836
1837 @param sourceLines list of source lines including eol marker
1838 (list of string)
1839 """
1840 # Raw file lines.
1841 self.raw = sourceLines
1842 self.after = []
1843
1844 # File lines, rstripped & tab-expanded. Dummy at start is so
1845 # that we can use tokenize's 1-based line numbering easily.
1846 # Note that a line is all-blank iff it's "\n".
1847 self.lines = [line.rstrip().expandtabs() + "\n"
1848 for line in self.raw]
1849 self.lines.insert(0, None)
1850 self.index = 1 # index into self.lines of next line
1851
1852 # List of (lineno, indentlevel) pairs, one for each stmt and
1853 # comment line. indentlevel is -1 for comment lines, as a
1854 # signal that tokenize doesn't know what to do about them;
1855 # indeed, they're our headache!
1856 self.stats = []
1857
1858 def run(self):
1859 """
1860 Public method to run the re-indenter.
1861
1862 @return flag indicating that a change was done (boolean)
1863 """
1864 try:
1865 stats = self.__genStats(tokenize.generate_tokens(self.getline))
1866 except (SyntaxError, tokenize.TokenError):
1867 return False
1868
1869 # Remove trailing empty lines.
1870 lines = self.lines
1871 while lines and lines[-1] == "\n":
1872 lines.pop()
1873 # Sentinel.
1874 stats.append((len(lines), 0))
1875 # Map count of leading spaces to # we want.
1876 have2want = {}
1877 # Program after transformation.
1878 after = self.after = []
1879 # Copy over initial empty lines -- there's nothing to do until
1880 # we see a line with *something* on it.
1881 i = stats[0][0]
1882 after.extend(lines[1:i])
1883 for i in range(len(stats) - 1):
1884 thisstmt, thislevel = stats[i]
1885 nextstmt = stats[i + 1][0]
1886 have = self.__getlspace(lines[thisstmt])
1887 want = thislevel * 4
1888 if want < 0:
1889 # A comment line.
1890 if have:
1891 # An indented comment line. If we saw the same
1892 # indentation before, reuse what it most recently
1893 # mapped to.
1894 want = have2want.get(have, -1)
1895 if want < 0:
1896 # Then it probably belongs to the next real stmt.
1897 for j in range(i + 1, len(stats) - 1):
1898 jline, jlevel = stats[j]
1899 if jlevel >= 0:
1900 if have == self.__getlspace(lines[jline]):
1901 want = jlevel * 4
1902 break
1903 if want < 0: # Maybe it's a hanging
1904 # comment like this one,
1905 # in which case we should shift it like its base
1906 # line got shifted.
1907 for j in range(i - 1, -1, -1):
1908 jline, jlevel = stats[j]
1909 if jlevel >= 0:
1910 want = \
1911 have + \
1912 self.__getlspace(after[jline - 1]) - \
1913 self.__getlspace(lines[jline])
1914 break
1915 if want < 0:
1916 # Still no luck -- leave it alone.
1917 want = have
1918 else:
1919 want = 0
1920 assert want >= 0
1921 have2want[have] = want
1922 diff = want - have
1923 if diff == 0 or have == 0:
1924 after.extend(lines[thisstmt:nextstmt])
1925 else:
1926 for line in lines[thisstmt:nextstmt]:
1927 if diff > 0:
1928 if line == "\n":
1929 after.append(line)
1930 else:
1931 after.append(" " * diff + line)
1932 else:
1933 remove = min(self.__getlspace(line), -diff)
1934 after.append(line[remove:])
1935 return self.raw != self.after
1936
1937 def fixedLine(self, line):
1938 """
1939 Public method to get a fixed line.
1940
1941 @param line number of the line to retrieve (integer)
1942 @return fixed line (string)
1943 """
1944 if line < len(self.after):
1945 return self.after[line]
1946
1947 def getline(self):
1948 """
1949 Public method to get a line of text for tokenize.
1950
1951 @return line of text (string)
1952 """
1953 if self.index >= len(self.lines):
1954 line = ""
1955 else:
1956 line = self.lines[self.index]
1957 self.index += 1
1958 return line
1959
1960 def __genStats(self, tokens):
1961 """
1962 Private method to generate the re-indent statistics.
1963
1964 @param tokens tokens generator (tokenize._tokenize)
1965 @return reference to the generated statistics
1966 """
1967 find_stmt = True # next token begins a fresh stmt?
1968 level = 0 # current indent level
1969 stats = []
1970
1971 for t in tokens:
1972 token_type = t[0]
1973 sline = t[2][0]
1974 line = t[4]
1975
1976 if token_type == tokenize.NEWLINE:
1977 # A program statement, or ENDMARKER, will eventually follow,
1978 # after some (possibly empty) run of tokens of the form
1979 # (NL | COMMENT)* (INDENT | DEDENT+)?
1980 self.find_stmt = True
1981
1982 elif token_type == tokenize.INDENT:
1983 find_stmt = True
1984 level += 1
1985
1986 elif token_type == tokenize.DEDENT:
1987 find_stmt = True
1988 level -= 1
1989
1990 elif token_type == tokenize.COMMENT:
1991 if find_stmt:
1992 stats.append((sline, -1))
1993 # but we're still looking for a new stmt, so leave
1994 # find_stmt alone
1995
1996 elif token_type == tokenize.NL:
1997 pass
1998
1999 elif find_stmt:
2000 # This is the first "real token" following a NEWLINE, so it
2001 # must be the first token of the next program statement, or an
2002 # ENDMARKER.
2003 find_stmt = False
2004 if line: # not endmarker
2005 stats.append((sline, level))
2006
2007 return stats
2008
2009 def __getlspace(self, line):
2010 """
2011 Private method to count number of leading blanks.
2012
2013 @param line line to check (string)
2014 @return number of leading blanks (integer)
2015 """
2016 i = 0
2017 n = len(line)
2018 while i < n and line[i] == " ":
2019 i += 1
2020 return i
2021
2022
2023 class IndentationWrapper(object):
2024 """
2025 Class used by fixers dealing with indentation.
2026
2027 Each instance operates on a single logical line.
2028 """
2029 SKIP_TOKENS = frozenset([
2030 tokenize.COMMENT, tokenize.NL, tokenize.INDENT,
2031 tokenize.DEDENT, tokenize.NEWLINE, tokenize.ENDMARKER
2032 ])
2033
2034 def __init__(self, physical_lines):
2035 """
2036 Constructor
2037
2038 @param physical_lines list of physical lines to operate on
2039 (list of strings)
2040 """
2041 self.lines = physical_lines
2042 self.tokens = []
2043 self.rel_indent = None
2044 sio = io.StringIO(''.join(physical_lines))
2045 for t in tokenize.generate_tokens(sio.readline):
2046 if not len(self.tokens) and t[0] in self.SKIP_TOKENS:
2047 continue
2048 if t[0] != tokenize.ENDMARKER:
2049 self.tokens.append(t)
2050
2051 self.logical_line = self.__buildTokensLogical(self.tokens)
2052
2053 def __buildTokensLogical(self, tokens):
2054 """
2055 Private method to build a logical line from a list of tokens.
2056
2057 @param tokens list of tokens as generated by tokenize.generate_tokens
2058 @return logical line (string)
2059 """
2060 # from pep8.py with minor modifications
2061 logical = []
2062 previous = None
2063 for t in tokens:
2064 token_type, text = t[0:2]
2065 if token_type in self.SKIP_TOKENS:
2066 continue
2067 if previous:
2068 end_line, end = previous[3]
2069 start_line, start = t[2]
2070 if end_line != start_line: # different row
2071 prev_text = self.lines[end_line - 1][end - 1]
2072 if prev_text == ',' or (prev_text not in '{[('
2073 and text not in '}])'):
2074 logical.append(' ')
2075 elif end != start: # different column
2076 fill = self.lines[end_line - 1][end:start]
2077 logical.append(fill)
2078 logical.append(text)
2079 previous = t
2080 logical_line = ''.join(logical)
2081 assert logical_line.lstrip() == logical_line
2082 assert logical_line.rstrip() == logical_line
2083 return logical_line
2084
2085 def pep8Expected(self):
2086 """
2087 Public method to replicate logic in pep8.py, to know what level to
2088 indent things to.
2089
2090 @return list of lists, where each list represents valid indent levels
2091 for the line in question, relative from the initial indent. However,
2092 the first entry is the indent level which was expected.
2093 """
2094 # What follows is an adjusted version of
2095 # pep8.py:continuation_line_indentation. All of the comments have been
2096 # stripped and the 'yield' statements replaced with 'pass'.
2097 if not self.tokens:
2098 return
2099
2100 first_row = self.tokens[0][2][0]
2101 nrows = 1 + self.tokens[-1][2][0] - first_row
2102
2103 # here are the return values
2104 valid_indents = [list()] * nrows
2105 indent_level = self.tokens[0][2][1]
2106 valid_indents[0].append(indent_level)
2107
2108 if nrows == 1:
2109 # bug, really.
2110 return valid_indents
2111
2112 indent_next = self.logical_line.endswith(':')
2113
2114 row = depth = 0
2115 parens = [0] * nrows
2116 self.rel_indent = rel_indent = [0] * nrows
2117 indent = [indent_level]
2118 indent_chances = {}
2119 last_indent = (0, 0)
2120 last_token_multiline = None
2121
2122 for token_type, text, start, end, line in self.tokens:
2123 newline = row < start[0] - first_row
2124 if newline:
2125 row = start[0] - first_row
2126 newline = (not last_token_multiline and
2127 token_type not in (tokenize.NL, tokenize.NEWLINE))
2128
2129 if newline:
2130 # This is where the differences start. Instead of looking at
2131 # the line and determining whether the observed indent matches
2132 # our expectations, we decide which type of indentation is in
2133 # use at the given indent level, and return the offset. This
2134 # algorithm is susceptible to "carried errors", but should
2135 # through repeated runs eventually solve indentation for
2136 # multiline expressions.
2137
2138 if depth:
2139 for open_row in range(row - 1, -1, -1):
2140 if parens[open_row]:
2141 break
2142 else:
2143 open_row = 0
2144
2145 # That's all we get to work with. This code attempts to
2146 # "reverse" the below logic, and place into the valid indents
2147 # list
2148 vi = []
2149 add_second_chances = False
2150 if token_type == tokenize.OP and text in ']})':
2151 # this line starts with a closing bracket, so it needs to
2152 # be closed at the same indent as the opening one.
2153 if indent[depth]:
2154 # hanging indent
2155 vi.append(indent[depth])
2156 else:
2157 # visual indent
2158 vi.append(indent_level + rel_indent[open_row])
2159 elif depth and indent[depth]:
2160 # visual indent was previously confirmed.
2161 vi.append(indent[depth])
2162 add_second_chances = True
2163 elif depth and True in indent_chances.values():
2164 # visual indent happened before, so stick to
2165 # visual indent this time.
2166 if depth > 1 and indent[depth - 1]:
2167 vi.append(indent[depth - 1])
2168 else:
2169 # stupid fallback
2170 vi.append(indent_level + 4)
2171 add_second_chances = True
2172 elif not depth:
2173 vi.append(indent_level + 4)
2174 else:
2175 # must be in hanging indent
2176 hang = rel_indent[open_row] + 4
2177 vi.append(indent_level + hang)
2178
2179 # about the best we can do without look-ahead
2180 if (indent_next and vi[0] == indent_level + 4 and
2181 nrows == row + 1):
2182 vi[0] += 4
2183
2184 if add_second_chances:
2185 # visual indenters like to line things up.
2186 min_indent = vi[0]
2187 for col, what in indent_chances.items():
2188 if col > min_indent and (
2189 what is True or
2190 (what == str and token_type == tokenize.STRING) or
2191 (what == text and token_type == tokenize.OP)
2192 ):
2193 vi.append(col)
2194 vi = sorted(vi)
2195
2196 valid_indents[row] = vi
2197
2198 # Returning to original continuation_line_indentation() from
2199 # pep8.
2200 visual_indent = indent_chances.get(start[1])
2201 last_indent = start
2202 rel_indent[row] = pep8.expand_indent(line) - indent_level
2203 hang = rel_indent[row] - rel_indent[open_row]
2204
2205 if token_type == tokenize.OP and text in ']})':
2206 pass
2207 elif visual_indent is True:
2208 if not indent[depth]:
2209 indent[depth] = start[1]
2210
2211 # line altered: comments shouldn't define a visual indent
2212 if parens[row] and not indent[depth] and token_type not in (
2213 tokenize.NL, tokenize.COMMENT
2214 ):
2215 indent[depth] = start[1]
2216 indent_chances[start[1]] = True
2217 elif token_type == tokenize.STRING or text in (
2218 'u', 'ur', 'b', 'br'
2219 ):
2220 indent_chances[start[1]] = str
2221
2222 if token_type == tokenize.OP:
2223 if text in '([{':
2224 depth += 1
2225 indent.append(0)
2226 parens[row] += 1
2227 elif text in ')]}' and depth > 0:
2228 prev_indent = indent.pop() or last_indent[1]
2229 for d in range(depth):
2230 if indent[d] > prev_indent:
2231 indent[d] = 0
2232 for ind in list(indent_chances):
2233 if ind >= prev_indent:
2234 del indent_chances[ind]
2235 depth -= 1
2236 if depth and indent[depth]: # modified
2237 indent_chances[indent[depth]] = True
2238 for idx in range(row, -1, -1):
2239 if parens[idx]:
2240 parens[idx] -= 1
2241 break
2242 assert len(indent) == depth + 1
2243 if start[1] not in indent_chances:
2244 indent_chances[start[1]] = text
2245
2246 last_token_multiline = (start[0] != end[0])
2247
2248 return valid_indents
2249
2250
2251 class LineShortener(object):
2252 """
2253 Class used to shorten lines to a given maximum of characters.
2254 """
2255 def __init__(self, curLine, prevLine, nextLine, maxLength=79, eol="\n",
2256 indentWord=" ", isDocString=False):
2257 """
2258 Constructor
2259
2260 @param curLine text to work on (string)
2261 @param prevLine line before the text to work on (string)
2262 @param nextLine line after the text to work on (string)
2263 @keyparam maxLength maximum allowed line length (integer)
2264 @keyparam eol eond-of-line marker (string)
2265 @keyparam indentWord string used for indentation (string)
2266 @keyparam isDocString flag indicating that the line belongs to
2267 a documentation string (boolean)
2268 """
2269 self.__text = curLine
2270 self.__prevText = prevLine
2271 self.__nextText = nextLine
2272 self.__maxLength = maxLength
2273 self.__eol = eol
2274 self.__indentWord = indentWord
2275 self.__isDocString = isDocString
2276
2277 def shorten(self):
2278 """
2279 Public method to shorten the line wrapped by the class instance.
2280
2281 @return tuple of a flag indicating successful shortening, the
2282 shortened line and the changed next line (boolean, string, string)
2283 """
2284 # 1. check for comment
2285 if self.__text.lstrip().startswith('#'):
2286 lastComment = True
2287 if self.__nextText.lstrip().startswith('#'):
2288 lastComment = False
2289
2290 # Wrap commented lines.
2291 newText = self.__shortenComment(lastComment)
2292 if newText == self.__text:
2293 return False, "", ""
2294 else:
2295 return True, newText, ""
2296 elif '#' in self.__text:
2297 pos = self.__text.rfind("#")
2298 newText = self.__text[:pos].rstrip() + self.__eol + \
2299 self.__getIndent(self.__text) + self.__text[pos:]
2300 if newText == self.__text:
2301 return False, "", ""
2302 else:
2303 return True, newText, ""
2304
2305 # Do multi line doc strings
2306 if self.__isDocString:
2307 source = self.__text.rstrip()
2308 blank = source.rfind(" ")
2309 while blank > self.__maxLength and blank != -1:
2310 blank = source.rfind(" ", 0, blank)
2311 if blank == -1:
2312 # Cannot break
2313 return False, "", ""
2314 else:
2315 first = self.__text[:blank]
2316 second = self.__text[blank:].lstrip()
2317 if self.__nextText.strip():
2318 if self.__nextText.lstrip().startswith("@"):
2319 # eric doc comment
2320 # create a new line and indent it
2321 newText = first + self.__eol + \
2322 self.__getIndent(first) + self.__indentWord + \
2323 second
2324 newNext = ""
2325 else:
2326 newText = first + self.__eol
2327 newNext = self.__getIndent(self.__nextText) + \
2328 second.rstrip() + " " + self.__nextText.lstrip()
2329 else:
2330 # empty line, add a new line
2331 newText = first + self.__eol + self.__getIndent(first) + \
2332 second
2333 newNext = ""
2334 return True, newText, newNext
2335
2336 indent = self.__getIndent(self.__text)
2337 source = self.__text[len(indent):]
2338 assert source.lstrip() == source
2339 sio = io.StringIO(source)
2340
2341 # Check for multi line string.
2342 try:
2343 tokens = list(tokenize.generate_tokens(sio.readline))
2344 except (SyntaxError, tokenize.TokenError):
2345 if source.rstrip().endswith("\\"):
2346 # just join the continuation line and let the next run
2347 # handle it once it tokenizes ok
2348 newText = indent + source.rstrip()[:-1].rstrip() + " " + \
2349 self.__nextText.lstrip()
2350 if indent:
2351 newNext = indent
2352 else:
2353 newNext = " "
2354 return True, newText, newNext
2355 else:
2356 multilineCandidate = self.__breakMultiline()
2357 if multilineCandidate:
2358 return True, multilineCandidate[0], multilineCandidate[1]
2359 else:
2360 return False, "", ""
2361
2362 # Handle statements by putting the right hand side on a line by itself.
2363 # This should let the next pass shorten it.
2364 if source.startswith('return '):
2365 newText = (
2366 indent +
2367 'return (' +
2368 self.__eol +
2369 indent + self.__indentWord + re.sub('^return ', '', source) +
2370 indent + ')' + self.__eol
2371 )
2372 return True, newText, ""
2373
2374 candidates = self.__shortenLine(tokens, source, indent)
2375 if candidates:
2376 candidates = list(sorted(
2377 set(candidates).union([self.__text]),
2378 key=lambda x: self.__lineShorteningRank(x)))
2379 if candidates[0] == self.__text:
2380 return False, "", ""
2381 return True, candidates[0], ""
2382
2383 source = self.__text
2384 rs = source.rstrip()
2385 if rs.endswith(("'", '"')) and " " in source:
2386 if rs.endswith(('"""', "'''")):
2387 quote = rs[-3:]
2388 else:
2389 quote = rs[-1]
2390 blank = source.rfind(" ")
2391 maxLen = self.__maxLength - 2 - len(quote)
2392 while blank > maxLen and blank != -1:
2393 blank = source.rfind(" ", 0, blank)
2394 if blank != -1:
2395 if source[blank + 1:].startswith(quote):
2396 first = source[:maxLen]
2397 second = source[maxLen:]
2398 else:
2399 first = source[:blank]
2400 second = source[blank + 1:]
2401 return (
2402 True,
2403 first + quote + " \\" + self.__eol +
2404 indent + self.__indentWord + quote + second,
2405 "")
2406 else:
2407 # Cannot break
2408 return False, "", ""
2409
2410 return False, "", ""
2411
2412 def __shortenComment(self, isLast):
2413 """
2414 Private method to shorten a comment line.
2415
2416 @param isLast flag indicating, that the line is the last comment line
2417 (boolean)
2418 @return shortened comment line (string)
2419 """
2420 if len(self.__text) <= self.__maxLength:
2421 return self.__text
2422
2423 newText = self.__text.rstrip()
2424
2425 # PEP 8 recommends 72 characters for comment text.
2426 indentation = self.__getIndent(newText) + '# '
2427 maxLength = min(self.__maxLength,
2428 len(indentation) + 72)
2429
2430 MIN_CHARACTER_REPEAT = 5
2431 if len(newText) - len(newText.rstrip(newText[-1])) >= \
2432 MIN_CHARACTER_REPEAT and \
2433 not newText[-1].isalnum():
2434 # Trim comments that end with things like ---------
2435 return newText[:maxLength] + self.__eol
2436 elif isLast and re.match(r"\s*#+\s*\w+", newText):
2437 import textwrap
2438 splitLines = textwrap.wrap(newText.lstrip(" \t#"),
2439 initial_indent=indentation,
2440 subsequent_indent=indentation,
2441 width=maxLength,
2442 break_long_words=False,
2443 break_on_hyphens=False)
2444 return self.__eol.join(splitLines) + self.__eol
2445 else:
2446 return newText + self.__eol
2447
2448 def __breakMultiline(self):
2449 """
2450 Private method to break multi line strings.
2451
2452 @return tuple of the shortened line and the changed next line
2453 (string, string)
2454 """
2455 indentation = self.__getIndent(self.__text)
2456
2457 # Handle special case.
2458 for symbol in '([{':
2459 # Only valid if symbol is not on a line by itself.
2460 if (
2461 symbol in self.__text and
2462 self.__text.strip() != symbol and
2463 self.__text.rstrip().endswith((',', '%'))
2464 ):
2465 index = 1 + self.__text.find(symbol)
2466
2467 if index <= len(self.__indentWord) + len(indentation):
2468 continue
2469
2470 if self.__isProbablyInsideStringOrComment(
2471 self.__text, index - 1):
2472 continue
2473
2474 return (self.__text[:index].rstrip() + self.__eol +
2475 indentation + self.__indentWord +
2476 self.__text[index:].lstrip(), "")
2477
2478 newText = self.__text
2479 newNext = self.__nextText
2480 blank = newText.rfind(" ")
2481 while blank > self.__maxLength and blank != -1:
2482 blank = newText.rfind(" ", 0, blank)
2483 if blank != -1:
2484 first = self.__text[:blank]
2485 second = self.__text[blank:].strip()
2486 if newNext.strip():
2487 newText = first + self.__eol
2488 if second.endswith(")"):
2489 # don't merge with next line
2490 newText += self.__getIndent(newText) + second + self.__eol
2491 newNext = ""
2492 else:
2493 newNext = self.__getIndent(newNext) + \
2494 second + " " + newNext.lstrip()
2495 else:
2496 # empty line, add a new line
2497 newText = first + self.__eol
2498 newNext = self.__getIndent(newNext) + \
2499 second + self.__eol + newNext.lstrip()
2500 return newText, newNext
2501 else:
2502 return None
2503
2504 def __isProbablyInsideStringOrComment(self, line, index):
2505 """
2506 Private method to check, if the given string might be inside a string
2507 or comment.
2508
2509 @param line line to check (string)
2510 @param index position inside line to check (integer)
2511 @return flag indicating the possibility of being inside a string
2512 or comment
2513 """
2514 # Check against being in a string.
2515 for quote in ['"', "'"]:
2516 pos = line.find(quote)
2517 if pos != -1 and pos <= index:
2518 return True
2519
2520 # Check against being in a comment.
2521 pos = line.find('#')
2522 if pos != -1 and pos <= index:
2523 return True
2524
2525 return False
2526
2527 def __shortenLine(self, tokens, source, indent):
2528 """
2529 Private method to shorten a line of code at an operator.
2530
2531 @param tokens tokens of the line as generated by tokenize
2532 (list of token)
2533 @param source code string to work at (string)
2534 @param indent indentation string of the code line (string)
2535 @return list of candidates (list of string)
2536 """
2537 candidates = []
2538
2539 for tkn in tokens:
2540 tokenType = tkn[0]
2541 tokenString = tkn[1]
2542
2543 if (
2544 tokenType == tokenize.COMMENT and
2545 not self.__prevText.rstrip().endswith('\\')
2546 ):
2547 # Move inline comments to previous line.
2548 offset = tkn[2][1]
2549 first = source[:offset]
2550 second = source[offset:]
2551 candidates.append(
2552 indent + second.strip() + self.__eol +
2553 indent + first.strip() + self.__eol)
2554 elif tokenType == tokenize.OP and tokenString != '=':
2555 # Don't break on '=' after keyword as this violates PEP 8.
2556
2557 assert tokenType != tokenize.INDENT
2558
2559 offset = tkn[2][1] + 1
2560 first = source[:offset]
2561
2562 secondIndent = indent
2563 if first.rstrip().endswith('('):
2564 secondIndent += self.__indentWord
2565 elif '(' in first:
2566 secondIndent += ' ' * (1 + first.find('('))
2567 else:
2568 secondIndent += self.__indentWord
2569
2570 second = (secondIndent + source[offset:].lstrip())
2571 if not second.strip():
2572 continue
2573
2574 # Do not begin a line with a comma
2575 if second.lstrip().startswith(','):
2576 continue
2577
2578 # Do end a line with a dot
2579 if first.rstrip().endswith('.'):
2580 continue
2581
2582 if tokenString in '+-*/,':
2583 newText = first + ' \\' + self.__eol + second
2584 else:
2585 newText = first + self.__eol + second
2586
2587 # Only fix if syntax is okay.
2588 if self.__checkSyntax(self.__normalizeMultiline(newText)):
2589 candidates.append(indent + newText)
2590
2591 return candidates
2592
2593 def __normalizeMultiline(self, text):
2594 """
2595 Private method to remove multiline-related code that will cause syntax
2596 error.
2597
2598 @param text code line to work on (string)
2599 @return normalized code line (string)
2600 """
2601 for quote in '\'"':
2602 dictPattern = r"^{q}[^{q}]*{q} *: *".format(q=quote)
2603 if re.match(dictPattern, text):
2604 if not text.strip().endswith('}'):
2605 text += '}'
2606 return '{' + text
2607
2608 if text.startswith('def ') and text.rstrip().endswith(':'):
2609 # Do not allow ':' to be alone. That is invalid.
2610 splitText = [item.strip() for item in text.split(self.__eol)]
2611 if ':' not in splitText and 'def' not in splitText:
2612 return text[len('def'):].strip().rstrip(':')
2613
2614 return text
2615
2616 def __lineShorteningRank(self, candidate):
2617 """
2618 Private method to rank a candidate.
2619
2620 @param candidate candidate line to rank (string)
2621 @return rank of the candidate (integer)
2622 """
2623 rank = 0
2624 if candidate.strip():
2625 if candidate == self.__text:
2626 # give the original a disadvantage
2627 rank += 50
2628
2629 lines = candidate.split(self.__eol)
2630
2631 offset = 0
2632 if lines[0].rstrip()[-1] not in '([{':
2633 for symbol in '([{':
2634 offset = max(offset, 1 + lines[0].find(symbol))
2635
2636 maxLength = max([offset + len(x.strip()) for x in lines])
2637 rank += maxLength
2638 rank += len(lines)
2639
2640 badStartingSymbol = {
2641 '(': ')',
2642 '[': ']',
2643 '{': '}'}.get(lines[0][-1], None)
2644
2645 if len(lines) > 1:
2646 if (badStartingSymbol and
2647 lines[1].lstrip().startswith(badStartingSymbol)):
2648 rank += 20
2649
2650 if re.match(r".*[+\-\*/] \($", lines[0]):
2651 # "1 * (\n" is ugly as hell.
2652 rank += 100
2653
2654 for currentLine in lines:
2655 for badStart in ['.', '%', '+', '-', '/']:
2656 if currentLine.startswith(badStart):
2657 rank += 100
2658
2659 for ending in '([{':
2660 # Avoid lonely opening. They result in longer lines.
2661 if currentLine.endswith(ending) and \
2662 len(currentLine.strip()) <= len(self.__indentWord):
2663 rank += 100
2664
2665 if currentLine.endswith('%'):
2666 rank -= 20
2667
2668 # Try to break list comprehensions at the "for".
2669 if currentLine.lstrip().startswith('for'):
2670 rank -= 50
2671
2672 rank += 10 * self.__countUnbalancedBrackets(currentLine)
2673 else:
2674 rank = 100000
2675
2676 return max(0, rank)
2677
2678 def __countUnbalancedBrackets(self, line):
2679 """
2680 Private method to determine the number of unmatched open/close
2681 brackets.
2682
2683 @param line line to work at (string)
2684 @return number of unmatched open/close brackets (integer)
2685 """
2686 count = 0
2687 for opening, closing in ['()', '[]', '{}']:
2688 count += abs(line.count(opening) - line.count(closing))
2689
2690 return count
2691
2692 def __getIndent(self, line):
2693 """
2694 Private method to get the indentation string.
2695
2696 @param line line to determine the indentation string from (string)
2697 @return indentation string (string)
2698 """
2699 # copied from CodeStyleFixer
2700 return line.replace(line.lstrip(), "")
2701
2702 def __checkSyntax(self, code):
2703 """
2704 Private method to check the syntax of the given code fragment.
2705
2706 @param code code fragment to check (string)
2707 @return flag indicating syntax is ok (boolean)
2708 """
2709 code = code.replace("\r\n", "\n").replace("\r", "\n")
2710 try:
2711 return compile(code, '<string>', 'exec')
2712 except (SyntaxError, TypeError, UnicodeDecodeError):
2713 return False

eric ide

mercurial