1111from typing import NamedTuple
1212
1313
14- class FileWarnings (NamedTuple ):
15- name : str
14+ class IgnoreRule (NamedTuple ):
15+ file_path : str
1616 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
1757
1858
1959def extract_warnings_from_compiler_output (
@@ -48,11 +88,15 @@ def extract_warnings_from_compiler_output(
4888 "line" : match .group ("line" ),
4989 "column" : match .group ("column" ),
5090 "message" : match .group ("message" ),
51- "option" : match .group ("option" ).lstrip ("[" ).rstrip ("]" ),
91+ "option" : match .group ("option" )
92+ .lstrip ("[" )
93+ .rstrip ("]" ),
5294 }
5395 )
5496 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+ )
56100 sys .exit (1 )
57101
58102 return compiler_warnings
@@ -78,9 +122,24 @@ def get_warnings_by_file(warnings: list[dict]) -> dict[str, list[dict]]:
78122 return warnings_by_file
79123
80124
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+
81140def 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 ],
84143) -> int :
85144 """
86145 Returns failure status if warnings discovered in list of warnings
@@ -89,14 +148,21 @@ def get_unexpected_warnings(
89148 """
90149 unexpected_warnings = {}
91150 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
100166 unexpected_warnings [file ] = (files_with_warnings [file ], 0 )
101167
102168 if unexpected_warnings :
@@ -115,19 +181,27 @@ def get_unexpected_warnings(
115181
116182
117183def 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 ],
120186) -> int :
121187 """
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
124191 """
125192 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+ )
131205
132206 if unexpected_improvements :
133207 print ("Unexpected improvements:" )
@@ -202,55 +276,39 @@ def main(argv: list[str] | None = None) -> int:
202276 "Warning ignore file not specified."
203277 " Continuing without it (no warnings ignored)."
204278 )
205- files_with_expected_warnings = set ()
279+ ignore_rules = set ()
206280 else :
207281 if not Path (args .warning_ignore_file_path ).is_file ():
208282 print (
209283 f"Warning ignore file does not exist:"
210284 f" { args .warning_ignore_file_path } "
211285 )
212286 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 )
226288
227289 with Path (args .compiler_output_file_path ).open (encoding = "UTF-8" ) as f :
228290 compiler_output_file_contents = f .read ()
229291
230292 warnings = extract_warnings_from_compiler_output (
231293 compiler_output_file_contents ,
232294 args .compiler_output_type ,
233- args .path_prefix
295+ args .path_prefix ,
234296 )
235297
236298 files_with_warnings = get_warnings_by_file (warnings )
237299
238- status = get_unexpected_warnings (
239- files_with_expected_warnings , files_with_warnings
240- )
300+ status = get_unexpected_warnings (ignore_rules , files_with_warnings )
241301 if args .fail_on_regression :
242302 exit_code |= status
243303
244- status = get_unexpected_improvements (
245- files_with_expected_warnings , files_with_warnings
246- )
304+ status = get_unexpected_improvements (ignore_rules , files_with_warnings )
247305 if args .fail_on_improvement :
248306 exit_code |= status
249307
250308 print (
251309 "For information about this tool and its configuration"
252310 " visit https://devguide.python.org/development-tools/warnings/"
253- )
311+ )
254312
255313 return exit_code
256314
0 commit comments