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,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("_"))