2929import os
3030import re
3131import sys
32- from typing import Dict , List , Optional , Sequence , Tuple , Union , overload
32+ from typing import Dict , List , Optional , Sequence , Tuple , NamedTuple
3333from operator import itemgetter
3434
3535# Matches a :doc:`label <path>` or :doc:`label` reference anywhere in text and
5050RELEASE_NOTES_DOC = os .path .join (DOCS_DIR , "ReleaseNotes.rst" )
5151
5252
53+ CheckLabel = str
54+ Lines = List [str ]
55+ BulletBlock = List [str ]
56+ BulletItem = Tuple [CheckLabel , BulletBlock ]
57+ BulletStart = int
58+
59+
60+ class BulletBlocks (NamedTuple ):
61+ """Structured result of parsing a bullet-list section.
62+
63+ - prefix: lines before the first bullet within the section range.
64+ - blocks: list of (label, block-lines) pairs for each bullet block.
65+ - suffix: lines after the last bullet within the section range.
66+ """
67+ prefix : Lines
68+ blocks : List [BulletItem ]
69+ suffix : Lines
70+
71+ class ScannedBlocks (NamedTuple ):
72+ """Result of scanning bullet blocks within a section range.
73+
74+ - blocks_with_pos: list of (start_index, block_lines) for each bullet block.
75+ - next_index: index where scanning stopped; start of the suffix region.
76+ """
77+ blocks_with_pos : List [Tuple [BulletStart , BulletBlock ]]
78+ next_index : int
79+
80+
81+ def _scan_bullet_blocks (
82+ lines : Sequence [str ], start : int , end : int
83+ ) -> ScannedBlocks :
84+ """Scan consecutive bullet blocks and return (blocks_with_pos, next_index).
85+
86+ Each entry in blocks_with_pos is a tuple of (start_index, block_lines).
87+ next_index is the index where scanning stopped (start of suffix).
88+ """
89+ i = start
90+ n = end
91+ blocks_with_pos : List [Tuple [BulletStart , BulletBlock ]] = []
92+ while i < n :
93+ if not _is_bullet_start (lines [i ]):
94+ break
95+ bstart = i
96+ i += 1
97+ while i < n and not _is_bullet_start (lines [i ]):
98+ if (
99+ i + 1 < n
100+ and set (lines [i + 1 ].rstrip ("\n " )) == {"^" }
101+ and lines [i ].strip ()
102+ ):
103+ break
104+ i += 1
105+ block : BulletBlock = list (lines [bstart :i ])
106+ blocks_with_pos .append ((bstart , block ))
107+ return ScannedBlocks (blocks_with_pos , i )
108+
109+
53110def read_text (path : str ) -> List [str ]:
54111 with io .open (path , "r" , encoding = "utf-8" ) as f :
55112 return f .read ().splitlines (True )
@@ -66,7 +123,7 @@ def _normalize_list_rst_lines(lines: Sequence[str]) -> List[str]:
66123 i = 0
67124 n = len (lines )
68125
69- def key_for (line : str ):
126+ def check_name (line : str ):
70127 m = DOC_LINE_RE .match (line )
71128 if not m :
72129 return (1 , "" )
@@ -89,7 +146,7 @@ def key_for(line: str):
89146 entries .append (lines [i ])
90147 i += 1
91148
92- entries_sorted = sorted (entries , key = key_for )
149+ entries_sorted = sorted (entries , key = check_name )
93150 out .extend (entries_sorted )
94151 continue
95152
@@ -99,27 +156,10 @@ def key_for(line: str):
99156 return out
100157
101158
102- @overload
103159def normalize_list_rst (data : str ) -> str :
104- ...
105-
106-
107- @overload
108- def normalize_list_rst (data : List [str ]) -> List [str ]:
109- ...
110-
111-
112- def normalize_list_rst (data : Union [str , List [str ]]) -> Union [str , List [str ]]:
113- """Normalize list.rst; returns same type as input (str or list).
114-
115- - If given a string, returns a single normalized string.
116- - If given a sequence of lines, returns a list of lines.
117- """
118- if isinstance (data , str ):
119- lines = data .splitlines (True )
120- return "" .join (_normalize_list_rst_lines (lines ))
121- else :
122- return _normalize_list_rst_lines (data )
160+ """Normalize list.rst content and return a string."""
161+ lines = data .splitlines (True )
162+ return "" .join (_normalize_list_rst_lines (lines ))
123163
124164
125165def find_heading (lines : Sequence [str ], title : str ) -> Optional [int ]:
@@ -134,7 +174,7 @@ def find_heading(lines: Sequence[str], title: str) -> Optional[int]:
134174 for i in range (len (lines ) - 1 ):
135175 if lines [i ].rstrip ("\n " ) == title :
136176 underline = lines [i + 1 ].rstrip ("\n " )
137- if underline and set (underline ) == {"^" } and len (underline ) > = len (title ):
177+ if underline and set (underline ) == {"^" } and len (underline ) = = len (title ):
138178 return i
139179 return None
140180
@@ -144,44 +184,31 @@ def extract_label(text: str) -> str:
144184 return m .group ("label" ) if m else text
145185
146186
147- def is_bullet_start (line : str ) -> bool :
187+ def _is_bullet_start (line : str ) -> bool :
148188 return line .startswith ("- " )
149189
150190
151- def parse_bullet_blocks (
191+ def _parse_bullet_blocks (
152192 lines : Sequence [str ], start : int , end : int
153- ) -> Tuple [ List [ str ], List [ Tuple [ str , List [ str ]]], List [ str ]] :
193+ ) -> BulletBlocks :
154194 i = start
155195 n = end
156196 first_bullet = i
157- while first_bullet < n and not is_bullet_start (lines [first_bullet ]):
197+ while first_bullet < n and not _is_bullet_start (lines [first_bullet ]):
158198 first_bullet += 1
159- prefix = list (lines [i :first_bullet ])
199+ prefix : Lines = list (lines [i :first_bullet ])
160200
161- blocks : List [Tuple [str , List [str ]]] = []
162- i = first_bullet
163- while i < n :
164- if not is_bullet_start (lines [i ]):
165- break
166- bstart = i
167- i += 1
168- while i < n and not is_bullet_start (lines [i ]):
169- if (
170- i + 1 < n
171- and set (lines [i + 1 ].rstrip ("\n " )) == {"^" }
172- and lines [i ].strip ()
173- ):
174- break
175- i += 1
176- block = list (lines [bstart :i ])
177- key = extract_label (block [0 ])
201+ blocks : List [BulletItem ] = []
202+ res = _scan_bullet_blocks (lines , first_bullet , n )
203+ for _ , block in res .blocks_with_pos :
204+ key : CheckLabel = extract_label (block [0 ])
178205 blocks .append ((key , block ))
179206
180- suffix = list (lines [i :n ])
181- return prefix , blocks , suffix
207+ suffix : Lines = list (lines [res . next_index :n ])
208+ return BulletBlocks ( prefix , blocks , suffix )
182209
183210
184- def sort_blocks (blocks : List [Tuple [ str , List [ str ]]] ) -> List [List [ str ] ]:
211+ def sort_blocks (blocks : List [BulletItem ] ) -> List [BulletBlock ]:
185212 """Return blocks sorted deterministically by their extracted label.
186213
187214 Duplicates are preserved; merging is left to authors to handle manually.
@@ -206,24 +233,12 @@ def find_duplicate_entries(
206233 i = sec_start
207234 n = sec_end
208235
209- while i < n and not is_bullet_start (lines [i ]):
236+ while i < n and not _is_bullet_start (lines [i ]):
210237 i += 1
211238
212239 blocks_with_pos : List [Tuple [str , int , List [str ]]] = []
213- while i < n :
214- if not is_bullet_start (lines [i ]):
215- break
216- bstart = i
217- i += 1
218- while i < n and not is_bullet_start (lines [i ]):
219- if (
220- i + 1 < n
221- and set (lines [i + 1 ].rstrip ("\n " )) == {"^" }
222- and lines [i ].strip ()
223- ):
224- break
225- i += 1
226- block = list (lines [bstart :i ])
240+ res = _scan_bullet_blocks (lines , i , n )
241+ for bstart , block in res .blocks_with_pos :
227242 key = extract_label (block [0 ])
228243 blocks_with_pos .append ((key , bstart , block ))
229244
@@ -287,7 +302,7 @@ def _normalize_release_notes_section(
287302 return list (lines )
288303 _ , sec_start , sec_end = bounds
289304
290- prefix , blocks , suffix = parse_bullet_blocks (lines , sec_start , sec_end )
305+ prefix , blocks , suffix = _parse_bullet_blocks (lines , sec_start , sec_end )
291306 sorted_blocks = sort_blocks (blocks )
292307
293308 new_section : List [str ] = []
@@ -374,20 +389,8 @@ def main(argv: Sequence[str]) -> int:
374389 else :
375390 return process_checks_list (out_path , list_doc )
376391
377- list_lines = read_text (list_doc )
378- rn_lines = read_text (rn_doc )
379- list_norm = normalize_list_rst ("" .join (list_lines ))
380- rn_norm = normalize_release_notes (rn_lines )
381- if "" .join (list_lines ) != list_norm :
382- write_text (list_doc , list_norm )
383- if "" .join (rn_lines ) != rn_norm :
384- write_text (rn_doc , rn_norm )
385-
386- report = _emit_duplicate_report (rn_lines , "Changes in existing checks" )
387- if report :
388- sys .stderr .write (report )
389- return 3
390- return 0
392+ process_checks_list (list_doc , list_doc )
393+ return process_release_notes (rn_doc , rn_doc )
391394
392395
393396if __name__ == "__main__" :
0 commit comments