eric6/Plugins/CheckerPlugins/CodeStyleChecker/CodeStyleFixer.py

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

eric ide

mercurial