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,131 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
FLAG_NAME = "python-iterable-sort"
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def reorder_flagged_iterables(src: str) -> str:
|
|
7
|
+
"""Sort items of flagged iterable blocks (typically list literals) alphabetically.
|
|
8
|
+
|
|
9
|
+
- Looks for lines with '# filestate: python-iterable-sort'.
|
|
10
|
+
- Sorts the contiguous following element lines until a blank line or closing bracket.
|
|
11
|
+
- Preserves comments: a contiguous block of comment lines is attached to the
|
|
12
|
+
item immediately below it and moves with that item.
|
|
13
|
+
- Preserves indentation and commas; compares using a case-insensitive key on
|
|
14
|
+
the item line's stripped content.
|
|
15
|
+
- If already sorted, returns original src unchanged.
|
|
16
|
+
"""
|
|
17
|
+
lines = src.splitlines()
|
|
18
|
+
if not lines:
|
|
19
|
+
return src
|
|
20
|
+
|
|
21
|
+
flag_lines = _find_flag_line_indices(src)
|
|
22
|
+
if not flag_lines:
|
|
23
|
+
return src
|
|
24
|
+
|
|
25
|
+
changed = False
|
|
26
|
+
|
|
27
|
+
def split_into_groups(block_lines: list[str]) -> list[list[str]]:
|
|
28
|
+
groups: list[list[str]] = []
|
|
29
|
+
pending_comments: list[str] = []
|
|
30
|
+
for ln in block_lines:
|
|
31
|
+
if ln.lstrip().startswith("#"):
|
|
32
|
+
pending_comments.append(ln)
|
|
33
|
+
continue
|
|
34
|
+
# item line
|
|
35
|
+
group = pending_comments + [ln]
|
|
36
|
+
groups.append(group)
|
|
37
|
+
pending_comments = []
|
|
38
|
+
# Any trailing comments without item are ignored for sorting and left in place
|
|
39
|
+
# (shouldn't occur in expected usage). If present, attach to last group to preserve.
|
|
40
|
+
if pending_comments:
|
|
41
|
+
if groups:
|
|
42
|
+
groups[-1].extend(pending_comments)
|
|
43
|
+
else:
|
|
44
|
+
groups.append(pending_comments)
|
|
45
|
+
return groups
|
|
46
|
+
|
|
47
|
+
def group_key(g: list[str]) -> str:
|
|
48
|
+
# Use the first non-comment line in group as key
|
|
49
|
+
for ln in g:
|
|
50
|
+
if not ln.lstrip().startswith("#"):
|
|
51
|
+
# Remove trailing comma for comparison but don't modify actual text
|
|
52
|
+
item = ln.strip()
|
|
53
|
+
if item.endswith(","):
|
|
54
|
+
item = item[:-1]
|
|
55
|
+
return item.lower()
|
|
56
|
+
return "" # fallback
|
|
57
|
+
|
|
58
|
+
for flag_idx in reversed(flag_lines):
|
|
59
|
+
start, end = _collect_iterable_block(lines, flag_idx)
|
|
60
|
+
if start >= end:
|
|
61
|
+
continue
|
|
62
|
+
block = lines[start:end]
|
|
63
|
+
|
|
64
|
+
groups = split_into_groups(block)
|
|
65
|
+
# Build current order keys for no-op detection
|
|
66
|
+
current_keys = [group_key(g) for g in groups]
|
|
67
|
+
sorted_groups = sorted(groups, key=group_key)
|
|
68
|
+
sorted_keys = [group_key(g) for g in sorted_groups]
|
|
69
|
+
|
|
70
|
+
if sorted_keys == current_keys:
|
|
71
|
+
continue
|
|
72
|
+
|
|
73
|
+
# Flatten groups back to lines
|
|
74
|
+
new_block: list[str] = []
|
|
75
|
+
for g in sorted_groups:
|
|
76
|
+
new_block.extend(g)
|
|
77
|
+
|
|
78
|
+
lines[start:end] = new_block
|
|
79
|
+
changed = True
|
|
80
|
+
|
|
81
|
+
return "\n".join(lines) if changed else src
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _collect_iterable_block(lines: list[str], flag_idx: int) -> tuple[int, int]:
|
|
85
|
+
"""Given the index of the flag line, collect the contiguous item block range.
|
|
86
|
+
|
|
87
|
+
Returns (start_idx, end_idx_exclusive) of lines to sort. We start at the next
|
|
88
|
+
non-empty, non-comment line after the flag, and stop before the first blank
|
|
89
|
+
line or the closing bracket ']' at the same or lesser indentation level.
|
|
90
|
+
"""
|
|
91
|
+
n = len(lines)
|
|
92
|
+
# Determine base indentation from the flag line
|
|
93
|
+
flag_line = lines[flag_idx]
|
|
94
|
+
base_indent = len(flag_line) - len(flag_line.lstrip(" \t"))
|
|
95
|
+
|
|
96
|
+
# Start scanning after the flag line
|
|
97
|
+
i = flag_idx + 1
|
|
98
|
+
# Skip immediate blank/comment lines (though the example shows none)
|
|
99
|
+
while i < n and (lines[i].strip() == "" or lines[i].lstrip().startswith("#")):
|
|
100
|
+
i += 1
|
|
101
|
+
start = i
|
|
102
|
+
|
|
103
|
+
# Scan until blank line or closing bracket ']' at indentation <= base
|
|
104
|
+
while i < n:
|
|
105
|
+
stripped = lines[i].strip()
|
|
106
|
+
# Stop at blank separator line
|
|
107
|
+
if stripped == "":
|
|
108
|
+
break
|
|
109
|
+
# Stop when list ends
|
|
110
|
+
curr_indent = len(lines[i]) - len(lines[i].lstrip(" \t"))
|
|
111
|
+
if stripped.startswith("]") and curr_indent <= base_indent:
|
|
112
|
+
break
|
|
113
|
+
# Stop if we encounter a trailing comment-only line
|
|
114
|
+
if lines[i].lstrip().startswith("#"):
|
|
115
|
+
break
|
|
116
|
+
i += 1
|
|
117
|
+
|
|
118
|
+
end = i
|
|
119
|
+
return start, end
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _find_flag_line_indices(src: str) -> list[int]:
|
|
123
|
+
"""Return line indices where the iterable sort flag appears."""
|
|
124
|
+
from wexample_filestate.helpers.flag import flag_exists
|
|
125
|
+
|
|
126
|
+
lines = src.splitlines()
|
|
127
|
+
indices: list[int] = []
|
|
128
|
+
for i, line in enumerate(lines):
|
|
129
|
+
if flag_exists(FLAG_NAME, line):
|
|
130
|
+
indices.append(i)
|
|
131
|
+
return indices
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import libcst as cst
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def find_main_guard_blocks(module: cst.Module) -> list[tuple[int, cst.If]]:
|
|
7
|
+
"""Return list of (index, IfNode) for all top-level __main__ guard blocks."""
|
|
8
|
+
res: list[tuple[int, cst.If]] = []
|
|
9
|
+
for i, stmt in enumerate(module.body):
|
|
10
|
+
if is_main_guard_if(stmt):
|
|
11
|
+
res.append((i, stmt))
|
|
12
|
+
return res
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def is_main_guard_at_end(module: cst.Module) -> bool:
|
|
16
|
+
blocks = find_main_guard_blocks(module)
|
|
17
|
+
if not blocks:
|
|
18
|
+
return True
|
|
19
|
+
last_index = blocks[-1][0]
|
|
20
|
+
# Consider "at the end" if it's the last non-empty node (ignoring trailing blank lines)
|
|
21
|
+
# Find last non-empty node index
|
|
22
|
+
last_non_empty = -1
|
|
23
|
+
for i in range(len(module.body) - 1, -1, -1):
|
|
24
|
+
if not isinstance(module.body[i], cst.EmptyLine):
|
|
25
|
+
last_non_empty = i
|
|
26
|
+
break
|
|
27
|
+
return last_index == last_non_empty
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def is_main_guard_if(node: cst.CSTNode) -> bool:
|
|
31
|
+
return isinstance(node, cst.If) and _is_name_eq_main(node.test)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def move_main_guard_to_end(module: cst.Module) -> cst.Module:
|
|
35
|
+
blocks = find_main_guard_blocks(module)
|
|
36
|
+
if not blocks:
|
|
37
|
+
return module
|
|
38
|
+
|
|
39
|
+
new_body = list(module.body)
|
|
40
|
+
|
|
41
|
+
# Remove all blocks first (from highest index to lowest)
|
|
42
|
+
removed: list[cst.If] = []
|
|
43
|
+
for idx, node in sorted(blocks, key=lambda t: t[0], reverse=True):
|
|
44
|
+
removed.append(new_body.pop(idx))
|
|
45
|
+
removed.reverse() # preserve original order
|
|
46
|
+
|
|
47
|
+
# Strip leading_lines of the first moved guard only if it would create extra blank lines at end
|
|
48
|
+
# In practice, we keep existing leading_lines to minimize diffs.
|
|
49
|
+
# Append guards at the end (before trailing EmptyLines, if any)
|
|
50
|
+
# Find insertion point: just before trailing EmptyLines
|
|
51
|
+
insert_at = len(new_body)
|
|
52
|
+
while insert_at > 0 and isinstance(new_body[insert_at - 1], cst.EmptyLine):
|
|
53
|
+
insert_at -= 1
|
|
54
|
+
|
|
55
|
+
for offset, node in enumerate(removed):
|
|
56
|
+
new_body.insert(insert_at + offset, node)
|
|
57
|
+
|
|
58
|
+
return module.with_changes(body=new_body)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _is_name_eq_main(test: cst.BaseExpression) -> bool:
|
|
62
|
+
# Match patterns: __name__ == "__main__" or '__main__'
|
|
63
|
+
if not isinstance(test, cst.Comparison):
|
|
64
|
+
return False
|
|
65
|
+
# Expect a single comparator with Eq
|
|
66
|
+
if len(test.comparisons) != 1:
|
|
67
|
+
return False
|
|
68
|
+
comp = test.comparisons[0]
|
|
69
|
+
if not isinstance(comp.operator, cst.Equal):
|
|
70
|
+
return False
|
|
71
|
+
# Left should be Name("__name__") (optionally with parentheses tolerated by CST?)
|
|
72
|
+
left_ok = isinstance(test.left, cst.Name) and test.left.value == "__name__"
|
|
73
|
+
if not left_ok:
|
|
74
|
+
return False
|
|
75
|
+
# Right should be SimpleString of __main__
|
|
76
|
+
right = comp.comparator
|
|
77
|
+
if isinstance(right, cst.SimpleString):
|
|
78
|
+
s = right.evaluated_value # libcst provides unescaped python value
|
|
79
|
+
return s == "__main__"
|
|
80
|
+
return False
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import libcst as cst
|
|
4
|
+
|
|
5
|
+
# Common, recognized module metadata names
|
|
6
|
+
METADATA_NAMES: tuple[str, ...] = (
|
|
7
|
+
"__all__",
|
|
8
|
+
"__version__",
|
|
9
|
+
"__author__",
|
|
10
|
+
"__email__",
|
|
11
|
+
"__license__",
|
|
12
|
+
"__copyright__",
|
|
13
|
+
"__title__",
|
|
14
|
+
"__description__",
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def find_module_metadata_statements(
|
|
19
|
+
module: cst.Module,
|
|
20
|
+
) -> list[tuple[int, cst.SimpleStatementLine, str]]:
|
|
21
|
+
"""Find all module-level metadata assignments.
|
|
22
|
+
|
|
23
|
+
Returns list of tuples: (index_in_body, node, metadata_name)
|
|
24
|
+
"""
|
|
25
|
+
results: list[tuple[int, cst.SimpleStatementLine, str]] = []
|
|
26
|
+
for i, stmt in enumerate(module.body):
|
|
27
|
+
name = _get_assignment_target_name(stmt)
|
|
28
|
+
if name is not None:
|
|
29
|
+
assert isinstance(stmt, cst.SimpleStatementLine)
|
|
30
|
+
results.append((i, stmt, name))
|
|
31
|
+
return results
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def group_and_sort_module_metadata(module: cst.Module) -> cst.Module:
|
|
35
|
+
"""Group and sort module metadata assignments by variable name (A–Z).
|
|
36
|
+
|
|
37
|
+
- Collect all recognized metadata assignments at module level
|
|
38
|
+
- Remove them from their current positions
|
|
39
|
+
- Sort them case-insensitively by the metadata name
|
|
40
|
+
- Insert them as a contiguous block at the target index
|
|
41
|
+
- Avoid introducing extra blank lines by clearing leading_lines
|
|
42
|
+
"""
|
|
43
|
+
found = find_module_metadata_statements(module)
|
|
44
|
+
if not found:
|
|
45
|
+
return module
|
|
46
|
+
|
|
47
|
+
# Determine insertion index before we mutate the body
|
|
48
|
+
insert_at = target_index_for_module_metadata(module)
|
|
49
|
+
|
|
50
|
+
# Remove from body (reverse order) and collect nodes with cleaned leading lines
|
|
51
|
+
to_remove_indices = sorted([i for i, _, _ in found], reverse=True)
|
|
52
|
+
new_body: list[cst.CSTNode] = list(module.body)
|
|
53
|
+
moved: list[tuple[str, cst.SimpleStatementLine]] = []
|
|
54
|
+
|
|
55
|
+
for idx in to_remove_indices:
|
|
56
|
+
node = new_body.pop(idx)
|
|
57
|
+
assert isinstance(node, cst.SimpleStatementLine)
|
|
58
|
+
name = _get_assignment_target_name(node)
|
|
59
|
+
if name is None:
|
|
60
|
+
# Should not happen; skip
|
|
61
|
+
continue
|
|
62
|
+
moved.append((name, node.with_changes(leading_lines=[])))
|
|
63
|
+
|
|
64
|
+
# Sort moved by name case-insensitively, '_' after letters rule not needed here
|
|
65
|
+
moved.sort(key=lambda t: t[0].lower())
|
|
66
|
+
|
|
67
|
+
# Adjust insertion index after removals that occurred before it
|
|
68
|
+
num_removed_before = sum(1 for idx in to_remove_indices if idx < insert_at)
|
|
69
|
+
adjusted_insert = insert_at - num_removed_before
|
|
70
|
+
|
|
71
|
+
# Insert in order
|
|
72
|
+
for offset, (_, node) in enumerate(moved):
|
|
73
|
+
new_body.insert(adjusted_insert + offset, node)
|
|
74
|
+
|
|
75
|
+
return module.with_changes(body=new_body)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def target_index_for_module_metadata(module: cst.Module) -> int:
|
|
79
|
+
"""Compute target index to insert grouped module metadata.
|
|
80
|
+
|
|
81
|
+
According to file-level ordering:
|
|
82
|
+
- after imports
|
|
83
|
+
- after TYPE_CHECKING block
|
|
84
|
+
- then module metadata
|
|
85
|
+
|
|
86
|
+
So we insert after the last TYPE_CHECKING block if present, else after last regular
|
|
87
|
+
import if present, else after last __future__ import, else after docstring, else 0.
|
|
88
|
+
"""
|
|
89
|
+
from wexample_filestate_python.utils.python_type_checking_utils import (
|
|
90
|
+
_is_future_import,
|
|
91
|
+
_is_regular_import,
|
|
92
|
+
find_type_checking_blocks,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
last_type_checking = -1
|
|
96
|
+
for idx, _if in find_type_checking_blocks(module):
|
|
97
|
+
last_type_checking = max(last_type_checking, idx)
|
|
98
|
+
|
|
99
|
+
last_regular_import = -1
|
|
100
|
+
last_future_import = -1
|
|
101
|
+
for i, stmt in enumerate(module.body):
|
|
102
|
+
if _is_regular_import(stmt):
|
|
103
|
+
last_regular_import = i
|
|
104
|
+
elif _is_future_import(stmt):
|
|
105
|
+
last_future_import = i
|
|
106
|
+
|
|
107
|
+
if last_type_checking != -1:
|
|
108
|
+
return last_type_checking + 1
|
|
109
|
+
if last_regular_import != -1:
|
|
110
|
+
return last_regular_import + 1
|
|
111
|
+
if last_future_import != -1:
|
|
112
|
+
return last_future_import + 1
|
|
113
|
+
if module.has_docstring:
|
|
114
|
+
return 1
|
|
115
|
+
return 0
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _get_assignment_target_name(stmt: cst.CSTNode) -> str | None:
|
|
119
|
+
"""Return the variable name if this statement is an assignment to a metadata name.
|
|
120
|
+
|
|
121
|
+
Only supports simple module-level assignments like `__version__ = ...` or
|
|
122
|
+
annotated assignment `__version__: str = ...`. Ignores destructuring or multiple targets.
|
|
123
|
+
"""
|
|
124
|
+
if not isinstance(stmt, cst.SimpleStatementLine):
|
|
125
|
+
return None
|
|
126
|
+
|
|
127
|
+
if len(stmt.body) != 1:
|
|
128
|
+
return None
|
|
129
|
+
|
|
130
|
+
small = stmt.body[0]
|
|
131
|
+
|
|
132
|
+
if isinstance(small, cst.Assign):
|
|
133
|
+
# Only allow single target
|
|
134
|
+
if len(small.targets) != 1:
|
|
135
|
+
return None
|
|
136
|
+
target = small.targets[0].target
|
|
137
|
+
if isinstance(target, cst.Name) and target.value in METADATA_NAMES:
|
|
138
|
+
return target.value
|
|
139
|
+
return None
|
|
140
|
+
|
|
141
|
+
if isinstance(small, cst.AnnAssign):
|
|
142
|
+
target = small.target
|
|
143
|
+
if isinstance(target, cst.Name) and target.value in METADATA_NAMES:
|
|
144
|
+
return target.value
|
|
145
|
+
return None
|
|
146
|
+
|
|
147
|
+
return None
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import libcst as cst
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def find_type_checking_blocks(module: cst.Module) -> list[tuple[int, cst.If]]:
|
|
7
|
+
"""Return list of (index, IfNode) for all top-level `if TYPE_CHECKING:` blocks."""
|
|
8
|
+
results: list[tuple[int, cst.If]] = []
|
|
9
|
+
for i, stmt in enumerate(module.body):
|
|
10
|
+
if isinstance(stmt, cst.If) and _is_type_checking_test(stmt.test):
|
|
11
|
+
results.append((i, stmt))
|
|
12
|
+
return results
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def move_type_checking_blocks_after_imports(module: cst.Module) -> cst.Module:
|
|
16
|
+
"""Move all `if TYPE_CHECKING:` blocks to just after regular imports.
|
|
17
|
+
Preserves the order of the blocks and removes extra leading blank lines.
|
|
18
|
+
"""
|
|
19
|
+
blocks = find_type_checking_blocks(module)
|
|
20
|
+
if not blocks:
|
|
21
|
+
return module
|
|
22
|
+
|
|
23
|
+
# Determine target position before removal to avoid index shifts
|
|
24
|
+
insert_at = target_index_for_type_checking(module)
|
|
25
|
+
|
|
26
|
+
# Remove blocks from body (from highest index to lowest to keep indices valid)
|
|
27
|
+
remove_indices = sorted((i for i, _ in blocks), reverse=True)
|
|
28
|
+
new_body = list(module.body)
|
|
29
|
+
moved_blocks: list[cst.If] = []
|
|
30
|
+
for idx in remove_indices:
|
|
31
|
+
node = new_body.pop(idx)
|
|
32
|
+
assert isinstance(node, cst.If)
|
|
33
|
+
# Strip leading lines to avoid introducing blank lines
|
|
34
|
+
moved_blocks.append(node.with_changes(leading_lines=[]))
|
|
35
|
+
|
|
36
|
+
# We removed in reverse; preserve original relative order by reversing back
|
|
37
|
+
moved_blocks.reverse()
|
|
38
|
+
|
|
39
|
+
# Adjust insert_at for prior removals that occurred before it
|
|
40
|
+
num_removed_before = sum(1 for idx in remove_indices if idx < insert_at)
|
|
41
|
+
adjusted_insert = insert_at - num_removed_before
|
|
42
|
+
|
|
43
|
+
for offset, block in enumerate(moved_blocks):
|
|
44
|
+
new_body.insert(adjusted_insert + offset, block)
|
|
45
|
+
|
|
46
|
+
return module.with_changes(body=new_body)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def target_index_for_type_checking(module: cst.Module) -> int:
|
|
50
|
+
"""Compute target insertion index for TYPE_CHECKING blocks.
|
|
51
|
+
After last regular import if any; otherwise after last __future__ import;
|
|
52
|
+
otherwise after module docstring if present; else position 0.
|
|
53
|
+
"""
|
|
54
|
+
last_regular_import = -1
|
|
55
|
+
last_future_import = -1
|
|
56
|
+
|
|
57
|
+
for i, stmt in enumerate(module.body):
|
|
58
|
+
if _is_regular_import(stmt):
|
|
59
|
+
last_regular_import = i
|
|
60
|
+
elif _is_future_import(stmt):
|
|
61
|
+
last_future_import = i
|
|
62
|
+
else:
|
|
63
|
+
# stop scanning when hitting first non-import after having passed imports?
|
|
64
|
+
# We still record all to get the last occurrence.
|
|
65
|
+
pass
|
|
66
|
+
|
|
67
|
+
if last_regular_import != -1:
|
|
68
|
+
return last_regular_import + 1
|
|
69
|
+
if last_future_import != -1:
|
|
70
|
+
return last_future_import + 1
|
|
71
|
+
|
|
72
|
+
# Module docstring at index 0?
|
|
73
|
+
if module.has_docstring:
|
|
74
|
+
return 1
|
|
75
|
+
|
|
76
|
+
return 0
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _is_future_import(stmt: cst.CSTNode) -> bool:
|
|
80
|
+
if isinstance(stmt, cst.SimpleStatementLine):
|
|
81
|
+
for small in stmt.body:
|
|
82
|
+
if isinstance(small, cst.ImportFrom):
|
|
83
|
+
# from __future__ import ...
|
|
84
|
+
mod = small.module
|
|
85
|
+
if isinstance(mod, cst.Name) and mod.value == "__future__":
|
|
86
|
+
return True
|
|
87
|
+
return False
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _is_regular_import(stmt: cst.CSTNode) -> bool:
|
|
91
|
+
if isinstance(stmt, cst.SimpleStatementLine):
|
|
92
|
+
for small in stmt.body:
|
|
93
|
+
if isinstance(small, (cst.Import, cst.ImportFrom)):
|
|
94
|
+
# Exclude __future__
|
|
95
|
+
if isinstance(small, cst.ImportFrom):
|
|
96
|
+
mod = small.module
|
|
97
|
+
if isinstance(mod, cst.Name) and mod.value == "__future__":
|
|
98
|
+
return False
|
|
99
|
+
return True
|
|
100
|
+
return False
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _is_type_checking_test(test: cst.BaseExpression) -> bool:
|
|
104
|
+
# TYPE_CHECKING
|
|
105
|
+
if isinstance(test, cst.Name) and test.value == "TYPE_CHECKING":
|
|
106
|
+
return True
|
|
107
|
+
# typing.TYPE_CHECKING
|
|
108
|
+
if isinstance(test, cst.Attribute):
|
|
109
|
+
if isinstance(test.attr, cst.Name) and test.attr.value == "TYPE_CHECKING":
|
|
110
|
+
# Optional: enforce base name 'typing'
|
|
111
|
+
if isinstance(test.value, cst.Name) and test.value.value == "typing":
|
|
112
|
+
return True
|
|
113
|
+
return False
|
|
File without changes
|