11
11
from typing import NamedTuple
12
12
13
13
14
- class FileWarnings (NamedTuple ):
15
- name : str
14
+ class IgnoreRule (NamedTuple ):
15
+ file_path : str
16
16
count : int
17
+ ignore_all : bool = False
18
+ is_directory : bool = False
19
+
20
+
21
+ def parse_warning_ignore_file (file_path : str ) -> set [IgnoreRule ]:
22
+ """
23
+ Parses the warning ignore file and returns a set of IgnoreRules
24
+ """
25
+ files_with_expected_warnings = set ()
26
+ with Path (file_path ).open (encoding = "UTF-8" ) as ignore_rules_file :
27
+ files_with_expected_warnings = set ()
28
+ for i , line in enumerate (ignore_rules_file ):
29
+ line = line .strip ()
30
+ if line and not line .startswith ("#" ):
31
+ line_parts = line .split ()
32
+ if len (line_parts ) >= 2 :
33
+ file_name = line_parts [0 ]
34
+ count = line_parts [1 ]
35
+ ignore_all = count == "*"
36
+ is_directory = file_name .endswith ("/" )
37
+
38
+ # Directories must have a wildcard count
39
+ if is_directory and count != "*" :
40
+ print (
41
+ f"Error parsing ignore file: { file_path } at line: { i } "
42
+ )
43
+ print (
44
+ f"Directory { file_name } must have count set to *"
45
+ )
46
+ sys .exit (1 )
47
+ if ignore_all :
48
+ count = 0
49
+
50
+ files_with_expected_warnings .add (
51
+ IgnoreRule (
52
+ file_name , int (count ), ignore_all , is_directory
53
+ )
54
+ )
55
+
56
+ return files_with_expected_warnings
17
57
18
58
19
59
def extract_warnings_from_compiler_output (
@@ -48,11 +88,15 @@ def extract_warnings_from_compiler_output(
48
88
"line" : match .group ("line" ),
49
89
"column" : match .group ("column" ),
50
90
"message" : match .group ("message" ),
51
- "option" : match .group ("option" ).lstrip ("[" ).rstrip ("]" ),
91
+ "option" : match .group ("option" )
92
+ .lstrip ("[" )
93
+ .rstrip ("]" ),
52
94
}
53
95
)
54
96
except :
55
- print (f"Error parsing compiler output. Unable to extract warning on line { i } :\n { line } " )
97
+ print (
98
+ f"Error parsing compiler output. Unable to extract warning on line { i } :\n { line } "
99
+ )
56
100
sys .exit (1 )
57
101
58
102
return compiler_warnings
@@ -78,9 +122,24 @@ def get_warnings_by_file(warnings: list[dict]) -> dict[str, list[dict]]:
78
122
return warnings_by_file
79
123
80
124
125
+ def is_file_ignored (
126
+ file_path : str , ignore_rules : set [IgnoreRule ]
127
+ ) -> IgnoreRule | None :
128
+ """
129
+ Returns the IgnoreRule object for the file path if there is a related rule for it
130
+ """
131
+ for rule in ignore_rules :
132
+ if rule .is_directory :
133
+ if file_path .startswith (rule .file_path ):
134
+ return rule
135
+ elif file_path == rule .file_path :
136
+ return rule
137
+ return None
138
+
139
+
81
140
def get_unexpected_warnings (
82
- files_with_expected_warnings : set [FileWarnings ],
83
- files_with_warnings : set [FileWarnings ],
141
+ ignore_rules : set [IgnoreRule ],
142
+ files_with_warnings : set [IgnoreRule ],
84
143
) -> int :
85
144
"""
86
145
Returns failure status if warnings discovered in list of warnings
@@ -89,14 +148,21 @@ def get_unexpected_warnings(
89
148
"""
90
149
unexpected_warnings = {}
91
150
for file in files_with_warnings .keys ():
92
- found_file_in_ignore_list = False
93
- for ignore_file in files_with_expected_warnings :
94
- if file == ignore_file .name :
95
- if len (files_with_warnings [file ]) > ignore_file .count :
96
- unexpected_warnings [file ] = (files_with_warnings [file ], ignore_file .count )
97
- found_file_in_ignore_list = True
98
- break
99
- if not found_file_in_ignore_list :
151
+
152
+ rule = is_file_ignored (file , ignore_rules )
153
+
154
+ if rule :
155
+ if rule .ignore_all :
156
+ continue
157
+
158
+ if len (files_with_warnings [file ]) > rule .count :
159
+ unexpected_warnings [file ] = (
160
+ files_with_warnings [file ],
161
+ rule .count ,
162
+ )
163
+ continue
164
+ elif rule is None :
165
+ # If the file is not in the ignore list, then it is unexpected
100
166
unexpected_warnings [file ] = (files_with_warnings [file ], 0 )
101
167
102
168
if unexpected_warnings :
@@ -115,19 +181,27 @@ def get_unexpected_warnings(
115
181
116
182
117
183
def get_unexpected_improvements (
118
- files_with_expected_warnings : set [FileWarnings ],
119
- files_with_warnings : set [FileWarnings ],
184
+ ignore_rules : set [IgnoreRule ],
185
+ files_with_warnings : set [IgnoreRule ],
120
186
) -> int :
121
187
"""
122
- Returns failure status if there are no warnings in the list of warnings
123
- for a file that is in the list of files with expected warnings
188
+ Returns failure status if the number of warnings for a file is greater
189
+ than the expected number of warnings for that file based on the ignore
190
+ rules
124
191
"""
125
192
unexpected_improvements = []
126
- for file in files_with_expected_warnings :
127
- if file .name not in files_with_warnings .keys ():
128
- unexpected_improvements .append ((file .name , file .count , 0 ))
129
- elif len (files_with_warnings [file .name ]) < file .count :
130
- unexpected_improvements .append ((file .name , file .count , len (files_with_warnings [file .name ])))
193
+ for rule in ignore_rules :
194
+ if not rule .ignore_all and rule .file_path not in files_with_warnings .keys ():
195
+ if rule .file_path not in files_with_warnings .keys ():
196
+ unexpected_improvements .append ((rule .file_path , rule .count , 0 ))
197
+ elif len (files_with_warnings [rule .file_path ]) < rule .count :
198
+ unexpected_improvements .append (
199
+ (
200
+ rule .file_path ,
201
+ rule .count ,
202
+ len (files_with_warnings [rule .file_path ]),
203
+ )
204
+ )
131
205
132
206
if unexpected_improvements :
133
207
print ("Unexpected improvements:" )
@@ -202,55 +276,39 @@ def main(argv: list[str] | None = None) -> int:
202
276
"Warning ignore file not specified."
203
277
" Continuing without it (no warnings ignored)."
204
278
)
205
- files_with_expected_warnings = set ()
279
+ ignore_rules = set ()
206
280
else :
207
281
if not Path (args .warning_ignore_file_path ).is_file ():
208
282
print (
209
283
f"Warning ignore file does not exist:"
210
284
f" { args .warning_ignore_file_path } "
211
285
)
212
286
return 1
213
- with Path (args .warning_ignore_file_path ).open (
214
- encoding = "UTF-8"
215
- ) as clean_files :
216
- # Files with expected warnings are stored as a set of tuples
217
- # where the first element is the file name and the second element
218
- # is the number of warnings expected in that file
219
- files_with_expected_warnings = {
220
- FileWarnings (
221
- file .strip ().split ()[0 ], int (file .strip ().split ()[1 ])
222
- )
223
- for file in clean_files
224
- if file .strip () and not file .startswith ("#" )
225
- }
287
+ ignore_rules = parse_warning_ignore_file (args .warning_ignore_file_path )
226
288
227
289
with Path (args .compiler_output_file_path ).open (encoding = "UTF-8" ) as f :
228
290
compiler_output_file_contents = f .read ()
229
291
230
292
warnings = extract_warnings_from_compiler_output (
231
293
compiler_output_file_contents ,
232
294
args .compiler_output_type ,
233
- args .path_prefix
295
+ args .path_prefix ,
234
296
)
235
297
236
298
files_with_warnings = get_warnings_by_file (warnings )
237
299
238
- status = get_unexpected_warnings (
239
- files_with_expected_warnings , files_with_warnings
240
- )
300
+ status = get_unexpected_warnings (ignore_rules , files_with_warnings )
241
301
if args .fail_on_regression :
242
302
exit_code |= status
243
303
244
- status = get_unexpected_improvements (
245
- files_with_expected_warnings , files_with_warnings
246
- )
304
+ status = get_unexpected_improvements (ignore_rules , files_with_warnings )
247
305
if args .fail_on_improvement :
248
306
exit_code |= status
249
307
250
308
print (
251
309
"For information about this tool and its configuration"
252
310
" visit https://devguide.python.org/development-tools/warnings/"
253
- )
311
+ )
254
312
255
313
return exit_code
256
314
0 commit comments