16 class PathlibChecker: |
16 class PathlibChecker: |
17 """ |
17 """ |
18 Class implementing a checker for functions that can be replaced by use of |
18 Class implementing a checker for functions that can be replaced by use of |
19 the pathlib module. |
19 the pathlib module. |
20 """ |
20 """ |
|
21 |
21 Codes = [ |
22 Codes = [ |
22 ## Replacements for the os module functions |
23 ## Replacements for the os module functions |
23 "P101", "P102", "P103", "P104", "P105", "P106", "P107", |
24 "P101", |
24 "P108", "P109", "P110", "P111", "P112", "P113", "P114", |
25 "P102", |
25 |
26 "P103", |
|
27 "P104", |
|
28 "P105", |
|
29 "P106", |
|
30 "P107", |
|
31 "P108", |
|
32 "P109", |
|
33 "P110", |
|
34 "P111", |
|
35 "P112", |
|
36 "P113", |
|
37 "P114", |
26 ## Replacements for the os.path module functions |
38 ## Replacements for the os.path module functions |
27 "P201", "P202", "P203", "P204", "P205", "P206", "P207", |
39 "P201", |
28 "P208", "P209", "P210", "P211", "P212", "P213", |
40 "P202", |
29 |
41 "P203", |
|
42 "P204", |
|
43 "P205", |
|
44 "P206", |
|
45 "P207", |
|
46 "P208", |
|
47 "P209", |
|
48 "P210", |
|
49 "P211", |
|
50 "P212", |
|
51 "P213", |
30 ## Replacements for some Python standrd library functions |
52 ## Replacements for some Python standrd library functions |
31 "P301", |
53 "P301", |
32 |
|
33 ## Replacements for py.path.local |
54 ## Replacements for py.path.local |
34 "P401", |
55 "P401", |
35 ] |
56 ] |
36 |
57 |
37 # map functions to be replaced to error codes |
58 # map functions to be replaced to error codes |
38 Function2Code = { |
59 Function2Code = { |
39 "os.chmod": "P101", |
60 "os.chmod": "P101", |
40 "os.mkdir": "P102", |
61 "os.mkdir": "P102", |
41 "os.makedirs": "P103", |
62 "os.makedirs": "P103", |
48 "os.readlink": "P110", |
69 "os.readlink": "P110", |
49 "os.stat": "P111", |
70 "os.stat": "P111", |
50 "os.listdir": "P112", |
71 "os.listdir": "P112", |
51 "os.link": "P113", |
72 "os.link": "P113", |
52 "os.symlink": "P114", |
73 "os.symlink": "P114", |
53 |
|
54 "os.path.abspath": "P201", |
74 "os.path.abspath": "P201", |
55 "os.path.exists": "P202", |
75 "os.path.exists": "P202", |
56 "os.path.expanduser": "P203", |
76 "os.path.expanduser": "P203", |
57 "os.path.isdir": "P204", |
77 "os.path.isdir": "P204", |
58 "os.path.isfile": "P205", |
78 "os.path.isfile": "P205", |
62 "os.path.basename": "P209", |
82 "os.path.basename": "P209", |
63 "os.path.dirname": "P210", |
83 "os.path.dirname": "P210", |
64 "os.path.samefile": "P211", |
84 "os.path.samefile": "P211", |
65 "os.path.splitext": "P212", |
85 "os.path.splitext": "P212", |
66 "os.path.relpath": "P213", |
86 "os.path.relpath": "P213", |
67 |
|
68 "open": "P301", |
87 "open": "P301", |
69 |
|
70 "py.path.local": "P401", |
88 "py.path.local": "P401", |
71 } |
89 } |
72 |
90 |
73 def __init__(self, source, filename, tree, selected, ignored, expected, |
91 def __init__(self, source, filename, tree, selected, ignored, expected, repeat): |
74 repeat): |
|
75 """ |
92 """ |
76 Constructor |
93 Constructor |
77 |
94 |
78 @param source source code to be checked |
95 @param source source code to be checked |
79 @type list of str |
96 @type list of str |
80 @param filename name of the source file |
97 @param filename name of the source file |
81 @type str |
98 @type str |
82 @param tree AST tree of the source code |
99 @param tree AST tree of the source code |
89 @type list of str |
106 @type list of str |
90 @param repeat flag indicating to report each occurrence of a code |
107 @param repeat flag indicating to report each occurrence of a code |
91 @type bool |
108 @type bool |
92 """ |
109 """ |
93 self.__select = tuple(selected) |
110 self.__select = tuple(selected) |
94 self.__ignore = ('',) if selected else tuple(ignored) |
111 self.__ignore = ("",) if selected else tuple(ignored) |
95 self.__expected = expected[:] |
112 self.__expected = expected[:] |
96 self.__repeat = repeat |
113 self.__repeat = repeat |
97 self.__filename = filename |
114 self.__filename = filename |
98 self.__source = source[:] |
115 self.__source = source[:] |
99 self.__tree = copy.deepcopy(tree) |
116 self.__tree = copy.deepcopy(tree) |
100 |
117 |
101 # statistics counters |
118 # statistics counters |
102 self.counters = {} |
119 self.counters = {} |
103 |
120 |
104 # collection of detected errors |
121 # collection of detected errors |
105 self.errors = [] |
122 self.errors = [] |
106 |
123 |
107 self.__checkCodes = (code for code in self.Codes |
124 self.__checkCodes = (code for code in self.Codes if not self.__ignoreCode(code)) |
108 if not self.__ignoreCode(code)) |
125 |
109 |
|
110 def __ignoreCode(self, code): |
126 def __ignoreCode(self, code): |
111 """ |
127 """ |
112 Private method to check if the message code should be ignored. |
128 Private method to check if the message code should be ignored. |
113 |
129 |
114 @param code message code to check for |
130 @param code message code to check for |
115 @type str |
131 @type str |
116 @return flag indicating to ignore the given code |
132 @return flag indicating to ignore the given code |
117 @rtype bool |
133 @rtype bool |
118 """ |
134 """ |
119 return (code.startswith(self.__ignore) and |
135 return code.startswith(self.__ignore) and not code.startswith(self.__select) |
120 not code.startswith(self.__select)) |
136 |
121 |
|
122 def __error(self, lineNumber, offset, code, *args): |
137 def __error(self, lineNumber, offset, code, *args): |
123 """ |
138 """ |
124 Private method to record an issue. |
139 Private method to record an issue. |
125 |
140 |
126 @param lineNumber line number of the issue |
141 @param lineNumber line number of the issue |
127 @type int |
142 @type int |
128 @param offset position within line of the issue |
143 @param offset position within line of the issue |
129 @type int |
144 @type int |
130 @param code message code |
145 @param code message code |
132 @param args arguments for the message |
147 @param args arguments for the message |
133 @type list |
148 @type list |
134 """ |
149 """ |
135 if self.__ignoreCode(code): |
150 if self.__ignoreCode(code): |
136 return |
151 return |
137 |
152 |
138 if code in self.counters: |
153 if code in self.counters: |
139 self.counters[code] += 1 |
154 self.counters[code] += 1 |
140 else: |
155 else: |
141 self.counters[code] = 1 |
156 self.counters[code] = 1 |
142 |
157 |
143 # Don't care about expected codes |
158 # Don't care about expected codes |
144 if code in self.__expected: |
159 if code in self.__expected: |
145 return |
160 return |
146 |
161 |
147 if code and (self.counters[code] == 1 or self.__repeat): |
162 if code and (self.counters[code] == 1 or self.__repeat): |
148 # record the issue with one based line number |
163 # record the issue with one based line number |
149 self.errors.append( |
164 self.errors.append( |
150 { |
165 { |
151 "file": self.__filename, |
166 "file": self.__filename, |
153 "offset": offset, |
168 "offset": offset, |
154 "code": code, |
169 "code": code, |
155 "args": args, |
170 "args": args, |
156 } |
171 } |
157 ) |
172 ) |
158 |
173 |
159 def run(self): |
174 def run(self): |
160 """ |
175 """ |
161 Public method to check the given source against functions |
176 Public method to check the given source against functions |
162 to be replaced by 'pathlib' equivalents. |
177 to be replaced by 'pathlib' equivalents. |
163 """ |
178 """ |
164 if not self.__filename: |
179 if not self.__filename: |
165 # don't do anything, if essential data is missing |
180 # don't do anything, if essential data is missing |
166 return |
181 return |
167 |
182 |
168 if not self.__checkCodes: |
183 if not self.__checkCodes: |
169 # don't do anything, if no codes were selected |
184 # don't do anything, if no codes were selected |
170 return |
185 return |
171 |
186 |
172 visitor = PathlibVisitor(self.__checkForReplacement) |
187 visitor = PathlibVisitor(self.__checkForReplacement) |
173 visitor.visit(self.__tree) |
188 visitor.visit(self.__tree) |
174 |
189 |
175 def __checkForReplacement(self, node, name): |
190 def __checkForReplacement(self, node, name): |
176 """ |
191 """ |
177 Private method to check the given node for the need for a |
192 Private method to check the given node for the need for a |
178 replacement. |
193 replacement. |
179 |
194 |
180 @param node reference to the AST node to check |
195 @param node reference to the AST node to check |
181 @type ast.AST |
196 @type ast.AST |
182 @param name resolved name of the node |
197 @param name resolved name of the node |
183 @type str |
198 @type str |
184 """ |
199 """ |
189 |
204 |
190 class PathlibVisitor(ast.NodeVisitor): |
205 class PathlibVisitor(ast.NodeVisitor): |
191 """ |
206 """ |
192 Class to traverse the AST node tree and check for potential issues. |
207 Class to traverse the AST node tree and check for potential issues. |
193 """ |
208 """ |
|
209 |
194 def __init__(self, checkCallback): |
210 def __init__(self, checkCallback): |
195 """ |
211 """ |
196 Constructor |
212 Constructor |
197 |
213 |
198 @param checkCallback callback function taking a reference to the |
214 @param checkCallback callback function taking a reference to the |
199 AST node and the resolved name |
215 AST node and the resolved name |
200 @type func |
216 @type func |
201 """ |
217 """ |
202 super().__init__() |
218 super().__init__() |
203 |
219 |
204 self.__checkCallback = checkCallback |
220 self.__checkCallback = checkCallback |
205 self.__importAlias = {} |
221 self.__importAlias = {} |
206 |
222 |
207 def visit_ImportFrom(self, node): |
223 def visit_ImportFrom(self, node): |
208 """ |
224 """ |
209 Public method handle the ImportFrom AST node. |
225 Public method handle the ImportFrom AST node. |
210 |
226 |
211 @param node reference to the ImportFrom AST node |
227 @param node reference to the ImportFrom AST node |
212 @type ast.ImportFrom |
228 @type ast.ImportFrom |
213 """ |
229 """ |
214 for imp in node.names: |
230 for imp in node.names: |
215 if imp.asname: |
231 if imp.asname: |
218 self.__importAlias[imp.name] = f"{node.module}.{imp.name}" |
234 self.__importAlias[imp.name] = f"{node.module}.{imp.name}" |
219 |
235 |
220 def visit_Import(self, node): |
236 def visit_Import(self, node): |
221 """ |
237 """ |
222 Public method to handle the Import AST node. |
238 Public method to handle the Import AST node. |
223 |
239 |
224 @param node reference to the Import AST node |
240 @param node reference to the Import AST node |
225 @type ast.Import |
241 @type ast.Import |
226 """ |
242 """ |
227 for imp in node.names: |
243 for imp in node.names: |
228 if imp.asname: |
244 if imp.asname: |
229 self.__importAlias[imp.asname] = imp.name |
245 self.__importAlias[imp.asname] = imp.name |
230 |
246 |
231 def visit_Call(self, node): |
247 def visit_Call(self, node): |
232 """ |
248 """ |
233 Public method to handle the Call AST node. |
249 Public method to handle the Call AST node. |
234 |
250 |
235 @param node reference to the Call AST node |
251 @param node reference to the Call AST node |
236 @type ast.Call |
252 @type ast.Call |
237 """ |
253 """ |
238 nameResolver = NameResolver(self.__importAlias) |
254 nameResolver = NameResolver(self.__importAlias) |
239 nameResolver.visit(node.func) |
255 nameResolver.visit(node.func) |
240 |
256 |
241 self.__checkCallback(node, nameResolver.name()) |
257 self.__checkCallback(node, nameResolver.name()) |
242 |
258 |
243 |
259 |
244 class NameResolver(ast.NodeVisitor): |
260 class NameResolver(ast.NodeVisitor): |
245 """ |
261 """ |
246 Class to resolve a Name or Attribute node. |
262 Class to resolve a Name or Attribute node. |
247 """ |
263 """ |
|
264 |
248 def __init__(self, importAlias): |
265 def __init__(self, importAlias): |
249 """ |
266 """ |
250 Constructor |
267 Constructor |
251 |
268 |
252 @param importAlias reference to the import aliases dictionary |
269 @param importAlias reference to the import aliases dictionary |
253 @type dict |
270 @type dict |
254 """ |
271 """ |
255 self.__importAlias = importAlias |
272 self.__importAlias = importAlias |
256 self.__names = [] |
273 self.__names = [] |
257 |
274 |
258 def name(self): |
275 def name(self): |
259 """ |
276 """ |
260 Public method to resolve the name. |
277 Public method to resolve the name. |
261 |
278 |
262 @return resolved name |
279 @return resolved name |
263 @rtype str |
280 @rtype str |
264 """ |
281 """ |
265 with contextlib.suppress(KeyError, IndexError): |
282 with contextlib.suppress(KeyError, IndexError): |
266 attr = self.__importAlias[self.__names[-1]] |
283 attr = self.__importAlias[self.__names[-1]] |
267 self.__names[-1] = attr |
284 self.__names[-1] = attr |
268 # do nothing if there is no such name or the names list is empty |
285 # do nothing if there is no such name or the names list is empty |
269 |
286 |
270 return ".".join(reversed(self.__names)) |
287 return ".".join(reversed(self.__names)) |
271 |
288 |
272 def visit_Name(self, node): |
289 def visit_Name(self, node): |
273 """ |
290 """ |
274 Public method to handle the Name AST node. |
291 Public method to handle the Name AST node. |
275 |
292 |
276 @param node reference to the Name AST node |
293 @param node reference to the Name AST node |
277 @type ast.Name |
294 @type ast.Name |
278 """ |
295 """ |
279 self.__names.append(node.id) |
296 self.__names.append(node.id) |
280 |
297 |
281 def visit_Attribute(self, node): |
298 def visit_Attribute(self, node): |
282 """ |
299 """ |
283 Public method to handle the Attribute AST node. |
300 Public method to handle the Attribute AST node. |
284 |
301 |
285 @param node reference to the Attribute AST node |
302 @param node reference to the Attribute AST node |
286 @type ast.Attribute |
303 @type ast.Attribute |
287 """ |
304 """ |
288 try: |
305 try: |
289 self.__names.append(node.attr) |
306 self.__names.append(node.attr) |