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,79 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
from wexample_helpers.decorator.base_class import base_class
|
|
6
|
+
|
|
7
|
+
from .abstract_python_file_content_option import AbstractPythonFileContentOption
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from wexample_filestate.const.types_state_items import TargetFileOrDirectoryType
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@base_class
|
|
14
|
+
class AddFutureAnnotationsOption(AbstractPythonFileContentOption):
|
|
15
|
+
def applicable_on_empty_content_file(self) -> bool:
|
|
16
|
+
return False
|
|
17
|
+
|
|
18
|
+
def get_description(self) -> str:
|
|
19
|
+
return "Add `from __future__ import annotations` at the proper location (after shebang/encoding and module docstring)."
|
|
20
|
+
|
|
21
|
+
def _apply_content_change(self, target: TargetFileOrDirectoryType) -> str:
|
|
22
|
+
"""Add `from __future__ import annotations` if not already present."""
|
|
23
|
+
src = target.get_local_file().read()
|
|
24
|
+
|
|
25
|
+
# Fast path: already present
|
|
26
|
+
if "from __future__ import annotations" in src:
|
|
27
|
+
return src
|
|
28
|
+
|
|
29
|
+
import ast
|
|
30
|
+
import re
|
|
31
|
+
|
|
32
|
+
lines = src.splitlines(keepends=True)
|
|
33
|
+
|
|
34
|
+
# Detect shebang and encoding cookie positions
|
|
35
|
+
idx = 0
|
|
36
|
+
if lines and lines[0].startswith("#!"):
|
|
37
|
+
idx = 1
|
|
38
|
+
# Encoding cookie can be on first or second line (after shebang)
|
|
39
|
+
enc_re = re.compile(r"^#.*coding[:=]\s*([-_.a-zA-Z0-9]+)")
|
|
40
|
+
for i in range(idx, min(idx + 2, len(lines))):
|
|
41
|
+
if enc_re.match(lines[i]):
|
|
42
|
+
idx = i + 1
|
|
43
|
+
|
|
44
|
+
# Parse to find module docstring span
|
|
45
|
+
try:
|
|
46
|
+
tree = ast.parse(src)
|
|
47
|
+
except SyntaxError:
|
|
48
|
+
# If parsing fails, be conservative: insert after header block
|
|
49
|
+
insert_at = idx
|
|
50
|
+
else:
|
|
51
|
+
body = getattr(tree, "body", [])
|
|
52
|
+
if (
|
|
53
|
+
body
|
|
54
|
+
and isinstance(body[0], ast.Expr)
|
|
55
|
+
and isinstance(getattr(body[0], "value", None), ast.Constant)
|
|
56
|
+
and isinstance(body[0].value.value, str)
|
|
57
|
+
):
|
|
58
|
+
# Module docstring present; insert after its end_lineno
|
|
59
|
+
doc_end = getattr(body[0], "end_lineno", body[0].lineno) # 1-based
|
|
60
|
+
insert_at = max(idx, doc_end) # line number where next stmt starts
|
|
61
|
+
else:
|
|
62
|
+
insert_at = idx
|
|
63
|
+
|
|
64
|
+
# lines is 0-based; insert_at is 1-based line count from AST
|
|
65
|
+
insert_index = max(0, min(len(lines), insert_at))
|
|
66
|
+
|
|
67
|
+
# Ensure there is a newline after the inserted import if needed
|
|
68
|
+
future_line = "from __future__ import annotations\n"
|
|
69
|
+
|
|
70
|
+
# Avoid inserting duplicate blank lines: if previous line is not blank and not newline, keep
|
|
71
|
+
# If there are existing future imports right after docstring, we can insert alongside them; no special handling needed
|
|
72
|
+
lines.insert(insert_index, future_line)
|
|
73
|
+
# If there isn't a blank line after the future import and next line is not blank, add one for readability
|
|
74
|
+
j = insert_index + 1
|
|
75
|
+
if j < len(lines):
|
|
76
|
+
if lines[j].strip() != "":
|
|
77
|
+
lines.insert(j, "\n")
|
|
78
|
+
|
|
79
|
+
return "".join(lines)
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
from wexample_helpers.decorator.base_class import base_class
|
|
6
|
+
|
|
7
|
+
from .abstract_python_file_content_option import AbstractPythonFileContentOption
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from wexample_filestate.const.types_state_items import TargetFileOrDirectoryType
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@base_class
|
|
14
|
+
class AddReturnTypesOption(AbstractPythonFileContentOption):
|
|
15
|
+
def get_description(self) -> str:
|
|
16
|
+
return "Add simple return type annotations (None/bool/str/int/float) when trivially inferable."
|
|
17
|
+
|
|
18
|
+
def _apply_content_change(self, target: TargetFileOrDirectoryType) -> str:
|
|
19
|
+
"""Add return type annotations for functions lacking them when trivially inferable.
|
|
20
|
+
|
|
21
|
+
Phase 1: annotate -> None, -> bool, -> str, -> int, -> float when all return
|
|
22
|
+
statements in a function agree on one of these literal types."""
|
|
23
|
+
import libcst as cst
|
|
24
|
+
|
|
25
|
+
src = target.get_local_file().read()
|
|
26
|
+
|
|
27
|
+
# We implement type inference and rewriting using LibCST to ensure
|
|
28
|
+
# robust, formatting-preserving edits. We extend inference to:
|
|
29
|
+
# - simple literals (None, bool, str, int, float)
|
|
30
|
+
# - simple class instantiation returns (MyClass() or via a variable assigned once)
|
|
31
|
+
def _infer_literal_type(expr: cst.BaseExpression) -> str | None:
|
|
32
|
+
if isinstance(expr, cst.Name):
|
|
33
|
+
if expr.value in ("True", "False"):
|
|
34
|
+
return "bool"
|
|
35
|
+
if expr.value == "None":
|
|
36
|
+
return "None"
|
|
37
|
+
return None
|
|
38
|
+
if isinstance(expr, cst.SimpleString):
|
|
39
|
+
return "str"
|
|
40
|
+
if isinstance(expr, cst.Integer):
|
|
41
|
+
return "int"
|
|
42
|
+
if isinstance(expr, cst.Float):
|
|
43
|
+
return "float"
|
|
44
|
+
return None
|
|
45
|
+
|
|
46
|
+
class _ReturnCollector(cst.CSTVisitor):
|
|
47
|
+
def __init__(self) -> None:
|
|
48
|
+
self.returns: list[cst.Return] = []
|
|
49
|
+
|
|
50
|
+
# Do not descend into nested scopes that could have their own returns
|
|
51
|
+
def visit_FunctionDef(self, node: cst.FunctionDef) -> bool: # type: ignore[override]
|
|
52
|
+
return False
|
|
53
|
+
|
|
54
|
+
def visit_AsyncFunctionDef(self, node: cst.AsyncFunctionDef) -> bool: # type: ignore[override]
|
|
55
|
+
return False
|
|
56
|
+
|
|
57
|
+
def visit_Lambda(self, node: cst.Lambda) -> bool: # type: ignore[override]
|
|
58
|
+
return False
|
|
59
|
+
|
|
60
|
+
def visit_ClassDef(self, node: cst.ClassDef) -> bool: # type: ignore[override]
|
|
61
|
+
return False
|
|
62
|
+
|
|
63
|
+
def visit_Return(self, node: cst.Return) -> None: # type: ignore[override]
|
|
64
|
+
self.returns.append(node)
|
|
65
|
+
|
|
66
|
+
class _KnownTypesCollector(cst.CSTVisitor):
|
|
67
|
+
"""Collect known simple type names from class defs and from-imports.
|
|
68
|
+
|
|
69
|
+
We stay conservative: only names directly available in the module namespace
|
|
70
|
+
(class definitions and `from x import Name [as Alias]`).
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
def __init__(self) -> None:
|
|
74
|
+
self.known: set[str] = set()
|
|
75
|
+
|
|
76
|
+
def visit_ClassDef(self, node: cst.ClassDef) -> None: # type: ignore[override]
|
|
77
|
+
# Record class name as a potential return type
|
|
78
|
+
self.known.add(node.name.value)
|
|
79
|
+
|
|
80
|
+
def visit_ImportFrom(self, node: cst.ImportFrom) -> None: # type: ignore[override]
|
|
81
|
+
# from pkg import A as B -> record B (or A if no alias)
|
|
82
|
+
for n in node.names:
|
|
83
|
+
if isinstance(n, cst.ImportAlias):
|
|
84
|
+
asname = n.asname.name.value if n.asname else None
|
|
85
|
+
name = n.name.value if isinstance(n.name, cst.Name) else None
|
|
86
|
+
if name:
|
|
87
|
+
self.known.add(asname or name)
|
|
88
|
+
|
|
89
|
+
class _FunctionAssignCollector(cst.CSTVisitor):
|
|
90
|
+
"""Collect simple var -> class-call assignments in a function body.
|
|
91
|
+
|
|
92
|
+
Only records the first simple assignment `x = MyClass(...)` where x is a Name
|
|
93
|
+
and call target resolves to a known type. If a variable is assigned multiple
|
|
94
|
+
times to different types, it is discarded.
|
|
95
|
+
"""
|
|
96
|
+
|
|
97
|
+
def __init__(self, known_types: set[str]) -> None:
|
|
98
|
+
self.known_types = known_types
|
|
99
|
+
self.var_type: dict[str, str] = {}
|
|
100
|
+
self.discarded: set[str] = set()
|
|
101
|
+
|
|
102
|
+
def _infer_call_type(self, call: cst.Call) -> str | None:
|
|
103
|
+
func = call.func
|
|
104
|
+
if isinstance(func, cst.Name):
|
|
105
|
+
# Only keep conservative matches: known type names
|
|
106
|
+
if func.value in self.known_types and func.value[:1].isupper():
|
|
107
|
+
return func.value
|
|
108
|
+
elif isinstance(func, cst.Attribute):
|
|
109
|
+
# module.MyClass(...) -> infer MyClass if it's a known type
|
|
110
|
+
if isinstance(func.attr, cst.Name):
|
|
111
|
+
attr_name = func.attr.value
|
|
112
|
+
if attr_name in self.known_types and attr_name[:1].isupper():
|
|
113
|
+
return attr_name
|
|
114
|
+
return None
|
|
115
|
+
|
|
116
|
+
def _record_assignment(
|
|
117
|
+
self, target: cst.BaseExpression, value: cst.BaseExpression
|
|
118
|
+
) -> None:
|
|
119
|
+
if not isinstance(target, cst.Name):
|
|
120
|
+
return
|
|
121
|
+
var = target.value
|
|
122
|
+
if var in self.discarded:
|
|
123
|
+
return
|
|
124
|
+
if not isinstance(value, cst.Call):
|
|
125
|
+
return
|
|
126
|
+
rtype = self._infer_call_type(value)
|
|
127
|
+
if rtype is None:
|
|
128
|
+
return
|
|
129
|
+
existing = self.var_type.get(var)
|
|
130
|
+
if existing is None:
|
|
131
|
+
self.var_type[var] = rtype
|
|
132
|
+
elif existing != rtype:
|
|
133
|
+
# conflicting assignments -> discard
|
|
134
|
+
self.discarded.add(var)
|
|
135
|
+
self.var_type.pop(var, None)
|
|
136
|
+
|
|
137
|
+
# Stop at nested scopes
|
|
138
|
+
def visit_FunctionDef(self, node: cst.FunctionDef) -> bool: # type: ignore[override]
|
|
139
|
+
return False
|
|
140
|
+
|
|
141
|
+
def visit_AsyncFunctionDef(self, node: cst.AsyncFunctionDef) -> bool: # type: ignore[override]
|
|
142
|
+
return False
|
|
143
|
+
|
|
144
|
+
def visit_ClassDef(self, node: cst.ClassDef) -> bool: # type: ignore[override]
|
|
145
|
+
return False
|
|
146
|
+
|
|
147
|
+
def visit_Lambda(self, node: cst.Lambda) -> bool: # type: ignore[override]
|
|
148
|
+
return False
|
|
149
|
+
|
|
150
|
+
def visit_Assign(self, node: cst.Assign) -> None: # type: ignore[override]
|
|
151
|
+
# Handle simple form: a = Call(...)
|
|
152
|
+
if len(node.targets) != 1:
|
|
153
|
+
return
|
|
154
|
+
target = node.targets[0].target
|
|
155
|
+
self._record_assignment(target, node.value)
|
|
156
|
+
|
|
157
|
+
class AddReturnTypesTransformer(cst.CSTTransformer):
|
|
158
|
+
def __init__(self, known_types: set[str]) -> None:
|
|
159
|
+
super().__init__()
|
|
160
|
+
self.known_types = known_types
|
|
161
|
+
|
|
162
|
+
def _infer_return_expr_type(
|
|
163
|
+
self, expr: cst.BaseExpression, var_types: dict[str, str]
|
|
164
|
+
) -> str | None:
|
|
165
|
+
# Literal simple types
|
|
166
|
+
lit = _infer_literal_type(expr)
|
|
167
|
+
if lit is not None:
|
|
168
|
+
return lit
|
|
169
|
+
|
|
170
|
+
# Call to a known class
|
|
171
|
+
if isinstance(expr, cst.Call):
|
|
172
|
+
func = expr.func
|
|
173
|
+
if (
|
|
174
|
+
isinstance(func, cst.Name)
|
|
175
|
+
and func.value in self.known_types
|
|
176
|
+
and func.value[:1].isupper()
|
|
177
|
+
):
|
|
178
|
+
return func.value
|
|
179
|
+
if isinstance(func, cst.Attribute) and isinstance(
|
|
180
|
+
func.attr, cst.Name
|
|
181
|
+
):
|
|
182
|
+
attr_name = func.attr.value
|
|
183
|
+
if attr_name in self.known_types and attr_name[:1].isupper():
|
|
184
|
+
return attr_name
|
|
185
|
+
return None
|
|
186
|
+
|
|
187
|
+
# Variable referring to a previously inferred var type
|
|
188
|
+
if isinstance(expr, cst.Name):
|
|
189
|
+
return var_types.get(expr.value)
|
|
190
|
+
|
|
191
|
+
return None
|
|
192
|
+
|
|
193
|
+
def _infer_for_function(self, func_node: cst.BaseFunctionDef) -> str | None:
|
|
194
|
+
# Collect returns in the function body (non-nested)
|
|
195
|
+
collector = _ReturnCollector()
|
|
196
|
+
# Visit only the immediate body of the function
|
|
197
|
+
if isinstance(func_node, cst.FunctionDef):
|
|
198
|
+
func_node.body.visit(collector)
|
|
199
|
+
elif isinstance(func_node, cst.AsyncFunctionDef):
|
|
200
|
+
func_node.body.visit(collector)
|
|
201
|
+
else:
|
|
202
|
+
return None
|
|
203
|
+
# If no return statements -> None
|
|
204
|
+
if not collector.returns:
|
|
205
|
+
return "None"
|
|
206
|
+
|
|
207
|
+
# Build a simple assignment map within the function
|
|
208
|
+
fac = _FunctionAssignCollector(self.known_types)
|
|
209
|
+
if isinstance(func_node, cst.FunctionDef):
|
|
210
|
+
func_node.body.visit(fac)
|
|
211
|
+
elif isinstance(func_node, cst.AsyncFunctionDef):
|
|
212
|
+
func_node.body.visit(fac)
|
|
213
|
+
|
|
214
|
+
kinds: set[str] = set()
|
|
215
|
+
for r in collector.returns:
|
|
216
|
+
if r.value is None:
|
|
217
|
+
kinds.add("None")
|
|
218
|
+
continue
|
|
219
|
+
inferred = self._infer_return_expr_type(r.value, fac.var_type)
|
|
220
|
+
if inferred is None:
|
|
221
|
+
return None
|
|
222
|
+
kinds.add(inferred)
|
|
223
|
+
|
|
224
|
+
if len(kinds) == 1:
|
|
225
|
+
return next(iter(kinds))
|
|
226
|
+
return None
|
|
227
|
+
|
|
228
|
+
def leave_FunctionDef(
|
|
229
|
+
self,
|
|
230
|
+
original_node: cst.FunctionDef,
|
|
231
|
+
updated_node: cst.FunctionDef,
|
|
232
|
+
) -> cst.FunctionDef:
|
|
233
|
+
if updated_node.returns is None:
|
|
234
|
+
rtype = self._infer_for_function(original_node)
|
|
235
|
+
if rtype is not None:
|
|
236
|
+
return updated_node.with_changes(
|
|
237
|
+
returns=cst.Annotation(annotation=cst.Name(rtype))
|
|
238
|
+
)
|
|
239
|
+
return updated_node
|
|
240
|
+
|
|
241
|
+
def leave_AsyncFunctionDef(
|
|
242
|
+
self,
|
|
243
|
+
original_node: cst.AsyncFunctionDef,
|
|
244
|
+
updated_node: cst.AsyncFunctionDef,
|
|
245
|
+
) -> cst.AsyncFunctionDef:
|
|
246
|
+
if updated_node.returns is None:
|
|
247
|
+
rtype = self._infer_for_function(original_node)
|
|
248
|
+
if rtype is not None:
|
|
249
|
+
return updated_node.with_changes(
|
|
250
|
+
returns=cst.Annotation(annotation=cst.Name(rtype))
|
|
251
|
+
)
|
|
252
|
+
return updated_node
|
|
253
|
+
|
|
254
|
+
try:
|
|
255
|
+
module = cst.parse_module(src)
|
|
256
|
+
except Exception:
|
|
257
|
+
# If parsing fails for any reason, return the original source unchanged
|
|
258
|
+
return src
|
|
259
|
+
|
|
260
|
+
# Collect known simple type names from the module
|
|
261
|
+
ktc = _KnownTypesCollector()
|
|
262
|
+
module.visit(ktc)
|
|
263
|
+
|
|
264
|
+
new_module = module.visit(AddReturnTypesTransformer(ktc.known))
|
|
265
|
+
return new_module.code
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
from wexample_helpers.decorator.base_class import base_class
|
|
6
|
+
|
|
7
|
+
from .abstract_python_file_content_option import AbstractPythonFileContentOption
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from wexample_filestate.const.types_state_items import TargetFileOrDirectoryType
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@base_class
|
|
14
|
+
class FixAttrsOption(AbstractPythonFileContentOption):
|
|
15
|
+
def get_description(self) -> str:
|
|
16
|
+
return (
|
|
17
|
+
"Ensure attrs decorators (@attrs.define, @attr.s) always use kw_only=True."
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
def _apply_content_change(self, target: TargetFileOrDirectoryType) -> str:
|
|
21
|
+
"""Fix attrs usage in Python files according to standardized rules.
|
|
22
|
+
|
|
23
|
+
Current rules:
|
|
24
|
+
- Ensure @attrs.define always uses kw_only=True
|
|
25
|
+
- Ensure @attr.s always uses kw_only=True
|
|
26
|
+
"""
|
|
27
|
+
import libcst as cst
|
|
28
|
+
|
|
29
|
+
from wexample_filestate_python.utils.python_attrs_utils import (
|
|
30
|
+
fix_attrs_kw_only,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
src = target.get_local_file().read()
|
|
34
|
+
module = cst.parse_module(src)
|
|
35
|
+
|
|
36
|
+
modified = fix_attrs_kw_only(module)
|
|
37
|
+
return modified.code
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
from wexample_helpers.decorator.base_class import base_class
|
|
6
|
+
|
|
7
|
+
from .abstract_python_file_content_option import AbstractPythonFileContentOption
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from wexample_filestate.const.types_state_items import TargetFileOrDirectoryType
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@base_class
|
|
14
|
+
class FixBlankLinesOption(AbstractPythonFileContentOption):
|
|
15
|
+
def get_description(self) -> str:
|
|
16
|
+
return "Remove blank lines immediately after function/method signatures, class definitions, and between signatures and docstrings."
|
|
17
|
+
|
|
18
|
+
def _apply_content_change(self, target: TargetFileOrDirectoryType) -> str:
|
|
19
|
+
"""Fix blank lines in Python files according to standardized rules.
|
|
20
|
+
|
|
21
|
+
Current rules:
|
|
22
|
+
- Remove blank lines immediately after function/method signatures
|
|
23
|
+
- Remove blank lines immediately after class definitions (except after docstrings)
|
|
24
|
+
- Ensure no blank lines between signature and docstring
|
|
25
|
+
- Ensure no blank lines between docstring and first code line (functions only)
|
|
26
|
+
- Normalize double blank lines inside function/class bodies to maximum 1 blank line
|
|
27
|
+
- Class properties: no blank lines between properties except UPPERCASE to lowercase transition
|
|
28
|
+
- Class properties: blank line before first method after properties section
|
|
29
|
+
- Dataclass properties: blank line between required and optional properties
|
|
30
|
+
- Module level: 0 blank lines before module docstring
|
|
31
|
+
- Module level: 1 blank line after module docstring (if present)
|
|
32
|
+
- Module level: 0 blank lines before first statement (if no docstring)
|
|
33
|
+
- Compatible with Black: allows blank line after class docstring
|
|
34
|
+
|
|
35
|
+
Note: Module-level spacing (between classes/functions/imports) is handled by Black.
|
|
36
|
+
"""
|
|
37
|
+
import libcst as cst
|
|
38
|
+
|
|
39
|
+
from wexample_filestate_python.utils.python_blank_lines_utils import (
|
|
40
|
+
fix_function_blank_lines,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
src = target.get_local_file().read()
|
|
44
|
+
module = cst.parse_module(src)
|
|
45
|
+
|
|
46
|
+
modified = fix_function_blank_lines(module)
|
|
47
|
+
return modified.code
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, ClassVar
|
|
4
|
+
|
|
5
|
+
from wexample_helpers.decorator.base_class import base_class
|
|
6
|
+
|
|
7
|
+
from .abstract_python_file_content_option import AbstractPythonFileContentOption
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from wexample_filestate.const.types_state_items import TargetFileOrDirectoryType
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@base_class
|
|
14
|
+
class FormatOption(AbstractPythonFileContentOption):
|
|
15
|
+
# Use ClassVar to avoid Pydantic treating it as a model field/private attr
|
|
16
|
+
_line_length: ClassVar[int] = 88
|
|
17
|
+
|
|
18
|
+
def get_description(self) -> str:
|
|
19
|
+
return "Format the Python file content using Black."
|
|
20
|
+
|
|
21
|
+
def _apply_content_change(self, target: TargetFileOrDirectoryType) -> str:
|
|
22
|
+
"""Format Python files using Black."""
|
|
23
|
+
import black
|
|
24
|
+
|
|
25
|
+
src = target.get_local_file().read()
|
|
26
|
+
mode = black.Mode(line_length=self._line_length)
|
|
27
|
+
|
|
28
|
+
try:
|
|
29
|
+
formatted = black.format_file_contents(src, fast=False, mode=mode)
|
|
30
|
+
return formatted
|
|
31
|
+
except black.NothingChanged:
|
|
32
|
+
return src
|
|
33
|
+
except Exception as e:
|
|
34
|
+
raise e
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
from wexample_helpers.decorator.base_class import base_class
|
|
6
|
+
|
|
7
|
+
from wexample_filestate_python.config_option.mixin.with_stdout_wrapping_mixin import (
|
|
8
|
+
WithStdoutWrappingMixin,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
from .abstract_python_file_content_option import AbstractPythonFileContentOption
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from wexample_filestate.const.types_state_items import TargetFileOrDirectoryType
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@base_class
|
|
18
|
+
class FstringifyOption(WithStdoutWrappingMixin, AbstractPythonFileContentOption):
|
|
19
|
+
def get_description(self) -> str:
|
|
20
|
+
return "Convert old-style string formatting to f-strings using flynt."
|
|
21
|
+
|
|
22
|
+
def _apply_content_change(self, target: TargetFileOrDirectoryType) -> str:
|
|
23
|
+
"""Convert string formatting to f-strings using flynt."""
|
|
24
|
+
from flynt.api import fstringify_code
|
|
25
|
+
from flynt.state import State
|
|
26
|
+
|
|
27
|
+
src = target.get_local_file().read()
|
|
28
|
+
state = State(aggressive=False, multiline=False, len_limit=120)
|
|
29
|
+
|
|
30
|
+
def _execute_fstringify():
|
|
31
|
+
return fstringify_code(src, state=state)
|
|
32
|
+
|
|
33
|
+
result = self._execute_and_wrap_stdout(_execute_fstringify)
|
|
34
|
+
return result.content
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
from wexample_helpers.decorator.base_class import base_class
|
|
6
|
+
|
|
7
|
+
from .abstract_python_file_content_option import AbstractPythonFileContentOption
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from wexample_filestate.const.types_state_items import TargetFileOrDirectoryType
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@base_class
|
|
14
|
+
class ModernizeTypingOption(AbstractPythonFileContentOption):
|
|
15
|
+
def get_description(self) -> str:
|
|
16
|
+
return "Modernize typing syntax (PEP 585/604) using pyupgrade for Python 3.12."
|
|
17
|
+
|
|
18
|
+
def _apply_content_change(self, target: TargetFileOrDirectoryType) -> str:
|
|
19
|
+
"""Modernize typing syntax (PEP 585/604) to Python 3.12 style."""
|
|
20
|
+
from pyupgrade._main import Settings, _fix_plugins
|
|
21
|
+
|
|
22
|
+
src = target.get_local_file().read()
|
|
23
|
+
settings = Settings(min_version=(3, 12))
|
|
24
|
+
updated = _fix_plugins(src, settings=settings)
|
|
25
|
+
return updated
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
from wexample_helpers.decorator.base_class import base_class
|
|
6
|
+
|
|
7
|
+
from .abstract_python_file_content_option import AbstractPythonFileContentOption
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from wexample_filestate.const.types_state_items import TargetFileOrDirectoryType
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@base_class
|
|
14
|
+
class OrderClassAttributesOption(AbstractPythonFileContentOption):
|
|
15
|
+
def get_description(self) -> str:
|
|
16
|
+
return "Sort class attribute blocks with special names prioritized, preserving comments and spacing."
|
|
17
|
+
|
|
18
|
+
def _apply_content_change(self, target: TargetFileOrDirectoryType) -> str:
|
|
19
|
+
"""Sort class attributes: special first, then public A–Z, then private/protected A–Z.
|
|
20
|
+
|
|
21
|
+
Special include __slots__, __match_args__, and inner class Config. Operates on
|
|
22
|
+
contiguous attribute blocks and preserves comments attached to each attribute.
|
|
23
|
+
"""
|
|
24
|
+
import libcst as cst
|
|
25
|
+
|
|
26
|
+
from wexample_filestate_python.utils.python_class_attributes_utils import (
|
|
27
|
+
ensure_order_class_attributes_in_module,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
src = target.get_local_file().read()
|
|
31
|
+
module = cst.parse_module(src)
|
|
32
|
+
|
|
33
|
+
modified = ensure_order_class_attributes_in_module(module)
|
|
34
|
+
return modified.code
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
from wexample_helpers.decorator.base_class import base_class
|
|
6
|
+
|
|
7
|
+
from .abstract_python_file_content_option import AbstractPythonFileContentOption
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from wexample_filestate.const.types_state_items import TargetFileOrDirectoryType
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@base_class
|
|
14
|
+
class OrderClassDocstringOption(AbstractPythonFileContentOption):
|
|
15
|
+
def get_description(self) -> str:
|
|
16
|
+
return "Ensure class docstrings are at the top of each class body, preserving headers and decorators."
|
|
17
|
+
|
|
18
|
+
def _apply_content_change(self, target: TargetFileOrDirectoryType) -> str:
|
|
19
|
+
"""Ensure each class keeps header/decorators and has its docstring at the top.
|
|
20
|
+
|
|
21
|
+
For every class definition in the module, if a class-level docstring exists but
|
|
22
|
+
is not the first statement in the class suite, move it to the top (after
|
|
23
|
+
decorators and the class header). Normalizes to double quotes. Avoids
|
|
24
|
+
whitespace-only diffs when already correct.
|
|
25
|
+
"""
|
|
26
|
+
import libcst as cst
|
|
27
|
+
|
|
28
|
+
from wexample_filestate_python.utils.python_class_docstring_utils import (
|
|
29
|
+
ensure_all_classes_docstring_first,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
src = target.get_local_file().read()
|
|
33
|
+
module = cst.parse_module(src)
|
|
34
|
+
|
|
35
|
+
modified = ensure_all_classes_docstring_first(module)
|
|
36
|
+
return modified.code
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
from wexample_helpers.decorator.base_class import base_class
|
|
6
|
+
|
|
7
|
+
from .abstract_python_file_content_option import AbstractPythonFileContentOption
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from wexample_filestate.const.types_state_items import TargetFileOrDirectoryType
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@base_class
|
|
14
|
+
class OrderClassMethodsOption(AbstractPythonFileContentOption):
|
|
15
|
+
def get_description(self) -> str:
|
|
16
|
+
return "Order class methods and properties according to standardized rules."
|
|
17
|
+
|
|
18
|
+
def _apply_content_change(self, target: TargetFileOrDirectoryType) -> str:
|
|
19
|
+
"""Order class methods according to rules 13–17.
|
|
20
|
+
|
|
21
|
+
- Special dunder methods in logical groups
|
|
22
|
+
- Classmethods: public A–Z, then private A–Z
|
|
23
|
+
- Staticmethods: public A–Z, then private A–Z
|
|
24
|
+
- Properties grouped by base name (getter/setter/deleter together), groups A–Z
|
|
25
|
+
- Instance methods: public A–Z, then private/protected A–Z
|
|
26
|
+
"""
|
|
27
|
+
import libcst as cst
|
|
28
|
+
|
|
29
|
+
from wexample_filestate_python.utils.python_class_methods_utils import (
|
|
30
|
+
ensure_order_class_methods_in_module,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
src = target.get_local_file().read()
|
|
34
|
+
module = cst.parse_module(src)
|
|
35
|
+
|
|
36
|
+
modified = ensure_order_class_methods_in_module(module)
|
|
37
|
+
return modified.code
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
from wexample_helpers.decorator.base_class import base_class
|
|
6
|
+
|
|
7
|
+
from .abstract_python_file_content_option import AbstractPythonFileContentOption
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from wexample_filestate.const.types_state_items import TargetFileOrDirectoryType
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@base_class
|
|
14
|
+
class OrderConstantsOption(AbstractPythonFileContentOption):
|
|
15
|
+
def get_description(self) -> str:
|
|
16
|
+
return "Sort contiguous UPPER_CASE constant blocks marked with '# filestate: python-constant-sort' alphabetically at module level."
|
|
17
|
+
|
|
18
|
+
def _apply_content_change(self, target: TargetFileOrDirectoryType) -> str:
|
|
19
|
+
"""Sort flagged constant blocks (UPPER_CASE) alphabetically A–Z at module level.
|
|
20
|
+
|
|
21
|
+
Only blocks marked by the inline flag '# filestate: python-constant-sort' are considered.
|
|
22
|
+
A block is a contiguous sequence of simple UPPER_CASE assignments (no blank line between).
|
|
23
|
+
Non-flagged constants and other contexts are ignored.
|
|
24
|
+
"""
|
|
25
|
+
import libcst as cst
|
|
26
|
+
|
|
27
|
+
from wexample_filestate_python.utils.python_constants_utils import (
|
|
28
|
+
reorder_flagged_constants_everywhere,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
src = target.get_local_file().read()
|
|
32
|
+
module = cst.parse_module(src)
|
|
33
|
+
|
|
34
|
+
modified = reorder_flagged_constants_everywhere(module, src)
|
|
35
|
+
return modified.code
|