88 separates them (space between ':' and first bullet point).
993) List item indentation must be a multiple of 4 spaces (0,4,8,...). Normalize to the next
1010 multiple of 4 for indented lists.
11+ 4) In Python fenced code blocks, clamp accidental over-indentation after block openers such as
12+ `with`, `if`, and `for` to a single additional indentation level.
1113"""
1214from __future__ import annotations
1315
@@ -51,6 +53,15 @@ def _parse_fence(line: str) -> Optional[Tuple[str, int]]:
5153 return fence [0 ], len (fence )
5254
5355
56+ def _is_python_fence (line : str ) -> bool :
57+ stripped = line .lstrip ()
58+ match = FENCE_RE .match (stripped )
59+ if not match :
60+ return False
61+ info = match .group (2 ).strip ().lower ()
62+ return info .startswith ("python" )
63+
64+
5465def _is_fence_close (line : str , fence_char : str , fence_len : int ) -> bool :
5566 stripped = line .lstrip ()
5667 if not stripped .startswith (fence_char * fence_len ):
@@ -67,25 +78,42 @@ def _is_fence_close(line: str, fence_char: str, fence_len: int) -> bool:
6778
6879def process_file (
6980 path : Path ,
70- ) -> Optional [Tuple [int , int , int , List [int ], List [int ], List [int ]]]:
81+ ) -> Optional [
82+ Tuple [
83+ int ,
84+ int ,
85+ int ,
86+ int ,
87+ List [int ],
88+ List [int ],
89+ List [int ],
90+ List [int ],
91+ List [int ],
92+ ]
93+ ]:
7194 original = path .read_text (encoding = "utf-8" )
7295 lines = original .splitlines ()
7396 new_lines : list [str ] = []
7497 in_fence = False
98+ in_python_fence = False
7599 fence_char : Optional [str ] = None
76100 fence_len : Optional [int ] = None
77101 changed = False
78102 header_fixes = 0
79103 blank_lines = 0
80104 heading_blank_lines = 0
81105 list_indent_fixes = 0
106+ code_indent_fixes = 0
82107 header_lines : List [int ] = []
83108 blank_lines_at : List [int ] = []
84109 heading_blank_lines_at : List [int ] = []
85110 list_indent_lines : List [int ] = []
111+ code_indent_lines : List [int ] = []
86112
87113 last_list_indent_len : Optional [int ] = None
88114 last_list_fixed_len : Optional [int ] = None
115+ last_code_indent_len : Optional [int ] = None
116+ last_code_line : Optional [str ] = None
89117
90118 i = 0
91119 while i < len (lines ):
@@ -95,16 +123,45 @@ def process_file(
95123 if fence_char is not None and fence_len is not None :
96124 if _is_fence_close (line , fence_char , fence_len ):
97125 in_fence = False
126+ in_python_fence = False
98127 fence_char = None
99128 fence_len = None
129+ last_code_indent_len = None
130+ last_code_line = None
131+ new_lines .append (line )
132+ i += 1
133+ continue
134+
135+ if in_python_fence and line .strip ():
136+ indent_len = len (line ) - len (line .lstrip (" " ))
137+ if (
138+ last_code_indent_len is not None
139+ and last_code_line is not None
140+ and last_code_line .rstrip ().endswith (":" )
141+ and indent_len > last_code_indent_len + 4
142+ ):
143+ line = " " * (last_code_indent_len + 4 ) + line .lstrip (" " )
144+ changed = True
145+ code_indent_fixes += 1
146+ code_indent_lines .append (i + 1 )
147+
148+ last_code_indent_len = len (line ) - len (line .lstrip (" " ))
149+ last_code_line = line
150+ elif in_python_fence :
151+ last_code_indent_len = None
152+ last_code_line = None
153+
100154 new_lines .append (line )
101155 i += 1
102156 continue
103157
104158 fence = _parse_fence (line )
105159 if fence :
106160 in_fence = True
161+ in_python_fence = _is_python_fence (line )
107162 fence_char , fence_len = fence
163+ last_code_indent_len = None
164+ last_code_line = None
108165 new_lines .append (line )
109166 i += 1
110167 continue
@@ -187,9 +244,11 @@ def process_file(
187244 header_fixes ,
188245 blank_lines ,
189246 list_indent_fixes ,
247+ code_indent_fixes ,
190248 header_lines ,
191249 blank_lines_at ,
192250 list_indent_lines ,
251+ code_indent_lines ,
193252 heading_blank_lines ,
194253 heading_blank_lines_at ,
195254 )
@@ -215,16 +274,19 @@ def main() -> int:
215274 total_header = 0
216275 total_blank = 0
217276 total_indent = 0
277+ total_code_indent = 0
218278 for md_file in iter_md_files (docs_root ):
219279 result = process_file (md_file )
220280 if result :
221281 (
222282 header_fixes ,
223283 blank_lines ,
224284 list_indent_fixes ,
285+ code_indent_fixes ,
225286 header_lines ,
226287 blank_lines_at ,
227288 list_indent_lines ,
289+ code_indent_lines ,
228290 heading_blank_lines ,
229291 heading_blank_lines_at ,
230292 ) = result
@@ -234,34 +296,40 @@ def main() -> int:
234296 header_fixes ,
235297 blank_lines ,
236298 list_indent_fixes ,
299+ code_indent_fixes ,
237300 header_lines ,
238301 blank_lines_at ,
239302 list_indent_lines ,
303+ code_indent_lines ,
240304 heading_blank_lines ,
241305 heading_blank_lines_at ,
242306 )
243307 )
244308 total_header += header_fixes
245309 total_blank += blank_lines
246310 total_indent += list_indent_fixes
311+ total_code_indent += code_indent_fixes
247312 total_blank += heading_blank_lines
248313
249314 print (f"Updated { len (changed_files )} files" )
250315 print (
251316 "Totals: "
252317 f"headers fixed={ total_header } , "
253318 f"blank lines inserted={ total_blank } , "
254- f"list indents normalized={ total_indent } "
319+ f"list indents normalized={ total_indent } , "
320+ f"python code indents normalized={ total_code_indent } "
255321 )
256322
257323 for (
258324 path ,
259325 header_fixes ,
260326 blank_lines ,
261327 list_indent_fixes ,
328+ code_indent_fixes ,
262329 header_lines ,
263330 blank_lines_at ,
264331 list_indent_lines ,
332+ code_indent_lines ,
265333 heading_blank_lines ,
266334 heading_blank_lines_at ,
267335 ) in changed_files :
@@ -278,6 +346,9 @@ def main() -> int:
278346 if list_indent_fixes :
279347 print (f" List indents normalized: { list_indent_fixes } " )
280348 print (f" Lines: { _format_line_list (list_indent_lines )} " )
349+ if code_indent_fixes :
350+ print (f" Python code indents normalized: { code_indent_fixes } " )
351+ print (f" Lines: { _format_line_list (code_indent_lines )} " )
281352
282353 return 0
283354
0 commit comments