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,324 @@
|
|
|
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 PythonLocalizeRuntimeImports(cst.CSTTransformer):
|
|
12
|
+
"""Inject local imports into functions for runtime-only names (category A).
|
|
13
|
+
|
|
14
|
+
For each function qualified name present in `functions_needing_local`, this transformer
|
|
15
|
+
inserts grouped `from <module> import Name` statements at the top of the function body,
|
|
16
|
+
after a docstring if present.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def __init__(
|
|
20
|
+
self,
|
|
21
|
+
idx: PythonParserImportIndex,
|
|
22
|
+
functions_needing_local: DefaultDict[str, set[str]],
|
|
23
|
+
skip_local_names: set[str] | None = None,
|
|
24
|
+
) -> None:
|
|
25
|
+
super().__init__()
|
|
26
|
+
self.idx = idx
|
|
27
|
+
self.functions_needing_local = functions_needing_local
|
|
28
|
+
self.skip_local_names = skip_local_names or set()
|
|
29
|
+
self.class_stack: list[str] = []
|
|
30
|
+
|
|
31
|
+
@staticmethod
|
|
32
|
+
def _build_module_expr(mod: str | None) -> cst.BaseExpression | None:
|
|
33
|
+
if not mod:
|
|
34
|
+
return None
|
|
35
|
+
parts = mod.split(".")
|
|
36
|
+
expr: cst.BaseExpression = cst.Name(parts[0])
|
|
37
|
+
for p in parts[1:]:
|
|
38
|
+
expr = cst.Attribute(value=expr, attr=cst.Name(p))
|
|
39
|
+
return expr
|
|
40
|
+
|
|
41
|
+
@staticmethod
|
|
42
|
+
def _flatten_module_expr_to_str(module: cst.BaseExpression | None) -> str | None:
|
|
43
|
+
if module is None:
|
|
44
|
+
return None
|
|
45
|
+
if isinstance(module, cst.Name):
|
|
46
|
+
return module.value
|
|
47
|
+
if isinstance(module, cst.Attribute):
|
|
48
|
+
parts: list[str] = []
|
|
49
|
+
cur: cst.BaseExpression | None = module
|
|
50
|
+
while isinstance(cur, cst.Attribute):
|
|
51
|
+
if isinstance(cur.attr, cst.Name):
|
|
52
|
+
parts.append(cur.attr.value)
|
|
53
|
+
else:
|
|
54
|
+
break
|
|
55
|
+
cur = cur.value
|
|
56
|
+
if isinstance(cur, cst.Name):
|
|
57
|
+
parts.append(cur.value)
|
|
58
|
+
parts.reverse()
|
|
59
|
+
return ".".join(parts) if parts else None
|
|
60
|
+
return None
|
|
61
|
+
|
|
62
|
+
def leave_AsyncFunctionDef(
|
|
63
|
+
self, original_node: cst.AsyncFunctionDef, updated_node: cst.AsyncFunctionDef
|
|
64
|
+
) -> cst.AsyncFunctionDef:
|
|
65
|
+
func_qname = (
|
|
66
|
+
".".join(self.class_stack + [original_node.name.value])
|
|
67
|
+
if self.class_stack
|
|
68
|
+
else original_node.name.value
|
|
69
|
+
)
|
|
70
|
+
to_inject, pairs = self._build_local_imports(func_qname)
|
|
71
|
+
if not to_inject:
|
|
72
|
+
return updated_node
|
|
73
|
+
|
|
74
|
+
# Collect existing imports
|
|
75
|
+
def _collect_current_pairs(
|
|
76
|
+
fn: cst.AsyncFunctionDef,
|
|
77
|
+
) -> set[tuple[str | None, str]]:
|
|
78
|
+
found: set[tuple[str | None, str]] = set()
|
|
79
|
+
|
|
80
|
+
class _Find(cst.CSTVisitor):
|
|
81
|
+
def leave_ImportFrom(self, node: cst.ImportFrom) -> None: # type: ignore[override]
|
|
82
|
+
if node.names is None or isinstance(node.names, cst.ImportStar):
|
|
83
|
+
return
|
|
84
|
+
mod = PythonLocalizeRuntimeImports._flatten_module_expr_to_str(
|
|
85
|
+
node.module
|
|
86
|
+
)
|
|
87
|
+
for alias in node.names:
|
|
88
|
+
if isinstance(alias, cst.ImportAlias) and isinstance(
|
|
89
|
+
alias.name, cst.Name
|
|
90
|
+
):
|
|
91
|
+
found.add((mod, alias.name.value))
|
|
92
|
+
|
|
93
|
+
fn.visit(_Find())
|
|
94
|
+
return found
|
|
95
|
+
|
|
96
|
+
existing = _collect_current_pairs(updated_node)
|
|
97
|
+
if pairs.issubset(existing):
|
|
98
|
+
return original_node
|
|
99
|
+
|
|
100
|
+
# Filter out pairs that already exist (exact module + name match)
|
|
101
|
+
# This avoids duplicate imports of the same symbol
|
|
102
|
+
pairs_to_add = pairs - existing
|
|
103
|
+
|
|
104
|
+
# If no new pairs to add after filtering, return original
|
|
105
|
+
if not pairs_to_add:
|
|
106
|
+
return original_node
|
|
107
|
+
|
|
108
|
+
class _PruneInner(cst.CSTTransformer):
|
|
109
|
+
def leave_ImportFrom(self, original_node: cst.ImportFrom, updated_node: cst.ImportFrom): # type: ignore[override]
|
|
110
|
+
if updated_node.names is None or isinstance(
|
|
111
|
+
updated_node.names, cst.ImportStar
|
|
112
|
+
):
|
|
113
|
+
return updated_node
|
|
114
|
+
mod = PythonLocalizeRuntimeImports._flatten_module_expr_to_str(
|
|
115
|
+
updated_node.module
|
|
116
|
+
)
|
|
117
|
+
kept_aliases: list[cst.ImportAlias] = []
|
|
118
|
+
for alias in updated_node.names:
|
|
119
|
+
if not isinstance(alias, cst.ImportAlias):
|
|
120
|
+
continue
|
|
121
|
+
name = (
|
|
122
|
+
alias.name.value if isinstance(alias.name, cst.Name) else None
|
|
123
|
+
)
|
|
124
|
+
if not name:
|
|
125
|
+
continue
|
|
126
|
+
if (mod, name) in pairs_to_add:
|
|
127
|
+
continue
|
|
128
|
+
kept_aliases.append(alias)
|
|
129
|
+
if not kept_aliases:
|
|
130
|
+
return cst.RemoveFromParent()
|
|
131
|
+
return updated_node.with_changes(names=tuple(kept_aliases))
|
|
132
|
+
|
|
133
|
+
pruned_node = updated_node.visit(_PruneInner())
|
|
134
|
+
body = list(pruned_node.body.body)
|
|
135
|
+
insert_at = 0
|
|
136
|
+
if (
|
|
137
|
+
body
|
|
138
|
+
and isinstance(body[0], cst.SimpleStatementLine)
|
|
139
|
+
and any(
|
|
140
|
+
isinstance(el, cst.Expr) and isinstance(el.value, cst.SimpleString)
|
|
141
|
+
for el in body[0].body
|
|
142
|
+
)
|
|
143
|
+
):
|
|
144
|
+
insert_at = 1
|
|
145
|
+
|
|
146
|
+
# Build only the imports we need to add (filtered)
|
|
147
|
+
to_inject_filtered = self._build_import_statements_from_pairs(pairs_to_add)
|
|
148
|
+
new_body = body[:insert_at] + to_inject_filtered + body[insert_at:]
|
|
149
|
+
return pruned_node.with_changes(
|
|
150
|
+
body=pruned_node.body.with_changes(body=new_body)
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
def leave_ClassDef(
|
|
154
|
+
self, original_node: cst.ClassDef, updated_node: cst.ClassDef
|
|
155
|
+
) -> cst.ClassDef: # type: ignore[override]
|
|
156
|
+
self.class_stack.pop()
|
|
157
|
+
return updated_node
|
|
158
|
+
|
|
159
|
+
def leave_FunctionDef(
|
|
160
|
+
self, original_node: cst.FunctionDef, updated_node: cst.FunctionDef
|
|
161
|
+
) -> cst.FunctionDef:
|
|
162
|
+
func_qname = (
|
|
163
|
+
".".join(self.class_stack + [original_node.name.value])
|
|
164
|
+
if self.class_stack
|
|
165
|
+
else original_node.name.value
|
|
166
|
+
)
|
|
167
|
+
# Build consolidated imports and list of pairs to hoist
|
|
168
|
+
to_inject, pairs = self._build_local_imports(func_qname)
|
|
169
|
+
if not to_inject:
|
|
170
|
+
return updated_node
|
|
171
|
+
|
|
172
|
+
# If all target imports are already present somewhere in the function body,
|
|
173
|
+
# avoid rewriting to preserve existing order/formatting.
|
|
174
|
+
def _collect_current_pairs(fn: cst.FunctionDef) -> set[tuple[str | None, str]]:
|
|
175
|
+
found: set[tuple[str | None, str]] = set()
|
|
176
|
+
|
|
177
|
+
class _Find(cst.CSTVisitor):
|
|
178
|
+
def leave_ImportFrom(self, node: cst.ImportFrom) -> None: # type: ignore[override]
|
|
179
|
+
if node.names is None or isinstance(node.names, cst.ImportStar):
|
|
180
|
+
return
|
|
181
|
+
mod = PythonLocalizeRuntimeImports._flatten_module_expr_to_str(
|
|
182
|
+
node.module
|
|
183
|
+
)
|
|
184
|
+
for alias in node.names:
|
|
185
|
+
if isinstance(alias, cst.ImportAlias) and isinstance(
|
|
186
|
+
alias.name, cst.Name
|
|
187
|
+
):
|
|
188
|
+
found.add((mod, alias.name.value))
|
|
189
|
+
|
|
190
|
+
def leave_Import(self, node: cst.Import) -> None: # type: ignore[override]
|
|
191
|
+
return
|
|
192
|
+
|
|
193
|
+
fn.visit(_Find())
|
|
194
|
+
return found
|
|
195
|
+
|
|
196
|
+
existing = _collect_current_pairs(updated_node)
|
|
197
|
+
if pairs.issubset(existing):
|
|
198
|
+
return original_node
|
|
199
|
+
|
|
200
|
+
# Filter out pairs that already exist (exact module + name match)
|
|
201
|
+
# This avoids duplicate imports of the same symbol
|
|
202
|
+
pairs_to_add = pairs - existing
|
|
203
|
+
|
|
204
|
+
# If no new pairs to add after filtering, return original
|
|
205
|
+
if not pairs_to_add:
|
|
206
|
+
return original_node
|
|
207
|
+
|
|
208
|
+
# First prune matching imports anywhere within the function body
|
|
209
|
+
class _PruneInner(cst.CSTTransformer):
|
|
210
|
+
def leave_ImportFrom(self, original_node: cst.ImportFrom, updated_node: cst.ImportFrom): # type: ignore[override]
|
|
211
|
+
if updated_node.names is None or isinstance(
|
|
212
|
+
updated_node.names, cst.ImportStar
|
|
213
|
+
):
|
|
214
|
+
return updated_node
|
|
215
|
+
mod = PythonLocalizeRuntimeImports._flatten_module_expr_to_str(
|
|
216
|
+
updated_node.module
|
|
217
|
+
)
|
|
218
|
+
kept_aliases: list[cst.ImportAlias] = []
|
|
219
|
+
for alias in updated_node.names:
|
|
220
|
+
if not isinstance(alias, cst.ImportAlias):
|
|
221
|
+
continue
|
|
222
|
+
name = (
|
|
223
|
+
alias.name.value if isinstance(alias.name, cst.Name) else None
|
|
224
|
+
)
|
|
225
|
+
if not name:
|
|
226
|
+
continue
|
|
227
|
+
if (mod, name) in pairs_to_add:
|
|
228
|
+
continue
|
|
229
|
+
kept_aliases.append(alias)
|
|
230
|
+
if not kept_aliases:
|
|
231
|
+
return cst.RemoveFromParent()
|
|
232
|
+
return updated_node.with_changes(names=tuple(kept_aliases))
|
|
233
|
+
|
|
234
|
+
pruned_node = updated_node.visit(_PruneInner())
|
|
235
|
+
body = list(pruned_node.body.body)
|
|
236
|
+
insert_at = 0
|
|
237
|
+
if (
|
|
238
|
+
body
|
|
239
|
+
and isinstance(body[0], cst.SimpleStatementLine)
|
|
240
|
+
and any(
|
|
241
|
+
isinstance(el, cst.Expr) and isinstance(el.value, cst.SimpleString)
|
|
242
|
+
for el in body[0].body
|
|
243
|
+
)
|
|
244
|
+
):
|
|
245
|
+
insert_at = 1
|
|
246
|
+
|
|
247
|
+
# Build only the imports we need to add (filtered)
|
|
248
|
+
to_inject_filtered = self._build_import_statements_from_pairs(pairs_to_add)
|
|
249
|
+
new_body = body[:insert_at] + to_inject_filtered + body[insert_at:]
|
|
250
|
+
return pruned_node.with_changes(
|
|
251
|
+
body=pruned_node.body.with_changes(body=new_body)
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
def visit_ClassDef(self, node: cst.ClassDef) -> bool: # type: ignore[override]
|
|
255
|
+
self.class_stack.append(node.name.value)
|
|
256
|
+
return True
|
|
257
|
+
|
|
258
|
+
def _build_import_statements_from_pairs(
|
|
259
|
+
self, pairs: set[tuple[str | None, str]]
|
|
260
|
+
) -> list[cst.BaseStatement]:
|
|
261
|
+
"""Build import statements from a set of (module, name) pairs."""
|
|
262
|
+
from collections import defaultdict
|
|
263
|
+
|
|
264
|
+
by_module: DefaultDict[str | None, list[str]] = defaultdict(list)
|
|
265
|
+
for mod, name in pairs:
|
|
266
|
+
by_module[mod].append(name)
|
|
267
|
+
|
|
268
|
+
stmts: list[cst.BaseStatement] = []
|
|
269
|
+
for mod, idents in by_module.items():
|
|
270
|
+
if not idents:
|
|
271
|
+
continue
|
|
272
|
+
import_names = [cst.ImportAlias(name=cst.Name(n)) for n in sorted(idents)]
|
|
273
|
+
stmts.append(
|
|
274
|
+
cst.SimpleStatementLine(
|
|
275
|
+
(
|
|
276
|
+
cst.ImportFrom(
|
|
277
|
+
module=self._build_module_expr(mod),
|
|
278
|
+
names=tuple(import_names),
|
|
279
|
+
),
|
|
280
|
+
)
|
|
281
|
+
)
|
|
282
|
+
)
|
|
283
|
+
return stmts
|
|
284
|
+
|
|
285
|
+
def _build_local_imports(
|
|
286
|
+
self, func_qname: str
|
|
287
|
+
) -> tuple[list[cst.BaseStatement], set[tuple[str | None, str]]]:
|
|
288
|
+
from collections import defaultdict
|
|
289
|
+
|
|
290
|
+
names = self.functions_needing_local.get(func_qname)
|
|
291
|
+
if not names:
|
|
292
|
+
return [], set()
|
|
293
|
+
# Group by module
|
|
294
|
+
by_module: DefaultDict[str | None, list[str]] = defaultdict(list)
|
|
295
|
+
for ident in sorted(names):
|
|
296
|
+
if ident in self.skip_local_names:
|
|
297
|
+
continue
|
|
298
|
+
mod, _ = self.idx.name_to_from.get(ident, (None, None))
|
|
299
|
+
# Skip unresolved modules to avoid invalid ImportFrom(module=None)
|
|
300
|
+
if mod is None:
|
|
301
|
+
continue
|
|
302
|
+
# Never inject local imports from typing; keep them at module level
|
|
303
|
+
if mod == "typing":
|
|
304
|
+
continue
|
|
305
|
+
by_module[mod].append(ident)
|
|
306
|
+
stmts: list[cst.BaseStatement] = []
|
|
307
|
+
pairs: set[tuple[str | None, str]] = set()
|
|
308
|
+
for mod, idents in by_module.items():
|
|
309
|
+
if not idents:
|
|
310
|
+
continue
|
|
311
|
+
for n in sorted(idents):
|
|
312
|
+
pairs.add((mod, n))
|
|
313
|
+
import_names = [cst.ImportAlias(name=cst.Name(n)) for n in sorted(idents)]
|
|
314
|
+
stmts.append(
|
|
315
|
+
cst.SimpleStatementLine(
|
|
316
|
+
(
|
|
317
|
+
cst.ImportFrom(
|
|
318
|
+
module=self._build_module_expr(mod),
|
|
319
|
+
names=tuple(import_names),
|
|
320
|
+
),
|
|
321
|
+
)
|
|
322
|
+
)
|
|
323
|
+
)
|
|
324
|
+
return stmts, pairs
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import libcst as cst
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
# Collect existing from-imports into a map: imported_name -> (module, alias)
|
|
7
|
+
# Only handle `from pkg import Name [as Alias]`. Skip star imports and bare `import pkg`.
|
|
8
|
+
class PythonParserImportIndex(cst.CSTVisitor):
|
|
9
|
+
def __init__(self) -> None:
|
|
10
|
+
super().__init__()
|
|
11
|
+
self.name_to_from: dict[str, tuple[str | None, str | None]] = {}
|
|
12
|
+
self.importfrom_nodes: list[cst.ImportFrom] = []
|
|
13
|
+
self.other_import_nodes: list[cst.Import] = []
|
|
14
|
+
|
|
15
|
+
@staticmethod
|
|
16
|
+
def _flatten_module_name(module: cst.BaseExpression | None) -> str | None:
|
|
17
|
+
if module is None:
|
|
18
|
+
return None
|
|
19
|
+
if isinstance(module, cst.Name):
|
|
20
|
+
return module.value
|
|
21
|
+
if isinstance(module, cst.Attribute):
|
|
22
|
+
parts: list[str] = []
|
|
23
|
+
cur: cst.BaseExpression | None = module
|
|
24
|
+
while isinstance(cur, cst.Attribute):
|
|
25
|
+
if isinstance(cur.attr, cst.Name):
|
|
26
|
+
parts.append(cur.attr.value)
|
|
27
|
+
else:
|
|
28
|
+
break
|
|
29
|
+
cur = cur.value
|
|
30
|
+
if isinstance(cur, cst.Name):
|
|
31
|
+
parts.append(cur.value)
|
|
32
|
+
parts.reverse()
|
|
33
|
+
return ".".join(parts) if parts else None
|
|
34
|
+
if isinstance(module, cst.SimpleString):
|
|
35
|
+
return module.evaluated_value
|
|
36
|
+
return None
|
|
37
|
+
|
|
38
|
+
def visit_Import(self, node: cst.Import) -> None: # type: ignore[override]
|
|
39
|
+
self.other_import_nodes.append(node)
|
|
40
|
+
# Index import statements: import os.path -> name_to_from["os"] = (None, None)
|
|
41
|
+
# The module name is stored as the imported name for bare imports
|
|
42
|
+
for name in node.names:
|
|
43
|
+
if isinstance(name, cst.ImportAlias):
|
|
44
|
+
# Get the module name (e.g., "os" from "import os.path")
|
|
45
|
+
if isinstance(name.name, cst.Name):
|
|
46
|
+
module_name = name.name.value
|
|
47
|
+
elif isinstance(name.name, cst.Attribute):
|
|
48
|
+
# For "import os.path", extract the base name "os"
|
|
49
|
+
module_name = self._flatten_module_name(name.name)
|
|
50
|
+
if module_name and "." in module_name:
|
|
51
|
+
# Store the base module name (first part before dot)
|
|
52
|
+
module_name = module_name.split(".")[0]
|
|
53
|
+
else:
|
|
54
|
+
continue
|
|
55
|
+
|
|
56
|
+
# Store the alias if present, otherwise the module name
|
|
57
|
+
alias_name = name.asname.name.value if name.asname else module_name
|
|
58
|
+
if alias_name:
|
|
59
|
+
# For bare imports, we store None as the module since it's not a from-import
|
|
60
|
+
self.name_to_from[alias_name] = (None, None)
|
|
61
|
+
|
|
62
|
+
def visit_ImportFrom(self, node: cst.ImportFrom) -> None: # type: ignore[override]
|
|
63
|
+
module_name = self._flatten_module_name(node.module)
|
|
64
|
+
|
|
65
|
+
# Skip __future__ imports - they must always stay at module top
|
|
66
|
+
if module_name == "__future__":
|
|
67
|
+
return
|
|
68
|
+
|
|
69
|
+
self.importfrom_nodes.append(node)
|
|
70
|
+
if node.names is None or isinstance(node.names, cst.ImportStar):
|
|
71
|
+
return
|
|
72
|
+
for alias in node.names:
|
|
73
|
+
if isinstance(alias, cst.ImportAlias):
|
|
74
|
+
asname = alias.asname.name.value if alias.asname else None
|
|
75
|
+
name = alias.name.value if isinstance(alias.name, cst.Name) else None
|
|
76
|
+
if name:
|
|
77
|
+
self.name_to_from[name if not asname else asname] = (
|
|
78
|
+
module_name,
|
|
79
|
+
asname,
|
|
80
|
+
)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import libcst as cst
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class PythonRuntimeSymbolCollector(cst.CSTVisitor):
|
|
7
|
+
"""Collect imported symbol names used in non-annotation runtime expressions.
|
|
8
|
+
|
|
9
|
+
It ignores names that appear inside annotations and records names that appear
|
|
10
|
+
elsewhere, so callers can conservatively treat them as runtime-used and keep
|
|
11
|
+
their imports at module level (and/or avoid moving them under TYPE_CHECKING).
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
def __init__(self, imported_value_names: set[str]) -> None:
|
|
15
|
+
super().__init__()
|
|
16
|
+
self.imported_value_names = imported_value_names
|
|
17
|
+
self.in_annotation_stack: list[bool] = []
|
|
18
|
+
self.runtime_used_anywhere: set[str] = set()
|
|
19
|
+
|
|
20
|
+
def leave_Annotation(self, node: cst.Annotation) -> None: # type: ignore[override]
|
|
21
|
+
self.in_annotation_stack.pop()
|
|
22
|
+
|
|
23
|
+
# Track entering/leaving annotations
|
|
24
|
+
def visit_Annotation(self, node: cst.Annotation) -> bool: # type: ignore[override]
|
|
25
|
+
self.in_annotation_stack.append(True)
|
|
26
|
+
return True
|
|
27
|
+
|
|
28
|
+
def visit_Name(self, node: cst.Name) -> None: # type: ignore[override]
|
|
29
|
+
if self.in_annotation_stack:
|
|
30
|
+
return
|
|
31
|
+
val = node.value
|
|
32
|
+
if val in self.imported_value_names:
|
|
33
|
+
self.runtime_used_anywhere.add(val)
|