wexample-filestate-python 0.0.48__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. wexample_filestate_python/__init__.py +0 -0
  2. wexample_filestate_python/__pycache__/__init__.py +0 -0
  3. wexample_filestate_python/common/__init__.py +0 -0
  4. wexample_filestate_python/common/__pycache__/__init__.py +0 -0
  5. wexample_filestate_python/common/pipy_gateway.py +20 -0
  6. wexample_filestate_python/config_option/__init__.py +0 -0
  7. wexample_filestate_python/config_option/__pycache__/__init__.py +0 -0
  8. wexample_filestate_python/config_option/mixin/__init__.py +0 -0
  9. wexample_filestate_python/config_option/mixin/__pycache__/__init__.py +0 -0
  10. wexample_filestate_python/config_option/mixin/with_stdout_wrapping_mixin.py +46 -0
  11. wexample_filestate_python/config_value/__init__.py +0 -0
  12. wexample_filestate_python/config_value/__pycache__/__init__.py +0 -0
  13. wexample_filestate_python/config_value/python_config_value.py +195 -0
  14. wexample_filestate_python/const/__init__.py +0 -0
  15. wexample_filestate_python/const/__pycache__/__init__.py +0 -0
  16. wexample_filestate_python/const/name_pattern.py +4 -0
  17. wexample_filestate_python/const/python_file.py +5 -0
  18. wexample_filestate_python/file/__init__.py +0 -0
  19. wexample_filestate_python/file/__pycache__/__init__.py +0 -0
  20. wexample_filestate_python/file/python_file.py +12 -0
  21. wexample_filestate_python/helpers/__init__.py +0 -0
  22. wexample_filestate_python/helpers/__pycache__/__init__.py +0 -0
  23. wexample_filestate_python/helpers/package.py +122 -0
  24. wexample_filestate_python/helpers/toml.py +116 -0
  25. wexample_filestate_python/option/__init__.py +0 -0
  26. wexample_filestate_python/option/__pycache__/__init__.py +0 -0
  27. wexample_filestate_python/option/abstract_python_file_content_option.py +45 -0
  28. wexample_filestate_python/option/add_future_annotations_option.py +79 -0
  29. wexample_filestate_python/option/add_return_types_option.py +265 -0
  30. wexample_filestate_python/option/fix_attrs_option.py +37 -0
  31. wexample_filestate_python/option/fix_blank_lines_option.py +47 -0
  32. wexample_filestate_python/option/format_option.py +34 -0
  33. wexample_filestate_python/option/fstringify_option.py +34 -0
  34. wexample_filestate_python/option/modernize_typing_option.py +25 -0
  35. wexample_filestate_python/option/order_class_attributes_option.py +34 -0
  36. wexample_filestate_python/option/order_class_docstring_option.py +36 -0
  37. wexample_filestate_python/option/order_class_methods_option.py +37 -0
  38. wexample_filestate_python/option/order_constants_option.py +35 -0
  39. wexample_filestate_python/option/order_iterable_items_option.py +31 -0
  40. wexample_filestate_python/option/order_main_guard_option.py +44 -0
  41. wexample_filestate_python/option/order_module_docstring_option.py +73 -0
  42. wexample_filestate_python/option/order_module_functions_option.py +42 -0
  43. wexample_filestate_python/option/order_module_metadata_option.py +62 -0
  44. wexample_filestate_python/option/order_type_checking_block_option.py +51 -0
  45. wexample_filestate_python/option/python_option.py +164 -0
  46. wexample_filestate_python/option/relocate_imports_option.py +189 -0
  47. wexample_filestate_python/option/remove_unused_option.py +45 -0
  48. wexample_filestate_python/option/sort_imports_option.py +26 -0
  49. wexample_filestate_python/option/unquote_annotations_option.py +85 -0
  50. wexample_filestate_python/options_provider/__init__.py +0 -0
  51. wexample_filestate_python/options_provider/__pycache__/__init__.py +0 -0
  52. wexample_filestate_python/options_provider/python_options_provider.py +24 -0
  53. wexample_filestate_python/py.typed +0 -0
  54. wexample_filestate_python/utils/__init__.py +0 -0
  55. wexample_filestate_python/utils/__pycache__/__init__.py +0 -0
  56. wexample_filestate_python/utils/python_attrs_utils.py +112 -0
  57. wexample_filestate_python/utils/python_blank_lines_utils.py +568 -0
  58. wexample_filestate_python/utils/python_class_attributes_utils.py +275 -0
  59. wexample_filestate_python/utils/python_class_docstring_utils.py +85 -0
  60. wexample_filestate_python/utils/python_class_methods_utils.py +230 -0
  61. wexample_filestate_python/utils/python_constants_utils.py +302 -0
  62. wexample_filestate_python/utils/python_docstring_utils.py +117 -0
  63. wexample_filestate_python/utils/python_functions_utils.py +212 -0
  64. wexample_filestate_python/utils/python_iterable_utils.py +131 -0
  65. wexample_filestate_python/utils/python_main_guard_utils.py +80 -0
  66. wexample_filestate_python/utils/python_module_metadata_utils.py +147 -0
  67. wexample_filestate_python/utils/python_type_checking_utils.py +113 -0
  68. wexample_filestate_python/utils/relocate_imports/__init__.py +7 -0
  69. wexample_filestate_python/utils/relocate_imports/__pycache__/__init__.py +0 -0
  70. wexample_filestate_python/utils/relocate_imports/python_import_rewriter.py +413 -0
  71. wexample_filestate_python/utils/relocate_imports/python_localize_runtime_imports.py +324 -0
  72. wexample_filestate_python/utils/relocate_imports/python_parser_import_index.py +80 -0
  73. wexample_filestate_python/utils/relocate_imports/python_runtime_symbol_collector.py +33 -0
  74. wexample_filestate_python/utils/relocate_imports/python_usage_collector.py +410 -0
  75. wexample_filestate_python/workdir/__init__.py +0 -0
  76. wexample_filestate_python/workdir/__pycache__/__init__.py +0 -0
  77. wexample_filestate_python-0.0.48.dist-info/METADATA +191 -0
  78. wexample_filestate_python-0.0.48.dist-info/RECORD +80 -0
  79. wexample_filestate_python-0.0.48.dist-info/WHEEL +4 -0
  80. wexample_filestate_python-0.0.48.dist-info/entry_points.txt +4 -0
@@ -0,0 +1,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