eric7/Plugins/CheckerPlugins/CodeStyleChecker/CodeStyleFixer.py

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

eric ide

mercurial