wexample-filestate-python 0.0.41__tar.gz → 0.0.44__tar.gz

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 (68) hide show
  1. {wexample_filestate_python-0.0.41 → wexample_filestate_python-0.0.44}/PKG-INFO +10 -7
  2. {wexample_filestate_python-0.0.41 → wexample_filestate_python-0.0.44}/README.md +5 -3
  3. {wexample_filestate_python-0.0.41 → wexample_filestate_python-0.0.44}/pyproject.toml +5 -7
  4. {wexample_filestate_python-0.0.41 → wexample_filestate_python-0.0.44}/src/wexample_filestate_python/common/pipy_gateway.py +4 -2
  5. wexample_filestate_python-0.0.44/src/wexample_filestate_python/config_option/python_config_option.py +49 -0
  6. {wexample_filestate_python-0.0.41 → wexample_filestate_python-0.0.44}/src/wexample_filestate_python/const/name_pattern.py +1 -0
  7. {wexample_filestate_python-0.0.41 → wexample_filestate_python-0.0.44}/src/wexample_filestate_python/helpers/package.py +73 -68
  8. {wexample_filestate_python-0.0.41 → wexample_filestate_python-0.0.44}/src/wexample_filestate_python/helpers/toml.py +69 -58
  9. wexample_filestate_python-0.0.44/src/wexample_filestate_python/operation/abstract_python_file_operation.py +84 -0
  10. {wexample_filestate_python-0.0.41 → wexample_filestate_python-0.0.44}/src/wexample_filestate_python/operation/python_add_future_annotations_operation.py +14 -10
  11. {wexample_filestate_python-0.0.41 → wexample_filestate_python-0.0.44}/src/wexample_filestate_python/operation/python_add_return_types_operation.py +7 -5
  12. wexample_filestate_python-0.0.44/src/wexample_filestate_python/operation/python_fix_attrs_operation.py +53 -0
  13. wexample_filestate_python-0.0.44/src/wexample_filestate_python/operation/python_fix_blank_lines_operation.py +62 -0
  14. {wexample_filestate_python-0.0.41 → wexample_filestate_python-0.0.44}/src/wexample_filestate_python/operation/python_format_operation.py +7 -6
  15. {wexample_filestate_python-0.0.41 → wexample_filestate_python-0.0.44}/src/wexample_filestate_python/operation/python_fstringify_operation.py +14 -7
  16. {wexample_filestate_python-0.0.41 → wexample_filestate_python-0.0.44}/src/wexample_filestate_python/operation/python_modernize_typing_operation.py +8 -5
  17. wexample_filestate_python-0.0.44/src/wexample_filestate_python/operation/python_order_class_attributes_operation.py +50 -0
  18. wexample_filestate_python-0.0.44/src/wexample_filestate_python/operation/python_order_class_docstring_operation.py +54 -0
  19. wexample_filestate_python-0.0.44/src/wexample_filestate_python/operation/python_order_class_methods_operation.py +53 -0
  20. wexample_filestate_python-0.0.44/src/wexample_filestate_python/operation/python_order_constants_operation.py +51 -0
  21. wexample_filestate_python-0.0.44/src/wexample_filestate_python/operation/python_order_iterable_items_operation.py +48 -0
  22. wexample_filestate_python-0.0.44/src/wexample_filestate_python/operation/python_order_main_guard_operation.py +58 -0
  23. wexample_filestate_python-0.0.44/src/wexample_filestate_python/operation/python_order_module_docstring_operation.py +87 -0
  24. wexample_filestate_python-0.0.44/src/wexample_filestate_python/operation/python_order_module_functions_operation.py +58 -0
  25. wexample_filestate_python-0.0.44/src/wexample_filestate_python/operation/python_order_module_metadata_operation.py +76 -0
  26. wexample_filestate_python-0.0.44/src/wexample_filestate_python/operation/python_order_type_checking_block_operation.py +65 -0
  27. wexample_filestate_python-0.0.44/src/wexample_filestate_python/operation/python_relocate_imports_operation.py +203 -0
  28. {wexample_filestate_python-0.0.41 → wexample_filestate_python-0.0.44}/src/wexample_filestate_python/operation/python_remove_unused_imports_operation.py +30 -12
  29. {wexample_filestate_python-0.0.41 → wexample_filestate_python-0.0.44}/src/wexample_filestate_python/operation/python_sort_imports_operation.py +9 -6
  30. {wexample_filestate_python-0.0.41 → wexample_filestate_python-0.0.44}/src/wexample_filestate_python/operation/python_unquote_annotations_operation.py +7 -4
  31. wexample_filestate_python-0.0.44/src/wexample_filestate_python/operation/utils/python_attrs_utils.py +112 -0
  32. wexample_filestate_python-0.0.44/src/wexample_filestate_python/operation/utils/python_blank_lines_utils.py +597 -0
  33. wexample_filestate_python-0.0.44/src/wexample_filestate_python/operation/utils/python_class_attributes_utils.py +261 -0
  34. wexample_filestate_python-0.0.44/src/wexample_filestate_python/operation/utils/python_class_docstring_utils.py +85 -0
  35. wexample_filestate_python-0.0.44/src/wexample_filestate_python/operation/utils/python_class_methods_utils.py +230 -0
  36. wexample_filestate_python-0.0.44/src/wexample_filestate_python/operation/utils/python_constants_utils.py +299 -0
  37. wexample_filestate_python-0.0.44/src/wexample_filestate_python/operation/utils/python_docstring_utils.py +117 -0
  38. wexample_filestate_python-0.0.44/src/wexample_filestate_python/operation/utils/python_functions_utils.py +212 -0
  39. wexample_filestate_python-0.0.44/src/wexample_filestate_python/operation/utils/python_iterable_utils.py +131 -0
  40. wexample_filestate_python-0.0.44/src/wexample_filestate_python/operation/utils/python_main_guard_utils.py +80 -0
  41. wexample_filestate_python-0.0.44/src/wexample_filestate_python/operation/utils/python_module_metadata_utils.py +146 -0
  42. wexample_filestate_python-0.0.44/src/wexample_filestate_python/operation/utils/python_type_checking_utils.py +113 -0
  43. wexample_filestate_python-0.0.44/src/wexample_filestate_python/operation/utils/relocate_imports/__init__.py +7 -0
  44. wexample_filestate_python-0.0.44/src/wexample_filestate_python/operation/utils/relocate_imports/python_import_rewriter.py +400 -0
  45. wexample_filestate_python-0.0.44/src/wexample_filestate_python/operation/utils/relocate_imports/python_localize_runtime_imports.py +249 -0
  46. wexample_filestate_python-0.0.44/src/wexample_filestate_python/operation/utils/relocate_imports/python_parser_import_index.py +54 -0
  47. wexample_filestate_python-0.0.44/src/wexample_filestate_python/operation/utils/relocate_imports/python_runtime_symbol_collector.py +33 -0
  48. wexample_filestate_python-0.0.44/src/wexample_filestate_python/operation/utils/relocate_imports/python_usage_collector.py +360 -0
  49. wexample_filestate_python-0.0.44/src/wexample_filestate_python/operations_provider/python_operations_provider.py +103 -0
  50. {wexample_filestate_python-0.0.41 → wexample_filestate_python-0.0.44}/src/wexample_filestate_python/options_provider/python_options_provider.py +4 -3
  51. wexample_filestate_python-0.0.44/tests/wexample_tests/__init__.py +0 -0
  52. wexample_filestate_python-0.0.41/src/wexample_filestate_python/config_option/python_config_option.py +0 -20
  53. wexample_filestate_python-0.0.41/src/wexample_filestate_python/operation/abstract_python_file_operation.py +0 -39
  54. wexample_filestate_python-0.0.41/src/wexample_filestate_python/operations_provider/python_operations_provider.py +0 -50
  55. {wexample_filestate_python-0.0.41 → wexample_filestate_python-0.0.44}/src/wexample_filestate_python/__init__.py +0 -0
  56. {wexample_filestate_python-0.0.41 → wexample_filestate_python-0.0.44}/src/wexample_filestate_python/common/__init__.py +0 -0
  57. {wexample_filestate_python-0.0.41 → wexample_filestate_python-0.0.44}/src/wexample_filestate_python/config_option/__init__.py +0 -0
  58. {wexample_filestate_python-0.0.41 → wexample_filestate_python-0.0.44}/src/wexample_filestate_python/const/__init__.py +0 -0
  59. {wexample_filestate_python-0.0.41 → wexample_filestate_python-0.0.44}/src/wexample_filestate_python/file/__init__.py +0 -0
  60. {wexample_filestate_python-0.0.41 → wexample_filestate_python-0.0.44}/src/wexample_filestate_python/file/python_file.py +0 -0
  61. {wexample_filestate_python-0.0.41 → wexample_filestate_python-0.0.44}/src/wexample_filestate_python/helpers/__init__.py +0 -0
  62. {wexample_filestate_python-0.0.41 → wexample_filestate_python-0.0.44}/src/wexample_filestate_python/operation/__init__.py +0 -0
  63. {wexample_filestate_python-0.0.41/src/wexample_filestate_python/operations_provider → wexample_filestate_python-0.0.44/src/wexample_filestate_python/operation/utils}/__init__.py +0 -0
  64. {wexample_filestate_python-0.0.41/src/wexample_filestate_python/options_provider → wexample_filestate_python-0.0.44/src/wexample_filestate_python/operations_provider}/__init__.py +0 -0
  65. {wexample_filestate_python-0.0.41/src/wexample_filestate_python/workdir → wexample_filestate_python-0.0.44/src/wexample_filestate_python/options_provider}/__init__.py +0 -0
  66. {wexample_filestate_python-0.0.41 → wexample_filestate_python-0.0.44}/src/wexample_filestate_python/py.typed +0 -0
  67. {wexample_filestate_python-0.0.41/tests/tests → wexample_filestate_python-0.0.44/src/wexample_filestate_python/workdir}/__init__.py +0 -0
  68. {wexample_filestate_python-0.0.41/tests/wexample_tests → wexample_filestate_python-0.0.44/tests/tests}/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: wexample-filestate-python
3
- Version: 0.0.41
3
+ Version: 0.0.44
4
4
  Summary: Helpers for Python.
5
5
  Author-Email: weeger <contact@wexample.com>
6
6
  License: MIT
@@ -9,9 +9,10 @@ Classifier: License :: OSI Approved :: MIT License
9
9
  Classifier: Operating System :: OS Independent
10
10
  Project-URL: homepage, https://github.com/wexample/python-filestate-python
11
11
  Requires-Python: >=3.10
12
- Requires-Dist: pydantic<3,>=2
13
- Requires-Dist: wexample-filestate==0.0.51
14
- Requires-Dist: wexample-helpers-api==0.0.32
12
+ Requires-Dist: attrs>=23.1.0
13
+ Requires-Dist: cattrs>=23.1.0
14
+ Requires-Dist: wexample-filestate==0.0.54
15
+ Requires-Dist: wexample-helpers-api==0.0.35
15
16
  Provides-Extra: dev
16
17
  Requires-Dist: pytest; extra == "dev"
17
18
  Description-Content-Type: text/markdown
@@ -20,7 +21,7 @@ Description-Content-Type: text/markdown
20
21
 
21
22
  Helpers for Python.
22
23
 
23
- Version: 0.0.41
24
+ Version: 0.0.42
24
25
 
25
26
  ## Requirements
26
27
 
@@ -28,8 +29,10 @@ Version: 0.0.41
28
29
 
29
30
  ## Dependencies
30
31
 
31
- - pydantic>=2,<3
32
- - wexample-filestate==0.0.51
32
+ - attrs>=23.1.0
33
+ - cattrs>=23.1.0
34
+ - wexample-filestate==0.0.52
35
+ - wexample-helpers-api==0.0.33
33
36
 
34
37
  ## Installation
35
38
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  Helpers for Python.
4
4
 
5
- Version: 0.0.41
5
+ Version: 0.0.42
6
6
 
7
7
  ## Requirements
8
8
 
@@ -10,8 +10,10 @@ Version: 0.0.41
10
10
 
11
11
  ## Dependencies
12
12
 
13
- - pydantic>=2,<3
14
- - wexample-filestate==0.0.51
13
+ - attrs>=23.1.0
14
+ - cattrs>=23.1.0
15
+ - wexample-filestate==0.0.52
16
+ - wexample-helpers-api==0.0.33
15
17
 
16
18
  ## Installation
17
19
 
@@ -6,7 +6,7 @@ build-backend = "pdm.backend"
6
6
 
7
7
  [project]
8
8
  name = "wexample-filestate-python"
9
- version = "0.0.41"
9
+ version = "0.0.44"
10
10
  description = "Helpers for Python."
11
11
  authors = [
12
12
  { name = "weeger", email = "contact@wexample.com" },
@@ -18,9 +18,10 @@ classifiers = [
18
18
  "Operating System :: OS Independent",
19
19
  ]
20
20
  dependencies = [
21
- "pydantic>=2,<3",
22
- "wexample-filestate==0.0.51",
23
- "wexample-helpers-api==0.0.32",
21
+ "attrs>=23.1.0",
22
+ "cattrs>=23.1.0",
23
+ "wexample-filestate==0.0.54",
24
+ "wexample-helpers-api==0.0.35",
24
25
  ]
25
26
 
26
27
  [project.readme]
@@ -42,9 +43,6 @@ dev = [
42
43
  distribution = true
43
44
 
44
45
  [tool.pdm.build]
45
- includes = [
46
- "src/wexample_filestate_python/*",
47
- ]
48
46
  package-dir = "src"
49
47
  packages = [
50
48
  { include = "wexample_filestate_python", from = "src" },
@@ -1,11 +1,13 @@
1
1
  from __future__ import annotations
2
2
 
3
- from pydantic import Field
3
+ from wexample_helpers.classes.field import public_field
4
4
  from wexample_helpers_api.common.abstract_gateway import AbstractGateway
5
+ from wexample_helpers.decorator.base_class import base_class
5
6
 
6
7
 
8
+ @base_class
7
9
  class PipyGateway(AbstractGateway):
8
- base_url: str | None = Field(
10
+ base_url: str | None = public_field(
9
11
  default="https://pypi.org/", description="Base Pipy API URL"
10
12
  )
11
13
 
@@ -0,0 +1,49 @@
1
+ from typing import Any, ClassVar
2
+
3
+ from wexample_config.config_option.abstract_config_option import AbstractConfigOption
4
+
5
+
6
+ class PythonConfigOption(AbstractConfigOption):
7
+ # filestate: python-constant-sort
8
+ # New preferred option name to add `from __future__ import annotations`
9
+ OPTION_NAME_ADD_FUTURE_ANNOTATIONS: ClassVar[str] = "add_future_annotations"
10
+ OPTION_NAME_ADD_RETURN_TYPES: ClassVar[str] = "add_return_types"
11
+ # Fix attrs usage (ensure kw_only=True, etc.)
12
+ OPTION_NAME_FIX_ATTRS: ClassVar[str] = "fix_attrs"
13
+ # Fix blank lines in Python files (after signatures, docstrings, etc.)
14
+ OPTION_NAME_FIX_BLANK_LINES: ClassVar[str] = "fix_blank_lines"
15
+ OPTION_NAME_FORMAT: ClassVar[str] = "format"
16
+ OPTION_NAME_FSTRINGIFY: ClassVar[str] = "fstringify"
17
+ OPTION_NAME_MODERNIZE_TYPING: ClassVar[str] = "modernize_typing"
18
+ # Sort class attributes: special first, then public A–Z, then private/protected A–Z
19
+ OPTION_NAME_ORDER_CLASS_ATTRIBUTES: ClassVar[str] = "order_class_attributes"
20
+ # Ensure class docstring is first statement after header/decorators
21
+ OPTION_NAME_ORDER_CLASS_DOCSTRING: ClassVar[str] = "order_class_docstring"
22
+ # Order class methods (dunders sequence, class/staticmethods, properties, instances)
23
+ OPTION_NAME_ORDER_CLASS_METHODS: ClassVar[str] = "order_class_methods"
24
+ # Sort flagged UPPER_CASE constant blocks at module level
25
+ OPTION_NAME_ORDER_CONSTANTS: ClassVar[str] = "order_constants"
26
+ # Sort items inside flagged iterable literals (lists/dicts)
27
+ OPTION_NAME_ORDER_ITERABLE_ITEMS: ClassVar[str] = "order_iterable_items"
28
+ # Ensure if __name__ == "__main__" block is at the very end
29
+ OPTION_NAME_ORDER_MAIN_GUARD: ClassVar[str] = "order_main_guard"
30
+ # Order module docstring to be at the top of the file
31
+ OPTION_NAME_ORDER_MODULE_DOCSTRING: ClassVar[str] = "order_module_docstring"
32
+ # Order module-level functions (public A–Z, then private)
33
+ OPTION_NAME_ORDER_MODULE_FUNCTIONS: ClassVar[str] = "order_module_functions"
34
+ # Group and sort module metadata at module level
35
+ OPTION_NAME_ORDER_MODULE_METADATA: ClassVar[str] = "order_module_metadata"
36
+ # Normalize blank lines between program structures (spacing rules)
37
+ OPTION_NAME_ORDER_SPACING: ClassVar[str] = "order_spacing"
38
+ # Move TYPE_CHECKING blocks to after regular imports
39
+ OPTION_NAME_ORDER_TYPE_CHECKING_BLOCK: ClassVar[str] = "order_type_checking_block"
40
+ # Relocate imports by usage (runtime-in-method, class property types, type-only)
41
+ OPTION_NAME_RELOCATE_IMPORTS: ClassVar[str] = "relocate_imports"
42
+ OPTION_NAME_REMOVE_UNUSED: ClassVar[str] = "remove_unused"
43
+ OPTION_NAME_SORT_IMPORTS: ClassVar[str] = "sort_imports"
44
+ # New policy: unquote annotations (remove string annotations)
45
+ OPTION_NAME_UNQUOTE_ANNOTATIONS: ClassVar[str] = "unquote_annotations"
46
+
47
+ @staticmethod
48
+ def get_raw_value_allowed_type() -> Any:
49
+ return list[str]
@@ -1,3 +1,4 @@
1
1
  from __future__ import annotations
2
2
 
3
+ # filestate: python-constant-sort
3
4
  NAME_PATTERN_PYTHON_NOT_PYCACHE = "^(?!__pycache__$).+$"
@@ -1,83 +1,20 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import ast
4
- from pathlib import Path
4
+ from typing import TYPE_CHECKING
5
5
 
6
6
  import tomli
7
7
 
8
-
9
- def package_parse_setup(path: Path) -> dict:
10
- """
11
- Parse a setup.py file to extract metadata.
12
- """
13
- with open(path) as f:
14
- content = f.read()
15
-
16
- tree = ast.parse(content)
17
- for node in ast.walk(tree):
18
- if (
19
- isinstance(node, ast.Call)
20
- and isinstance(node.func, ast.Name)
21
- and node.func.id == "setup"
22
- ):
23
- result = {}
24
- for kw in node.keywords:
25
- if isinstance(kw.value, ast.Str):
26
- result[kw.arg] = kw.value.s
27
- elif isinstance(kw.value, ast.List):
28
- result[kw.arg] = [
29
- elt.s for elt in kw.value.elts if isinstance(elt, ast.Str)
30
- ]
31
- return result
32
- return {}
33
-
34
-
35
- def package_parse_toml(path: Path) -> dict:
36
- """
37
- Parse a pyproject.toml file to extract metadata.
38
- """
39
- try:
40
- with open(path, "rb") as f:
41
- data = tomli.load(f)
42
- if "project" in data:
43
- project_data = data["project"]
44
- return {
45
- "name": project_data.get("name"),
46
- "install_requires": project_data.get("dependencies", []),
47
- }
48
- except Exception as e:
49
- print(f"Error parsing {path}: {e}")
50
- return {}
51
-
52
-
53
- def package_get_info(package_dir: Path) -> tuple[str, set[str]] | None:
54
- """
55
- Get package name and its dependencies from setup.py or pyproject.toml.
56
- """
57
- # Try pyproject.toml first
58
- toml_path = package_dir / "pyproject.toml"
59
- if toml_path.exists():
60
- metadata = package_parse_toml(toml_path)
61
- else:
62
- # Fallback to setup.py
63
- setup_py_path = package_dir / "setup.py"
64
- if setup_py_path.exists():
65
- metadata = package_parse_setup(setup_py_path)
66
- else:
67
- return None
68
-
69
- name = metadata.get("name")
70
- if not name:
71
- return None
72
-
73
- deps = metadata.get("install_requires", [])
74
- return name, set(deps)
8
+ if TYPE_CHECKING:
9
+ from pathlib import Path
75
10
 
76
11
 
77
12
  def package_get_dependencies(root_dir: str | Path) -> dict[str, set[str]]:
78
13
  """
79
14
  Get dependencies between packages in a directory.
80
15
  """
16
+ from pathlib import Path
17
+
81
18
  packages_root = Path(root_dir)
82
19
  if not packages_root.exists() or not packages_root.is_dir():
83
20
  raise ValueError(f"Error: {packages_root} does not exist or is not a directory")
@@ -109,6 +46,30 @@ def package_get_dependencies(root_dir: str | Path) -> dict[str, set[str]]:
109
46
  return dependencies
110
47
 
111
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
+
112
73
  def package_list_sorted(root_dir: str | Path) -> list[str]:
113
74
  """
114
75
  Get a list of package names sorted by dependency order.
@@ -134,3 +95,47 @@ def package_normalize_name(val: str) -> str:
134
95
  # strip extras, versions, markers
135
96
  base = _re.split(r"[\s<>=!~;\[]", val, maxsplit=1)[0]
136
97
  return base.strip().lower()
98
+
99
+
100
+ def package_parse_setup(path: Path) -> dict:
101
+ """
102
+ Parse a setup.py file to extract metadata.
103
+ """
104
+ with open(path) as f:
105
+ content = f.read()
106
+
107
+ tree = ast.parse(content)
108
+ for node in ast.walk(tree):
109
+ if (
110
+ isinstance(node, ast.Call)
111
+ and isinstance(node.func, ast.Name)
112
+ and node.func.id == "setup"
113
+ ):
114
+ result = {}
115
+ for kw in node.keywords:
116
+ if isinstance(kw.value, ast.Str):
117
+ result[kw.arg] = kw.value.s
118
+ elif isinstance(kw.value, ast.List):
119
+ result[kw.arg] = [
120
+ elt.s for elt in kw.value.elts if isinstance(elt, ast.Str)
121
+ ]
122
+ return result
123
+ return {}
124
+
125
+
126
+ def package_parse_toml(path: Path) -> dict:
127
+ """
128
+ Parse a pyproject.toml file to extract metadata.
129
+ """
130
+ try:
131
+ with open(path, "rb") as f:
132
+ data = tomli.load(f)
133
+ if "project" in data:
134
+ project_data = data["project"]
135
+ return {
136
+ "name": project_data.get("name"),
137
+ "install_requires": project_data.get("dependencies", []),
138
+ }
139
+ except Exception as e:
140
+ print(f"Error parsing {path}: {e}")
141
+ return {}
@@ -1,45 +1,38 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import Any
3
+ from typing import TYPE_CHECKING, Any
4
4
 
5
- from tomlkit import array as _tk_array
6
- from tomlkit import table as _tk_table
7
- from tomlkit.items import Array, String
5
+ if TYPE_CHECKING:
6
+ from tomlkit.items import Array
8
7
 
9
8
 
10
- def toml_sort_string_array(arr: Any) -> bool:
9
+ def toml_ensure_array(tbl: Any, key: str) -> tuple[Any, bool]:
11
10
  """
12
- Sort a tomlkit Array of String items in-place (case-insensitive) while
13
- preserving the existing multiline/style flags.
14
-
15
- Returns True if the array was changed.
11
+ Ensure an array exists at tbl[key] and return (array, changed).
12
+ Uses tomlkit.array() for creation.
16
13
  """
17
- # Validate array type
18
- if not isinstance(arr, Array):
19
- return False
14
+ from tomlkit import array
20
15
 
21
- items = list(arr)
22
- if not items or not all(isinstance(i, String) for i in items):
23
- return False
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
24
22
 
25
- values = [i.value for i in items]
26
- sorted_items = [
27
- x
28
- for _, x in sorted(zip([v.lower() for v in values], items), key=lambda t: t[0])
29
- ]
30
23
 
31
- if items == sorted_items:
32
- return False
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
33
30
 
34
- multiline_flag = getattr(arr, "multiline", None)
35
- # Clear and re-append to preserve tomlkit item identity
36
- while len(arr):
37
- arr.pop()
38
- for item in sorted_items:
39
- arr.append(item)
40
- if multiline_flag is not None:
41
- arr.multiline(multiline_flag)
42
- return True
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
43
36
 
44
37
 
45
38
  def toml_ensure_table(doc: Any, path: list[str]) -> tuple[Any, bool]:
@@ -47,6 +40,8 @@ def toml_ensure_table(doc: Any, path: list[str]) -> tuple[Any, bool]:
47
40
  Ensure a nested TOML table exists and return (table, changed).
48
41
  Path example: ["tool", "pdm", "build"]. Uses tomlkit.table() for missing parts.
49
42
  """
43
+ from tomlkit import table
44
+
50
45
  if not isinstance(path, list) or not path:
51
46
  raise ValueError("path must be a non-empty list of keys")
52
47
 
@@ -55,51 +50,67 @@ def toml_ensure_table(doc: Any, path: list[str]) -> tuple[Any, bool]:
55
50
  for key in path:
56
51
  tbl = current.get(key) if isinstance(current, dict) else None
57
52
  if not tbl or not isinstance(tbl, dict):
58
- tbl = _tk_table()
53
+ tbl = table()
59
54
  current[key] = tbl
60
55
  changed = True
61
56
  current = tbl
62
57
  return current, changed
63
58
 
64
59
 
65
- def toml_ensure_array(tbl: Any, key: str) -> tuple[Any, bool]:
66
- """
67
- Ensure an array exists at tbl[key] and return (array, changed).
68
- Uses tomlkit.array() for creation.
69
- """
70
- arr = tbl.get(key) if isinstance(tbl, dict) else None
71
- if arr is None:
72
- arr = _tk_array()
73
- tbl[key] = arr
74
- return arr, True
75
- return arr, False
76
-
77
-
78
60
  def toml_get_string_value(item: Any) -> str:
79
61
  """Return the string content of a tomlkit String or generic item as str."""
62
+ from tomlkit.items import String
63
+
80
64
  if isinstance(item, String):
81
65
  return item.value
82
66
  return str(item)
83
67
 
84
68
 
85
- def toml_ensure_array_multiline(tbl: Any, key: str) -> tuple[Array, bool]:
86
- """
87
- Ensure an array exists at tbl[key] and force multiline formatting.
88
- Returns (array, changed_created).
89
- """
90
- arr, changed = toml_ensure_array(tbl, key)
91
- # Force multiline for readability when dumping
92
- if isinstance(arr, Array):
93
- arr.multiline(True)
94
- return arr, changed
95
-
96
-
97
69
  def toml_set_array_multiline(tbl: Any, key: str, values: list[Any]) -> Array:
98
70
  """
99
71
  Replace tbl[key] with a tomlkit array built from values and set multiline(True).
100
72
  Returns the created Array instance.
101
73
  """
102
- arr = _tk_array(values)
74
+ from tomlkit import array
75
+
76
+ arr = array(values)
103
77
  arr.multiline(True)
104
78
  tbl[key] = arr
105
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
@@ -0,0 +1,84 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ from wexample_filestate.operation.abstract_existing_file_operation import (
6
+ AbstractExistingFileOperation,
7
+ )
8
+
9
+ if TYPE_CHECKING:
10
+ from wexample_config.config_option.abstract_config_option import (
11
+ AbstractConfigOption,
12
+ )
13
+ from wexample_filestate.enum.scopes import Scope
14
+
15
+
16
+ class AbstractPythonFileOperation(AbstractExistingFileOperation):
17
+ @classmethod
18
+ def get_option_name(cls) -> str:
19
+ raise NotImplementedError
20
+
21
+ @classmethod
22
+ def get_scope(cls) -> Scope:
23
+ from wexample_filestate.enum.scopes import Scope
24
+
25
+ return Scope.CONTENT
26
+
27
+ @classmethod
28
+ def _execute_and_wrap_stdout(cls, callback):
29
+ """Execute a callback and wrap any stdout/stderr output with additional newlines.
30
+
31
+ This ensures that output from external tools doesn't interfere with progress indicators
32
+ by adding a newline after any captured output.
33
+
34
+ Args:
35
+ callback: Function to execute that may produce stdout/stderr output
36
+
37
+ Returns:
38
+ The return value of the callback function
39
+ """
40
+ import io
41
+ import sys
42
+
43
+ old_stdout = sys.stdout
44
+ old_stderr = sys.stderr
45
+ captured_stdout = io.StringIO()
46
+ captured_stderr = io.StringIO()
47
+ sys.stdout = captured_stdout
48
+ sys.stderr = captured_stderr
49
+
50
+ try:
51
+ result = callback()
52
+ finally:
53
+ sys.stdout = old_stdout
54
+ sys.stderr = old_stderr
55
+
56
+ stdout_content = captured_stdout.getvalue()
57
+ stderr_content = captured_stderr.getvalue()
58
+
59
+ if stdout_content.strip():
60
+ print(stdout_content.rstrip())
61
+ print()
62
+ if stderr_content.strip():
63
+ print(stderr_content.rstrip(), file=sys.stderr)
64
+ print(file=sys.stderr)
65
+
66
+ return result
67
+
68
+ def applicable_for_option(self, option: AbstractConfigOption) -> bool:
69
+ """Generic applicability for Python file transforms controlled by a single option name."""
70
+ from wexample_filestate_python.config_option.python_config_option import (
71
+ PythonConfigOption,
72
+ )
73
+
74
+ # Option type
75
+ if not isinstance(option, PythonConfigOption):
76
+ return False
77
+
78
+ # Option value must contain our specific option name
79
+ value = option.get_value()
80
+ if value is None or not value.has_item_in_list(self.get_option_name()):
81
+ return False
82
+
83
+ # Delegate change detection to the base helper
84
+ return self.source_need_change(self.target)
@@ -1,7 +1,12 @@
1
+ from typing import TYPE_CHECKING
2
+
1
3
  from wexample_filestate.const.types_state_items import TargetFileOrDirectoryType
2
4
 
3
5
  from .abstract_python_file_operation import AbstractPythonFileOperation
4
6
 
7
+ if TYPE_CHECKING:
8
+ from wexample_filestate.const.types_state_items import TargetFileOrDirectoryType
9
+
5
10
 
6
11
  class PythonAddFutureAnnotationsOperation(AbstractPythonFileOperation):
7
12
  """Ensure `from __future__ import annotations` is present at module top.
@@ -20,15 +25,6 @@ class PythonAddFutureAnnotationsOperation(AbstractPythonFileOperation):
20
25
 
21
26
  return PythonConfigOption.OPTION_NAME_ADD_FUTURE_ANNOTATIONS
22
27
 
23
- def describe_before(self) -> str:
24
- return "The file may be missing `from __future__ import annotations` at the module top."
25
-
26
- def describe_after(self) -> str:
27
- return "`from __future__ import annotations` has been added in the correct position."
28
-
29
- def description(self) -> str:
30
- return "Add `from __future__ import annotations` at the proper location (after shebang/encoding and module docstring)."
31
-
32
28
  @classmethod
33
29
  def preview_source_change(cls, target: TargetFileOrDirectoryType) -> str | None:
34
30
  """Return source with a `from __future__ import annotations` inserted
@@ -90,7 +86,6 @@ class PythonAddFutureAnnotationsOperation(AbstractPythonFileOperation):
90
86
 
91
87
  # Avoid inserting duplicate blank lines: if previous line is not blank and not newline, keep
92
88
  # If there are existing future imports right after docstring, we can insert alongside them; no special handling needed
93
-
94
89
  lines.insert(insert_index, future_line)
95
90
  # If there isn't a blank line after the future import and next line is not blank, add one for readability
96
91
  j = insert_index + 1
@@ -99,3 +94,12 @@ class PythonAddFutureAnnotationsOperation(AbstractPythonFileOperation):
99
94
  lines.insert(j, "\n")
100
95
 
101
96
  return "".join(lines)
97
+
98
+ def describe_after(self) -> str:
99
+ return "`from __future__ import annotations` has been added in the correct position."
100
+
101
+ def describe_before(self) -> str:
102
+ return "The file may be missing `from __future__ import annotations` at the module top."
103
+
104
+ def description(self) -> str:
105
+ return "Add `from __future__ import annotations` at the proper location (after shebang/encoding and module docstring)."
@@ -1,9 +1,12 @@
1
1
  from __future__ import annotations
2
2
 
3
- from wexample_filestate.const.types_state_items import TargetFileOrDirectoryType
3
+ from typing import TYPE_CHECKING
4
4
 
5
5
  from .abstract_python_file_operation import AbstractPythonFileOperation
6
6
 
7
+ if TYPE_CHECKING:
8
+ from wexample_filestate.const.types_state_items import TargetFileOrDirectoryType
9
+
7
10
 
8
11
  class PythonAddReturnTypesOperation(AbstractPythonFileOperation):
9
12
  """Annotate return types for functions lacking them when trivially inferable.
@@ -36,7 +39,6 @@ class PythonAddReturnTypesOperation(AbstractPythonFileOperation):
36
39
  # robust, formatting-preserving edits. We extend inference to:
37
40
  # - simple literals (None, bool, str, int, float)
38
41
  # - simple class instantiation returns (MyClass() or via a variable assigned once)
39
-
40
42
  def _infer_literal_type(expr: cst.BaseExpression) -> str | None:
41
43
  if isinstance(expr, cst.Name):
42
44
  if expr.value in ("True", "False"):
@@ -273,11 +275,11 @@ class PythonAddReturnTypesOperation(AbstractPythonFileOperation):
273
275
  new_module = module.visit(AddReturnTypesTransformer(ktc.known))
274
276
  return new_module.code
275
277
 
276
- def describe_before(self) -> str:
277
- return "Some Python functions are missing obvious return type annotations."
278
-
279
278
  def describe_after(self) -> str:
280
279
  return "Functions have been annotated with simple return types where obvious."
281
280
 
281
+ def describe_before(self) -> str:
282
+ return "Some Python functions are missing obvious return type annotations."
283
+
282
284
  def description(self) -> str:
283
285
  return "Add simple return type annotations (None/bool/str/int/float) when trivially inferable."