|
1 # -*- coding: utf-8 -*- |
|
2 |
|
3 # Copyright (c) 2025 Detlev Offenbach <detlev@die-offenbachs.de> |
|
4 # |
|
5 |
|
6 """ |
|
7 Module implementing a node visitor to check the use of sys.version and sys.version_info. |
|
8 """ |
|
9 |
|
10 import ast |
|
11 |
|
12 import AstUtilities |
|
13 |
|
14 |
|
15 class SysVersionVisitor(ast.NodeVisitor): |
|
16 """ |
|
17 Class implementing a node visitor to check the use of sys.version and |
|
18 sys.version_info. |
|
19 |
|
20 Note: This class is modeled after flake8-2020 v1.8.1. |
|
21 """ |
|
22 |
|
23 def __init__(self): |
|
24 """ |
|
25 Constructor |
|
26 """ |
|
27 super().__init__() |
|
28 |
|
29 self.violations = [] |
|
30 self.__fromImports = {} |
|
31 |
|
32 def visit_ImportFrom(self, node): |
|
33 """ |
|
34 Public method to handle a from ... import ... statement. |
|
35 |
|
36 @param node reference to the node to be processed |
|
37 @type ast.ImportFrom |
|
38 """ |
|
39 for alias in node.names: |
|
40 if node.module is not None and not alias.asname: |
|
41 self.__fromImports[alias.name] = node.module |
|
42 |
|
43 self.generic_visit(node) |
|
44 |
|
45 def __isSys(self, attr, node): |
|
46 """ |
|
47 Private method to check for a reference to sys attribute. |
|
48 |
|
49 @param attr attribute name |
|
50 @type str |
|
51 @param node reference to the node to be checked |
|
52 @type ast.Node |
|
53 @return flag indicating a match |
|
54 @rtype bool |
|
55 """ |
|
56 match = False |
|
57 if ( |
|
58 isinstance(node, ast.Attribute) |
|
59 and isinstance(node.value, ast.Name) |
|
60 and node.value.id == "sys" |
|
61 and node.attr == attr |
|
62 ) or ( |
|
63 isinstance(node, ast.Name) |
|
64 and node.id == attr |
|
65 and self.__fromImports.get(node.id) == "sys" |
|
66 ): |
|
67 match = True |
|
68 |
|
69 return match |
|
70 |
|
71 def __isSysVersionUpperSlice(self, node, n): |
|
72 """ |
|
73 Private method to check the upper slice of sys.version. |
|
74 |
|
75 @param node reference to the node to be checked |
|
76 @type ast.Node |
|
77 @param n slice value to check against |
|
78 @type int |
|
79 @return flag indicating a match |
|
80 @rtype bool |
|
81 """ |
|
82 return ( |
|
83 self.__isSys("version", node.value) |
|
84 and isinstance(node.slice, ast.Slice) |
|
85 and node.slice.lower is None |
|
86 and AstUtilities.isNumber(node.slice.upper) |
|
87 and AstUtilities.getValue(node.slice.upper) == n |
|
88 and node.slice.step is None |
|
89 ) |
|
90 |
|
91 def visit_Subscript(self, node): |
|
92 """ |
|
93 Public method to handle a subscript. |
|
94 |
|
95 @param node reference to the node to be processed |
|
96 @type ast.Subscript |
|
97 """ |
|
98 if self.__isSysVersionUpperSlice(node, 1): |
|
99 self.violations.append((node.value, "M-423")) |
|
100 elif self.__isSysVersionUpperSlice(node, 3): |
|
101 self.violations.append((node.value, "M-401")) |
|
102 elif ( |
|
103 self.__isSys("version", node.value) |
|
104 and isinstance(node.slice, ast.Index) |
|
105 and AstUtilities.isNumber(node.slice.value) |
|
106 and AstUtilities.getValue(node.slice.value) == 2 |
|
107 ): |
|
108 self.violations.append((node.value, "M-402")) |
|
109 elif ( |
|
110 self.__isSys("version", node.value) |
|
111 and isinstance(node.slice, ast.Index) |
|
112 and AstUtilities.isNumber(node.slice.value) |
|
113 and AstUtilities.getValue(node.slice.value) == 0 |
|
114 ): |
|
115 self.violations.append((node.value, "M-421")) |
|
116 |
|
117 self.generic_visit(node) |
|
118 |
|
119 def visit_Compare(self, node): |
|
120 """ |
|
121 Public method to handle a comparison. |
|
122 |
|
123 @param node reference to the node to be processed |
|
124 @type ast.Compare |
|
125 """ |
|
126 if ( |
|
127 isinstance(node.left, ast.Subscript) |
|
128 and self.__isSys("version_info", node.left.value) |
|
129 and isinstance(node.left.slice, ast.Index) |
|
130 and AstUtilities.isNumber(node.left.slice.value) |
|
131 and AstUtilities.getValue(node.left.slice.value) == 0 |
|
132 and len(node.ops) == 1 |
|
133 and isinstance(node.ops[0], ast.Eq) |
|
134 and AstUtilities.isNumber(node.comparators[0]) |
|
135 and AstUtilities.getValue(node.comparators[0]) == 3 |
|
136 ): |
|
137 self.violations.append((node.left, "M-411")) |
|
138 elif ( |
|
139 self.__isSys("version", node.left) |
|
140 and len(node.ops) == 1 |
|
141 and isinstance(node.ops[0], (ast.Lt, ast.LtE, ast.Gt, ast.GtE)) |
|
142 and AstUtilities.isString(node.comparators[0]) |
|
143 ): |
|
144 if len(AstUtilities.getValue(node.comparators[0])) == 1: |
|
145 errorCode = "M-422" |
|
146 else: |
|
147 errorCode = "M-403" |
|
148 self.violations.append((node.left, errorCode)) |
|
149 elif ( |
|
150 isinstance(node.left, ast.Subscript) |
|
151 and self.__isSys("version_info", node.left.value) |
|
152 and isinstance(node.left.slice, ast.Index) |
|
153 and AstUtilities.isNumber(node.left.slice.value) |
|
154 and AstUtilities.getValue(node.left.slice.value) == 1 |
|
155 and len(node.ops) == 1 |
|
156 and isinstance(node.ops[0], (ast.Lt, ast.LtE, ast.Gt, ast.GtE)) |
|
157 and AstUtilities.isNumber(node.comparators[0]) |
|
158 ): |
|
159 self.violations.append((node, "M-413")) |
|
160 elif ( |
|
161 isinstance(node.left, ast.Attribute) |
|
162 and self.__isSys("version_info", node.left.value) |
|
163 and node.left.attr == "minor" |
|
164 and len(node.ops) == 1 |
|
165 and isinstance(node.ops[0], (ast.Lt, ast.LtE, ast.Gt, ast.GtE)) |
|
166 and AstUtilities.isNumber(node.comparators[0]) |
|
167 ): |
|
168 self.violations.append((node, "M-414")) |
|
169 |
|
170 self.generic_visit(node) |
|
171 |
|
172 def visit_Attribute(self, node): |
|
173 """ |
|
174 Public method to handle an attribute. |
|
175 |
|
176 @param node reference to the node to be processed |
|
177 @type ast.Attribute |
|
178 """ |
|
179 if ( |
|
180 isinstance(node.value, ast.Name) |
|
181 and node.value.id == "six" |
|
182 and node.attr == "PY3" |
|
183 ): |
|
184 self.violations.append((node, "M-412")) |
|
185 |
|
186 self.generic_visit(node) |
|
187 |
|
188 def visit_Name(self, node): |
|
189 """ |
|
190 Public method to handle an name. |
|
191 |
|
192 @param node reference to the node to be processed |
|
193 @type ast.Name |
|
194 """ |
|
195 if node.id == "PY3" and self.__fromImports.get(node.id) == "six": |
|
196 self.violations.append((node, "M-412")) |
|
197 |
|
198 self.generic_visit(node) |