Plugins/CheckerPlugins/Pep8/NamingStyleChecker.py

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

eric ide

mercurial