1+ """Maintenance Chart Checker
2+
3+ This script analyzes maintenance charts in reStructuredText (.rst) files. It recursively scans
4+ through directories looking for .rst files containing maintenance charts in the format:
5+
6+ **Maintenance chart**
7+
8+ +--------------+-------------------------------+----------------+--------------------------------+
9+ | Review Date | Working Group Reviewer | Release |Test situation |
10+ +--------------+-------------------------------+----------------+--------------------------------+
11+ | 2025-04-13 | reviewer | Release | Pass/Fail |
12+ +--------------+-------------------------------+----------------+--------------------------------+
13+
14+ The script generates a summary report showing:
15+ - Total number of files with maintenance charts in each directory
16+ - Number of empty maintenance charts
17+ - Number of charts with passing test situations
18+ - Number of charts with failing test situations
19+
20+ Usage:
21+ # Run from current directory (default depth=1)
22+ python maintenance_chart_checker.py
23+
24+ # Run from specific directory with custom depth
25+ python maintenance_chart_checker.py path/to/docs --depth 2
26+
27+ # Show full directory paths (depth=0)
28+ python maintenance_chart_checker.py --depth 0
29+
30+ # Show help
31+ python maintenance_chart_checker.py -h
32+
33+ Example output with depth=1:
34+ ------------------------------------------------------------
35+ | Folder | Total | Empty | Passed | Failed |
36+ ------------------------------------------------------------
37+ | educators | 12 | 3 | 5 | 4 |
38+ | faculty | 8 | 1 | 4 | 3 |
39+ ------------------------------------------------------------
40+
41+ Example output with depth=2:
42+ ------------------------------------------------------------
43+ | Folder | Total | Empty | Passed | Failed |
44+ ------------------------------------------------------------
45+ | educators/how-tos | 8 | 2 | 4 | 2 |
46+ | educators/reference | 4 | 1 | 1 | 2 |
47+ | faculty/guides | 8 | 1 | 4 | 3 |
48+ ------------------------------------------------------------
49+ """
50+
51+ import os
52+ import argparse
53+ from typing import Dict , List , NamedTuple
54+ from collections import defaultdict
55+
56+ class MaintenanceStatus (NamedTuple ):
57+ is_empty : bool
58+ has_fail : bool
59+ has_pass : bool
60+ file_path : str
61+
62+ def parse_maintenance_chart (content : str ) -> MaintenanceStatus | None :
63+ """Parse the maintenance chart in the content and return its status."""
64+ # Check if maintenance chart exists
65+ if "**Maintenance chart**" not in content :
66+ return None
67+
68+ # Split content into lines and find the maintenance chart section
69+ lines = content .split ('\n ' )
70+ try :
71+ chart_start = next (i for i , line in enumerate (lines ) if "**Maintenance chart**" in line )
72+ except StopIteration :
73+ return None
74+
75+ # Find both header separator lines
76+ try :
77+ separators = [i for i in range (chart_start , len (lines ))
78+ if '+-' in lines [i ] and '-+' in lines [i ]]
79+ if len (separators ) < 2 : # Need both top and bottom separator
80+ return None
81+ first_data_idx = separators [1 ] + 1 # Take the line after the second separator
82+ except (StopIteration , IndexError ):
83+ return None
84+
85+ # Get the first data row
86+ if first_data_idx >= len (lines ):
87+ return None
88+
89+ first_data_row = lines [first_data_idx ].strip ()
90+
91+ # Check if the row is empty (just separators and spaces)
92+ is_empty = all (cell .strip () in ['|' , '' , ' ' ] for cell in first_data_row .split ('|' ))
93+
94+ # Look for pass/fail in test situation column (last column)
95+ columns = [col .strip ().lower () for col in first_data_row .split ('|' ) if col .strip ()]
96+ if not columns : # Empty row
97+ return MaintenanceStatus (True , False , False , "" )
98+
99+ # The test situation is in the last column
100+ test_situation = columns [- 1 ] if len (columns ) >= 4 else ""
101+ has_fail = 'fail' in test_situation .lower ()
102+ has_pass = 'pass' in test_situation .lower ()
103+
104+ return MaintenanceStatus (is_empty , has_fail , has_pass , "" )
105+
106+ def get_folder_path (path : str , start_path : str , depth : int ) -> str :
107+ """Get the directory path at specified depth relative to start_path."""
108+ rel_path = os .path .relpath (path , start_path )
109+ if depth == 0 :
110+ return rel_path
111+ parts = rel_path .split (os .sep )
112+ return os .sep .join (parts [:depth ]) if depth < len (parts ) else rel_path
113+
114+ def scan_directory (start_path : str , depth : int ) -> Dict [str , List [MaintenanceStatus ]]:
115+ """Recursively scan directory for .rst files and analyze maintenance charts."""
116+ results = defaultdict (list )
117+
118+ for root , _ , files in os .walk (start_path ):
119+ rst_files = [f for f in files if f .endswith ('.rst' )]
120+ if not rst_files :
121+ continue
122+
123+ for file in rst_files :
124+ file_path = os .path .join (root , file )
125+ try :
126+ with open (file_path , 'r' , encoding = 'utf-8' ) as f :
127+ content = f .read ()
128+ status = parse_maintenance_chart (content )
129+ if status is not None : # Only include files with maintenance charts
130+ folder_path = get_folder_path (root , start_path , depth )
131+ results [folder_path ].append (MaintenanceStatus (
132+ status .is_empty ,
133+ status .has_fail ,
134+ status .has_pass ,
135+ file_path
136+ ))
137+ except Exception as e :
138+ print (f"Error processing { file_path } : { e } " )
139+
140+ return results
141+
142+ def print_report (results : Dict [str , List [MaintenanceStatus ]]):
143+ """Print a formatted report table of the maintenance chart analysis."""
144+ print ("\n Maintenance Chart Analysis Report" )
145+ print ("================================\n " )
146+
147+ # Print table header
148+ header = "| {:<30} | {:>8} | {:>8} | {:>8} | {:>8} |" .format (
149+ "Folder" , "Total" , "Empty" , "Passed" , "Failed"
150+ )
151+ separator = "-" * len (header )
152+
153+ print (separator )
154+ print (header )
155+ print (separator )
156+
157+ # Print table rows
158+ for folder , statuses in sorted (results .items ()):
159+ total = len (statuses )
160+ empty = sum (1 for s in statuses if s .is_empty )
161+ passed = sum (1 for s in statuses if s .has_pass )
162+ failed = sum (1 for s in statuses if s .has_fail )
163+
164+ row = "| {:<30} | {:>8} | {:>8} | {:>8} | {:>8} |" .format (
165+ folder , total , empty , passed , failed
166+ )
167+ print (row )
168+
169+ print (separator )
170+
171+ def main ():
172+ parser = argparse .ArgumentParser (
173+ description = 'Analyze maintenance charts in .rst files.' ,
174+ formatter_class = argparse .RawDescriptionHelpFormatter ,
175+ epilog = """
176+ Examples:
177+ %(prog)s # Run from current directory (depth=1)
178+ %(prog)s path/to/docs # Run from specified directory
179+ %(prog)s --depth 2 # Group by two directory levels
180+ %(prog)s --depth 0 # Show full directory paths
181+ %(prog)s -h # Show this help message
182+ """
183+ )
184+
185+ parser .add_argument (
186+ 'start_dir' ,
187+ nargs = '?' ,
188+ default = '.' ,
189+ help = 'Directory to start scanning from (default: current directory)'
190+ )
191+
192+ parser .add_argument (
193+ '--depth' , '-d' ,
194+ type = int ,
195+ default = 1 ,
196+ help = 'Number of directory levels to show in report (0 for full path, default: 1)'
197+ )
198+
199+ args = parser .parse_args ()
200+
201+ # Convert to absolute path and verify directory exists
202+ start_path = os .path .abspath (args .start_dir )
203+ if not os .path .isdir (start_path ):
204+ print (f"Error: Directory '{ args .start_dir } ' does not exist" )
205+ return
206+
207+ results = scan_directory (start_path , args .depth )
208+ print_report (results )
209+
210+ if __name__ == "__main__" :
211+ main ()
0 commit comments