2020import configparser
2121import subprocess
2222import sys
23+ import tempfile
2324from pathlib import Path
2425from typing import List , Tuple
2526
@@ -32,50 +33,73 @@ def __init__(self, verbose: bool = False, fix: bool = False):
3233 self .fix = fix
3334 self .failed_checks = []
3435 self .project_root = Path (__file__ ).parent .parent .parent
36+ self .temp_file = None
37+ self .output_lines = []
38+
39+ def log (self , message : str ):
40+ """Log message to both console and temp file."""
41+ print (message )
42+ self .output_lines .append (message )
43+
44+ def write_temp_file (self ):
45+ """Write all output to temporary file."""
46+ if not self .temp_file :
47+ self .temp_file = tempfile .NamedTemporaryFile (
48+ mode = 'w' ,
49+ suffix = '.txt' ,
50+ prefix = 'ci_check_' ,
51+ delete = False
52+ )
53+
54+ self .temp_file .write ('\n ' .join (self .output_lines ))
55+ self .temp_file .flush ()
56+ return self .temp_file .name
3557
3658 def run_command (self , cmd : List [str ], description : str , allow_failure : bool = False ) -> bool :
3759 """Run a command and return success status."""
3860 if self .verbose :
39- print (f"Running: { ' ' .join (cmd )} " )
61+ self . log (f"Running: { ' ' .join (cmd )} " )
4062
4163 try :
4264 result = subprocess .run (
4365 cmd ,
4466 cwd = self .project_root ,
45- capture_output = not self . verbose ,
67+ capture_output = True ,
4668 text = True ,
4769 check = True
4870 )
49- print (f"PASS: { description } " )
71+ self .log (f"PASS: { description } " )
72+ if self .verbose and result .stdout :
73+ self .log (f"Output: { result .stdout } " )
5074 return True
5175 except subprocess .CalledProcessError as e :
5276 if allow_failure :
53- print (f"WARN: { description } (allowed to fail)" )
77+ self . log (f"WARN: { description } (allowed to fail)" )
5478 if self .verbose and e .stdout :
55- print (f"Output: { e .stdout } " )
79+ self . log (f"Output: { e .stdout } " )
5680 if self .verbose and e .stderr :
57- print (f"Error: { e .stderr } " )
81+ self . log (f"Error: { e .stderr } " )
5882 return True
5983 else :
60- print (f"FAIL: { description } " )
84+ self . log (f"FAIL: { description } " )
6185 if e .stdout :
62- print (f"Output: { e .stdout } " )
86+ self . log (f"Output: { e .stdout } " )
6387 if e .stderr :
64- print (f"Error: { e .stderr } " )
88+ self . log (f"Error: { e .stderr } " )
6589 self .failed_checks .append (description )
6690 return False
6791 except FileNotFoundError :
68- print (f"FAIL: { description } (command not found)" )
92+ self . log (f"FAIL: { description } (command not found)" )
6993 self .failed_checks .append (f"{ description } (command not found)" )
7094 return False
7195
7296 def check_mypy_config (self ) -> bool :
7397 """Check mypy.ini configuration for common issues."""
74- print ("Checking mypy.ini configuration..." )
98+ self . log ("Checking mypy.ini configuration..." )
7599
76100 mypy_ini = self .project_root / "mypy.ini"
77101 if not mypy_ini .exists ():
78- print ("FAIL: mypy.ini not found" )
102+ self . log ("FAIL: mypy.ini not found" )
79103 self .failed_checks .append ("mypy.ini not found" )
80104 return False
81105
@@ -95,13 +119,13 @@ def check_mypy_config(self) -> bool:
95119 version_tuple = (major , minor )
96120
97121 if version_tuple < (3 , 9 ):
98- print (f"FAIL: Python version { python_version } is too old (need 3.9+)" )
122+ self . log (f"FAIL: Python version { python_version } is too old (need 3.9+)" )
99123 self .failed_checks .append (f"Python version { python_version } too old" )
100124 return False
101125 else :
102126 raise ValueError ("Invalid version format" )
103127 except (ValueError , IndexError ):
104- print (f"FAIL: Invalid Python version format: { python_version } " )
128+ self . log (f"FAIL: Invalid Python version format: { python_version } " )
105129 self .failed_checks .append (f"Invalid Python version format: { python_version } " )
106130 return False
107131
@@ -113,37 +137,37 @@ def check_mypy_config(self) -> bool:
113137 allowed_flags = {'ignore_missing_imports' , 'follow_imports' , 'disallow_untyped_defs' }
114138 for key in section .keys ():
115139 if key not in allowed_flags :
116- print (f"FAIL: Invalid per-module flag in { section_name } : { key } " )
140+ self . log (f"FAIL: Invalid per-module flag in { section_name } : { key } " )
117141 self .failed_checks .append (f"Invalid per-module flag: { section_name } .{ key } " )
118142 return False
119143
120- print ("PASS: mypy.ini configuration" )
144+ self . log ("PASS: mypy.ini configuration" )
121145 return True
122146
123147 except Exception as e :
124- print (f"FAIL: mypy.ini configuration error: { e } " )
148+ self . log (f"FAIL: mypy.ini configuration error: { e } " )
125149 self .failed_checks .append (f"mypy.ini error: { e } " )
126150 return False
127151
128152 def check_python_version_consistency (self ) -> bool :
129153 """Check Python version consistency across configuration files."""
130- print ("Checking Python version consistency..." )
154+ self . log ("Checking Python version consistency..." )
131155
132156 # Check pyproject.toml
133157 pyproject_toml = self .project_root / "pyproject.toml"
134158 if pyproject_toml .exists ():
135159 content = pyproject_toml .read_text ()
136160 if 'python_version = "3.8"' in content :
137- print ("FAIL: pyproject.toml still has Python 3.8 (need 3.11+)" )
161+ self . log ("FAIL: pyproject.toml still has Python 3.8 (need 3.11+)" )
138162 self .failed_checks .append ("pyproject.toml Python version too old" )
139163 return False
140164
141- print ("PASS: Python version consistency" )
165+ self . log ("PASS: Python version consistency" )
142166 return True
143167
144168 def check_syntax_errors (self ) -> bool :
145169 """Check for Python syntax errors in key files."""
146- print ("Checking Python syntax..." )
170+ self . log ("Checking Python syntax..." )
147171
148172 # Files that commonly have syntax issues
149173 problem_files = [
@@ -166,7 +190,7 @@ def check_syntax_errors(self) -> bool:
166190
167191 def run_formatting_checks (self ) -> bool :
168192 """Run code formatting checks (Black, isort)."""
169- print ("\n === Code Formatting Checks ===" )
193+ self . log ("\n === Code Formatting Checks ===" )
170194
171195 all_passed = True
172196
@@ -190,7 +214,7 @@ def run_formatting_checks(self) -> bool:
190214
191215 def run_linting_checks (self ) -> bool :
192216 """Run linting checks (flake8, mypy, pylint)."""
193- print ("\n === Linting Checks ===" )
217+ self . log ("\n === Linting Checks ===" )
194218
195219 all_passed = True
196220
@@ -279,9 +303,9 @@ def run_tests(self, quick: bool = False) -> bool:
279303
280304 def run_all_checks (self , quick : bool = False ) -> bool :
281305 """Run all CI checks."""
282- print ("=== Comprehensive CI Testing ===" )
283- print ("Running the same checks as GitHub Actions CI pipeline..." )
284- print ( )
306+ self . log ("=== Comprehensive CI Testing ===" )
307+ self . log ("Running the same checks as GitHub Actions CI pipeline..." )
308+ self . log ( "" )
285309
286310 # Configuration checks
287311 config_passed = (
@@ -291,7 +315,7 @@ def run_all_checks(self, quick: bool = False) -> bool:
291315 )
292316
293317 if not config_passed :
294- print ("\n Configuration checks failed. Fix these before running other checks." )
318+ self . log ("\n Configuration checks failed. Fix these before running other checks." )
295319 return False
296320
297321 # Code quality checks
@@ -309,18 +333,26 @@ def run_all_checks(self, quick: bool = False) -> bool:
309333 tests_passed = True # Skip tests in quick mode
310334
311335 # Summary
312- print ("\n === CI Check Summary ===" )
336+ self . log ("\n === CI Check Summary ===" )
313337 if self .failed_checks :
314- print ("FAILED CHECKS:" )
338+ self . log ("FAILED CHECKS:" )
315339 for check in self .failed_checks :
316- print (f" - { check } " )
317- print ()
318- print ("Fix these issues before pushing to avoid CI failures." )
340+ self .log (f" - { check } " )
341+ self .log ("" )
342+ self .log ("Fix these issues before pushing to avoid CI failures." )
343+
344+ # Write temp file and show location
345+ temp_file_path = self .write_temp_file ()
346+ self .log (f"\n Detailed output written to: { temp_file_path } " )
319347 return False
320348 else :
321- print ("All critical CI checks passed!" )
349+ self . log ("All critical CI checks passed!" )
322350 if self .fix :
323- print ("Formatting issues have been automatically fixed." )
351+ self .log ("Formatting issues have been automatically fixed." )
352+
353+ # Write temp file for reference
354+ temp_file_path = self .write_temp_file ()
355+ self .log (f"\n Full output written to: { temp_file_path } " )
324356 return True
325357
326358
@@ -329,12 +361,22 @@ def main():
329361 parser .add_argument ("--quick" , action = "store_true" , help = "Run only fast checks" )
330362 parser .add_argument ("--fix" , action = "store_true" , help = "Fix formatting issues automatically" )
331363 parser .add_argument ("--verbose" , action = "store_true" , help = "Show detailed output" )
364+ parser .add_argument ("--output-file" , help = "Write output to specific file instead of temp file" )
332365
333366 args = parser .parse_args ()
334367
335368 checker = CIChecker (verbose = args .verbose , fix = args .fix )
369+
370+ # Override temp file if specified
371+ if args .output_file :
372+ checker .temp_file = open (args .output_file , 'w' )
373+
336374 success = checker .run_all_checks (quick = args .quick )
337375
376+ # Clean up temp file if we created it
377+ if checker .temp_file and not args .output_file :
378+ checker .temp_file .close ()
379+
338380 sys .exit (0 if success else 1 )
339381
340382
0 commit comments