|
1 # -*- coding: utf-8 -*- |
|
2 |
|
3 # Copyright (c) 2011 Detlev Offenbach <detlev@die-offenbachs.de> |
|
4 # |
|
5 |
|
6 """ |
|
7 Module implementing a class to fix certain PEP 8 issues. |
|
8 """ |
|
9 |
|
10 import os |
|
11 import re |
|
12 |
|
13 from PyQt4.QtCore import QObject |
|
14 |
|
15 from E5Gui import E5MessageBox |
|
16 |
|
17 import Utilities |
|
18 |
|
19 Pep8FixableIssues = ["W191", "W291", "W292", "W293", "E301", "E303", "E304", "W391", "W603"] |
|
20 |
|
21 class Pep8Fixer(QObject): |
|
22 """ |
|
23 Class implementing a fixer for certain PEP 8 issues. |
|
24 """ |
|
25 def __init__(self, project, filename, sourceLines, fixCodes, inPlace): |
|
26 """ |
|
27 Constructor |
|
28 |
|
29 @param project reference to the project object (Project) |
|
30 @param filename name of the file to be fixed (string) |
|
31 @param sourceLines list of source lines including eol marker |
|
32 (list of string) |
|
33 @param fixCodes list of codes to be fixed as a comma separated |
|
34 string (string) |
|
35 @param inPlace flag indicating to modify the file in place (boolean) |
|
36 """ |
|
37 QObject.__init__(self) |
|
38 |
|
39 self.__project = project |
|
40 self.__filename = filename |
|
41 self.__origName = "" |
|
42 self.__source = sourceLines[:] # save a copy |
|
43 self.__fixCodes = [c.strip() for c in fixCodes.split(",") if c.strip()] |
|
44 |
|
45 if not inPlace: |
|
46 self.__origName = self.__filename |
|
47 self.__filename = os.path.join(os.path.dirname(self.__filename), |
|
48 "fixed_" + os.path.basename(self.__filename)) |
|
49 |
|
50 self.__fixes = { |
|
51 "W191" : self.__fixTabs, |
|
52 "W291" : self.__fixWhitespace, |
|
53 "W292" : self.__fixNewline, |
|
54 "W293" : self.__fixWhitespace, |
|
55 "E301" : self.__fixOneBlankLine, |
|
56 "E303" : self.__fixTooManyBlankLines, |
|
57 "E304" : self.__fixBlankLinesAfterDecorator, |
|
58 "W391" : self.__fixTrailingBlankLines, |
|
59 "W603" : self.__fixNotEqual, |
|
60 } |
|
61 self.__modified = False |
|
62 self.__stack = [] # these need to be fixed before the file is saved |
|
63 # but after all inline fixes |
|
64 |
|
65 def saveFile(self, encoding): |
|
66 """ |
|
67 Public method to save the modified file. |
|
68 |
|
69 @param encoding encoding of the source file (string) |
|
70 @return flag indicating success (boolean) |
|
71 """ |
|
72 if not self.__modified: |
|
73 # no need to write |
|
74 return True |
|
75 |
|
76 # apply deferred fixes |
|
77 self.__finalize() |
|
78 |
|
79 txt = "".join(self.__source) |
|
80 try: |
|
81 Utilities.writeEncodedFile(self.__filename, txt, encoding) |
|
82 except (IOError, Utilities.CodingError, UnicodeError) as err: |
|
83 E5MessageBox.critical(self, |
|
84 self.trUtf8("Fix PEP 8 issues"), |
|
85 self.trUtf8( |
|
86 """<p>Could not save the file <b>{0}</b>.""" |
|
87 """ Skipping it.</p><p>Reason: {1}</p>""")\ |
|
88 .format(self.__filename, str(err)) |
|
89 ) |
|
90 return False |
|
91 |
|
92 return True |
|
93 |
|
94 def fixIssue(self, line, pos, message): |
|
95 """ |
|
96 Public method to fix the fixable issues. |
|
97 |
|
98 @param line line number of issue (integer or string) |
|
99 @param pos character position of issue (integer or string) |
|
100 @param message message text (string) |
|
101 @return flag indicating an applied fix (boolean) and a message for |
|
102 the fix (string) |
|
103 """ |
|
104 code = message.split(None, 1)[0].strip() |
|
105 |
|
106 if (code in self.__fixCodes or len(self.__fixCodes) == 0) and \ |
|
107 code in self.__fixes: |
|
108 res = self.__fixes[code](code, line, pos) |
|
109 if res[0]: |
|
110 self.__modified = True |
|
111 else: |
|
112 res = (False, "") |
|
113 |
|
114 return res |
|
115 |
|
116 def __finalize(self): |
|
117 """ |
|
118 Private method to apply all deferred fixes. |
|
119 """ |
|
120 for code, line, pos in reversed(self.__stack): |
|
121 self.__fixes[code](code, line, pos, apply=True) |
|
122 |
|
123 def __getEol(self): |
|
124 """ |
|
125 Private method to get the applicable eol string. |
|
126 |
|
127 @return eol string (string) |
|
128 """ |
|
129 if self.__origName: |
|
130 fn = self.__origName |
|
131 else: |
|
132 fn = self.__filename |
|
133 |
|
134 if self.__project.isOpen() and self.__project.isProjectFile(fn): |
|
135 eol = self.__project.getLineSeparator() |
|
136 else: |
|
137 eol = Utilities.linesep() |
|
138 return eol |
|
139 |
|
140 def __fixTabs(self, code, line, pos): |
|
141 """ |
|
142 Private method to fix obsolete tab usage. |
|
143 |
|
144 @param code code of the issue (string) |
|
145 @param line line number of the issue (integer) |
|
146 @param pos position inside line (integer) |
|
147 @return flag indicating an applied fix (boolean) and a message for |
|
148 the fix (string) |
|
149 """ |
|
150 self.__source[line - 1] = self.__source[line - 1].replace("\t", " ") |
|
151 return (True, self.trUtf8("Tab converted to 4 spaces.")) |
|
152 |
|
153 def __fixWhitespace(self, code, line, pos): |
|
154 """ |
|
155 Private method to fix trailing whitespace. |
|
156 |
|
157 @param code code of the issue (string) |
|
158 @param line line number of the issue (integer) |
|
159 @param pos position inside line (integer) |
|
160 @return flag indicating an applied fix (boolean) and a message for |
|
161 the fix (string) |
|
162 """ |
|
163 self.__source[line - 1] = re.sub(r'[\t ]*$', "", |
|
164 self.__source[line - 1]) |
|
165 return (True, self.trUtf8("Whitespace stripped from end of line.")) |
|
166 |
|
167 def __fixNewline(self, code, line, pos): |
|
168 """ |
|
169 Private method to fix a missing newline at the end of file. |
|
170 |
|
171 @param code code of the issue (string) |
|
172 @param line line number of the issue (integer) |
|
173 @param pos position inside line (integer) |
|
174 @return flag indicating an applied fix (boolean) and a message for |
|
175 the fix (string) |
|
176 """ |
|
177 self.__source[line - 1] += self.__getEol() |
|
178 return (True, self.trUtf8("newline added to end of file.")) |
|
179 |
|
180 def __fixTrailingBlankLines(self, code, line, pos): |
|
181 """ |
|
182 Private method to fix trailing blank lines. |
|
183 |
|
184 @param code code of the issue (string) |
|
185 @param line line number of the issue (integer) |
|
186 @param pos position inside line (integer) |
|
187 @return flag indicating an applied fix (boolean) and a message for |
|
188 the fix (string) |
|
189 """ |
|
190 index = line - 1 |
|
191 while index: |
|
192 if self.__source[index].strip() == "": |
|
193 del self.__source[index] |
|
194 index -= 1 |
|
195 else: |
|
196 break |
|
197 return (True, self.trUtf8( |
|
198 "Superfluous trailing blank lines removed from end of file.")) |
|
199 |
|
200 def __fixNotEqual(self, code, line, pos): |
|
201 """ |
|
202 Private method to fix the not equal notation. |
|
203 |
|
204 @param code code of the issue (string) |
|
205 @param line line number of the issue (integer) |
|
206 @param pos position inside line (integer) |
|
207 @return flag indicating an applied fix (boolean) and a message for |
|
208 the fix (string) |
|
209 """ |
|
210 self.__source[line - 1] = self.__source[line - 1].replace("<>", "!=") |
|
211 return (True, self.trUtf8("'<>' replaced by '!='.")) |
|
212 |
|
213 def __fixBlankLinesAfterDecorator(self, code, line, pos, apply=False): |
|
214 """ |
|
215 Private method to fix superfluous blank lines after a function |
|
216 decorator. |
|
217 |
|
218 @param code code of the issue (string) |
|
219 @param line line number of the issue (integer) |
|
220 @param pos position inside line (integer) |
|
221 @keyparam apply flag indicating, that the fix should be applied |
|
222 (boolean) |
|
223 @return flag indicating an applied fix (boolean) and a message for |
|
224 the fix (string) |
|
225 """ |
|
226 if apply: |
|
227 index = line - 2 |
|
228 while index: |
|
229 if self.__source[index].strip() == "": |
|
230 del self.__source[index] |
|
231 index -= 1 |
|
232 else: |
|
233 break |
|
234 else: |
|
235 self.__stack.append((code, line, pos)) |
|
236 return (True, self.trUtf8( |
|
237 "Superfluous blank lines after function decorator removed.")) |
|
238 |
|
239 def __fixTooManyBlankLines(self, code, line, pos, apply=False): |
|
240 """ |
|
241 Private method to fix superfluous blank lines. |
|
242 |
|
243 @param code code of the issue (string) |
|
244 @param line line number of the issue (integer) |
|
245 @param pos position inside line (integer) |
|
246 @keyparam apply flag indicating, that the fix should be applied |
|
247 (boolean) |
|
248 @return flag indicating an applied fix (boolean) and a message for |
|
249 the fix (string) |
|
250 """ |
|
251 if apply: |
|
252 index = line - 3 |
|
253 while index: |
|
254 if self.__source[index].strip() == "": |
|
255 del self.__source[index] |
|
256 index -= 1 |
|
257 else: |
|
258 break |
|
259 else: |
|
260 self.__stack.append((code, line, pos)) |
|
261 return (True, self.trUtf8("Superfluous blank lines removed.")) |
|
262 |
|
263 def __fixOneBlankLine(self, code, line, pos, apply=False): |
|
264 """ |
|
265 Private method to fix the need for one blank line. |
|
266 |
|
267 @param code code of the issue (string) |
|
268 @param line line number of the issue (integer) |
|
269 @param pos position inside line (integer) |
|
270 @keyparam apply flag indicating, that the fix should be applied |
|
271 (boolean) |
|
272 @return flag indicating an applied fix (boolean) and a message for |
|
273 the fix (string) |
|
274 """ |
|
275 if apply: |
|
276 self.__source.insert(line - 1, self.__getEol()) |
|
277 else: |
|
278 self.__stack.append((code, line, pos)) |
|
279 return (True, self.trUtf8("One blank line inserted.")) |