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
File without changes
File without changes
File without changes
@@ -0,0 +1,20 @@
1
+ from __future__ import annotations
2
+
3
+ from wexample_helpers.classes.field import public_field
4
+ from wexample_helpers.decorator.base_class import base_class
5
+ from wexample_helpers_api.common.abstract_gateway import AbstractGateway
6
+
7
+
8
+ @base_class
9
+ class PipyGateway(AbstractGateway):
10
+ base_url: str | None = public_field(
11
+ default="https://pypi.org/", description="Base Pipy API URL"
12
+ )
13
+
14
+ def package_release_exists(self, package_name: str, version: str) -> bool:
15
+ response = self.make_request(f"pypi/{package_name}/json")
16
+ # Package exists
17
+ if response.status_code == 200:
18
+ return bool(response.json().get("releases", {}).get(version))
19
+
20
+ return False
File without changes
@@ -0,0 +1,46 @@
1
+ from __future__ import annotations
2
+
3
+
4
+ class WithStdoutWrappingMixin:
5
+ @classmethod
6
+ def _execute_and_wrap_stdout(cls, callback):
7
+ """Execute a callback and wrap any stdout/stderr output with additional newlines.
8
+
9
+ This ensures that output from external tools doesn't interfere with progress indicators
10
+ by adding a newline after any captured output.
11
+
12
+ Args:
13
+ callback: Function to execute that may produce stdout/stderr output
14
+
15
+ Returns:
16
+ The return value of the callback function
17
+ """
18
+ import io
19
+ import sys
20
+
21
+ old_stdout = sys.stdout
22
+ old_stderr = sys.stderr
23
+ captured_stdout = io.StringIO()
24
+ captured_stderr = io.StringIO()
25
+ sys.stdout = captured_stdout
26
+ sys.stderr = captured_stderr
27
+
28
+ try:
29
+ result = callback()
30
+ finally:
31
+ sys.stdout = old_stdout
32
+ sys.stderr = old_stderr
33
+
34
+ stdout_content = captured_stdout.getvalue()
35
+ stderr_content = captured_stderr.getvalue()
36
+
37
+ if stdout_content.strip():
38
+ sys.stdout.write(stdout_content.rstrip())
39
+ sys.stdout.write("\n")
40
+ sys.stdout.write("\n")
41
+ if stderr_content.strip():
42
+ sys.stderr.write(stderr_content.rstrip())
43
+ sys.stderr.write("\n")
44
+ sys.stderr.write("\n")
45
+
46
+ return result
File without changes
@@ -0,0 +1,195 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from wexample_config.config_value.config_value import ConfigValue
6
+ from wexample_helpers.classes.field import public_field
7
+ from wexample_helpers.decorator.base_class import base_class
8
+
9
+
10
+ @base_class
11
+ class PythonConfigValue(ConfigValue):
12
+ add_future_annotations: bool | None = public_field(
13
+ default=None,
14
+ description="Add `from __future__ import annotations`",
15
+ )
16
+ add_return_types: bool | None = public_field(
17
+ default=None,
18
+ description="Add return type annotations",
19
+ )
20
+ fix_attrs: bool | None = public_field(
21
+ default=None,
22
+ description="Fix attrs usage (ensure kw_only=True, etc.)",
23
+ )
24
+ fix_blank_lines: bool | None = public_field(
25
+ default=None,
26
+ description="Fix blank lines in Python files",
27
+ )
28
+ format: bool | None = public_field(
29
+ default=None,
30
+ description="Format Python code",
31
+ )
32
+ fstringify: bool | None = public_field(
33
+ default=None,
34
+ description="Convert string formatting to f-strings",
35
+ )
36
+ modernize_typing: bool | None = public_field(
37
+ default=None,
38
+ description="Modernize typing annotations",
39
+ )
40
+ order_class_attributes: bool | None = public_field(
41
+ default=None,
42
+ description="Sort class attributes: special first, then public A–Z, then private/protected A–Z",
43
+ )
44
+ order_class_docstring: bool | None = public_field(
45
+ default=None,
46
+ description="Ensure class docstring is first statement after header/decorators",
47
+ )
48
+ order_class_methods: bool | None = public_field(
49
+ default=None,
50
+ description="Order class methods (dunders sequence, class/staticmethods, properties, instances)",
51
+ )
52
+ order_constants: bool | None = public_field(
53
+ default=None,
54
+ description="Sort flagged UPPER_CASE constant blocks at module level",
55
+ )
56
+ order_iterable_items: bool | None = public_field(
57
+ default=None,
58
+ description="Sort items inside flagged iterable literals (lists/dicts)",
59
+ )
60
+ order_main_guard: bool | None = public_field(
61
+ default=None,
62
+ description="Ensure if __name__ == '__main__' block is at the very end",
63
+ )
64
+ order_module_docstring: bool | None = public_field(
65
+ default=None,
66
+ description="Order module docstring to be at the top of the file",
67
+ )
68
+ order_module_functions: bool | None = public_field(
69
+ default=None,
70
+ description="Order module-level functions (public A–Z, then private)",
71
+ )
72
+ order_module_metadata: bool | None = public_field(
73
+ default=None,
74
+ description="Group and sort module metadata at module level",
75
+ )
76
+ order_spacing: bool | None = public_field(
77
+ default=None,
78
+ description="Normalize blank lines between program structures (spacing rules)",
79
+ )
80
+ order_type_checking_block: bool | None = public_field(
81
+ default=None,
82
+ description="Move TYPE_CHECKING blocks to after regular imports",
83
+ )
84
+ raw: Any = public_field(
85
+ default=None, description="Disabled raw value for this config."
86
+ )
87
+ relocate_imports: bool | None = public_field(
88
+ default=None,
89
+ description="Relocate imports by usage (runtime-in-method, class property types, type-only)",
90
+ )
91
+ remove_unused: bool | None = public_field(
92
+ default=None,
93
+ description="Remove unused imports",
94
+ )
95
+ sort_imports: bool | None = public_field(
96
+ default=None,
97
+ description="Sort imports",
98
+ )
99
+ unquote_annotations: bool | None = public_field(
100
+ default=None,
101
+ description="Unquote annotations (remove string annotations)",
102
+ )
103
+
104
+ def to_option_raw_value(self) -> Any:
105
+ from wexample_filestate_python.config_option.add_future_annotations_config_option import (
106
+ AddFutureAnnotationsConfigOption,
107
+ )
108
+ from wexample_filestate_python.config_option.add_return_types_config_option import (
109
+ AddReturnTypesConfigOption,
110
+ )
111
+ from wexample_filestate_python.config_option.fix_attrs_config_option import (
112
+ FixAttrsConfigOption,
113
+ )
114
+ from wexample_filestate_python.config_option.fix_blank_lines_config_option import (
115
+ FixBlankLinesConfigOption,
116
+ )
117
+ from wexample_filestate_python.config_option.format_config_option import (
118
+ FormatConfigOption,
119
+ )
120
+ from wexample_filestate_python.config_option.fstringify_config_option import (
121
+ FstringifyConfigOption,
122
+ )
123
+ from wexample_filestate_python.config_option.modernize_typing_config_option import (
124
+ ModernizeTypingConfigOption,
125
+ )
126
+ from wexample_filestate_python.config_option.order_class_attributes_config_option import (
127
+ OrderClassAttributesConfigOption,
128
+ )
129
+ from wexample_filestate_python.config_option.order_class_docstring_config_option import (
130
+ OrderClassDocstringConfigOption,
131
+ )
132
+ from wexample_filestate_python.config_option.order_class_methods_config_option import (
133
+ OrderClassMethodsConfigOption,
134
+ )
135
+ from wexample_filestate_python.config_option.order_constants_config_option import (
136
+ OrderConstantsConfigOption,
137
+ )
138
+ from wexample_filestate_python.config_option.order_iterable_items_config_option import (
139
+ OrderIterableItemsConfigOption,
140
+ )
141
+ from wexample_filestate_python.config_option.order_main_guard_config_option import (
142
+ OrderMainGuardConfigOption,
143
+ )
144
+ from wexample_filestate_python.config_option.order_module_docstring_config_option import (
145
+ OrderModuleDocstringConfigOption,
146
+ )
147
+ from wexample_filestate_python.config_option.order_module_functions_config_option import (
148
+ OrderModuleFunctionsConfigOption,
149
+ )
150
+ from wexample_filestate_python.config_option.order_module_metadata_config_option import (
151
+ OrderModuleMetadataConfigOption,
152
+ )
153
+ from wexample_filestate_python.config_option.order_spacing_config_option import (
154
+ OrderSpacingConfigOption,
155
+ )
156
+ from wexample_filestate_python.config_option.order_type_checking_block_config_option import (
157
+ OrderTypeCheckingBlockConfigOption,
158
+ )
159
+ from wexample_filestate_python.config_option.relocate_imports_config_option import (
160
+ RelocateImportsConfigOption,
161
+ )
162
+ from wexample_filestate_python.config_option.remove_unused_config_option import (
163
+ RemoveUnusedConfigOption,
164
+ )
165
+ from wexample_filestate_python.config_option.sort_imports_config_option import (
166
+ SortImportsConfigOption,
167
+ )
168
+ from wexample_filestate_python.config_option.unquote_annotations_config_option import (
169
+ UnquoteAnnotationsConfigOption,
170
+ )
171
+
172
+ return {
173
+ AddFutureAnnotationsConfigOption.get_name(): self.add_future_annotations,
174
+ AddReturnTypesConfigOption.get_name(): self.add_return_types,
175
+ FixAttrsConfigOption.get_name(): self.fix_attrs,
176
+ FixBlankLinesConfigOption.get_name(): self.fix_blank_lines,
177
+ FormatConfigOption.get_name(): self.format,
178
+ FstringifyConfigOption.get_name(): self.fstringify,
179
+ ModernizeTypingConfigOption.get_name(): self.modernize_typing,
180
+ OrderClassAttributesConfigOption.get_name(): self.order_class_attributes,
181
+ OrderClassDocstringConfigOption.get_name(): self.order_class_docstring,
182
+ OrderClassMethodsConfigOption.get_name(): self.order_class_methods,
183
+ OrderConstantsConfigOption.get_name(): self.order_constants,
184
+ OrderIterableItemsConfigOption.get_name(): self.order_iterable_items,
185
+ OrderMainGuardConfigOption.get_name(): self.order_main_guard,
186
+ OrderModuleDocstringConfigOption.get_name(): self.order_module_docstring,
187
+ OrderModuleFunctionsConfigOption.get_name(): self.order_module_functions,
188
+ OrderModuleMetadataConfigOption.get_name(): self.order_module_metadata,
189
+ OrderSpacingConfigOption.get_name(): self.order_spacing,
190
+ OrderTypeCheckingBlockConfigOption.get_name(): self.order_type_checking_block,
191
+ RelocateImportsConfigOption.get_name(): self.relocate_imports,
192
+ RemoveUnusedConfigOption.get_name(): self.remove_unused,
193
+ SortImportsConfigOption.get_name(): self.sort_imports,
194
+ UnquoteAnnotationsConfigOption.get_name(): self.unquote_annotations,
195
+ }
File without changes
@@ -0,0 +1,4 @@
1
+ from __future__ import annotations
2
+
3
+ # filestate: python-constant-sort
4
+ NAME_PATTERN_PYTHON_NOT_PYCACHE = "^(?!__pycache__$).+$"
@@ -0,0 +1,5 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+ PYTHON_FILE_PYTEST_COVERAGE_JSON: Path = Path("coverage.json")
File without changes
File without changes
@@ -0,0 +1,12 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import ClassVar
4
+
5
+ from wexample_filestate.item.item_target_file import ItemTargetFile
6
+
7
+
8
+ class PythonFile(ItemTargetFile):
9
+ EXTENSION_ENV: ClassVar[str] = "py"
10
+
11
+ def _expected_file_name_extension(self) -> str | None:
12
+ return PythonFile.EXTENSION_ENV
File without changes
@@ -0,0 +1,122 @@
1
+ from __future__ import annotations
2
+
3
+ import ast
4
+ from typing import TYPE_CHECKING
5
+
6
+ import tomli
7
+
8
+ if TYPE_CHECKING:
9
+ from pathlib import Path
10
+
11
+
12
+ def package_get_dependencies(root_dir: str | Path) -> dict[str, set[str]]:
13
+ """
14
+ Get dependencies between packages in a directory.
15
+ """
16
+ from pathlib import Path
17
+
18
+ packages_root = Path(root_dir)
19
+ if not packages_root.exists() or not packages_root.is_dir():
20
+ raise ValueError(f"Error: {packages_root} does not exist or is not a directory")
21
+
22
+ dependencies = {}
23
+
24
+ # First pass: collect all local packages
25
+ for package_dir in packages_root.iterdir():
26
+ if not package_dir.is_dir():
27
+ continue
28
+
29
+ package_info = package_get_info(package_dir)
30
+ if package_info:
31
+ name, _ = package_info
32
+ dependencies[name] = set()
33
+
34
+ # Second pass: analyze dependencies
35
+ for package_dir in packages_root.iterdir():
36
+ if not package_dir.is_dir():
37
+ continue
38
+
39
+ package_info = package_get_info(package_dir)
40
+ if package_info:
41
+ name, deps = package_info
42
+ if name in dependencies:
43
+ # Only keep dependencies that are local packages
44
+ dependencies[name] = {dep for dep in deps if dep in dependencies}
45
+
46
+ return dependencies
47
+
48
+
49
+ def package_get_info(package_dir: Path) -> tuple[str, set[str]] | None:
50
+ """
51
+ Get package name and its dependencies from setup.py or pyproject.toml.
52
+ """
53
+ # Try pyproject.toml first
54
+ toml_path = package_dir / "pyproject.toml"
55
+ if toml_path.exists():
56
+ metadata = package_parse_toml(toml_path)
57
+ else:
58
+ # Fallback to setup.py
59
+ setup_py_path = package_dir / "setup.py"
60
+ if setup_py_path.exists():
61
+ metadata = package_parse_setup(setup_py_path)
62
+ else:
63
+ return None
64
+
65
+ name = metadata.get("name")
66
+ if not name:
67
+ return None
68
+
69
+ deps = metadata.get("install_requires", [])
70
+ return name, set(deps)
71
+
72
+
73
+ def package_normalize_name(val: str) -> str:
74
+ import re as _re
75
+
76
+ # strip extras, versions, markers
77
+ base = _re.split(r"[\s<>=!~;\[]", val, maxsplit=1)[0]
78
+ return base.strip().lower()
79
+
80
+
81
+ def package_parse_setup(path: Path) -> dict:
82
+ """
83
+ Parse a setup.py file to extract metadata.
84
+ """
85
+ with open(path) as f:
86
+ content = f.read()
87
+
88
+ tree = ast.parse(content)
89
+ for node in ast.walk(tree):
90
+ if (
91
+ isinstance(node, ast.Call)
92
+ and isinstance(node.func, ast.Name)
93
+ and node.func.id == "setup"
94
+ ):
95
+ result = {}
96
+ for kw in node.keywords:
97
+ if isinstance(kw.value, ast.Str):
98
+ result[kw.arg] = kw.value.s
99
+ elif isinstance(kw.value, ast.List):
100
+ result[kw.arg] = [
101
+ elt.s for elt in kw.value.elts if isinstance(elt, ast.Str)
102
+ ]
103
+ return result
104
+ return {}
105
+
106
+
107
+ def package_parse_toml(path: Path) -> dict:
108
+ """
109
+ Parse a pyproject.toml file to extract metadata.
110
+ """
111
+ try:
112
+ with open(path, "rb") as f:
113
+ data = tomli.load(f)
114
+ if "project" in data:
115
+ project_data = data["project"]
116
+ return {
117
+ "name": project_data.get("name"),
118
+ "install_requires": project_data.get("dependencies", []),
119
+ }
120
+ except Exception as e:
121
+ print(f"Error parsing {path}: {e}")
122
+ return {}
@@ -0,0 +1,116 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Any
4
+
5
+ if TYPE_CHECKING:
6
+ from tomlkit.items import Array
7
+
8
+
9
+ def toml_ensure_array(tbl: Any, key: str) -> tuple[Any, bool]:
10
+ """
11
+ Ensure an array exists at tbl[key] and return (array, changed).
12
+ Uses tomlkit.array() for creation.
13
+ """
14
+ from tomlkit import array
15
+
16
+ arr = tbl.get(key) if isinstance(tbl, dict) else None
17
+ if arr is None:
18
+ arr = array()
19
+ tbl[key] = arr
20
+ return arr, True
21
+ return arr, False
22
+
23
+
24
+ def toml_ensure_array_multiline(tbl: Any, key: str) -> tuple[Array, bool]:
25
+ """
26
+ Ensure an array exists at tbl[key] and force multiline formatting.
27
+ Returns (array, changed_created).
28
+ """
29
+ from tomlkit.items import Array
30
+
31
+ arr, changed = toml_ensure_array(tbl, key)
32
+ # Force multiline for readability when dumping
33
+ if isinstance(arr, Array):
34
+ arr.multiline(True)
35
+ return arr, changed
36
+
37
+
38
+ def toml_ensure_table(doc: Any, path: list[str]) -> tuple[Any, bool]:
39
+ """
40
+ Ensure a nested TOML table exists and return (table, changed).
41
+ Path example: ["tool", "pdm", "build"]. Uses tomlkit.table() for missing parts.
42
+ """
43
+ from tomlkit import table
44
+
45
+ if not isinstance(path, list) or not path:
46
+ raise ValueError("path must be a non-empty list of keys")
47
+
48
+ changed = False
49
+ current = doc
50
+ for key in path:
51
+ tbl = current.get(key) if isinstance(current, dict) else None
52
+ if not tbl or not isinstance(tbl, dict):
53
+ tbl = table()
54
+ current[key] = tbl
55
+ changed = True
56
+ current = tbl
57
+ return current, changed
58
+
59
+
60
+ def toml_get_string_value(item: Any) -> str:
61
+ """Return the string content of a tomlkit String or generic item as str."""
62
+ from tomlkit.items import String
63
+
64
+ if isinstance(item, String):
65
+ return item.value
66
+ return str(item)
67
+
68
+
69
+ def toml_set_array_multiline(tbl: Any, key: str, values: list[Any]) -> Array:
70
+ """
71
+ Replace tbl[key] with a tomlkit array built from values and set multiline(True).
72
+ Returns the created Array instance.
73
+ """
74
+ from tomlkit import array
75
+
76
+ arr = array(values)
77
+ arr.multiline(True)
78
+ tbl[key] = arr
79
+ return arr
80
+
81
+
82
+ def toml_sort_string_array(arr: Any) -> bool:
83
+ """
84
+ Sort a tomlkit Array of String items in-place (case-insensitive) while
85
+ preserving the existing multiline/style flags.
86
+
87
+ Returns True if the array was changed.
88
+ """
89
+ from tomlkit.items import Array, String
90
+
91
+ # Validate array type
92
+ if not isinstance(arr, Array):
93
+ return False
94
+
95
+ items = list(arr)
96
+ if not items or not all(isinstance(i, String) for i in items):
97
+ return False
98
+
99
+ values = [i.value for i in items]
100
+ sorted_items = [
101
+ x
102
+ for _, x in sorted(zip([v.lower() for v in values], items), key=lambda t: t[0])
103
+ ]
104
+
105
+ if items == sorted_items:
106
+ return False
107
+
108
+ multiline_flag = getattr(arr, "multiline", None)
109
+ # Clear and re-append to preserve tomlkit item identity
110
+ while len(arr):
111
+ arr.pop()
112
+ for item in sorted_items:
113
+ arr.append(item)
114
+ if multiline_flag is not None:
115
+ arr.multiline(multiline_flag)
116
+ return True
File without changes
@@ -0,0 +1,45 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Any
4
+
5
+ from wexample_config.config_option.abstract_config_option import AbstractConfigOption
6
+ from wexample_filestate.option.mixin.option_mixin import OptionMixin
7
+ from wexample_helpers.classes.abstract_method import abstract_method
8
+ from wexample_helpers.decorator.base_class import base_class
9
+
10
+ if TYPE_CHECKING:
11
+ from wexample_filestate.const.types_state_items import TargetFileOrDirectoryType
12
+
13
+
14
+ @base_class
15
+ class AbstractPythonFileContentOption(OptionMixin, AbstractConfigOption):
16
+ @staticmethod
17
+ def get_raw_value_allowed_type() -> Any:
18
+ return bool
19
+
20
+ def create_required_operation(
21
+ self, target: TargetFileOrDirectoryType
22
+ ) -> AbstractOperation | None:
23
+ from wexample_filestate.operation.file_write_operation import FileWriteOperation
24
+
25
+ """Create FileWriteOperation if add_future_annotations is enabled and needed."""
26
+ # Get current content
27
+ current_content = target.get_local_file().read()
28
+
29
+ # Apply add_future_annotations transformation
30
+ new_content = self._apply_content_change(target=target)
31
+
32
+ # If content changed, create FileWriteOperation
33
+ if new_content != current_content:
34
+ return FileWriteOperation(
35
+ option=self,
36
+ target=target,
37
+ content=new_content,
38
+ description=self.get_description(),
39
+ )
40
+
41
+ return None
42
+
43
+ @abstract_method
44
+ def _apply_content_change(self, target: TargetFileOrDirectoryType) -> None:
45
+ pass