UtilitiesPython2/Pep8NamingCheckerPy2.py

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

eric ide

mercurial