UtilitiesPython2/NamingStyleCheckerPy2.py

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

eric ide

mercurial