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.
- wexample_filestate_python/__init__.py +0 -0
- wexample_filestate_python/__pycache__/__init__.py +0 -0
- wexample_filestate_python/common/__init__.py +0 -0
- wexample_filestate_python/common/__pycache__/__init__.py +0 -0
- wexample_filestate_python/common/pipy_gateway.py +20 -0
- wexample_filestate_python/config_option/__init__.py +0 -0
- wexample_filestate_python/config_option/__pycache__/__init__.py +0 -0
- wexample_filestate_python/config_option/mixin/__init__.py +0 -0
- wexample_filestate_python/config_option/mixin/__pycache__/__init__.py +0 -0
- wexample_filestate_python/config_option/mixin/with_stdout_wrapping_mixin.py +46 -0
- wexample_filestate_python/config_value/__init__.py +0 -0
- wexample_filestate_python/config_value/__pycache__/__init__.py +0 -0
- wexample_filestate_python/config_value/python_config_value.py +195 -0
- wexample_filestate_python/const/__init__.py +0 -0
- wexample_filestate_python/const/__pycache__/__init__.py +0 -0
- wexample_filestate_python/const/name_pattern.py +4 -0
- wexample_filestate_python/const/python_file.py +5 -0
- wexample_filestate_python/file/__init__.py +0 -0
- wexample_filestate_python/file/__pycache__/__init__.py +0 -0
- wexample_filestate_python/file/python_file.py +12 -0
- wexample_filestate_python/helpers/__init__.py +0 -0
- wexample_filestate_python/helpers/__pycache__/__init__.py +0 -0
- wexample_filestate_python/helpers/package.py +122 -0
- wexample_filestate_python/helpers/toml.py +116 -0
- wexample_filestate_python/option/__init__.py +0 -0
- wexample_filestate_python/option/__pycache__/__init__.py +0 -0
- wexample_filestate_python/option/abstract_python_file_content_option.py +45 -0
- wexample_filestate_python/option/add_future_annotations_option.py +79 -0
- wexample_filestate_python/option/add_return_types_option.py +265 -0
- wexample_filestate_python/option/fix_attrs_option.py +37 -0
- wexample_filestate_python/option/fix_blank_lines_option.py +47 -0
- wexample_filestate_python/option/format_option.py +34 -0
- wexample_filestate_python/option/fstringify_option.py +34 -0
- wexample_filestate_python/option/modernize_typing_option.py +25 -0
- wexample_filestate_python/option/order_class_attributes_option.py +34 -0
- wexample_filestate_python/option/order_class_docstring_option.py +36 -0
- wexample_filestate_python/option/order_class_methods_option.py +37 -0
- wexample_filestate_python/option/order_constants_option.py +35 -0
- wexample_filestate_python/option/order_iterable_items_option.py +31 -0
- wexample_filestate_python/option/order_main_guard_option.py +44 -0
- wexample_filestate_python/option/order_module_docstring_option.py +73 -0
- wexample_filestate_python/option/order_module_functions_option.py +42 -0
- wexample_filestate_python/option/order_module_metadata_option.py +62 -0
- wexample_filestate_python/option/order_type_checking_block_option.py +51 -0
- wexample_filestate_python/option/python_option.py +164 -0
- wexample_filestate_python/option/relocate_imports_option.py +189 -0
- wexample_filestate_python/option/remove_unused_option.py +45 -0
- wexample_filestate_python/option/sort_imports_option.py +26 -0
- wexample_filestate_python/option/unquote_annotations_option.py +85 -0
- wexample_filestate_python/options_provider/__init__.py +0 -0
- wexample_filestate_python/options_provider/__pycache__/__init__.py +0 -0
- wexample_filestate_python/options_provider/python_options_provider.py +24 -0
- wexample_filestate_python/py.typed +0 -0
- wexample_filestate_python/utils/__init__.py +0 -0
- wexample_filestate_python/utils/__pycache__/__init__.py +0 -0
- wexample_filestate_python/utils/python_attrs_utils.py +112 -0
- wexample_filestate_python/utils/python_blank_lines_utils.py +568 -0
- wexample_filestate_python/utils/python_class_attributes_utils.py +275 -0
- wexample_filestate_python/utils/python_class_docstring_utils.py +85 -0
- wexample_filestate_python/utils/python_class_methods_utils.py +230 -0
- wexample_filestate_python/utils/python_constants_utils.py +302 -0
- wexample_filestate_python/utils/python_docstring_utils.py +117 -0
- wexample_filestate_python/utils/python_functions_utils.py +212 -0
- wexample_filestate_python/utils/python_iterable_utils.py +131 -0
- wexample_filestate_python/utils/python_main_guard_utils.py +80 -0
- wexample_filestate_python/utils/python_module_metadata_utils.py +147 -0
- wexample_filestate_python/utils/python_type_checking_utils.py +113 -0
- wexample_filestate_python/utils/relocate_imports/__init__.py +7 -0
- wexample_filestate_python/utils/relocate_imports/__pycache__/__init__.py +0 -0
- wexample_filestate_python/utils/relocate_imports/python_import_rewriter.py +413 -0
- wexample_filestate_python/utils/relocate_imports/python_localize_runtime_imports.py +324 -0
- wexample_filestate_python/utils/relocate_imports/python_parser_import_index.py +80 -0
- wexample_filestate_python/utils/relocate_imports/python_runtime_symbol_collector.py +33 -0
- wexample_filestate_python/utils/relocate_imports/python_usage_collector.py +410 -0
- wexample_filestate_python/workdir/__init__.py +0 -0
- wexample_filestate_python/workdir/__pycache__/__init__.py +0 -0
- wexample_filestate_python-0.0.48.dist-info/METADATA +191 -0
- wexample_filestate_python-0.0.48.dist-info/RECORD +80 -0
- wexample_filestate_python-0.0.48.dist-info/WHEEL +4 -0
- 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
|