Plugins/CheckerPlugins/Pep8/Pep8NamingChecker.py

changeset 2894
8e4264045fc9
child 2895
4a44d92757f9
equal deleted inserted replaced
2893:150de635fa29 2894:8e4264045fc9
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2013 Detlev Offenbach <detlev@die-offenbachs.de>
4 #
5
6 """
7 Module implementing a checker for PEP-8 naming conventions.
8 """
9
10 import collections
11 import ast
12 import re
13 import os
14
15 from PyQt4.QtCore import QT_TRANSLATE_NOOP, QCoreApplication
16
17
18 # TODO: port this to Py2
19 class Pep8NamingChecker(object):
20 """
21 Class implementing a checker for PEP-8 naming conventions.
22 """
23 LowercaseRegex = re.compile(r"[_a-z][_a-z0-9]*$")
24 UppercaseRegexp = re.compile(r"[_A-Z][_A-Z0-9]*$")
25 CamelcaseRegexp = re.compile(r"_?[A-Z][a-zA-Z0-9]*$")
26 MixedcaseRegexp = re.compile(r"_?[a-z][a-zA-Z0-9]*$")
27
28 Codes = [
29 "N801", "N802", "N803", "N804", "N805", "N806", "N807", "N808",
30 "N811", "N812", "N813", "N814", "N821", "N831"
31 ]
32 Messages = {
33 "N801": QT_TRANSLATE_NOOP("Pep8NamingChecker",
34 "class names should use CapWords convention"),
35 "N802": QT_TRANSLATE_NOOP("Pep8NamingChecker",
36 "function name should be lowercase"),
37 "N803": QT_TRANSLATE_NOOP("Pep8NamingChecker",
38 "argument name should be lowercase"),
39 "N804": QT_TRANSLATE_NOOP("Pep8NamingChecker",
40 "first argument of a class method should be named 'cls'"),
41 "N805": QT_TRANSLATE_NOOP("Pep8NamingChecker",
42 "first argument of a method should be named 'self'"),
43 "N806": QT_TRANSLATE_NOOP("Pep8NamingChecker",
44 "first argument of a static method should not be named"
45 " 'self' or 'cls"),
46 "N807": QT_TRANSLATE_NOOP("Pep8NamingChecker",
47 "module names should be lowercase"),
48 "N808": QT_TRANSLATE_NOOP("Pep8NamingChecker",
49 "package names should be lowercase"),
50 "N811": QT_TRANSLATE_NOOP("Pep8NamingChecker",
51 "constant imported as non constant"),
52 "N812": QT_TRANSLATE_NOOP("Pep8NamingChecker",
53 "lowercase imported as non lowercase"),
54 "N813": QT_TRANSLATE_NOOP("Pep8NamingChecker",
55 "camelcase imported as lowercase"),
56 "N814": QT_TRANSLATE_NOOP("Pep8NamingChecker",
57 "camelcase imported as constant"),
58 "N821": QT_TRANSLATE_NOOP("Pep8NamingChecker",
59 "variable in function should be lowercase"),
60 "N831": QT_TRANSLATE_NOOP("Pep8NamingChecker",
61 "names 'l', 'O' and 'I' should be avoided"),
62 }
63
64 def __init__(self, tree, filename):
65 """
66 Constructor (according to pep8.py API)
67
68 @param tree AST tree of the source file
69 @param filename name of the source file (string)
70 """
71 self.__parents = collections.deque()
72 self.__tree = tree
73 self.__filename = filename
74
75 self.__checkers = {
76 "classdef": [self.__checkClassName],
77 "functiondef": [self.__checkFuntionName,
78 self.__checkFunctionArgumentNames,
79 ],
80 "assign": [self.__checkVariablesInFunction],
81 "importfrom": [self.__checkImportAs],
82 "module": [self.__checkModule],
83 }
84
85 def run(self):
86 """
87 Public method run by the pep8.py checker.
88
89 @return tuple giving line number, offset within line, code and
90 checker function
91 """
92 if self.__tree:
93 return self.__visitTree(self.__tree)
94 else:
95 return ()
96
97 @classmethod
98 def getMessage(cls, code, *args):
99 """
100 Class method to get a translated and formatted message for a
101 given code.
102
103 @param code message code (string)
104 @param args arguments for a formatted message (list)
105 @return translated and formatted message (string)
106 """
107 if code in cls.Messages:
108 return code + " " + QCoreApplication.translate("Pep8NamingChecker",
109 cls.Messages[code]).format(*args)
110 else:
111 return code + " " + QCoreApplication.translate("Pep8NamingChecker",
112 "no message for this code defined")
113
114 def __visitTree(self, node):
115 """
116 Private method to scan the given AST tree.
117
118 @param node AST tree node to scan
119 @return tuple giving line number, offset within line, code and
120 checker function
121 """
122 for error in self.__visitNode(node):
123 yield error
124 self.__parents.append(node)
125 for child in ast.iter_child_nodes(node):
126 for error in self.__visitTree(child):
127 yield error
128 self.__parents.pop()
129
130 def __visitNode(self, node):
131 """
132 Private method to inspect the given AST node.
133
134 @param node AST tree node to inspect
135 @return tuple giving line number, offset within line, code and
136 checker function
137 """
138 if isinstance(node, ast.ClassDef):
139 self.__tagClassFunctions(node)
140 elif isinstance(node, ast.FunctionDef):
141 self.__findGlobalDefs(node)
142
143 checkerName = node.__class__.__name__.lower()
144 if checkerName in self.__checkers:
145 for checker in self.__checkers[checkerName]:
146 for error in checker(node, self.__parents):
147 yield error + (self.__checkers[checkerName],)
148
149 def __tagClassFunctions(self, classNode):
150 """
151 Private method to tag functions if they are methods, class methods or
152 static methods.
153
154 @param classNode AST tree node to tag
155 """
156 # try to find all 'old style decorators' like
157 # m = staticmethod(m)
158 lateDecoration = {}
159 for node in ast.iter_child_nodes(classNode):
160 if not (isinstance(node, ast.Assign) and
161 isinstance(node.value, ast.Call) and
162 isinstance(node.value.func, ast.Name)):
163 continue
164 funcName = node.value.func.id
165 if funcName in ("classmethod", "staticmethod"):
166 meth = (len(node.value.args) == 1 and node.value.args[0])
167 if isinstance(meth, ast.Name):
168 lateDecoration[meth.id] = funcName
169
170 # iterate over all functions and tag them
171 for node in ast.iter_child_nodes(classNode):
172 if not isinstance(node, ast.FunctionDef):
173 continue
174
175 node.function_type = 'method'
176 if node.name == "__new__":
177 node.function_type = "classmethod"
178
179 if node.name in lateDecoration:
180 node.function_type = lateDecoration[node.name]
181 elif node.decorator_list:
182 names = [d.id for d in node.decorator_list
183 if isinstance(d, ast.Name) and
184 d.id in ("classmethod", "staticmethod")]
185 if names:
186 node.function_type = names[0]
187
188 def __findGlobalDefs(self, functionNode):
189 """
190 Private method amend a node with global definitions information.
191
192 @param functionNode AST tree node to amend
193 """
194 globalNames = set()
195 nodesToCheck = collections.deque(ast.iter_child_nodes(functionNode))
196 while nodesToCheck:
197 node = nodesToCheck.pop()
198 if isinstance(node, ast.Global):
199 globalNames.update(node.names)
200
201 if not isinstance(node, (ast.FunctionDef, ast.ClassDef)):
202 nodesToCheck.extend(ast.iter_child_nodes(node))
203 functionNode.global_names = globalNames
204
205 def __getArgNames(self, node):
206 """
207 Private method to get the argument names of a function node.
208
209 @param node AST node to extract arguments names from
210 @return list of argument names (list of string)
211 """
212 posArgs = [arg.arg for arg in node.args.args]
213 kwOnly = [arg.arg for arg in node.args.kwonlyargs]
214 return posArgs + kwOnly
215
216 def __error(self, node, code):
217 """
218 Private method to build the error information
219
220 @param node AST node to report an error for
221 @param code error code to report (string)
222 @return tuple giving line number, offset within line and error code
223 (integer, integer, string)
224 """
225 if isinstance(node, ast.Module):
226 lineno = 0
227 offset = 0
228 else:
229 lineno = node.lineno
230 offset = node.col_offset
231 if isinstance(node, ast.ClassDef):
232 lineno += len(node.decorator_list)
233 offset += 6
234 elif isinstance(node, ast.FunctionDef):
235 lineno += len(node.decorator_list)
236 offset += 4
237 return (lineno, offset, code)
238
239 def __isNameToBeAvoided(self, name):
240 """
241 Private method to check, if the given name should be avoided.
242
243 @param name name to be checked (string)
244 @return flag indicating to avoid it (boolen)
245 """
246 return name in ("l", "O", "I")
247
248 def __checkClassName(self, node, parents):
249 """
250 Private class to check the given node for class name
251 conventions (N801, N831).
252
253 Almost without exception, class names use the CapWords convention.
254 Classes for internal use have a leading underscore in addition.
255
256 @param node AST note to check
257 @return tuple giving line number, offset within line and error code
258 (integer, integer, string)
259 """
260 if self.__isNameToBeAvoided(node.name):
261 yield self.__error(node, "N831")
262 return
263
264 if not self.CamelcaseRegexp.match(node.name):
265 yield self.__error(node, "N801")
266
267 def __checkFuntionName(self, node, parents):
268 """
269 Private class to check the given node for function name
270 conventions (N802, N831).
271
272 Function names should be lowercase, with words separated by underscores
273 as necessary to improve readability. Functions <b>not</b> being
274 methods '__' in front and back are not allowed. Mixed case is allowed
275 only in contexts where that's already the prevailing style
276 (e.g. threading.py), to retain backwards compatibility.
277
278 @param node AST note to check
279 @return tuple giving line number, offset within line and error code
280 (integer, integer, string)
281 """
282 functionType = getattr(node, "function_type", "function")
283 name = node.name
284 if self.__isNameToBeAvoided(name):
285 yield self.__error(node, "N831")
286 return
287
288 if (functionType == "function" and "__" in (name[:2], name[-2:])) or \
289 not self.LowercaseRegex.match(name):
290 yield self.__error(node, "N802")
291
292 def __checkFunctionArgumentNames(self, node, parents):
293 """
294 Private class to check the argument names of functions
295 (N803, N804, N805, N806, N831).
296
297 The argument names of a function should be lowercase, with words
298 separated by underscores. A class method should have 'cls' as the
299 first argument. A method should have 'self' as the first argument.
300
301 @param node AST note to check
302 @return tuple giving line number, offset within line and error code
303 (integer, integer, string)
304 """
305 if node.args.kwarg is not None:
306 if not self.LowercaseRegex.match(node.args.kwarg):
307 yield self.__error(node, "N803")
308 return
309
310 if node.args.vararg is not None:
311 if not self.LowercaseRegex.match(node.args.vararg):
312 yield self.__error(node, "N803")
313 return
314
315 argNames = self.__getArgNames(node)
316 if not argNames:
317 return
318
319 functionType = getattr(node, "function_type", "function")
320 if functionType == "method":
321 if argNames[0] != "self":
322 yield self.__error(node, "N805")
323 elif functionType == "classmethod":
324 if argNames[0] != "cls":
325 yield self.__error(node, "N804")
326 elif functionType == "staticmethod":
327 if argNames[0] in ("cls", "self"):
328 yield self.__error(node, "N806")
329 for arg in argNames:
330 if self.__isNameToBeAvoided(arg):
331 yield self.__error(node, "N831")
332 return
333
334 if not self.LowercaseRegex.match(arg):
335 yield self.__error(node, "N803")
336 return
337
338 def __checkVariablesInFunction(self, node, parents):
339 """
340 Private method to check local variables in functions (N821, N831).
341
342 Local variables in functions should be lowercase.
343
344 @param node AST note to check
345 @return tuple giving line number, offset within line and error code
346 (integer, integer, string)
347 """
348 for parentFunc in reversed(parents):
349 if isinstance(parentFunc, ast.ClassDef):
350 return
351 if isinstance(parentFunc, ast.FunctionDef):
352 break
353 else:
354 return
355 for target in node.targets:
356 name = isinstance(target, ast.Name) and target.id
357 if not name or name in parentFunc.global_names:
358 return
359
360 if self.__isNameToBeAvoided(name):
361 yield self.__error(node, "N831")
362 return
363
364 if not self.LowercaseRegex.match(name) and name[:1] != '_':
365 yield self.__error(target, "N821")
366
367 def __checkModule(self, node, parents):
368 """
369 Private method to check module naming conventions (N807, N808).
370
371 Module and package names should be lowercase.
372
373 @param node AST note to check
374 @return tuple giving line number, offset within line and error code
375 (integer, integer, string)
376 """
377 if self.__filename:
378 moduleName = os.path.splitext(os.path.basename(self.__filename))[0]
379 if moduleName.lower() != moduleName:
380 yield self.__error(node, "N807")
381
382 if moduleName == "__init__":
383 # we got a package
384 packageName = \
385 os.path.split(os.path.dirname(self.__filename))[1]
386 if packageName.lower != packageName:
387 yield self.__error(node, "N808")
388
389 def __checkImportAs(self, node, parents):
390 """
391 Private method to check that imports don't change the
392 naming convention (N811, N812, N813, N814).
393
394 @param node AST note to check
395 @return tuple giving line number, offset within line and error code
396 (integer, integer, string)
397 """
398 for name in node.names:
399 if not name.asname:
400 continue
401
402 if self.UppercaseRegexp.match(name.name):
403 if not self.UppercaseRegexp.match(name.asname):
404 yield self.__error(node, "N811")
405 elif self.LowercaseRegex.match(name.name):
406 if not self.LowercaseRegex.match(name.asname):
407 yield self.__error(node, "N812")
408 elif self.LowercaseRegex.match(name.asname):
409 yield self.__error(node, "N813")
410 elif self.UppercaseRegexp.match(name.asname):
411 yield self.__error(node, "N814")

eric ide

mercurial