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,275 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import libcst as cst
|
|
4
|
+
|
|
5
|
+
# Special attribute names and inner classes to prioritize
|
|
6
|
+
SPECIAL_ATTR_NAMES = {"__slots__", "__match_args__"}
|
|
7
|
+
SPECIAL_INNER_CLASS_NAMES = {"Config"}
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def ensure_order_class_attributes_in_module(module: cst.Module) -> cst.Module:
|
|
11
|
+
changed = False
|
|
12
|
+
new_body = list(module.body)
|
|
13
|
+
for idx, node in enumerate(new_body):
|
|
14
|
+
if isinstance(node, cst.ClassDef):
|
|
15
|
+
updated = reorder_class_attributes(node)
|
|
16
|
+
if updated is not node:
|
|
17
|
+
new_body[idx] = updated
|
|
18
|
+
changed = True
|
|
19
|
+
if not changed:
|
|
20
|
+
return module
|
|
21
|
+
return module.with_changes(body=new_body)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def find_attribute_blocks_in_class(
|
|
25
|
+
classdef: cst.ClassDef,
|
|
26
|
+
) -> list[tuple[int, int, list[cst.CSTNode]]]:
|
|
27
|
+
"""Find contiguous blocks of class attributes within the class body.
|
|
28
|
+
|
|
29
|
+
A block starts at an attribute statement and continues through subsequent
|
|
30
|
+
attribute statements; it stops when hitting a blank separator (empty line without
|
|
31
|
+
comment) or a non-attribute node (e.g., def, @decorated def, etc.).
|
|
32
|
+
Inline and preceding comment lines are considered attached to the following node.
|
|
33
|
+
|
|
34
|
+
Returns list of (start_index, end_index_exclusive, nodes)
|
|
35
|
+
where indices refer to classdef.body.body positions.
|
|
36
|
+
"""
|
|
37
|
+
body_list = list(classdef.body.body)
|
|
38
|
+
n = len(body_list)
|
|
39
|
+
blocks: list[tuple[int, int, list[cst.CSTNode]]] = []
|
|
40
|
+
i = 0
|
|
41
|
+
while i < n:
|
|
42
|
+
node = body_list[i]
|
|
43
|
+
if _is_attribute_statement(node):
|
|
44
|
+
# Start a block
|
|
45
|
+
j = i
|
|
46
|
+
nodes: list[cst.CSTNode] = []
|
|
47
|
+
while j < n:
|
|
48
|
+
cur = body_list[j]
|
|
49
|
+
if _is_attribute_statement(cur):
|
|
50
|
+
# If not the first, ensure there is no blank separator before cur
|
|
51
|
+
if j != i:
|
|
52
|
+
# Stop if there is a blank separator among leading_lines (for simple statements)
|
|
53
|
+
if isinstance(cur, cst.SimpleStatementLine):
|
|
54
|
+
if any(el.comment is None for el in cur.leading_lines):
|
|
55
|
+
break
|
|
56
|
+
# For ClassDef, presence of a preceding blank line is represented
|
|
57
|
+
# by a separate EmptyLine node; if previous sibling is a blank separator, stop.
|
|
58
|
+
prev = body_list[j - 1]
|
|
59
|
+
if _is_blank_separator(prev):
|
|
60
|
+
break
|
|
61
|
+
nodes.append(cur)
|
|
62
|
+
j += 1
|
|
63
|
+
continue
|
|
64
|
+
# Stop on first non-attribute node
|
|
65
|
+
break
|
|
66
|
+
if nodes:
|
|
67
|
+
blocks.append((i, j, nodes))
|
|
68
|
+
i = j
|
|
69
|
+
continue
|
|
70
|
+
i += 1
|
|
71
|
+
return blocks
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def reorder_attribute_block(
|
|
75
|
+
nodes: list[cst.CSTNode], *, dataclass_mode: bool = False
|
|
76
|
+
) -> list[cst.CSTNode]:
|
|
77
|
+
"""Reorder one attribute block by categories, preserving per-node leading comments.
|
|
78
|
+
|
|
79
|
+
Order:
|
|
80
|
+
1) Special (__slots__, __match_args__, inner class Config)
|
|
81
|
+
2) Public A–Z
|
|
82
|
+
3) Private/protected A–Z
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
def cat(node: cst.CSTNode) -> tuple:
|
|
86
|
+
name = _attr_name(node) or ""
|
|
87
|
+
if _is_special_attribute(node):
|
|
88
|
+
# Category 0: special always first
|
|
89
|
+
return (0, _sort_key(name), False)
|
|
90
|
+
if dataclass_mode:
|
|
91
|
+
# In dataclass mode, prioritize fields without defaults (required),
|
|
92
|
+
# then fields with defaults/default_factory; non-field attributes come after.
|
|
93
|
+
if _is_dataclass_field(node):
|
|
94
|
+
if _dataclass_field_has_default(node):
|
|
95
|
+
# Category 2: defaulted fields
|
|
96
|
+
return (2, _sort_key(name), False)
|
|
97
|
+
# Category 1: required fields
|
|
98
|
+
return (1, _sort_key(name), False)
|
|
99
|
+
# Non-field attributes: keep public before private after fields
|
|
100
|
+
if _is_public(name):
|
|
101
|
+
return (3, _sort_key(name), False)
|
|
102
|
+
return (4, _sort_key(name), False)
|
|
103
|
+
# Default (non-dataclass) ordering: public, then private
|
|
104
|
+
if _is_public(name):
|
|
105
|
+
# Category 1
|
|
106
|
+
return (1, _sort_key(name), False)
|
|
107
|
+
# Category 2 (private/protected)
|
|
108
|
+
return (2, _sort_key(name), False)
|
|
109
|
+
|
|
110
|
+
original = list(nodes)
|
|
111
|
+
sorted_nodes = sorted(nodes, key=cat)
|
|
112
|
+
|
|
113
|
+
# If unchanged order, return original to avoid diffs
|
|
114
|
+
if sorted_nodes == original:
|
|
115
|
+
return original
|
|
116
|
+
|
|
117
|
+
# Normalize leading_lines: only the first node keeps its leading_lines,
|
|
118
|
+
# others should have minimal leading_lines (preserve comments but remove blank lines).
|
|
119
|
+
out: list[cst.CSTNode] = []
|
|
120
|
+
for idx, node in enumerate(sorted_nodes):
|
|
121
|
+
if idx == 0:
|
|
122
|
+
# First node: keep its leading_lines as-is
|
|
123
|
+
out.append(node)
|
|
124
|
+
else:
|
|
125
|
+
# Subsequent nodes: strip blank leading_lines, keep only comment lines
|
|
126
|
+
if isinstance(node, cst.SimpleStatementLine):
|
|
127
|
+
# Filter leading_lines to keep only comment lines
|
|
128
|
+
comment_lines = [
|
|
129
|
+
line for line in node.leading_lines if line.comment is not None
|
|
130
|
+
]
|
|
131
|
+
out.append(node.with_changes(leading_lines=comment_lines))
|
|
132
|
+
else:
|
|
133
|
+
# For ClassDef or other nodes, just append as-is
|
|
134
|
+
# (blank separators are handled by EmptyLine nodes between body items)
|
|
135
|
+
out.append(node)
|
|
136
|
+
return out
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def reorder_class_attributes(classdef: cst.ClassDef) -> cst.ClassDef:
|
|
140
|
+
blocks = find_attribute_blocks_in_class(classdef)
|
|
141
|
+
if not blocks:
|
|
142
|
+
return classdef
|
|
143
|
+
|
|
144
|
+
changed = False
|
|
145
|
+
body_list = list(classdef.body.body)
|
|
146
|
+
dc_mode = _is_dataclass(classdef)
|
|
147
|
+
for start, end, nodes in reversed(blocks):
|
|
148
|
+
new_nodes = reorder_attribute_block(nodes, dataclass_mode=dc_mode)
|
|
149
|
+
if new_nodes != nodes:
|
|
150
|
+
body_list[start:end] = new_nodes
|
|
151
|
+
changed = True
|
|
152
|
+
if not changed:
|
|
153
|
+
return classdef
|
|
154
|
+
return classdef.with_changes(body=classdef.body.with_changes(body=body_list))
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _attr_name(node: cst.CSTNode) -> str | None:
|
|
158
|
+
if isinstance(node, cst.SimpleStatementLine) and len(node.body) == 1:
|
|
159
|
+
small = node.body[0]
|
|
160
|
+
if isinstance(small, cst.Assign):
|
|
161
|
+
tgt = small.targets[0].target
|
|
162
|
+
if isinstance(tgt, cst.Name):
|
|
163
|
+
return tgt.value
|
|
164
|
+
if isinstance(small, cst.AnnAssign):
|
|
165
|
+
tgt = small.target
|
|
166
|
+
if isinstance(tgt, cst.Name):
|
|
167
|
+
return tgt.value
|
|
168
|
+
if isinstance(node, cst.ClassDef):
|
|
169
|
+
return node.name.value
|
|
170
|
+
return None
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _dataclass_field_has_default(node: cst.CSTNode) -> bool:
|
|
174
|
+
"""Return True if the dataclass field has a default value or default_factory.
|
|
175
|
+
|
|
176
|
+
We treat any AnnAssign with a non-None value as having a default.
|
|
177
|
+
This includes calls like field(default=..., default_factory=...).
|
|
178
|
+
"""
|
|
179
|
+
if not _is_dataclass_field(node):
|
|
180
|
+
return False
|
|
181
|
+
assert isinstance(node, cst.SimpleStatementLine)
|
|
182
|
+
small = node.body[0]
|
|
183
|
+
assert isinstance(small, cst.AnnAssign)
|
|
184
|
+
return small.value is not None
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _is_attribute_statement(node: cst.CSTNode) -> bool:
|
|
188
|
+
"""Return True if node is a class-level attribute we should order.
|
|
189
|
+
|
|
190
|
+
We consider:
|
|
191
|
+
- Simple single-target Name assignments (Assign/AnnAssign) whose target name is NOT UPPER_CASE
|
|
192
|
+
- Inner class definitions (e.g., Config) as attributes for ordering
|
|
193
|
+
|
|
194
|
+
Uppercase names are ignored here (treated as constants in another operation).
|
|
195
|
+
"""
|
|
196
|
+
if isinstance(node, cst.SimpleStatementLine) and len(node.body) == 1:
|
|
197
|
+
small = node.body[0]
|
|
198
|
+
if isinstance(small, cst.Assign):
|
|
199
|
+
# Only simple single-target Name assignments
|
|
200
|
+
if len(small.targets) != 1:
|
|
201
|
+
return False
|
|
202
|
+
target = small.targets[0].target
|
|
203
|
+
if isinstance(target, cst.Name):
|
|
204
|
+
# Ignore UPPER_CASE constants
|
|
205
|
+
if target.value.isupper():
|
|
206
|
+
return False
|
|
207
|
+
return True
|
|
208
|
+
return False
|
|
209
|
+
if isinstance(small, cst.AnnAssign):
|
|
210
|
+
target = small.target
|
|
211
|
+
if isinstance(target, cst.Name):
|
|
212
|
+
if target.value.isupper():
|
|
213
|
+
return False
|
|
214
|
+
return True
|
|
215
|
+
return False
|
|
216
|
+
# Pydantic / config style inner classes are considered attributes for ordering
|
|
217
|
+
if isinstance(node, cst.ClassDef) and isinstance(node.name, cst.Name):
|
|
218
|
+
return True
|
|
219
|
+
return False
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def _is_blank_separator(node: cst.CSTNode) -> bool:
|
|
223
|
+
# A blank separator is an EmptyLine without a comment
|
|
224
|
+
return isinstance(node, cst.EmptyLine) and node.comment is None
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def _is_comment_line(node: cst.CSTNode) -> bool:
|
|
228
|
+
return isinstance(node, cst.EmptyLine) and node.comment is not None
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _is_dataclass(classdef: cst.ClassDef) -> bool:
|
|
232
|
+
"""Detect if class has a @dataclass decorator (dataclass or dataclasses.dataclass)."""
|
|
233
|
+
for dec in classdef.decorators:
|
|
234
|
+
try:
|
|
235
|
+
expr = dec.decorator
|
|
236
|
+
if isinstance(expr, cst.Name) and expr.value == "dataclass":
|
|
237
|
+
return True
|
|
238
|
+
if isinstance(expr, cst.Attribute) and isinstance(expr.attr, cst.Name):
|
|
239
|
+
if expr.attr.value == "dataclass":
|
|
240
|
+
return True
|
|
241
|
+
except Exception:
|
|
242
|
+
continue
|
|
243
|
+
return False
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _is_dataclass_field(node: cst.CSTNode) -> bool:
|
|
247
|
+
"""Dataclass fields are typically annotated assignments (AnnAssign) with non-UPPER names."""
|
|
248
|
+
if isinstance(node, cst.SimpleStatementLine) and len(node.body) == 1:
|
|
249
|
+
small = node.body[0]
|
|
250
|
+
if isinstance(small, cst.AnnAssign):
|
|
251
|
+
tgt = small.target
|
|
252
|
+
if isinstance(tgt, cst.Name) and not tgt.value.isupper():
|
|
253
|
+
return True
|
|
254
|
+
return False
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def _is_public(name: str) -> bool:
|
|
258
|
+
return not name.startswith("_")
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def _is_special_attribute(node: cst.CSTNode) -> bool:
|
|
262
|
+
name = _attr_name(node)
|
|
263
|
+
if name is None:
|
|
264
|
+
return False
|
|
265
|
+
if name in SPECIAL_ATTR_NAMES:
|
|
266
|
+
return True
|
|
267
|
+
if isinstance(node, cst.ClassDef) and name in SPECIAL_INNER_CLASS_NAMES:
|
|
268
|
+
return True
|
|
269
|
+
return False
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def _sort_key(name: str) -> tuple:
|
|
273
|
+
# Case-insensitive A-Z; ensure '_' sorts after letters
|
|
274
|
+
# We achieve this by key: (name without leading '_', is_private)
|
|
275
|
+
return (name.lstrip("_").lower(), name.startswith("_"))
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import libcst as cst
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def ensure_all_classes_docstring_first(module: cst.Module) -> cst.Module:
|
|
7
|
+
"""For each class in the module, ensure its docstring (if present) is first."""
|
|
8
|
+
changed = False
|
|
9
|
+
new_body = list(module.body)
|
|
10
|
+
|
|
11
|
+
for i, node in enumerate(new_body):
|
|
12
|
+
if isinstance(node, cst.ClassDef):
|
|
13
|
+
if not is_class_docstring_first(node):
|
|
14
|
+
updated = move_class_docstring_to_top(node)
|
|
15
|
+
if updated is not node:
|
|
16
|
+
new_body[i] = updated
|
|
17
|
+
changed = True
|
|
18
|
+
if not changed:
|
|
19
|
+
return module
|
|
20
|
+
return module.with_changes(body=new_body)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def find_class_docstring_nodes(
|
|
24
|
+
classdef: cst.ClassDef,
|
|
25
|
+
) -> tuple[cst.SimpleStatementLine | None, int]:
|
|
26
|
+
"""Return (docstring_node, index) inside class body if present, else (None, -1).
|
|
27
|
+
|
|
28
|
+
According to Python conventions, a class docstring is a first statement that's a
|
|
29
|
+
string literal expression. We detect any top-level string literal in the class body.
|
|
30
|
+
"""
|
|
31
|
+
body_elems = list(classdef.body.body)
|
|
32
|
+
for i, elem in enumerate(body_elems):
|
|
33
|
+
if isinstance(elem, cst.SimpleStatementLine) and len(elem.body) == 1:
|
|
34
|
+
small = elem.body[0]
|
|
35
|
+
if isinstance(small, cst.Expr) and isinstance(
|
|
36
|
+
small.value, cst.SimpleString
|
|
37
|
+
):
|
|
38
|
+
return elem, i
|
|
39
|
+
# If we hit a non-simple line before finding a docstring, there's no docstring
|
|
40
|
+
if not isinstance(elem, cst.SimpleStatementLine):
|
|
41
|
+
break
|
|
42
|
+
return None, -1
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def is_class_docstring_first(classdef: cst.ClassDef) -> bool:
|
|
46
|
+
node, idx = find_class_docstring_nodes(classdef)
|
|
47
|
+
# Docstring is considered first if at index 0
|
|
48
|
+
return node is not None and idx == 0
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def move_class_docstring_to_top(classdef: cst.ClassDef) -> cst.ClassDef:
|
|
52
|
+
"""Move existing class docstring to be the first statement in the class body.
|
|
53
|
+
|
|
54
|
+
Also normalizes quotes to double quotes and removes leading blank lines for the docstring.
|
|
55
|
+
"""
|
|
56
|
+
doc, idx = find_class_docstring_nodes(classdef)
|
|
57
|
+
if doc is None or idx == 0:
|
|
58
|
+
return classdef
|
|
59
|
+
|
|
60
|
+
normalized = normalize_docstring_quotes_stmt(doc).with_changes(leading_lines=[])
|
|
61
|
+
|
|
62
|
+
body_list = list(classdef.body.body)
|
|
63
|
+
body_list.pop(idx)
|
|
64
|
+
body_list.insert(0, normalized)
|
|
65
|
+
new_suite = classdef.body.with_changes(body=body_list)
|
|
66
|
+
return classdef.with_changes(body=new_suite)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def normalize_docstring_quotes_stmt(
|
|
70
|
+
stmt: cst.SimpleStatementLine,
|
|
71
|
+
) -> cst.SimpleStatementLine:
|
|
72
|
+
"""Normalize class docstring quotes to double quotes, similar to module behavior."""
|
|
73
|
+
if not (isinstance(stmt, cst.SimpleStatementLine) and len(stmt.body) == 1):
|
|
74
|
+
return stmt
|
|
75
|
+
small = stmt.body[0]
|
|
76
|
+
if not (isinstance(small, cst.Expr) and isinstance(small.value, cst.SimpleString)):
|
|
77
|
+
return stmt
|
|
78
|
+
q = small.value.quote
|
|
79
|
+
if q.startswith('"""') or q.startswith('"'):
|
|
80
|
+
return stmt
|
|
81
|
+
# Convert starting quote to double
|
|
82
|
+
new_quote = '"""' if q.startswith("'''") else '"'
|
|
83
|
+
new_value = small.value.with_changes(quote=new_quote)
|
|
84
|
+
new_small = small.with_changes(value=new_value)
|
|
85
|
+
return stmt.with_changes(body=[new_small])
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import libcst as cst
|
|
4
|
+
|
|
5
|
+
# Dunder ordering groups per guideline
|
|
6
|
+
DUnderGroups: list[list[str]] = [
|
|
7
|
+
["__new__", "__init__"],
|
|
8
|
+
["__repr__", "__str__"],
|
|
9
|
+
["__lt__", "__le__", "__eq__", "__ne__", "__gt__", "__ge__", "__hash__"],
|
|
10
|
+
["__bool__"],
|
|
11
|
+
["__getattribute__", "__getattr__", "__setattr__", "__delattr__"],
|
|
12
|
+
["__len__", "__iter__", "__getitem__", "__setitem__", "__delitem__"],
|
|
13
|
+
["__call__"],
|
|
14
|
+
["__enter__", "__exit__", "__aenter__", "__aexit__"],
|
|
15
|
+
["__await__", "__aiter__", "__anext__"],
|
|
16
|
+
["__get__", "__set__", "__delete__", "__getstate__", "__setstate__"],
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
# Build a lookup map: name -> (group_index, index_within_group)
|
|
20
|
+
_DUNDER_ORDER: dict[str, tuple[int, int]] = {}
|
|
21
|
+
for gi, group in enumerate(DUnderGroups):
|
|
22
|
+
for si, name in enumerate(group):
|
|
23
|
+
_DUNDER_ORDER[name] = (gi, si)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def ensure_order_class_methods_in_module(module: cst.Module) -> cst.Module:
|
|
27
|
+
changed = False
|
|
28
|
+
new_body = list(module.body)
|
|
29
|
+
for idx, node in enumerate(new_body):
|
|
30
|
+
if isinstance(node, cst.ClassDef):
|
|
31
|
+
updated = reorder_class_methods(node)
|
|
32
|
+
if updated is not node:
|
|
33
|
+
new_body[idx] = updated
|
|
34
|
+
changed = True
|
|
35
|
+
if not changed:
|
|
36
|
+
return module
|
|
37
|
+
return module.with_changes(body=new_body)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def reorder_class_methods(classdef: cst.ClassDef) -> cst.ClassDef:
|
|
41
|
+
"""Reorder methods and properties in the class body according to rules 13-17.
|
|
42
|
+
|
|
43
|
+
- Dunder methods ordered in logical groups
|
|
44
|
+
- Classmethods sorted public A–Z then private A–Z
|
|
45
|
+
- Staticmethods sorted public A–Z then private A–Z
|
|
46
|
+
- Properties grouped by base name (getter, setter, deleter together), groups A–Z
|
|
47
|
+
- Instance methods sorted public A–Z then private/protected A–Z
|
|
48
|
+
|
|
49
|
+
Non-method nodes (attributes, inner classes, etc.) retain their relative positions; the
|
|
50
|
+
collection of methods/properties is reordered as a single subsequence in place of the
|
|
51
|
+
original methods/properties (stable placement among non-method nodes).
|
|
52
|
+
"""
|
|
53
|
+
body_list = list(classdef.body.body)
|
|
54
|
+
|
|
55
|
+
# Collect method nodes and their indices
|
|
56
|
+
method_indices: list[int] = []
|
|
57
|
+
method_nodes: list[cst.FunctionDef] = []
|
|
58
|
+
for idx, node in enumerate(body_list):
|
|
59
|
+
if _is_method_node(node):
|
|
60
|
+
method_indices.append(idx)
|
|
61
|
+
method_nodes.append(node)
|
|
62
|
+
|
|
63
|
+
if not method_nodes:
|
|
64
|
+
return classdef
|
|
65
|
+
|
|
66
|
+
# Classify
|
|
67
|
+
dunders: list[dict] = []
|
|
68
|
+
classmethods: list[dict] = []
|
|
69
|
+
staticmethods: list[dict] = []
|
|
70
|
+
properties: list[dict] = []
|
|
71
|
+
instances: list[dict] = []
|
|
72
|
+
|
|
73
|
+
for f in method_nodes:
|
|
74
|
+
kind, meta = _classify_method(f)
|
|
75
|
+
if kind == "dunder":
|
|
76
|
+
dunders.append(meta)
|
|
77
|
+
elif kind == "classmethod":
|
|
78
|
+
classmethods.append(meta)
|
|
79
|
+
elif kind == "staticmethod":
|
|
80
|
+
staticmethods.append(meta)
|
|
81
|
+
elif kind == "property":
|
|
82
|
+
properties.append(meta)
|
|
83
|
+
else:
|
|
84
|
+
instances.append(meta)
|
|
85
|
+
|
|
86
|
+
# Order dunders by (group, within) then keep unknown dunders after known, alpha by name
|
|
87
|
+
known = [m for m in dunders if m["order"][0] != 999]
|
|
88
|
+
unknown = [m for m in dunders if m["order"][0] == 999]
|
|
89
|
+
known_sorted = sorted(known, key=lambda m: (m["order"][0], m["order"][1]))
|
|
90
|
+
unknown_sorted = sorted(unknown, key=lambda m: _sort_key_alpha(m["name"]))
|
|
91
|
+
dunder_ordered = [m["node"] for m in known_sorted + unknown_sorted]
|
|
92
|
+
|
|
93
|
+
# Sort classmethods/staticmethods and instances: public first then private
|
|
94
|
+
def sort_by_visibility(items: list[dict]) -> list[cst.FunctionDef]:
|
|
95
|
+
pub = [i for i in items if not _is_private(i["name"])]
|
|
96
|
+
priv = [i for i in items if _is_private(i["name"])]
|
|
97
|
+
pub_sorted = [
|
|
98
|
+
i["node"] for i in sorted(pub, key=lambda x: _sort_key_alpha(x["name"]))
|
|
99
|
+
]
|
|
100
|
+
priv_sorted = [
|
|
101
|
+
i["node"] for i in sorted(priv, key=lambda x: _sort_key_alpha(x["name"]))
|
|
102
|
+
]
|
|
103
|
+
return pub_sorted + priv_sorted
|
|
104
|
+
|
|
105
|
+
classmethods_ordered = sort_by_visibility(classmethods)
|
|
106
|
+
staticmethods_ordered = sort_by_visibility(staticmethods)
|
|
107
|
+
instances_ordered = sort_by_visibility(instances)
|
|
108
|
+
|
|
109
|
+
# Group properties by base name, order getter, setter, deleter within group
|
|
110
|
+
prop_groups: dict[str, dict[str, cst.FunctionDef | None]] = {}
|
|
111
|
+
for m in properties:
|
|
112
|
+
base = m["base"]
|
|
113
|
+
kind = m["kind"]
|
|
114
|
+
node = m["node"]
|
|
115
|
+
g = prop_groups.setdefault(
|
|
116
|
+
base, {"getter": None, "setter": None, "deleter": None}
|
|
117
|
+
)
|
|
118
|
+
g[kind] = node
|
|
119
|
+
|
|
120
|
+
def prop_group_to_nodes(base: str, g: dict[str, cst.FunctionDef | None]):
|
|
121
|
+
order = []
|
|
122
|
+
if g.get("getter") is not None:
|
|
123
|
+
order.append(g["getter"]) # type: ignore
|
|
124
|
+
if g.get("setter") is not None:
|
|
125
|
+
order.append(g["setter"]) # type: ignore
|
|
126
|
+
if g.get("deleter") is not None:
|
|
127
|
+
order.append(g["deleter"]) # type: ignore
|
|
128
|
+
return order
|
|
129
|
+
|
|
130
|
+
props_ordered: list[cst.FunctionDef] = []
|
|
131
|
+
for base in sorted(prop_groups.keys(), key=lambda n: n.lower()):
|
|
132
|
+
props_ordered.extend(prop_group_to_nodes(base, prop_groups[base]))
|
|
133
|
+
|
|
134
|
+
# Final ordered list: dunder -> classmethods -> staticmethods -> properties -> instances
|
|
135
|
+
ordered_methods: list[cst.FunctionDef] = (
|
|
136
|
+
dunder_ordered
|
|
137
|
+
+ classmethods_ordered
|
|
138
|
+
+ staticmethods_ordered
|
|
139
|
+
+ props_ordered
|
|
140
|
+
+ instances_ordered
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
# If order unchanged, return original
|
|
144
|
+
if ordered_methods == method_nodes:
|
|
145
|
+
return classdef
|
|
146
|
+
|
|
147
|
+
# Reconstruct body: replace the method nodes subsequence positions with ordered ones,
|
|
148
|
+
# keeping non-method nodes in place.
|
|
149
|
+
new_body = list(body_list)
|
|
150
|
+
for pos, new_node in zip(method_indices, ordered_methods):
|
|
151
|
+
new_body[pos] = new_node
|
|
152
|
+
|
|
153
|
+
new_suite = classdef.body.with_changes(body=new_body)
|
|
154
|
+
return classdef.with_changes(body=new_suite)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _classify_method(func: cst.FunctionDef) -> tuple[str, dict]:
|
|
158
|
+
"""Classify a FunctionDef into one of: dunder, classmethod, staticmethod, property, instance.
|
|
159
|
+
Returns (kind, meta) where meta provides extra info like name, dunder order, property base/kind.
|
|
160
|
+
"""
|
|
161
|
+
name = func.name.value
|
|
162
|
+
# Properties first (they may also have classmethod/staticmethod in theory, ignore that)
|
|
163
|
+
base, pkind = _property_kind(func)
|
|
164
|
+
if base is not None:
|
|
165
|
+
return "property", {"base": base, "kind": pkind, "node": func}
|
|
166
|
+
# Classmethod / staticmethod
|
|
167
|
+
if _has_decorator(func, "classmethod"):
|
|
168
|
+
return "classmethod", {"name": name, "node": func}
|
|
169
|
+
if _has_decorator(func, "staticmethod"):
|
|
170
|
+
return "staticmethod", {"name": name, "node": func}
|
|
171
|
+
# Dunder methods
|
|
172
|
+
if _is_dunder(name):
|
|
173
|
+
order = _DUNDER_ORDER.get(name, (999, 999))
|
|
174
|
+
return "dunder", {"name": name, "order": order, "node": func}
|
|
175
|
+
# Instance method
|
|
176
|
+
return "instance", {"name": name, "node": func}
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _has_decorator(func: cst.FunctionDef, decorator_name: str) -> bool:
|
|
180
|
+
for dec in func.decorators:
|
|
181
|
+
expr = dec.decorator
|
|
182
|
+
# @decorator
|
|
183
|
+
if isinstance(expr, cst.Name) and expr.value == decorator_name:
|
|
184
|
+
return True
|
|
185
|
+
# @module.decorator
|
|
186
|
+
if (
|
|
187
|
+
isinstance(expr, cst.Attribute)
|
|
188
|
+
and isinstance(expr.attr, cst.Name)
|
|
189
|
+
and expr.attr.value == decorator_name
|
|
190
|
+
):
|
|
191
|
+
return True
|
|
192
|
+
return False
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _is_dunder(name: str) -> bool:
|
|
196
|
+
return name.startswith("__") and name.endswith("__")
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _is_method_node(node: cst.CSTNode) -> bool:
|
|
200
|
+
return isinstance(node, cst.FunctionDef)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _is_private(name: str) -> bool:
|
|
204
|
+
return name.startswith("_") and not _is_dunder(name)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _property_kind(func: cst.FunctionDef) -> tuple[str | None, str | None]:
|
|
208
|
+
"""Return (base_name, kind) where kind in {getter, setter, deleter} or (None, None).
|
|
209
|
+
For setters/deleters, decorator is like @<name>.setter or @<name>.deleter.
|
|
210
|
+
"""
|
|
211
|
+
# Getter: @property on a function whose name is the property name
|
|
212
|
+
if _has_decorator(func, "property"):
|
|
213
|
+
return func.name.value, "getter"
|
|
214
|
+
# Setter/deleter: look for Attribute decorator <name>.setter / <name>.deleter
|
|
215
|
+
for dec in func.decorators:
|
|
216
|
+
expr = dec.decorator
|
|
217
|
+
if isinstance(expr, cst.Attribute) and isinstance(expr.attr, cst.Name):
|
|
218
|
+
if expr.attr.value in {"setter", "deleter"}:
|
|
219
|
+
# base name is left side of the attribute if it's a Name
|
|
220
|
+
base = expr.value
|
|
221
|
+
if isinstance(base, cst.Name):
|
|
222
|
+
return base.value, (
|
|
223
|
+
"setter" if expr.attr.value == "setter" else "deleter"
|
|
224
|
+
)
|
|
225
|
+
return None, None
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _sort_key_alpha(name: str) -> tuple:
|
|
229
|
+
# Case-insensitive, underscore after letters
|
|
230
|
+
return (name.lstrip("_").lower(), name.startswith("_"))
|