|
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 datetime function calls. |
|
8 """ |
|
9 |
|
10 import ast |
|
11 |
|
12 import AstUtilities |
|
13 |
|
14 |
|
15 class DateTimeVisitor(ast.NodeVisitor): |
|
16 """ |
|
17 Class implementing a node visitor to check datetime function calls. |
|
18 |
|
19 Note: This class is modeled after flake8_datetimez v20.10.0 checker. |
|
20 """ |
|
21 |
|
22 def __init__(self): |
|
23 """ |
|
24 Constructor |
|
25 """ |
|
26 super().__init__() |
|
27 |
|
28 self.violations = [] |
|
29 |
|
30 def __getFromKeywords(self, keywords, name): |
|
31 """ |
|
32 Private method to get a keyword node given its name. |
|
33 |
|
34 @param keywords list of keyword argument nodes |
|
35 @type list of ast.AST |
|
36 @param name name of the keyword node |
|
37 @type str |
|
38 @return keyword node |
|
39 @rtype ast.AST |
|
40 """ |
|
41 for keyword in keywords: |
|
42 if keyword.arg == name: |
|
43 return keyword |
|
44 |
|
45 return None |
|
46 |
|
47 def visit_Call(self, node): |
|
48 """ |
|
49 Public method to handle a function call. |
|
50 |
|
51 Every datetime related function call is check for use of the naive |
|
52 variant (i.e. use without TZ info). |
|
53 |
|
54 @param node reference to the node to be processed |
|
55 @type ast.Call |
|
56 """ |
|
57 # datetime.something() |
|
58 isDateTimeClass = ( |
|
59 isinstance(node.func, ast.Attribute) |
|
60 and isinstance(node.func.value, ast.Name) |
|
61 and node.func.value.id == "datetime" |
|
62 ) |
|
63 |
|
64 # datetime.datetime.something() |
|
65 isDateTimeModuleAndClass = ( |
|
66 isinstance(node.func, ast.Attribute) |
|
67 and isinstance(node.func.value, ast.Attribute) |
|
68 and node.func.value.attr == "datetime" |
|
69 and isinstance(node.func.value.value, ast.Name) |
|
70 and node.func.value.value.id == "datetime" |
|
71 ) |
|
72 |
|
73 if isDateTimeClass: |
|
74 if node.func.attr == "datetime": |
|
75 # datetime.datetime(2000, 1, 1, 0, 0, 0, 0, |
|
76 # datetime.timezone.utc) |
|
77 isCase1 = len(node.args) >= 8 and not ( |
|
78 AstUtilities.isNameConstant(node.args[7]) |
|
79 and AstUtilities.getValue(node.args[7]) is None |
|
80 ) |
|
81 |
|
82 # datetime.datetime(2000, 1, 1, tzinfo=datetime.timezone.utc) |
|
83 tzinfoKeyword = self.__getFromKeywords(node.keywords, "tzinfo") |
|
84 isCase2 = tzinfoKeyword is not None and not ( |
|
85 AstUtilities.isNameConstant(tzinfoKeyword.value) |
|
86 and AstUtilities.getValue(tzinfoKeyword.value) is None |
|
87 ) |
|
88 |
|
89 if not (isCase1 or isCase2): |
|
90 self.violations.append((node, "M-301")) |
|
91 |
|
92 elif node.func.attr == "time": |
|
93 # time(12, 10, 45, 0, datetime.timezone.utc) |
|
94 isCase1 = len(node.args) >= 5 and not ( |
|
95 AstUtilities.isNameConstant(node.args[4]) |
|
96 and AstUtilities.getValue(node.args[4]) is None |
|
97 ) |
|
98 |
|
99 # datetime.time(12, 10, 45, tzinfo=datetime.timezone.utc) |
|
100 tzinfoKeyword = self.__getFromKeywords(node.keywords, "tzinfo") |
|
101 isCase2 = tzinfoKeyword is not None and not ( |
|
102 AstUtilities.isNameConstant(tzinfoKeyword.value) |
|
103 and AstUtilities.getValue(tzinfoKeyword.value) is None |
|
104 ) |
|
105 |
|
106 if not (isCase1 or isCase2): |
|
107 self.violations.append((node, "M-321")) |
|
108 |
|
109 elif node.func.attr == "date": |
|
110 self.violations.append((node, "M-311")) |
|
111 |
|
112 if isDateTimeClass or isDateTimeModuleAndClass: |
|
113 if node.func.attr == "today": |
|
114 self.violations.append((node, "M-302")) |
|
115 |
|
116 elif node.func.attr == "utcnow": |
|
117 self.violations.append((node, "M-303")) |
|
118 |
|
119 elif node.func.attr == "utcfromtimestamp": |
|
120 self.violations.append((node, "M-304")) |
|
121 |
|
122 elif node.func.attr in "now": |
|
123 # datetime.now(UTC) |
|
124 isCase1 = ( |
|
125 len(node.args) == 1 |
|
126 and len(node.keywords) == 0 |
|
127 and not ( |
|
128 AstUtilities.isNameConstant(node.args[0]) |
|
129 and AstUtilities.getValue(node.args[0]) is None |
|
130 ) |
|
131 ) |
|
132 |
|
133 # datetime.now(tz=UTC) |
|
134 tzKeyword = self.__getFromKeywords(node.keywords, "tz") |
|
135 isCase2 = tzKeyword is not None and not ( |
|
136 AstUtilities.isNameConstant(tzKeyword.value) |
|
137 and AstUtilities.getValue(tzKeyword.value) is None |
|
138 ) |
|
139 |
|
140 if not (isCase1 or isCase2): |
|
141 self.violations.append((node, "M-305")) |
|
142 |
|
143 elif node.func.attr == "fromtimestamp": |
|
144 # datetime.fromtimestamp(1234, UTC) |
|
145 isCase1 = ( |
|
146 len(node.args) == 2 |
|
147 and len(node.keywords) == 0 |
|
148 and not ( |
|
149 AstUtilities.isNameConstant(node.args[1]) |
|
150 and AstUtilities.getValue(node.args[1]) is None |
|
151 ) |
|
152 ) |
|
153 |
|
154 # datetime.fromtimestamp(1234, tz=UTC) |
|
155 tzKeyword = self.__getFromKeywords(node.keywords, "tz") |
|
156 isCase2 = tzKeyword is not None and not ( |
|
157 AstUtilities.isNameConstant(tzKeyword.value) |
|
158 and AstUtilities.getValue(tzKeyword.value) is None |
|
159 ) |
|
160 |
|
161 if not (isCase1 or isCase2): |
|
162 self.violations.append((node, "M-306")) |
|
163 |
|
164 elif node.func.attr == "strptime": |
|
165 # datetime.strptime(...).replace(tzinfo=UTC) |
|
166 parent = getattr(node, "_dtCheckerParent", None) |
|
167 pparent = getattr(parent, "_dtCheckerParent", None) |
|
168 if not ( |
|
169 isinstance(parent, ast.Attribute) and parent.attr == "replace" |
|
170 ) or not isinstance(pparent, ast.Call): |
|
171 isCase1 = False |
|
172 else: |
|
173 tzinfoKeyword = self.__getFromKeywords(pparent.keywords, "tzinfo") |
|
174 isCase1 = tzinfoKeyword is not None and not ( |
|
175 AstUtilities.isNameConstant(tzinfoKeyword.value) |
|
176 and AstUtilities.getValue(tzinfoKeyword.value) is None |
|
177 ) |
|
178 |
|
179 if not isCase1: |
|
180 self.violations.append((node, "M-307")) |
|
181 |
|
182 elif node.func.attr == "fromordinal": |
|
183 self.violations.append((node, "M-308")) |
|
184 |
|
185 # date.something() |
|
186 isDateClass = ( |
|
187 isinstance(node.func, ast.Attribute) |
|
188 and isinstance(node.func.value, ast.Name) |
|
189 and node.func.value.id == "date" |
|
190 ) |
|
191 |
|
192 # datetime.date.something() |
|
193 isDateModuleAndClass = ( |
|
194 isinstance(node.func, ast.Attribute) |
|
195 and isinstance(node.func.value, ast.Attribute) |
|
196 and node.func.value.attr == "date" |
|
197 and isinstance(node.func.value.value, ast.Name) |
|
198 and node.func.value.value.id == "datetime" |
|
199 ) |
|
200 |
|
201 if isDateClass or isDateModuleAndClass: |
|
202 if node.func.attr == "today": |
|
203 self.violations.append((node, "M-312")) |
|
204 |
|
205 elif node.func.attr == "fromtimestamp": |
|
206 self.violations.append((node, "M-313")) |
|
207 |
|
208 elif node.func.attr == "fromordinal": |
|
209 self.violations.append((node, "M-314")) |
|
210 |
|
211 elif node.func.attr == "fromisoformat": |
|
212 self.violations.append((node, "M-315")) |
|
213 |
|
214 self.generic_visit(node) |
|
215 |
|
216 |
|
217 # |
|
218 # eflag: noqa = M-891 |