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,31 @@
|
|
|
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 OrderIterableItemsOption(AbstractPythonFileContentOption):
|
|
15
|
+
def get_description(self) -> str:
|
|
16
|
+
return "Sort items inside iterable literals following the '# filestate: python-iterable-sort' flag (primarily lists with one-item-per-line)."
|
|
17
|
+
|
|
18
|
+
def _apply_content_change(self, target: TargetFileOrDirectoryType) -> str:
|
|
19
|
+
"""Sort items inside flagged iterable literals (lists, and simple dicts where applicable).
|
|
20
|
+
|
|
21
|
+
Looks for the inline flag '# filestate: python-iterable-sort' and sorts the following
|
|
22
|
+
contiguous block of iterable items alphabetically (case-insensitive), preserving
|
|
23
|
+
indentation and punctuation. Intended primarily for list literals written one-item-per-line.
|
|
24
|
+
"""
|
|
25
|
+
from wexample_filestate_python.utils.python_iterable_utils import (
|
|
26
|
+
reorder_flagged_iterables,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
src = target.get_local_file().read()
|
|
30
|
+
modified = reorder_flagged_iterables(src)
|
|
31
|
+
return modified
|
|
@@ -0,0 +1,44 @@
|
|
|
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 OrderMainGuardOption(AbstractPythonFileContentOption):
|
|
15
|
+
def get_description(self) -> str:
|
|
16
|
+
return "Ensure the if __name__ == '__main__': block is the last non-empty statement in the module."
|
|
17
|
+
|
|
18
|
+
def _apply_content_change(self, target: TargetFileOrDirectoryType) -> str:
|
|
19
|
+
"""Ensure the `if __name__ == "__main__":` block is at the very end of the file.
|
|
20
|
+
|
|
21
|
+
Moves any top-level main-guard blocks to be the last non-empty statement in the
|
|
22
|
+
module (before trailing blank lines), preserving content and spacing as much as possible.
|
|
23
|
+
"""
|
|
24
|
+
import libcst as cst
|
|
25
|
+
|
|
26
|
+
from wexample_filestate_python.utils.python_main_guard_utils import (
|
|
27
|
+
find_main_guard_blocks,
|
|
28
|
+
is_main_guard_at_end,
|
|
29
|
+
move_main_guard_to_end,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
src = target.get_local_file().read()
|
|
33
|
+
module = cst.parse_module(src)
|
|
34
|
+
|
|
35
|
+
# No main guard present => nothing to do
|
|
36
|
+
if not find_main_guard_blocks(module):
|
|
37
|
+
return src
|
|
38
|
+
|
|
39
|
+
# Already at end => avoid whitespace-only diffs
|
|
40
|
+
if is_main_guard_at_end(module):
|
|
41
|
+
return src
|
|
42
|
+
|
|
43
|
+
modified = move_main_guard_to_end(module)
|
|
44
|
+
return modified.code
|
|
@@ -0,0 +1,73 @@
|
|
|
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 OrderModuleDocstringOption(AbstractPythonFileContentOption):
|
|
15
|
+
def get_description(self) -> str:
|
|
16
|
+
return "Move module docstring to the top of Python files. Ensures the module docstring appears as the first element before any imports or code."
|
|
17
|
+
|
|
18
|
+
def _apply_content_change(self, target: TargetFileOrDirectoryType) -> str:
|
|
19
|
+
"""Ensure module docstring is positioned at the very top of Python files.
|
|
20
|
+
|
|
21
|
+
Moves the module docstring (if present) to be the first element in the file,
|
|
22
|
+
before any imports or other code elements.
|
|
23
|
+
"""
|
|
24
|
+
import libcst as cst
|
|
25
|
+
|
|
26
|
+
from wexample_filestate_python.utils.python_docstring_utils import (
|
|
27
|
+
find_module_docstring,
|
|
28
|
+
is_module_docstring_at_top,
|
|
29
|
+
move_docstring_to_top,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
src = target.get_local_file().read()
|
|
33
|
+
module = cst.parse_module(src)
|
|
34
|
+
|
|
35
|
+
# Check if there's a docstring and if it needs to be moved
|
|
36
|
+
docstring_node, position = find_module_docstring(module)
|
|
37
|
+
|
|
38
|
+
if docstring_node is None:
|
|
39
|
+
# No docstring found, nothing to do
|
|
40
|
+
return src
|
|
41
|
+
|
|
42
|
+
if is_module_docstring_at_top(module):
|
|
43
|
+
# Check if quotes need normalization
|
|
44
|
+
if len(docstring_node.body) > 0 and isinstance(
|
|
45
|
+
docstring_node.body[0], cst.Expr
|
|
46
|
+
):
|
|
47
|
+
expr = docstring_node.body[0]
|
|
48
|
+
if isinstance(expr.value, cst.SimpleString):
|
|
49
|
+
quote = expr.value.quote
|
|
50
|
+
if quote.startswith("'''") or (
|
|
51
|
+
quote.startswith("'") and not quote.startswith('"')
|
|
52
|
+
):
|
|
53
|
+
# Need to normalize quotes
|
|
54
|
+
from wexample_filestate_python.utils.python_docstring_utils import (
|
|
55
|
+
normalize_docstring_quotes,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
normalized_docstring = normalize_docstring_quotes(
|
|
59
|
+
docstring_node
|
|
60
|
+
)
|
|
61
|
+
# Ensure no leading whitespace for the docstring at top
|
|
62
|
+
clean_docstring = normalized_docstring.with_changes(
|
|
63
|
+
leading_lines=[]
|
|
64
|
+
)
|
|
65
|
+
new_body = [clean_docstring] + list(module.body[1:])
|
|
66
|
+
modified_module = module.with_changes(body=new_body)
|
|
67
|
+
return modified_module.code
|
|
68
|
+
# Already at top and quotes are fine
|
|
69
|
+
return src
|
|
70
|
+
|
|
71
|
+
# Move docstring to top (this also normalizes quotes)
|
|
72
|
+
modified_module = move_docstring_to_top(module)
|
|
73
|
+
return modified_module.code
|
|
@@ -0,0 +1,42 @@
|
|
|
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 OrderModuleFunctionsOption(AbstractPythonFileContentOption):
|
|
15
|
+
def get_description(self) -> str:
|
|
16
|
+
return "Order module-level functions: public A–Z, then private (_*), keeping @overload groups, and move them before classes."
|
|
17
|
+
|
|
18
|
+
def _apply_content_change(self, target: TargetFileOrDirectoryType) -> str:
|
|
19
|
+
"""Order module-level functions: public A–Z, then private (_*) A–Z, before classes.
|
|
20
|
+
|
|
21
|
+
- Keeps @overload groups attached to their implementation.
|
|
22
|
+
- Preserves spacing/comments by keeping each group's first function's leading_lines.
|
|
23
|
+
"""
|
|
24
|
+
import libcst as cst
|
|
25
|
+
|
|
26
|
+
from wexample_filestate_python.utils.python_functions_utils import (
|
|
27
|
+
module_functions_sorted_before_classes,
|
|
28
|
+
reorder_module_functions,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
src = target.get_local_file().read()
|
|
32
|
+
module = cst.parse_module(src)
|
|
33
|
+
|
|
34
|
+
# Quick no-op detection: if there are no functions, or functions already sorted
|
|
35
|
+
# and placed before classes, the transformation may be a noop.
|
|
36
|
+
if module_functions_sorted_before_classes(module):
|
|
37
|
+
# We still need to check sorting (public then private) and alpha order.
|
|
38
|
+
# We will compute the transformed module and compare.
|
|
39
|
+
pass
|
|
40
|
+
|
|
41
|
+
modified = reorder_module_functions(module)
|
|
42
|
+
return modified.code
|
|
@@ -0,0 +1,62 @@
|
|
|
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 OrderModuleMetadataOption(AbstractPythonFileContentOption):
|
|
15
|
+
def get_description(self) -> str:
|
|
16
|
+
return "Group and sort module metadata (e.g., __all__, __version__, __author__) at module level with minimal spacing changes."
|
|
17
|
+
|
|
18
|
+
def _apply_content_change(self, target: TargetFileOrDirectoryType) -> str:
|
|
19
|
+
"""Group and sort module metadata assignments at module level.
|
|
20
|
+
|
|
21
|
+
Collects recognized metadata variables like `__all__`, `__version__`, `__author__`, etc.,
|
|
22
|
+
groups them as a contiguous block, and sorts them alphabetically by variable name.
|
|
23
|
+
|
|
24
|
+
Placement: after imports and `if TYPE_CHECKING:` blocks, before other module-level code.
|
|
25
|
+
"""
|
|
26
|
+
import libcst as cst
|
|
27
|
+
|
|
28
|
+
from wexample_filestate_python.utils.python_module_metadata_utils import (
|
|
29
|
+
find_module_metadata_statements,
|
|
30
|
+
group_and_sort_module_metadata,
|
|
31
|
+
target_index_for_module_metadata,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
src = target.get_local_file().read()
|
|
35
|
+
module = cst.parse_module(src)
|
|
36
|
+
|
|
37
|
+
found = find_module_metadata_statements(module)
|
|
38
|
+
if not found:
|
|
39
|
+
return src
|
|
40
|
+
|
|
41
|
+
# Determine if already grouped and sorted at the correct position
|
|
42
|
+
indices = [i for i, _, _ in found]
|
|
43
|
+
# contiguous block?
|
|
44
|
+
contiguous = (
|
|
45
|
+
indices == list(range(indices[0], indices[0] + len(indices)))
|
|
46
|
+
if indices
|
|
47
|
+
else True
|
|
48
|
+
)
|
|
49
|
+
# names sorted?
|
|
50
|
+
names = [name for _, __, name in found]
|
|
51
|
+
names_sorted = sorted(names, key=lambda n: n.lower())
|
|
52
|
+
already_sorted = names == names_sorted
|
|
53
|
+
# at correct position?
|
|
54
|
+
desired_index = target_index_for_module_metadata(module)
|
|
55
|
+
at_target_position = indices and indices[0] == desired_index
|
|
56
|
+
|
|
57
|
+
# If everything is already correct, avoid making whitespace-only changes
|
|
58
|
+
if contiguous and already_sorted and at_target_position:
|
|
59
|
+
return src
|
|
60
|
+
|
|
61
|
+
modified = group_and_sort_module_metadata(module)
|
|
62
|
+
return modified.code
|
|
@@ -0,0 +1,51 @@
|
|
|
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 OrderTypeCheckingBlockOption(AbstractPythonFileContentOption):
|
|
15
|
+
def get_description(self) -> str:
|
|
16
|
+
return "Move if TYPE_CHECKING blocks after imports. Keeps code layout predictable while preserving behavior."
|
|
17
|
+
|
|
18
|
+
def _apply_content_change(self, target: TargetFileOrDirectoryType) -> str:
|
|
19
|
+
"""Move `if TYPE_CHECKING:` blocks after regular imports.
|
|
20
|
+
|
|
21
|
+
Ensures that all top-level `if TYPE_CHECKING:` blocks are placed immediately
|
|
22
|
+
after the last regular import section (or after `from __future__ import ...`
|
|
23
|
+
if no regular imports exist). Keeps spacing minimal and preserves content.
|
|
24
|
+
"""
|
|
25
|
+
import libcst as cst
|
|
26
|
+
|
|
27
|
+
from wexample_filestate_python.utils.python_type_checking_utils import (
|
|
28
|
+
find_type_checking_blocks,
|
|
29
|
+
move_type_checking_blocks_after_imports,
|
|
30
|
+
target_index_for_type_checking,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
src = target.get_local_file().read()
|
|
34
|
+
module = cst.parse_module(src)
|
|
35
|
+
|
|
36
|
+
blocks = find_type_checking_blocks(module)
|
|
37
|
+
if not blocks:
|
|
38
|
+
return src
|
|
39
|
+
|
|
40
|
+
# Compute current positions; if already correctly positioned, no change
|
|
41
|
+
current_indices = [i for i, _ in blocks]
|
|
42
|
+
desired_index = target_index_for_type_checking(module)
|
|
43
|
+
# Already correct if first block starts at desired index and blocks are contiguous
|
|
44
|
+
contiguous = current_indices == list(
|
|
45
|
+
range(current_indices[0], current_indices[0] + len(current_indices))
|
|
46
|
+
)
|
|
47
|
+
if contiguous and current_indices[0] == desired_index:
|
|
48
|
+
return src
|
|
49
|
+
|
|
50
|
+
modified = move_type_checking_blocks_after_imports(module)
|
|
51
|
+
return modified.code
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Any, ClassVar, Union
|
|
4
|
+
|
|
5
|
+
from wexample_config.config_option.abstract_config_option import AbstractConfigOption
|
|
6
|
+
from wexample_config.config_option.abstract_nested_config_option import (
|
|
7
|
+
AbstractNestedConfigOption,
|
|
8
|
+
)
|
|
9
|
+
from wexample_filestate.operation.abstract_operation import AbstractOperation
|
|
10
|
+
from wexample_filestate.option.mixin.option_mixin import OptionMixin
|
|
11
|
+
from wexample_helpers.decorator.base_class import base_class
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from wexample_filestate.const.types_state_items import TargetFileOrDirectoryType
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@base_class
|
|
18
|
+
class PythonOption(OptionMixin, AbstractNestedConfigOption):
|
|
19
|
+
# filestate: python-constant-sort
|
|
20
|
+
# New preferred option name to add `from __future__ import annotations`
|
|
21
|
+
OPTION_NAME_ADD_FUTURE_ANNOTATIONS: ClassVar[str] = "add_future_annotations"
|
|
22
|
+
OPTION_NAME_ADD_RETURN_TYPES: ClassVar[str] = "add_return_types"
|
|
23
|
+
# Fix attrs usage (ensure kw_only=True, etc.)
|
|
24
|
+
OPTION_NAME_FIX_ATTRS: ClassVar[str] = "fix_attrs"
|
|
25
|
+
# Fix blank lines in Python files (after signatures, docstrings, etc.)
|
|
26
|
+
OPTION_NAME_FIX_BLANK_LINES: ClassVar[str] = "fix_blank_lines"
|
|
27
|
+
OPTION_NAME_FORMAT: ClassVar[str] = "format"
|
|
28
|
+
OPTION_NAME_FSTRINGIFY: ClassVar[str] = "fstringify"
|
|
29
|
+
OPTION_NAME_MODERNIZE_TYPING: ClassVar[str] = "modernize_typing"
|
|
30
|
+
# Sort class attributes: special first, then public A–Z, then private/protected A–Z
|
|
31
|
+
OPTION_NAME_ORDER_CLASS_ATTRIBUTES: ClassVar[str] = "order_class_attributes"
|
|
32
|
+
# Ensure class docstring is first statement after header/decorators
|
|
33
|
+
OPTION_NAME_ORDER_CLASS_DOCSTRING: ClassVar[str] = "order_class_docstring"
|
|
34
|
+
# Order class methods (dunders sequence, class/staticmethods, properties, instances)
|
|
35
|
+
OPTION_NAME_ORDER_CLASS_METHODS: ClassVar[str] = "order_class_methods"
|
|
36
|
+
# Sort flagged UPPER_CASE constant blocks at module level
|
|
37
|
+
OPTION_NAME_ORDER_CONSTANTS: ClassVar[str] = "order_constants"
|
|
38
|
+
# Sort items inside flagged iterable literals (lists/dicts)
|
|
39
|
+
OPTION_NAME_ORDER_ITERABLE_ITEMS: ClassVar[str] = "order_iterable_items"
|
|
40
|
+
# Ensure if __name__ == "__main__" block is at the very end
|
|
41
|
+
OPTION_NAME_ORDER_MAIN_GUARD: ClassVar[str] = "order_main_guard"
|
|
42
|
+
# Order module docstring to be at the top of the file
|
|
43
|
+
OPTION_NAME_ORDER_MODULE_DOCSTRING: ClassVar[str] = "order_module_docstring"
|
|
44
|
+
# Order module-level functions (public A–Z, then private)
|
|
45
|
+
OPTION_NAME_ORDER_MODULE_FUNCTIONS: ClassVar[str] = "order_module_functions"
|
|
46
|
+
# Group and sort module metadata at module level
|
|
47
|
+
OPTION_NAME_ORDER_MODULE_METADATA: ClassVar[str] = "order_module_metadata"
|
|
48
|
+
# Normalize blank lines between program structures (spacing rules)
|
|
49
|
+
OPTION_NAME_ORDER_SPACING: ClassVar[str] = "order_spacing"
|
|
50
|
+
# Move TYPE_CHECKING blocks to after regular imports
|
|
51
|
+
OPTION_NAME_ORDER_TYPE_CHECKING_BLOCK: ClassVar[str] = "order_type_checking_block"
|
|
52
|
+
# Relocate imports by usage (runtime-in-method, class property types, type-only)
|
|
53
|
+
OPTION_NAME_RELOCATE_IMPORTS: ClassVar[str] = "relocate_imports"
|
|
54
|
+
OPTION_NAME_REMOVE_UNUSED: ClassVar[str] = "remove_unused"
|
|
55
|
+
OPTION_NAME_SORT_IMPORTS: ClassVar[str] = "sort_imports"
|
|
56
|
+
# New policy: unquote annotations (remove string annotations)
|
|
57
|
+
OPTION_NAME_UNQUOTE_ANNOTATIONS: ClassVar[str] = "unquote_annotations"
|
|
58
|
+
|
|
59
|
+
@staticmethod
|
|
60
|
+
def get_raw_value_allowed_type() -> Any:
|
|
61
|
+
from wexample_filestate_python.config_value.python_config_value import (
|
|
62
|
+
PythonConfigValue,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
return Union[list[str], dict, PythonConfigValue]
|
|
66
|
+
|
|
67
|
+
def create_required_operation(
|
|
68
|
+
self, target: TargetFileOrDirectoryType
|
|
69
|
+
) -> AbstractOperation | None:
|
|
70
|
+
return self._create_child_required_operation(target=target)
|
|
71
|
+
|
|
72
|
+
def get_allowed_options(self) -> list[type[AbstractConfigOption]]:
|
|
73
|
+
# Import all the config options for each Python operation
|
|
74
|
+
from wexample_filestate_python.option.add_future_annotations_option import (
|
|
75
|
+
AddFutureAnnotationsOption,
|
|
76
|
+
)
|
|
77
|
+
from wexample_filestate_python.option.add_return_types_option import (
|
|
78
|
+
AddReturnTypesOption,
|
|
79
|
+
)
|
|
80
|
+
from wexample_filestate_python.option.fix_attrs_option import FixAttrsOption
|
|
81
|
+
from wexample_filestate_python.option.fix_blank_lines_option import (
|
|
82
|
+
FixBlankLinesOption,
|
|
83
|
+
)
|
|
84
|
+
from wexample_filestate_python.option.format_option import FormatOption
|
|
85
|
+
from wexample_filestate_python.option.fstringify_option import FstringifyOption
|
|
86
|
+
from wexample_filestate_python.option.modernize_typing_option import (
|
|
87
|
+
ModernizeTypingOption,
|
|
88
|
+
)
|
|
89
|
+
from wexample_filestate_python.option.order_class_attributes_option import (
|
|
90
|
+
OrderClassAttributesOption,
|
|
91
|
+
)
|
|
92
|
+
from wexample_filestate_python.option.order_class_docstring_option import (
|
|
93
|
+
OrderClassDocstringOption,
|
|
94
|
+
)
|
|
95
|
+
from wexample_filestate_python.option.order_class_methods_option import (
|
|
96
|
+
OrderClassMethodsOption,
|
|
97
|
+
)
|
|
98
|
+
from wexample_filestate_python.option.order_constants_option import (
|
|
99
|
+
OrderConstantsOption,
|
|
100
|
+
)
|
|
101
|
+
from wexample_filestate_python.option.order_iterable_items_option import (
|
|
102
|
+
OrderIterableItemsOption,
|
|
103
|
+
)
|
|
104
|
+
from wexample_filestate_python.option.order_main_guard_option import (
|
|
105
|
+
OrderMainGuardOption,
|
|
106
|
+
)
|
|
107
|
+
from wexample_filestate_python.option.order_module_docstring_option import (
|
|
108
|
+
OrderModuleDocstringOption,
|
|
109
|
+
)
|
|
110
|
+
from wexample_filestate_python.option.order_module_functions_option import (
|
|
111
|
+
OrderModuleFunctionsOption,
|
|
112
|
+
)
|
|
113
|
+
from wexample_filestate_python.option.order_module_metadata_option import (
|
|
114
|
+
OrderModuleMetadataOption,
|
|
115
|
+
)
|
|
116
|
+
from wexample_filestate_python.option.order_type_checking_block_option import (
|
|
117
|
+
OrderTypeCheckingBlockOption,
|
|
118
|
+
)
|
|
119
|
+
from wexample_filestate_python.option.relocate_imports_option import (
|
|
120
|
+
RelocateImportsOption,
|
|
121
|
+
)
|
|
122
|
+
from wexample_filestate_python.option.remove_unused_option import (
|
|
123
|
+
RemoveUnusedOption,
|
|
124
|
+
)
|
|
125
|
+
from wexample_filestate_python.option.sort_imports_option import (
|
|
126
|
+
SortImportsOption,
|
|
127
|
+
)
|
|
128
|
+
from wexample_filestate_python.option.unquote_annotations_option import (
|
|
129
|
+
UnquoteAnnotationsOption,
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
return [
|
|
133
|
+
AddFutureAnnotationsOption,
|
|
134
|
+
AddReturnTypesOption,
|
|
135
|
+
FixAttrsOption,
|
|
136
|
+
FixBlankLinesOption,
|
|
137
|
+
FormatOption,
|
|
138
|
+
FstringifyOption,
|
|
139
|
+
ModernizeTypingOption,
|
|
140
|
+
OrderClassAttributesOption,
|
|
141
|
+
OrderClassDocstringOption,
|
|
142
|
+
OrderClassMethodsOption,
|
|
143
|
+
OrderConstantsOption,
|
|
144
|
+
OrderIterableItemsOption,
|
|
145
|
+
OrderMainGuardOption,
|
|
146
|
+
OrderModuleDocstringOption,
|
|
147
|
+
OrderModuleFunctionsOption,
|
|
148
|
+
OrderModuleMetadataOption,
|
|
149
|
+
OrderTypeCheckingBlockOption,
|
|
150
|
+
RelocateImportsOption,
|
|
151
|
+
RemoveUnusedOption,
|
|
152
|
+
SortImportsOption,
|
|
153
|
+
UnquoteAnnotationsOption,
|
|
154
|
+
]
|
|
155
|
+
|
|
156
|
+
def set_value(self, raw_value: Any) -> None:
|
|
157
|
+
# Convert list form to dict form for consistency
|
|
158
|
+
if isinstance(raw_value, list):
|
|
159
|
+
dict_value = {}
|
|
160
|
+
for option_name in raw_value:
|
|
161
|
+
dict_value[option_name] = True
|
|
162
|
+
raw_value = dict_value
|
|
163
|
+
|
|
164
|
+
super().set_value(raw_value=raw_value)
|
|
@@ -0,0 +1,189 @@
|
|
|
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 RelocateImportsOption(AbstractPythonFileContentOption):
|
|
15
|
+
def get_description(self) -> str:
|
|
16
|
+
return "Keep module-level and type imports at the top; move runtime-only imports used inside methods into those methods."
|
|
17
|
+
|
|
18
|
+
def _apply_content_change(self, target: TargetFileOrDirectoryType) -> str:
|
|
19
|
+
"""Relocate imports according to usage categories:
|
|
20
|
+
|
|
21
|
+
Rules:
|
|
22
|
+
- runtime_local (formerly A): names used at runtime inside functions/methods
|
|
23
|
+
(e.g., return MyClass(), typing.cast(x, MyClass)).
|
|
24
|
+
-> import locally at the top of each function using it.
|
|
25
|
+
- class_level (formerly B): names required at class-definition time
|
|
26
|
+
(e.g., class attribute annotations, base class references).
|
|
27
|
+
-> keep/import at module top level.
|
|
28
|
+
- type_only (formerly C): names used only in type annotations (function
|
|
29
|
+
params/returns, module-level annotations) and not in runtime_local or class_level.
|
|
30
|
+
-> move under `if TYPE_CHECKING:` at module top (add "from typing import TYPE_CHECKING"
|
|
31
|
+
if missing). Files already have `from __future__ import annotations`.
|
|
32
|
+
"""
|
|
33
|
+
from collections import defaultdict
|
|
34
|
+
from typing import DefaultDict
|
|
35
|
+
|
|
36
|
+
import libcst as cst
|
|
37
|
+
|
|
38
|
+
from wexample_filestate_python.utils.relocate_imports.python_import_rewriter import (
|
|
39
|
+
PythonImportRewriter,
|
|
40
|
+
)
|
|
41
|
+
from wexample_filestate_python.utils.relocate_imports.python_localize_runtime_imports import (
|
|
42
|
+
PythonLocalizeRuntimeImports,
|
|
43
|
+
)
|
|
44
|
+
from wexample_filestate_python.utils.relocate_imports.python_parser_import_index import (
|
|
45
|
+
PythonParserImportIndex,
|
|
46
|
+
)
|
|
47
|
+
from wexample_filestate_python.utils.relocate_imports.python_runtime_symbol_collector import (
|
|
48
|
+
PythonRuntimeSymbolCollector,
|
|
49
|
+
)
|
|
50
|
+
from wexample_filestate_python.utils.relocate_imports.python_usage_collector import (
|
|
51
|
+
PythonUsageCollector,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
src = target.get_local_file().read()
|
|
55
|
+
module = cst.parse_module(src)
|
|
56
|
+
|
|
57
|
+
# Index current imports using shared utility
|
|
58
|
+
idx = PythonParserImportIndex()
|
|
59
|
+
module.visit(idx)
|
|
60
|
+
|
|
61
|
+
imported_value_names: set[str] = set(idx.name_to_from.keys())
|
|
62
|
+
|
|
63
|
+
# Usage collection
|
|
64
|
+
# runtime_local: usage inside function bodies
|
|
65
|
+
# class_level: usage inside class body annotations (needed at definition time)
|
|
66
|
+
# type_only: type-only annotations across module if not in runtime_local or class_level
|
|
67
|
+
functions_needing_local: DefaultDict[str, set[str]] = defaultdict(
|
|
68
|
+
set
|
|
69
|
+
) # func_qualified_name -> names
|
|
70
|
+
class_level_names: set[str] = set()
|
|
71
|
+
type_annotation_names: set[str] = set()
|
|
72
|
+
cast_type_names_anywhere: set[str] = set()
|
|
73
|
+
uc = PythonUsageCollector(
|
|
74
|
+
imported_value_names=imported_value_names,
|
|
75
|
+
functions_needing_local=functions_needing_local,
|
|
76
|
+
used_in_B=class_level_names,
|
|
77
|
+
used_in_C_annot=type_annotation_names,
|
|
78
|
+
cast_type_names_anywhere=cast_type_names_anywhere,
|
|
79
|
+
idx=idx,
|
|
80
|
+
)
|
|
81
|
+
module.visit(uc)
|
|
82
|
+
|
|
83
|
+
# Conservative fallback: collect any imported names used in non-annotation expressions
|
|
84
|
+
rsc = PythonRuntimeSymbolCollector(imported_value_names=imported_value_names)
|
|
85
|
+
module.visit(rsc)
|
|
86
|
+
runtime_used_anywhere: set[str] = rsc.runtime_used_anywhere
|
|
87
|
+
|
|
88
|
+
# Resolve categories
|
|
89
|
+
runtime_local_all: set[str] = (
|
|
90
|
+
set().union(*functions_needing_local.values())
|
|
91
|
+
if functions_needing_local
|
|
92
|
+
else set()
|
|
93
|
+
)
|
|
94
|
+
# class_level has priority over runtime_local: if a name is class-level, we do NOT local-import it
|
|
95
|
+
runtime_local_final: set[str] = {
|
|
96
|
+
n for n in runtime_local_all if n not in class_level_names
|
|
97
|
+
}
|
|
98
|
+
# type_only = in type annotations but not runtime_local_final or class_level
|
|
99
|
+
# Exclude any names that appear in cast() type expressions anywhere from C-only,
|
|
100
|
+
# since casts require runtime availability of the symbol.
|
|
101
|
+
type_only_names: set[str] = {
|
|
102
|
+
n
|
|
103
|
+
for n in type_annotation_names
|
|
104
|
+
if n not in runtime_local_final
|
|
105
|
+
and n not in class_level_names
|
|
106
|
+
and n not in cast_type_names_anywhere
|
|
107
|
+
and n not in runtime_used_anywhere
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
# Names to include under TYPE_CHECKING:
|
|
111
|
+
# Use all names that appear in annotations (params/returns/module-level), excluding class-level.
|
|
112
|
+
# Then subtract names that are already locally imported inside some function where
|
|
113
|
+
# they are used (to avoid redundant TYPE_CHECKING imports when a function-scoped
|
|
114
|
+
# import suffices, e.g., `from multiprocessing import Queue` inside a method).
|
|
115
|
+
annotation_names_candidate: set[str] = {
|
|
116
|
+
n for n in type_annotation_names if n not in class_level_names
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
# Collect names imported locally inside functions anywhere in the module
|
|
120
|
+
class _LocalImportNameCollector(cst.CSTVisitor):
|
|
121
|
+
def __init__(self) -> None:
|
|
122
|
+
self.stack: list[str] = []
|
|
123
|
+
self.local_imported: set[str] = set()
|
|
124
|
+
|
|
125
|
+
def visit_FunctionDef(self, node: cst.FunctionDef) -> bool: # type: ignore[override]
|
|
126
|
+
self.stack.append(node.name.value)
|
|
127
|
+
return True
|
|
128
|
+
|
|
129
|
+
def leave_FunctionDef(self, node: cst.FunctionDef) -> None: # type: ignore[override]
|
|
130
|
+
self.stack.pop()
|
|
131
|
+
|
|
132
|
+
def visit_ImportFrom(self, node: cst.ImportFrom) -> bool: # type: ignore[override]
|
|
133
|
+
if not self.stack:
|
|
134
|
+
return True
|
|
135
|
+
if node.names is None or isinstance(node.names, cst.ImportStar):
|
|
136
|
+
return True
|
|
137
|
+
for alias in node.names:
|
|
138
|
+
if isinstance(alias, cst.ImportAlias) and isinstance(
|
|
139
|
+
alias.name, cst.Name
|
|
140
|
+
):
|
|
141
|
+
name = (
|
|
142
|
+
alias.asname.name.value
|
|
143
|
+
if alias.asname
|
|
144
|
+
else alias.name.value
|
|
145
|
+
)
|
|
146
|
+
self.local_imported.add(name)
|
|
147
|
+
return True
|
|
148
|
+
|
|
149
|
+
lic = _LocalImportNameCollector()
|
|
150
|
+
module.visit(lic)
|
|
151
|
+
type_only_for_block: set[str] = annotation_names_candidate - lic.local_imported
|
|
152
|
+
|
|
153
|
+
# For names used inside cast() anywhere in the module:
|
|
154
|
+
# - do NOT auto-add to TYPE_CHECKING (unless also in annotations via type_only_for_block)
|
|
155
|
+
# - remove module-level import unless also class_level
|
|
156
|
+
names_to_remove_from_module = (
|
|
157
|
+
set(runtime_local_final)
|
|
158
|
+
| set(type_only_names)
|
|
159
|
+
| (set(cast_type_names_anywhere) - set(class_level_names))
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
# Do not add to TYPE_CHECKING if the name's module-level import is kept
|
|
163
|
+
kept_module_imports: set[str] = {
|
|
164
|
+
n for n in imported_value_names if n not in names_to_remove_from_module
|
|
165
|
+
}
|
|
166
|
+
used_in_C_only_final: set[str] = set(type_only_for_block) - kept_module_imports
|
|
167
|
+
|
|
168
|
+
# Debug summary removed
|
|
169
|
+
rewritten = module.visit(
|
|
170
|
+
PythonImportRewriter(
|
|
171
|
+
used_in_B=class_level_names,
|
|
172
|
+
names_to_remove_from_module=names_to_remove_from_module,
|
|
173
|
+
used_in_C_only=used_in_C_only_final,
|
|
174
|
+
idx=idx,
|
|
175
|
+
)
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
# 2) Inject local imports into functions for runtime_local names
|
|
179
|
+
# For each function with names, add `from <module> import Name` at top of body.
|
|
180
|
+
final_module = rewritten.visit(
|
|
181
|
+
PythonLocalizeRuntimeImports(
|
|
182
|
+
idx=idx,
|
|
183
|
+
functions_needing_local=functions_needing_local,
|
|
184
|
+
# Do not skip cast-used names so they are localized per method.
|
|
185
|
+
skip_local_names=set(class_level_names),
|
|
186
|
+
)
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
return final_module.code
|