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,413 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, DefaultDict
|
|
4
|
+
|
|
5
|
+
import libcst as cst
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from python_parser_import_index import PythonParserImportIndex
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class PythonImportRewriter(cst.CSTTransformer):
|
|
12
|
+
"""Rewrite module-level imports by:
|
|
13
|
+
- Removing symbols that will be localized to functions (A) or moved under TYPE_CHECKING (C-only)
|
|
14
|
+
- Keeping symbols used at class-level property annotations (B)
|
|
15
|
+
- Adding a TYPE_CHECKING block with required imports for C-only symbols
|
|
16
|
+
- Ensuring `from typing import TYPE_CHECKING` is present
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def __init__(
|
|
20
|
+
self,
|
|
21
|
+
*,
|
|
22
|
+
used_in_B: set[str],
|
|
23
|
+
names_to_remove_from_module: set[str],
|
|
24
|
+
used_in_C_only: set[str],
|
|
25
|
+
idx: PythonParserImportIndex,
|
|
26
|
+
) -> None:
|
|
27
|
+
super().__init__()
|
|
28
|
+
self.used_in_B = used_in_B
|
|
29
|
+
self.names_to_remove_from_module = names_to_remove_from_module
|
|
30
|
+
self.used_in_C_only = used_in_C_only
|
|
31
|
+
self.idx = idx
|
|
32
|
+
self.found_type_checking_import = False
|
|
33
|
+
# Track whether we are at module level; only prune at module level
|
|
34
|
+
self._inside_class_func_stack: list[str] = []
|
|
35
|
+
self.need_type_checking_block: bool = len(used_in_C_only) > 0
|
|
36
|
+
self._inside_type_checking_stack: list[bool] = []
|
|
37
|
+
|
|
38
|
+
@staticmethod
|
|
39
|
+
def _build_module_expr(mod: str | None) -> cst.BaseExpression | None:
|
|
40
|
+
if not mod:
|
|
41
|
+
return None
|
|
42
|
+
parts = mod.split(".")
|
|
43
|
+
expr: cst.BaseExpression = cst.Name(parts[0])
|
|
44
|
+
for p in parts[1:]:
|
|
45
|
+
expr = cst.Attribute(value=expr, attr=cst.Name(p))
|
|
46
|
+
return expr
|
|
47
|
+
|
|
48
|
+
@staticmethod
|
|
49
|
+
def _flatten_module_expr_to_str(module: cst.BaseExpression | None) -> str | None:
|
|
50
|
+
if module is None:
|
|
51
|
+
return None
|
|
52
|
+
if isinstance(module, cst.Name):
|
|
53
|
+
return module.value
|
|
54
|
+
if isinstance(module, cst.Attribute):
|
|
55
|
+
parts: list[str] = []
|
|
56
|
+
cur: cst.BaseExpression | None = module
|
|
57
|
+
while isinstance(cur, cst.Attribute):
|
|
58
|
+
if isinstance(cur.attr, cst.Name):
|
|
59
|
+
parts.append(cur.attr.value)
|
|
60
|
+
else:
|
|
61
|
+
break
|
|
62
|
+
cur = cur.value
|
|
63
|
+
if isinstance(cur, cst.Name):
|
|
64
|
+
parts.append(cur.value)
|
|
65
|
+
parts.reverse()
|
|
66
|
+
return ".".join(parts) if parts else None
|
|
67
|
+
return None
|
|
68
|
+
|
|
69
|
+
def leave_ClassDef(self, original_node: cst.ClassDef, updated_node: cst.ClassDef) -> cst.BaseStatement: # type: ignore[override]
|
|
70
|
+
self._inside_class_func_stack.pop()
|
|
71
|
+
return updated_node
|
|
72
|
+
|
|
73
|
+
def leave_FunctionDef(self, original_node: cst.FunctionDef, updated_node: cst.FunctionDef) -> cst.BaseStatement: # type: ignore[override]
|
|
74
|
+
self._inside_class_func_stack.pop()
|
|
75
|
+
return updated_node
|
|
76
|
+
|
|
77
|
+
def leave_If(self, original_node: cst.If, updated_node: cst.If) -> cst.If: # type: ignore[override]
|
|
78
|
+
self._inside_type_checking_stack.pop()
|
|
79
|
+
return updated_node
|
|
80
|
+
|
|
81
|
+
def leave_Import(
|
|
82
|
+
self, original_node: cst.Import, updated_node: cst.Import
|
|
83
|
+
) -> cst.RemovalSentinel | cst.BaseSmallStatement:
|
|
84
|
+
# Only handle module-level imports
|
|
85
|
+
if self._inside_class_func_stack:
|
|
86
|
+
return updated_node
|
|
87
|
+
kept_aliases: list[cst.ImportAlias] = []
|
|
88
|
+
removed_any = False
|
|
89
|
+
for alias in updated_node.names:
|
|
90
|
+
if not isinstance(alias, cst.ImportAlias):
|
|
91
|
+
continue
|
|
92
|
+
|
|
93
|
+
# Extract the base module name (e.g., "os" from "import os.path")
|
|
94
|
+
if isinstance(alias.name, cst.Name):
|
|
95
|
+
name = alias.name.value
|
|
96
|
+
elif isinstance(alias.name, cst.Attribute):
|
|
97
|
+
# For "import os.path", extract the base name "os"
|
|
98
|
+
full_name = self._flatten_module_expr_to_str(alias.name)
|
|
99
|
+
if full_name and "." in full_name:
|
|
100
|
+
name = full_name.split(".")[0]
|
|
101
|
+
else:
|
|
102
|
+
name = full_name
|
|
103
|
+
else:
|
|
104
|
+
continue
|
|
105
|
+
|
|
106
|
+
if not name:
|
|
107
|
+
continue
|
|
108
|
+
alias_ident = alias.asname.name.value if alias.asname else name
|
|
109
|
+
|
|
110
|
+
# Keep B at module level
|
|
111
|
+
if alias_ident in self.used_in_B:
|
|
112
|
+
kept_aliases.append(alias)
|
|
113
|
+
continue
|
|
114
|
+
|
|
115
|
+
# Drop if moved to TYPE_CHECKING or localized (A or C-only)
|
|
116
|
+
if alias_ident in self.names_to_remove_from_module:
|
|
117
|
+
removed_any = True
|
|
118
|
+
continue
|
|
119
|
+
|
|
120
|
+
kept_aliases.append(alias)
|
|
121
|
+
|
|
122
|
+
if not kept_aliases:
|
|
123
|
+
return cst.RemoveFromParent()
|
|
124
|
+
|
|
125
|
+
# If nothing changed (same number of aliases kept as original and none removed),
|
|
126
|
+
# return the original node to preserve exact formatting (commas, parentheses, whitespace).
|
|
127
|
+
try:
|
|
128
|
+
original_alias_count = len(original_node.names) if not isinstance(original_node.names, cst.ImportStar) else 0 # type: ignore[arg-type]
|
|
129
|
+
except Exception:
|
|
130
|
+
original_alias_count = -1
|
|
131
|
+
if original_alias_count == len(kept_aliases) and not removed_any:
|
|
132
|
+
return original_node
|
|
133
|
+
|
|
134
|
+
# Rebuild aliases freshly to drop any stale trailing comma tokens.
|
|
135
|
+
rebuilt_aliases: list[cst.ImportAlias] = []
|
|
136
|
+
for alias in kept_aliases:
|
|
137
|
+
rebuilt_aliases.append(
|
|
138
|
+
cst.ImportAlias(
|
|
139
|
+
name=alias.name,
|
|
140
|
+
asname=alias.asname,
|
|
141
|
+
)
|
|
142
|
+
)
|
|
143
|
+
return updated_node.with_changes(names=tuple(rebuilt_aliases))
|
|
144
|
+
|
|
145
|
+
def leave_ImportFrom(
|
|
146
|
+
self, original_node: cst.ImportFrom, updated_node: cst.ImportFrom
|
|
147
|
+
) -> cst.RemovalSentinel | cst.BaseSmallStatement:
|
|
148
|
+
# Only handle module-level imports
|
|
149
|
+
if self._inside_class_func_stack:
|
|
150
|
+
return updated_node
|
|
151
|
+
if updated_node.names is None or isinstance(updated_node.names, cst.ImportStar):
|
|
152
|
+
return updated_node
|
|
153
|
+
|
|
154
|
+
# If we're already inside a TYPE_CHECKING block, do not filter out C-only names here.
|
|
155
|
+
if self._inside_type_checking_stack and self._inside_type_checking_stack[-1]:
|
|
156
|
+
return updated_node
|
|
157
|
+
|
|
158
|
+
mod_str = self._flatten_module_expr_to_str(updated_node.module)
|
|
159
|
+
|
|
160
|
+
kept_aliases: list[cst.ImportAlias] = []
|
|
161
|
+
removed_any = False
|
|
162
|
+
for alias in updated_node.names:
|
|
163
|
+
if not isinstance(alias, cst.ImportAlias):
|
|
164
|
+
continue
|
|
165
|
+
name = alias.name.value if isinstance(alias.name, cst.Name) else None
|
|
166
|
+
if not name:
|
|
167
|
+
continue
|
|
168
|
+
alias_ident = alias.asname.name.value if alias.asname else name
|
|
169
|
+
|
|
170
|
+
# Always keep non-TYPE_CHECKING imports from typing at module level
|
|
171
|
+
if mod_str == "typing" and name != "TYPE_CHECKING":
|
|
172
|
+
kept_aliases.append(alias)
|
|
173
|
+
continue
|
|
174
|
+
|
|
175
|
+
# Keep B at module level
|
|
176
|
+
if alias_ident in self.used_in_B:
|
|
177
|
+
kept_aliases.append(alias)
|
|
178
|
+
continue
|
|
179
|
+
|
|
180
|
+
# Drop if moved to TYPE_CHECKING or localized (A or C-only)
|
|
181
|
+
if alias_ident in self.names_to_remove_from_module:
|
|
182
|
+
removed_any = True
|
|
183
|
+
continue
|
|
184
|
+
|
|
185
|
+
kept_aliases.append(alias)
|
|
186
|
+
|
|
187
|
+
if not kept_aliases:
|
|
188
|
+
return cst.RemoveFromParent()
|
|
189
|
+
|
|
190
|
+
# If nothing changed (same number of aliases kept as original and none removed),
|
|
191
|
+
# return the original node to preserve exact formatting (commas, parentheses, whitespace).
|
|
192
|
+
try:
|
|
193
|
+
original_alias_count = len(original_node.names) if not isinstance(original_node.names, cst.ImportStar) else 0 # type: ignore[arg-type]
|
|
194
|
+
except Exception:
|
|
195
|
+
original_alias_count = -1
|
|
196
|
+
if original_alias_count == len(kept_aliases) and not removed_any:
|
|
197
|
+
return original_node
|
|
198
|
+
|
|
199
|
+
# Rebuild aliases freshly to drop any stale trailing comma tokens.
|
|
200
|
+
rebuilt_aliases: list[cst.ImportAlias] = []
|
|
201
|
+
for alias in kept_aliases:
|
|
202
|
+
rebuilt_aliases.append(
|
|
203
|
+
cst.ImportAlias(
|
|
204
|
+
name=alias.name,
|
|
205
|
+
asname=alias.asname,
|
|
206
|
+
)
|
|
207
|
+
)
|
|
208
|
+
return updated_node.with_changes(names=tuple(rebuilt_aliases))
|
|
209
|
+
|
|
210
|
+
def leave_Module(
|
|
211
|
+
self, original_node: cst.Module, updated_node: cst.Module
|
|
212
|
+
) -> cst.Module:
|
|
213
|
+
from collections import defaultdict
|
|
214
|
+
|
|
215
|
+
if not self.need_type_checking_block or not self.used_in_C_only:
|
|
216
|
+
return updated_node
|
|
217
|
+
|
|
218
|
+
# Build desired imports for C-only
|
|
219
|
+
desired_by_module: DefaultDict[str | None, set[str]] = defaultdict(set)
|
|
220
|
+
for ident in sorted(self.used_in_C_only):
|
|
221
|
+
mod, _ = self.idx.name_to_from.get(ident, (None, None))
|
|
222
|
+
# Never add typing.* under TYPE_CHECKING; keep them at module level only.
|
|
223
|
+
if mod == "typing":
|
|
224
|
+
continue
|
|
225
|
+
desired_by_module[mod].add(ident)
|
|
226
|
+
|
|
227
|
+
# Look for existing TYPE_CHECKING block(s)
|
|
228
|
+
existing_tc_index = None
|
|
229
|
+
existing_tc_body: list[cst.BaseStatement] | None = None
|
|
230
|
+
existing_imported: set[tuple[str | None, str]] = set()
|
|
231
|
+
|
|
232
|
+
for i, stmt in enumerate(updated_node.body):
|
|
233
|
+
if (
|
|
234
|
+
isinstance(stmt, cst.If)
|
|
235
|
+
and isinstance(stmt.test, cst.Name)
|
|
236
|
+
and stmt.test.value == "TYPE_CHECKING"
|
|
237
|
+
):
|
|
238
|
+
existing_tc_index = i
|
|
239
|
+
existing_tc_body = list(stmt.body.body)
|
|
240
|
+
# Collect names already imported there
|
|
241
|
+
for s in existing_tc_body:
|
|
242
|
+
if (
|
|
243
|
+
isinstance(s, cst.SimpleStatementLine)
|
|
244
|
+
and len(s.body) == 1
|
|
245
|
+
and isinstance(s.body[0], cst.ImportFrom)
|
|
246
|
+
):
|
|
247
|
+
imp: cst.ImportFrom = s.body[0]
|
|
248
|
+
mod = self._flatten_module_expr_to_str(imp.module)
|
|
249
|
+
if imp.names and not isinstance(imp.names, cst.ImportStar):
|
|
250
|
+
for alias in imp.names:
|
|
251
|
+
if isinstance(alias, cst.ImportAlias) and isinstance(
|
|
252
|
+
alias.name, cst.Name
|
|
253
|
+
):
|
|
254
|
+
existing_imported.add((mod, alias.name.value))
|
|
255
|
+
break
|
|
256
|
+
|
|
257
|
+
# Compute missing imports
|
|
258
|
+
missing_by_module: DefaultDict[str | None, list[str]] = defaultdict(list)
|
|
259
|
+
for mod, names in desired_by_module.items():
|
|
260
|
+
for n in sorted(names):
|
|
261
|
+
if (mod, n) not in existing_imported:
|
|
262
|
+
missing_by_module[mod].append(n)
|
|
263
|
+
|
|
264
|
+
if existing_tc_index is not None and existing_tc_body is not None:
|
|
265
|
+
# Append missing imports to existing TYPE_CHECKING block
|
|
266
|
+
additions: list[cst.BaseStatement] = []
|
|
267
|
+
for mod, names in missing_by_module.items():
|
|
268
|
+
if not names:
|
|
269
|
+
continue
|
|
270
|
+
import_names = [
|
|
271
|
+
cst.ImportAlias(name=cst.Name(n)) for n in sorted(names)
|
|
272
|
+
]
|
|
273
|
+
imp_stmt = cst.SimpleStatementLine(
|
|
274
|
+
(
|
|
275
|
+
cst.ImportFrom(
|
|
276
|
+
module=self._build_module_expr(mod),
|
|
277
|
+
names=tuple(import_names),
|
|
278
|
+
),
|
|
279
|
+
)
|
|
280
|
+
)
|
|
281
|
+
additions.append(imp_stmt)
|
|
282
|
+
if not additions:
|
|
283
|
+
return original_node
|
|
284
|
+
new_tc = updated_node.body[existing_tc_index].with_changes(
|
|
285
|
+
body=cst.IndentedBlock(body=existing_tc_body + additions)
|
|
286
|
+
)
|
|
287
|
+
new_body = list(updated_node.body)
|
|
288
|
+
new_body[existing_tc_index] = new_tc
|
|
289
|
+
return updated_node.with_changes(body=new_body)
|
|
290
|
+
|
|
291
|
+
# Otherwise, create a new TYPE_CHECKING block placed after imports
|
|
292
|
+
type_checking_body: list[cst.BaseStatement] = []
|
|
293
|
+
for mod, names in missing_by_module.items():
|
|
294
|
+
if not names:
|
|
295
|
+
continue
|
|
296
|
+
import_names = [cst.ImportAlias(name=cst.Name(n)) for n in sorted(names)]
|
|
297
|
+
imp = cst.ImportFrom(
|
|
298
|
+
module=self._build_module_expr(mod), names=tuple(import_names)
|
|
299
|
+
)
|
|
300
|
+
type_checking_body.append(cst.SimpleStatementLine((imp,)))
|
|
301
|
+
|
|
302
|
+
if not type_checking_body:
|
|
303
|
+
return updated_node
|
|
304
|
+
|
|
305
|
+
typing_import_stmt = cst.SimpleStatementLine(
|
|
306
|
+
(
|
|
307
|
+
cst.ImportFrom(
|
|
308
|
+
module=cst.Name("typing"),
|
|
309
|
+
names=(cst.ImportAlias(name=cst.Name("TYPE_CHECKING")),),
|
|
310
|
+
),
|
|
311
|
+
)
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
# Find insertion index: after module docstring (if any) and after __future__ imports,
|
|
315
|
+
# then after consecutive top-level imports. Never before the docstring or __future__.
|
|
316
|
+
insert_index = 0
|
|
317
|
+
i = 0
|
|
318
|
+
# Skip module docstring at very top
|
|
319
|
+
if i < len(updated_node.body):
|
|
320
|
+
stmt0 = updated_node.body[i]
|
|
321
|
+
if isinstance(stmt0, cst.SimpleStatementLine) and any(
|
|
322
|
+
isinstance(el, cst.Expr) and isinstance(el.value, cst.SimpleString)
|
|
323
|
+
for el in stmt0.body
|
|
324
|
+
):
|
|
325
|
+
i += 1
|
|
326
|
+
insert_index = i
|
|
327
|
+
|
|
328
|
+
# Skip __future__ imports
|
|
329
|
+
while i < len(updated_node.body):
|
|
330
|
+
stmt = updated_node.body[i]
|
|
331
|
+
if (
|
|
332
|
+
isinstance(stmt, cst.SimpleStatementLine)
|
|
333
|
+
and stmt.body
|
|
334
|
+
and isinstance(stmt.body[0], cst.ImportFrom)
|
|
335
|
+
):
|
|
336
|
+
imp: cst.ImportFrom = stmt.body[0]
|
|
337
|
+
if (
|
|
338
|
+
imp.module
|
|
339
|
+
and isinstance(imp.module, cst.Name)
|
|
340
|
+
and imp.module.value == "__future__"
|
|
341
|
+
):
|
|
342
|
+
i += 1
|
|
343
|
+
insert_index = i
|
|
344
|
+
continue
|
|
345
|
+
break
|
|
346
|
+
# Walk through consecutive import statements
|
|
347
|
+
while i < len(updated_node.body):
|
|
348
|
+
stmt = updated_node.body[i]
|
|
349
|
+
if (
|
|
350
|
+
isinstance(stmt, cst.SimpleStatementLine)
|
|
351
|
+
and stmt.body
|
|
352
|
+
and (
|
|
353
|
+
isinstance(stmt.body[0], cst.ImportFrom)
|
|
354
|
+
or isinstance(stmt.body[0], cst.Import)
|
|
355
|
+
)
|
|
356
|
+
):
|
|
357
|
+
i += 1
|
|
358
|
+
insert_index = i
|
|
359
|
+
else:
|
|
360
|
+
break
|
|
361
|
+
|
|
362
|
+
new_body = list(updated_node.body)
|
|
363
|
+
# Ensure typing import exists somewhere before the TYPE_CHECKING block
|
|
364
|
+
if not self.found_type_checking_import:
|
|
365
|
+
new_body.insert(insert_index, typing_import_stmt)
|
|
366
|
+
insert_index += 1
|
|
367
|
+
|
|
368
|
+
type_checking_if = cst.If(
|
|
369
|
+
test=cst.Name("TYPE_CHECKING"),
|
|
370
|
+
body=cst.IndentedBlock(body=type_checking_body),
|
|
371
|
+
)
|
|
372
|
+
new_body.insert(insert_index, type_checking_if)
|
|
373
|
+
|
|
374
|
+
return updated_node.with_changes(body=new_body)
|
|
375
|
+
|
|
376
|
+
def leave_SimpleStatementLine(
|
|
377
|
+
self,
|
|
378
|
+
original_node: cst.SimpleStatementLine,
|
|
379
|
+
updated_node: cst.SimpleStatementLine,
|
|
380
|
+
) -> cst.BaseStatement:
|
|
381
|
+
# Detect `from typing import TYPE_CHECKING`
|
|
382
|
+
if len(updated_node.body) == 1 and isinstance(
|
|
383
|
+
updated_node.body[0], cst.ImportFrom
|
|
384
|
+
):
|
|
385
|
+
imp: cst.ImportFrom = updated_node.body[0]
|
|
386
|
+
if (
|
|
387
|
+
imp.module
|
|
388
|
+
and isinstance(imp.module, cst.Name)
|
|
389
|
+
and imp.module.value == "typing"
|
|
390
|
+
and imp.names
|
|
391
|
+
and not isinstance(imp.names, cst.ImportStar)
|
|
392
|
+
):
|
|
393
|
+
for alias in imp.names:
|
|
394
|
+
if isinstance(alias, cst.ImportAlias) and isinstance(
|
|
395
|
+
alias.name, cst.Name
|
|
396
|
+
):
|
|
397
|
+
if alias.name.value == "TYPE_CHECKING":
|
|
398
|
+
self.found_type_checking_import = True
|
|
399
|
+
return updated_node
|
|
400
|
+
|
|
401
|
+
def visit_ClassDef(self, node: cst.ClassDef) -> bool: # type: ignore[override]
|
|
402
|
+
self._inside_class_func_stack.append("class")
|
|
403
|
+
return True
|
|
404
|
+
|
|
405
|
+
def visit_FunctionDef(self, node: cst.FunctionDef) -> bool: # type: ignore[override]
|
|
406
|
+
self._inside_class_func_stack.append("function")
|
|
407
|
+
return True
|
|
408
|
+
|
|
409
|
+
def visit_If(self, node: cst.If) -> bool: # type: ignore[override]
|
|
410
|
+
# Track whether we are under `if TYPE_CHECKING:`
|
|
411
|
+
inside = isinstance(node.test, cst.Name) and node.test.value == "TYPE_CHECKING"
|
|
412
|
+
self._inside_type_checking_stack.append(inside)
|
|
413
|
+
return True
|