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,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