wexample-filestate-python 0.0.48__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. wexample_filestate_python/__init__.py +0 -0
  2. wexample_filestate_python/__pycache__/__init__.py +0 -0
  3. wexample_filestate_python/common/__init__.py +0 -0
  4. wexample_filestate_python/common/__pycache__/__init__.py +0 -0
  5. wexample_filestate_python/common/pipy_gateway.py +20 -0
  6. wexample_filestate_python/config_option/__init__.py +0 -0
  7. wexample_filestate_python/config_option/__pycache__/__init__.py +0 -0
  8. wexample_filestate_python/config_option/mixin/__init__.py +0 -0
  9. wexample_filestate_python/config_option/mixin/__pycache__/__init__.py +0 -0
  10. wexample_filestate_python/config_option/mixin/with_stdout_wrapping_mixin.py +46 -0
  11. wexample_filestate_python/config_value/__init__.py +0 -0
  12. wexample_filestate_python/config_value/__pycache__/__init__.py +0 -0
  13. wexample_filestate_python/config_value/python_config_value.py +195 -0
  14. wexample_filestate_python/const/__init__.py +0 -0
  15. wexample_filestate_python/const/__pycache__/__init__.py +0 -0
  16. wexample_filestate_python/const/name_pattern.py +4 -0
  17. wexample_filestate_python/const/python_file.py +5 -0
  18. wexample_filestate_python/file/__init__.py +0 -0
  19. wexample_filestate_python/file/__pycache__/__init__.py +0 -0
  20. wexample_filestate_python/file/python_file.py +12 -0
  21. wexample_filestate_python/helpers/__init__.py +0 -0
  22. wexample_filestate_python/helpers/__pycache__/__init__.py +0 -0
  23. wexample_filestate_python/helpers/package.py +122 -0
  24. wexample_filestate_python/helpers/toml.py +116 -0
  25. wexample_filestate_python/option/__init__.py +0 -0
  26. wexample_filestate_python/option/__pycache__/__init__.py +0 -0
  27. wexample_filestate_python/option/abstract_python_file_content_option.py +45 -0
  28. wexample_filestate_python/option/add_future_annotations_option.py +79 -0
  29. wexample_filestate_python/option/add_return_types_option.py +265 -0
  30. wexample_filestate_python/option/fix_attrs_option.py +37 -0
  31. wexample_filestate_python/option/fix_blank_lines_option.py +47 -0
  32. wexample_filestate_python/option/format_option.py +34 -0
  33. wexample_filestate_python/option/fstringify_option.py +34 -0
  34. wexample_filestate_python/option/modernize_typing_option.py +25 -0
  35. wexample_filestate_python/option/order_class_attributes_option.py +34 -0
  36. wexample_filestate_python/option/order_class_docstring_option.py +36 -0
  37. wexample_filestate_python/option/order_class_methods_option.py +37 -0
  38. wexample_filestate_python/option/order_constants_option.py +35 -0
  39. wexample_filestate_python/option/order_iterable_items_option.py +31 -0
  40. wexample_filestate_python/option/order_main_guard_option.py +44 -0
  41. wexample_filestate_python/option/order_module_docstring_option.py +73 -0
  42. wexample_filestate_python/option/order_module_functions_option.py +42 -0
  43. wexample_filestate_python/option/order_module_metadata_option.py +62 -0
  44. wexample_filestate_python/option/order_type_checking_block_option.py +51 -0
  45. wexample_filestate_python/option/python_option.py +164 -0
  46. wexample_filestate_python/option/relocate_imports_option.py +189 -0
  47. wexample_filestate_python/option/remove_unused_option.py +45 -0
  48. wexample_filestate_python/option/sort_imports_option.py +26 -0
  49. wexample_filestate_python/option/unquote_annotations_option.py +85 -0
  50. wexample_filestate_python/options_provider/__init__.py +0 -0
  51. wexample_filestate_python/options_provider/__pycache__/__init__.py +0 -0
  52. wexample_filestate_python/options_provider/python_options_provider.py +24 -0
  53. wexample_filestate_python/py.typed +0 -0
  54. wexample_filestate_python/utils/__init__.py +0 -0
  55. wexample_filestate_python/utils/__pycache__/__init__.py +0 -0
  56. wexample_filestate_python/utils/python_attrs_utils.py +112 -0
  57. wexample_filestate_python/utils/python_blank_lines_utils.py +568 -0
  58. wexample_filestate_python/utils/python_class_attributes_utils.py +275 -0
  59. wexample_filestate_python/utils/python_class_docstring_utils.py +85 -0
  60. wexample_filestate_python/utils/python_class_methods_utils.py +230 -0
  61. wexample_filestate_python/utils/python_constants_utils.py +302 -0
  62. wexample_filestate_python/utils/python_docstring_utils.py +117 -0
  63. wexample_filestate_python/utils/python_functions_utils.py +212 -0
  64. wexample_filestate_python/utils/python_iterable_utils.py +131 -0
  65. wexample_filestate_python/utils/python_main_guard_utils.py +80 -0
  66. wexample_filestate_python/utils/python_module_metadata_utils.py +147 -0
  67. wexample_filestate_python/utils/python_type_checking_utils.py +113 -0
  68. wexample_filestate_python/utils/relocate_imports/__init__.py +7 -0
  69. wexample_filestate_python/utils/relocate_imports/__pycache__/__init__.py +0 -0
  70. wexample_filestate_python/utils/relocate_imports/python_import_rewriter.py +413 -0
  71. wexample_filestate_python/utils/relocate_imports/python_localize_runtime_imports.py +324 -0
  72. wexample_filestate_python/utils/relocate_imports/python_parser_import_index.py +80 -0
  73. wexample_filestate_python/utils/relocate_imports/python_runtime_symbol_collector.py +33 -0
  74. wexample_filestate_python/utils/relocate_imports/python_usage_collector.py +410 -0
  75. wexample_filestate_python/workdir/__init__.py +0 -0
  76. wexample_filestate_python/workdir/__pycache__/__init__.py +0 -0
  77. wexample_filestate_python-0.0.48.dist-info/METADATA +191 -0
  78. wexample_filestate_python-0.0.48.dist-info/RECORD +80 -0
  79. wexample_filestate_python-0.0.48.dist-info/WHEEL +4 -0
  80. wexample_filestate_python-0.0.48.dist-info/entry_points.txt +4 -0
@@ -0,0 +1,568 @@
1
+ from __future__ import annotations
2
+
3
+ import libcst as cst
4
+
5
+
6
+ def fix_function_blank_lines(module: cst.Module) -> cst.Module:
7
+ """Remove blank lines after function/method signatures and class definitions throughout the module.
8
+
9
+ This applies to:
10
+ - Module-level functions
11
+ - Class methods
12
+ - Nested functions
13
+ - Class definitions (no blank lines after class signature)
14
+ """
15
+
16
+ class BlankLinesFixer(cst.CSTTransformer):
17
+ def leave_FunctionDef(
18
+ self, original_node: cst.FunctionDef, updated_node: cst.FunctionDef
19
+ ) -> cst.FunctionDef:
20
+ # Fix blank lines in the function body
21
+ new_body = _remove_leading_blank_lines_from_suite(updated_node.body)
22
+ if new_body is not updated_node.body:
23
+ return updated_node.with_changes(body=new_body)
24
+ return updated_node
25
+
26
+ def leave_ClassDef(
27
+ self, original_node: cst.ClassDef, updated_node: cst.ClassDef
28
+ ) -> cst.ClassDef:
29
+ # Fix blank lines in the class body
30
+ new_body = _remove_leading_blank_lines_from_class_suite(
31
+ updated_node.body, class_node=updated_node
32
+ )
33
+ if new_body is not updated_node.body:
34
+ return updated_node.with_changes(body=new_body)
35
+ return updated_node
36
+
37
+ transformer = BlankLinesFixer()
38
+ modified_module = module.visit(transformer)
39
+
40
+ # Also fix module-level docstring spacing
41
+ modified_module = _fix_module_docstring_spacing(modified_module)
42
+
43
+ # Note: Module-level blank line normalization (between classes/functions/imports)
44
+ # is handled by Black, so we don't duplicate that logic here.
45
+ return modified_module
46
+
47
+
48
+ def _contains_union_operator(node: cst.CSTNode) -> bool:
49
+ """Recursively check if a node contains the union operator (|)."""
50
+ if isinstance(node, cst.BinaryOperation):
51
+ if isinstance(node.operator, cst.BitOr): # | operator
52
+ return True
53
+ return _contains_union_operator(node.left) or _contains_union_operator(
54
+ node.right
55
+ )
56
+ return False
57
+
58
+
59
+ def _fix_module_docstring_spacing(module: cst.Module) -> cst.Module:
60
+ """Fix spacing around module docstring: 0 lines before, 1 line after."""
61
+ body_list = list(module.body)
62
+ if not body_list:
63
+ return module
64
+
65
+ changed = False
66
+
67
+ # Check if module has header with blank lines
68
+ if module.header:
69
+ # Remove blank lines from module header
70
+ new_header = [
71
+ line
72
+ for line in module.header
73
+ if not (isinstance(line, cst.EmptyLine) and line.comment is None)
74
+ ]
75
+ if len(new_header) != len(module.header):
76
+ module = module.with_changes(header=new_header)
77
+ changed = True
78
+
79
+ # First, remove any leading EmptyLine elements at the start of the module
80
+ while (
81
+ body_list
82
+ and isinstance(body_list[0], cst.EmptyLine)
83
+ and body_list[0].comment is None
84
+ ):
85
+ body_list.pop(0)
86
+ changed = True
87
+
88
+ # Find module docstring (first statement that's a string literal)
89
+ docstring_idx = -1
90
+ for i, stmt in enumerate(body_list):
91
+ if isinstance(stmt, cst.SimpleStatementLine) and len(stmt.body) == 1:
92
+ small = stmt.body[0]
93
+ if isinstance(small, cst.Expr) and isinstance(
94
+ small.value, cst.SimpleString
95
+ ):
96
+ docstring_idx = i
97
+ break
98
+ # Stop at first non-simple statement
99
+ elif not isinstance(stmt, cst.SimpleStatementLine):
100
+ break
101
+
102
+ if docstring_idx == -1:
103
+ # No docstring found - ensure first statement has no leading blank lines
104
+ if body_list:
105
+ first_stmt = body_list[0]
106
+ if hasattr(first_stmt, "leading_lines") and first_stmt.leading_lines:
107
+ new_leading = [
108
+ line
109
+ for line in first_stmt.leading_lines
110
+ if not (isinstance(line, cst.EmptyLine) and line.comment is None)
111
+ ]
112
+ if len(new_leading) != len(first_stmt.leading_lines):
113
+ body_list[0] = first_stmt.with_changes(leading_lines=new_leading)
114
+ changed = True
115
+
116
+ if not changed:
117
+ return module
118
+ return module.with_changes(body=body_list)
119
+
120
+ docstring_stmt = body_list[docstring_idx]
121
+
122
+ # Rule 1: 0 blank lines before module docstring
123
+ if docstring_idx == 0:
124
+ # Docstring is first - remove any leading blank lines
125
+ if docstring_stmt.leading_lines:
126
+ new_leading = [
127
+ line
128
+ for line in docstring_stmt.leading_lines
129
+ if not (isinstance(line, cst.EmptyLine) and line.comment is None)
130
+ ]
131
+ if len(new_leading) != len(docstring_stmt.leading_lines):
132
+ body_list[docstring_idx] = docstring_stmt.with_changes(
133
+ leading_lines=new_leading
134
+ )
135
+ changed = True
136
+
137
+ # Rule 2: 1 blank line after module docstring
138
+ next_idx = docstring_idx + 1
139
+ if next_idx < len(body_list):
140
+ next_stmt = body_list[next_idx]
141
+
142
+ # Count existing blank lines after docstring
143
+ blank_count = 0
144
+ if isinstance(next_stmt, cst.SimpleStatementLine):
145
+ # Count blank leading_lines
146
+ for line in next_stmt.leading_lines:
147
+ if isinstance(line, cst.EmptyLine) and line.comment is None:
148
+ blank_count += 1
149
+
150
+ # Ensure exactly 1 blank line
151
+ if blank_count != 1:
152
+ if isinstance(next_stmt, cst.SimpleStatementLine):
153
+ # Remove all blank leading lines and add exactly one
154
+ non_blank_leading = [
155
+ line
156
+ for line in next_stmt.leading_lines
157
+ if not (isinstance(line, cst.EmptyLine) and line.comment is None)
158
+ ]
159
+ new_leading = [cst.EmptyLine()] + non_blank_leading
160
+ body_list[next_idx] = next_stmt.with_changes(leading_lines=new_leading)
161
+ changed = True
162
+
163
+ if not changed:
164
+ return module
165
+
166
+ return module.with_changes(body=body_list)
167
+
168
+
169
+ def _has_default_value(node: cst.CSTNode) -> bool:
170
+ """Check if a property assignment has a default value."""
171
+ if isinstance(node, cst.SimpleStatementLine):
172
+ if len(node.body) == 1:
173
+ stmt = node.body[0]
174
+ # Check for annotated assignment with default (e.g., x: int = 5)
175
+ if isinstance(stmt, cst.AnnAssign):
176
+ return stmt.value is not None
177
+ # Check for regular assignment (e.g., x = 5)
178
+ elif isinstance(stmt, cst.Assign):
179
+ return True
180
+ return False
181
+
182
+
183
+ def _is_blank_line(node: cst.CSTNode) -> bool:
184
+ """Return True if node is an EmptyLine without a comment (blank line)."""
185
+ return isinstance(node, cst.EmptyLine) and node.comment is None
186
+
187
+
188
+ def _is_class_definition(node: cst.CSTNode) -> bool:
189
+ """Check if node is a class definition."""
190
+ return isinstance(node, cst.ClassDef)
191
+
192
+
193
+ def _is_class_property(node: cst.CSTNode) -> bool:
194
+ """Check if node is a class property (assignment statement)."""
195
+ if isinstance(node, cst.SimpleStatementLine):
196
+ if len(node.body) == 1 and isinstance(node.body[0], cst.Assign):
197
+ # Check if it's a simple assignment (not a method or function)
198
+ assign = node.body[0]
199
+ if len(assign.targets) == 1:
200
+ target = assign.targets[0].target
201
+ return isinstance(target, cst.Name)
202
+ return False
203
+
204
+
205
+ def _is_dataclass(class_node: cst.ClassDef) -> bool:
206
+ """Check if a class has @dataclass decorator."""
207
+ for decorator in class_node.decorators:
208
+ if isinstance(decorator.decorator, cst.Name):
209
+ if decorator.decorator.value == "dataclass":
210
+ return True
211
+ elif isinstance(decorator.decorator, cst.Call):
212
+ if isinstance(decorator.decorator.func, cst.Name):
213
+ if decorator.decorator.func.value == "dataclass":
214
+ return True
215
+ return False
216
+
217
+
218
+ def _is_function_definition(node: cst.CSTNode) -> bool:
219
+ """Check if node is a function definition."""
220
+ return isinstance(node, cst.FunctionDef)
221
+
222
+
223
+ def _is_import_statement(node: cst.CSTNode) -> bool:
224
+ """Check if node is an import statement."""
225
+ return isinstance(node, (cst.Import, cst.ImportFrom))
226
+
227
+
228
+ def _is_lowercase_property(node: cst.CSTNode) -> bool:
229
+ """Check if node is a lowercase class property."""
230
+ if _is_class_property(node):
231
+ assign = node.body[0]
232
+ target = assign.targets[0].target
233
+ if isinstance(target, cst.Name):
234
+ return target.value.islower()
235
+ return False
236
+
237
+
238
+ def _is_main_guard(node: cst.CSTNode) -> bool:
239
+ """Check if node is an if __name__ == '__main__' block (Black compatibility)."""
240
+ if isinstance(node, cst.If):
241
+ test = node.test
242
+ # Check for __name__ == "__main__" pattern
243
+ if isinstance(test, cst.Comparison):
244
+ if (
245
+ len(test.comparisons) == 1
246
+ and isinstance(test.left, cst.Name)
247
+ and test.left.value == "__name__"
248
+ ):
249
+ comparison = test.comparisons[0]
250
+ if (
251
+ isinstance(comparison.operator, cst.Equal)
252
+ and isinstance(comparison.comparator, cst.SimpleString)
253
+ and comparison.comparator.value in ('"__main__"', "'__main__'")
254
+ ):
255
+ return True
256
+ return False
257
+
258
+
259
+ def _is_type_alias(node: cst.CSTNode) -> bool:
260
+ """Check if node is a type alias assignment (Black compatibility)."""
261
+ if isinstance(node, cst.SimpleStatementLine):
262
+ if len(node.body) == 1 and isinstance(node.body[0], cst.Assign):
263
+ assign = node.body[0]
264
+ if len(assign.targets) == 1:
265
+ target = assign.targets[0].target
266
+ # Type alias: variable name starts with uppercase or contains union (|)
267
+ if isinstance(target, cst.Name):
268
+ name = target.value
269
+ # Check if it's a type alias pattern (starts with uppercase)
270
+ if name[0].isupper():
271
+ return True
272
+ # Check if assignment contains union operator (|) indicating type alias
273
+ if isinstance(assign.value, cst.BinaryOperation):
274
+ return _contains_union_operator(assign.value)
275
+ return False
276
+
277
+
278
+ def _is_uppercase_property(node: cst.CSTNode) -> bool:
279
+ """Check if node is an UPPERCASE class property."""
280
+ if _is_class_property(node):
281
+ assign = node.body[0]
282
+ target = assign.targets[0].target
283
+ if isinstance(target, cst.Name):
284
+ return target.value.isupper()
285
+ return False
286
+
287
+
288
+ def _normalize_class_properties_spacing(
289
+ suite: cst.Suite, is_dataclass: bool = False
290
+ ) -> cst.Suite:
291
+ """Normalize spacing in class properties section.
292
+
293
+ Rules:
294
+ - No blank lines between properties
295
+ - Exception: blank line when transitioning from UPPERCASE to lowercase properties
296
+ - Exception (dataclass): blank line between required properties (no default) and optional properties (with default)
297
+ - Blank line before first method after properties section
298
+ """
299
+ body_list = list(suite.body)
300
+ if len(body_list) <= 1:
301
+ return suite
302
+
303
+ changed = False
304
+
305
+ # Find the properties section (before first method)
306
+ first_method_idx = -1
307
+ for i, node in enumerate(body_list):
308
+ if isinstance(node, cst.FunctionDef):
309
+ first_method_idx = i
310
+ break
311
+
312
+ if first_method_idx == -1:
313
+ # No methods found, apply to entire body
314
+ first_method_idx = len(body_list)
315
+
316
+ # Process properties section (skip if first element is docstring to avoid Black conflicts)
317
+ start_idx = 1
318
+ if (
319
+ body_list
320
+ and isinstance(body_list[0], cst.SimpleStatementLine)
321
+ and len(body_list[0].body) == 1
322
+ and isinstance(body_list[0].body[0], cst.Expr)
323
+ and isinstance(body_list[0].body[0].value, cst.SimpleString)
324
+ ):
325
+ # First element is a docstring, start processing from index 2 to avoid modifying after docstring
326
+ start_idx = 2
327
+
328
+ for i in range(start_idx, first_method_idx):
329
+ current_node = body_list[i]
330
+ prev_node = body_list[i - 1]
331
+
332
+ if not hasattr(current_node, "leading_lines"):
333
+ continue
334
+
335
+ # Count blank lines
336
+ blank_count = sum(
337
+ 1
338
+ for line in current_node.leading_lines
339
+ if isinstance(line, cst.EmptyLine) and line.comment is None
340
+ )
341
+
342
+ # Determine if we should have a blank line
343
+ should_have_blank = False
344
+
345
+ # Check for UPPERCASE to lowercase transition
346
+ if _is_uppercase_property(prev_node) and (
347
+ _is_lowercase_property(current_node)
348
+ or isinstance(current_node, cst.FunctionDef)
349
+ ):
350
+ should_have_blank = True
351
+
352
+ # Check for dataclass: transition from no-default to with-default
353
+ if is_dataclass:
354
+ prev_has_default = _has_default_value(prev_node)
355
+ current_has_default = _has_default_value(current_node)
356
+ # Add blank line when transitioning from required to optional properties
357
+ if not prev_has_default and current_has_default:
358
+ should_have_blank = True
359
+
360
+ # Normalize blank lines
361
+ target_blanks = 1 if should_have_blank else 0
362
+
363
+ if blank_count != target_blanks:
364
+ non_blank_leading = [
365
+ line
366
+ for line in current_node.leading_lines
367
+ if not (isinstance(line, cst.EmptyLine) and line.comment is None)
368
+ ]
369
+ new_leading = [cst.EmptyLine()] * target_blanks + non_blank_leading
370
+ body_list[i] = current_node.with_changes(leading_lines=new_leading)
371
+ changed = True
372
+
373
+ # Ensure blank line before first method (if there are properties before it)
374
+ if first_method_idx < len(body_list) and first_method_idx > 0:
375
+ method_node = body_list[first_method_idx]
376
+ prev_node = body_list[first_method_idx - 1]
377
+
378
+ # Only add blank line if previous node is a property
379
+ if _is_class_property(prev_node):
380
+ if hasattr(method_node, "leading_lines"):
381
+ blank_count = sum(
382
+ 1
383
+ for line in method_node.leading_lines
384
+ if isinstance(line, cst.EmptyLine) and line.comment is None
385
+ )
386
+
387
+ if blank_count != 1:
388
+ non_blank_leading = [
389
+ line
390
+ for line in method_node.leading_lines
391
+ if not (
392
+ isinstance(line, cst.EmptyLine) and line.comment is None
393
+ )
394
+ ]
395
+ new_leading = [cst.EmptyLine()] + non_blank_leading
396
+ body_list[first_method_idx] = method_node.with_changes(
397
+ leading_lines=new_leading
398
+ )
399
+ changed = True
400
+
401
+ if not changed:
402
+ return suite
403
+
404
+ return suite.with_changes(body=body_list)
405
+
406
+
407
+ def _normalize_double_blank_lines_in_suite(suite: cst.Suite) -> cst.Suite:
408
+ """Normalize double blank lines inside function/method/class bodies to single blank lines."""
409
+ body_list = list(suite.body)
410
+ if len(body_list) <= 1:
411
+ return suite
412
+
413
+ changed = False
414
+
415
+ for i in range(1, len(body_list)):
416
+ current_node = body_list[i]
417
+
418
+ if not hasattr(current_node, "leading_lines"):
419
+ continue
420
+
421
+ # Count blank lines in leading_lines
422
+ blank_count = sum(
423
+ 1
424
+ for line in current_node.leading_lines
425
+ if isinstance(line, cst.EmptyLine) and line.comment is None
426
+ )
427
+
428
+ # Inside function/class bodies, allow maximum 1 blank line
429
+ if blank_count > 1:
430
+ # Keep non-blank leading lines and add exactly 1 blank line
431
+ non_blank_leading = [
432
+ line
433
+ for line in current_node.leading_lines
434
+ if not (isinstance(line, cst.EmptyLine) and line.comment is None)
435
+ ]
436
+ new_leading = [cst.EmptyLine()] + non_blank_leading
437
+ body_list[i] = current_node.with_changes(leading_lines=new_leading)
438
+ changed = True
439
+
440
+ if not changed:
441
+ return suite
442
+
443
+ return suite.with_changes(body=body_list)
444
+
445
+
446
+ def _remove_leading_blank_lines_from_class_suite(
447
+ suite: cst.Suite, class_node: cst.ClassDef | None = None
448
+ ) -> cst.Suite:
449
+ """Remove any leading blank lines from a class body suite.
450
+
451
+ This ensures no blank lines appear immediately after the class signature.
452
+ The first item (docstring, property, or method) should be directly under the class signature.
453
+ If the first item is a docstring, the next item should be directly after the docstring.
454
+ """
455
+ body_list = list(suite.body)
456
+ if not body_list:
457
+ return suite
458
+
459
+ changed = False
460
+ is_dataclass = _is_dataclass(class_node) if class_node else False
461
+
462
+ # Check first element for leading_lines with blank lines
463
+ if body_list and isinstance(
464
+ body_list[0], (cst.SimpleStatementLine, cst.FunctionDef, cst.ClassDef)
465
+ ):
466
+ first_stmt = body_list[0]
467
+ if hasattr(first_stmt, "leading_lines") and first_stmt.leading_lines:
468
+ # Remove blank leading lines from the first statement
469
+ new_leading = [
470
+ line
471
+ for line in first_stmt.leading_lines
472
+ if not (isinstance(line, cst.EmptyLine) and line.comment is None)
473
+ ]
474
+ if len(new_leading) != len(first_stmt.leading_lines):
475
+ body_list[0] = first_stmt.with_changes(leading_lines=new_leading)
476
+ changed = True
477
+
478
+ # Remove leading blank EmptyLine nodes (but only before non-docstring elements)
479
+ # Skip removal if first element is a docstring to avoid conflict with Black
480
+ if body_list and not (
481
+ isinstance(body_list[0], cst.SimpleStatementLine)
482
+ and len(body_list[0].body) == 1
483
+ and isinstance(body_list[0].body[0], cst.Expr)
484
+ and isinstance(body_list[0].body[0].value, cst.SimpleString)
485
+ ):
486
+ # First element is not a docstring, safe to remove blank lines
487
+ while body_list and _is_blank_line(body_list[0]):
488
+ body_list.pop(0)
489
+ changed = True
490
+
491
+ # Allow Black's formatting for class docstrings - no blank line modifications
492
+ # Normalize class properties spacing
493
+ temp_suite = suite.with_changes(body=body_list) if changed else suite
494
+ properties_normalized = _normalize_class_properties_spacing(
495
+ temp_suite, is_dataclass=is_dataclass
496
+ )
497
+
498
+ # Normalize double blank lines in the rest of the class body
499
+ normalized_suite = _normalize_double_blank_lines_in_suite(properties_normalized)
500
+
501
+ return normalized_suite
502
+
503
+
504
+ def _remove_leading_blank_lines_from_suite(suite: cst.Suite) -> cst.Suite:
505
+ """Remove any leading blank lines from a function/method body suite.
506
+
507
+ This ensures no blank lines appear immediately after the function signature,
508
+ whether there's a docstring or not.
509
+ """
510
+ body_list = list(suite.body)
511
+ if not body_list:
512
+ return suite
513
+
514
+ changed = False
515
+
516
+ # Check first element for leading_lines with blank lines
517
+ if body_list and isinstance(body_list[0], cst.SimpleStatementLine):
518
+ first_stmt = body_list[0]
519
+ if first_stmt.leading_lines:
520
+ # Remove blank leading lines from the first statement
521
+ new_leading = [
522
+ line
523
+ for line in first_stmt.leading_lines
524
+ if not (isinstance(line, cst.EmptyLine) and line.comment is None)
525
+ ]
526
+ if len(new_leading) != len(first_stmt.leading_lines):
527
+ body_list[0] = first_stmt.with_changes(leading_lines=new_leading)
528
+ changed = True
529
+
530
+ # Remove leading blank EmptyLine nodes
531
+ while body_list and _is_blank_line(body_list[0]):
532
+ body_list.pop(0)
533
+ changed = True
534
+
535
+ # If first statement is a docstring, ensure no blank lines after it
536
+ if body_list and isinstance(body_list[0], cst.SimpleStatementLine):
537
+ first_stmt = body_list[0]
538
+ if (
539
+ len(first_stmt.body) == 1
540
+ and isinstance(first_stmt.body[0], cst.Expr)
541
+ and isinstance(first_stmt.body[0].value, cst.SimpleString)
542
+ ):
543
+ # This is a docstring, remove blank lines after it
544
+ i = 1
545
+ while i < len(body_list) and _is_blank_line(body_list[i]):
546
+ body_list.pop(i)
547
+ changed = True
548
+
549
+ # Also check if the next statement has blank leading_lines
550
+ if i < len(body_list) and isinstance(body_list[i], cst.SimpleStatementLine):
551
+ next_stmt = body_list[i]
552
+ if next_stmt.leading_lines:
553
+ new_leading = [
554
+ line
555
+ for line in next_stmt.leading_lines
556
+ if not (
557
+ isinstance(line, cst.EmptyLine) and line.comment is None
558
+ )
559
+ ]
560
+ if len(new_leading) != len(next_stmt.leading_lines):
561
+ body_list[i] = next_stmt.with_changes(leading_lines=new_leading)
562
+ changed = True
563
+
564
+ # Normalize double blank lines in the function body
565
+ temp_suite = suite.with_changes(body=body_list) if changed else suite
566
+ normalized_suite = _normalize_double_blank_lines_in_suite(temp_suite)
567
+
568
+ return normalized_suite