wexample-filestate-python 6.4.5__tar.gz → 6.5.0__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 (89) hide show
  1. {wexample_filestate_python-6.4.5 → wexample_filestate_python-6.5.0}/PKG-INFO +4 -4
  2. {wexample_filestate_python-6.4.5 → wexample_filestate_python-6.5.0}/README.md +2 -2
  3. {wexample_filestate_python-6.4.5 → wexample_filestate_python-6.5.0}/pyproject.toml +2 -2
  4. {wexample_filestate_python-6.4.5 → wexample_filestate_python-6.5.0}/src/wexample_filestate_python/option/python/add_return_types_option.py +8 -7
  5. {wexample_filestate_python-6.4.5 → wexample_filestate_python-6.5.0}/src/wexample_filestate_python/option/python/fix_attrs_option.py +4 -4
  6. {wexample_filestate_python-6.4.5 → wexample_filestate_python-6.5.0}/src/wexample_filestate_python/option/python/fix_blank_lines_option.py +4 -4
  7. wexample_filestate_python-6.5.0/src/wexample_filestate_python/option/python/format_option.py +48 -0
  8. {wexample_filestate_python-6.4.5 → wexample_filestate_python-6.5.0}/src/wexample_filestate_python/option/python/order_class_attributes_option.py +4 -4
  9. {wexample_filestate_python-6.4.5 → wexample_filestate_python-6.5.0}/src/wexample_filestate_python/option/python/order_class_docstring_option.py +4 -4
  10. {wexample_filestate_python-6.4.5 → wexample_filestate_python-6.5.0}/src/wexample_filestate_python/option/python/order_class_methods_option.py +4 -4
  11. {wexample_filestate_python-6.4.5 → wexample_filestate_python-6.5.0}/src/wexample_filestate_python/option/python/order_constants_option.py +4 -4
  12. {wexample_filestate_python-6.4.5 → wexample_filestate_python-6.5.0}/src/wexample_filestate_python/option/python/order_main_guard_option.py +4 -4
  13. {wexample_filestate_python-6.4.5 → wexample_filestate_python-6.5.0}/src/wexample_filestate_python/option/python/order_module_docstring_option.py +4 -2
  14. {wexample_filestate_python-6.4.5 → wexample_filestate_python-6.5.0}/src/wexample_filestate_python/option/python/order_module_functions_option.py +4 -4
  15. {wexample_filestate_python-6.4.5 → wexample_filestate_python-6.5.0}/src/wexample_filestate_python/option/python/order_module_metadata_option.py +4 -4
  16. {wexample_filestate_python-6.4.5 → wexample_filestate_python-6.5.0}/src/wexample_filestate_python/option/python/order_type_checking_block_option.py +4 -4
  17. {wexample_filestate_python-6.4.5 → wexample_filestate_python-6.5.0}/src/wexample_filestate_python/option/python/relocate_imports_option.py +4 -2
  18. wexample_filestate_python-6.5.0/src/wexample_filestate_python/option/python/remove_unused_option.py +38 -0
  19. {wexample_filestate_python-6.4.5 → wexample_filestate_python-6.5.0}/src/wexample_filestate_python/option/python/unquote_annotations_option.py +5 -2
  20. wexample_filestate_python-6.5.0/src/wexample_filestate_python/utils/cst_cache.py +37 -0
  21. wexample_filestate_python-6.4.5/src/wexample_filestate_python/option/python/format_option.py +0 -34
  22. wexample_filestate_python-6.4.5/src/wexample_filestate_python/option/python/remove_unused_option.py +0 -45
  23. {wexample_filestate_python-6.4.5 → wexample_filestate_python-6.5.0}/src/wexample_filestate_python/__init__.py +0 -0
  24. {wexample_filestate_python-6.4.5 → wexample_filestate_python-6.5.0}/src/wexample_filestate_python/__pycache__/__init__.py +0 -0
  25. {wexample_filestate_python-6.4.5 → wexample_filestate_python-6.5.0}/src/wexample_filestate_python/common/__init__.py +0 -0
  26. {wexample_filestate_python-6.4.5 → wexample_filestate_python-6.5.0}/src/wexample_filestate_python/common/__pycache__/__init__.py +0 -0
  27. {wexample_filestate_python-6.4.5 → wexample_filestate_python-6.5.0}/src/wexample_filestate_python/common/pipy_gateway.py +0 -0
  28. {wexample_filestate_python-6.4.5 → wexample_filestate_python-6.5.0}/src/wexample_filestate_python/config_option/__init__.py +0 -0
  29. {wexample_filestate_python-6.4.5 → wexample_filestate_python-6.5.0}/src/wexample_filestate_python/config_option/__pycache__/__init__.py +0 -0
  30. {wexample_filestate_python-6.4.5 → wexample_filestate_python-6.5.0}/src/wexample_filestate_python/config_option/mixin/__init__.py +0 -0
  31. {wexample_filestate_python-6.4.5 → wexample_filestate_python-6.5.0}/src/wexample_filestate_python/config_option/mixin/__pycache__/__init__.py +0 -0
  32. {wexample_filestate_python-6.4.5 → wexample_filestate_python-6.5.0}/src/wexample_filestate_python/config_option/mixin/with_stdout_wrapping_mixin.py +0 -0
  33. {wexample_filestate_python-6.4.5 → wexample_filestate_python-6.5.0}/src/wexample_filestate_python/config_value/__init__.py +0 -0
  34. {wexample_filestate_python-6.4.5 → wexample_filestate_python-6.5.0}/src/wexample_filestate_python/config_value/__pycache__/__init__.py +0 -0
  35. {wexample_filestate_python-6.4.5 → wexample_filestate_python-6.5.0}/src/wexample_filestate_python/config_value/python_config_value.py +0 -0
  36. {wexample_filestate_python-6.4.5 → wexample_filestate_python-6.5.0}/src/wexample_filestate_python/const/__init__.py +0 -0
  37. {wexample_filestate_python-6.4.5 → wexample_filestate_python-6.5.0}/src/wexample_filestate_python/const/__pycache__/__init__.py +0 -0
  38. {wexample_filestate_python-6.4.5 → wexample_filestate_python-6.5.0}/src/wexample_filestate_python/const/name_pattern.py +0 -0
  39. {wexample_filestate_python-6.4.5 → wexample_filestate_python-6.5.0}/src/wexample_filestate_python/const/path.py +0 -0
  40. {wexample_filestate_python-6.4.5 → wexample_filestate_python-6.5.0}/src/wexample_filestate_python/const/python_file.py +0 -0
  41. {wexample_filestate_python-6.4.5 → wexample_filestate_python-6.5.0}/src/wexample_filestate_python/event/__init__.py +0 -0
  42. {wexample_filestate_python-6.4.5 → wexample_filestate_python-6.5.0}/src/wexample_filestate_python/event/src/__init__.py +0 -0
  43. {wexample_filestate_python-6.4.5 → wexample_filestate_python-6.5.0}/src/wexample_filestate_python/event/src/wexample_event/__init__.py +0 -0
  44. {wexample_filestate_python-6.4.5 → wexample_filestate_python-6.5.0}/src/wexample_filestate_python/file/__init__.py +0 -0
  45. {wexample_filestate_python-6.4.5 → wexample_filestate_python-6.5.0}/src/wexample_filestate_python/file/__pycache__/__init__.py +0 -0
  46. {wexample_filestate_python-6.4.5 → wexample_filestate_python-6.5.0}/src/wexample_filestate_python/file/python_file.py +0 -0
  47. {wexample_filestate_python-6.4.5 → wexample_filestate_python-6.5.0}/src/wexample_filestate_python/helpers/__init__.py +0 -0
  48. {wexample_filestate_python-6.4.5 → wexample_filestate_python-6.5.0}/src/wexample_filestate_python/helpers/__pycache__/__init__.py +0 -0
  49. {wexample_filestate_python-6.4.5 → wexample_filestate_python-6.5.0}/src/wexample_filestate_python/helpers/package.py +0 -0
  50. {wexample_filestate_python-6.4.5 → wexample_filestate_python-6.5.0}/src/wexample_filestate_python/helpers/python/__init__.py +0 -0
  51. {wexample_filestate_python-6.4.5 → wexample_filestate_python-6.5.0}/src/wexample_filestate_python/helpers/toml.py +0 -0
  52. {wexample_filestate_python-6.4.5 → wexample_filestate_python-6.5.0}/src/wexample_filestate_python/option/__init__.py +0 -0
  53. {wexample_filestate_python-6.4.5 → wexample_filestate_python-6.5.0}/src/wexample_filestate_python/option/__pycache__/__init__.py +0 -0
  54. {wexample_filestate_python-6.4.5 → wexample_filestate_python-6.5.0}/src/wexample_filestate_python/option/python/__init__.py +0 -0
  55. {wexample_filestate_python-6.4.5 → wexample_filestate_python-6.5.0}/src/wexample_filestate_python/option/python/abstract_python_file_content_option.py +0 -0
  56. {wexample_filestate_python-6.4.5 → wexample_filestate_python-6.5.0}/src/wexample_filestate_python/option/python/add_future_annotations_option.py +0 -0
  57. {wexample_filestate_python-6.4.5 → wexample_filestate_python-6.5.0}/src/wexample_filestate_python/option/python/class_name_matches_file_name_option.py +0 -0
  58. {wexample_filestate_python-6.4.5 → wexample_filestate_python-6.5.0}/src/wexample_filestate_python/option/python/fstringify_option.py +0 -0
  59. {wexample_filestate_python-6.4.5 → wexample_filestate_python-6.5.0}/src/wexample_filestate_python/option/python/modernize_typing_option.py +0 -0
  60. {wexample_filestate_python-6.4.5 → wexample_filestate_python-6.5.0}/src/wexample_filestate_python/option/python/order_iterable_items_option.py +0 -0
  61. {wexample_filestate_python-6.4.5 → wexample_filestate_python-6.5.0}/src/wexample_filestate_python/option/python/sort_imports_option.py +0 -0
  62. {wexample_filestate_python-6.4.5 → wexample_filestate_python-6.5.0}/src/wexample_filestate_python/option/python_option.py +0 -0
  63. {wexample_filestate_python-6.4.5 → wexample_filestate_python-6.5.0}/src/wexample_filestate_python/options_provider/__init__.py +0 -0
  64. {wexample_filestate_python-6.4.5 → wexample_filestate_python-6.5.0}/src/wexample_filestate_python/options_provider/__pycache__/__init__.py +0 -0
  65. {wexample_filestate_python-6.4.5 → wexample_filestate_python-6.5.0}/src/wexample_filestate_python/options_provider/python_options_provider.py +0 -0
  66. {wexample_filestate_python-6.4.5 → wexample_filestate_python-6.5.0}/src/wexample_filestate_python/py.typed +0 -0
  67. {wexample_filestate_python-6.4.5 → wexample_filestate_python-6.5.0}/src/wexample_filestate_python/utils/__init__.py +0 -0
  68. {wexample_filestate_python-6.4.5 → wexample_filestate_python-6.5.0}/src/wexample_filestate_python/utils/__pycache__/__init__.py +0 -0
  69. {wexample_filestate_python-6.4.5 → wexample_filestate_python-6.5.0}/src/wexample_filestate_python/utils/python_attrs_utils.py +0 -0
  70. {wexample_filestate_python-6.4.5 → wexample_filestate_python-6.5.0}/src/wexample_filestate_python/utils/python_blank_lines_utils.py +0 -0
  71. {wexample_filestate_python-6.4.5 → wexample_filestate_python-6.5.0}/src/wexample_filestate_python/utils/python_class_attributes_utils.py +0 -0
  72. {wexample_filestate_python-6.4.5 → wexample_filestate_python-6.5.0}/src/wexample_filestate_python/utils/python_class_docstring_utils.py +0 -0
  73. {wexample_filestate_python-6.4.5 → wexample_filestate_python-6.5.0}/src/wexample_filestate_python/utils/python_class_methods_utils.py +0 -0
  74. {wexample_filestate_python-6.4.5 → wexample_filestate_python-6.5.0}/src/wexample_filestate_python/utils/python_constants_utils.py +0 -0
  75. {wexample_filestate_python-6.4.5 → wexample_filestate_python-6.5.0}/src/wexample_filestate_python/utils/python_docstring_utils.py +0 -0
  76. {wexample_filestate_python-6.4.5 → wexample_filestate_python-6.5.0}/src/wexample_filestate_python/utils/python_functions_utils.py +0 -0
  77. {wexample_filestate_python-6.4.5 → wexample_filestate_python-6.5.0}/src/wexample_filestate_python/utils/python_iterable_utils.py +0 -0
  78. {wexample_filestate_python-6.4.5 → wexample_filestate_python-6.5.0}/src/wexample_filestate_python/utils/python_main_guard_utils.py +0 -0
  79. {wexample_filestate_python-6.4.5 → wexample_filestate_python-6.5.0}/src/wexample_filestate_python/utils/python_module_metadata_utils.py +0 -0
  80. {wexample_filestate_python-6.4.5 → wexample_filestate_python-6.5.0}/src/wexample_filestate_python/utils/python_type_checking_utils.py +0 -0
  81. {wexample_filestate_python-6.4.5 → wexample_filestate_python-6.5.0}/src/wexample_filestate_python/utils/relocate_imports/__init__.py +0 -0
  82. {wexample_filestate_python-6.4.5 → wexample_filestate_python-6.5.0}/src/wexample_filestate_python/utils/relocate_imports/__pycache__/__init__.py +0 -0
  83. {wexample_filestate_python-6.4.5 → wexample_filestate_python-6.5.0}/src/wexample_filestate_python/utils/relocate_imports/python_import_rewriter.py +0 -0
  84. {wexample_filestate_python-6.4.5 → wexample_filestate_python-6.5.0}/src/wexample_filestate_python/utils/relocate_imports/python_localize_runtime_imports.py +0 -0
  85. {wexample_filestate_python-6.4.5 → wexample_filestate_python-6.5.0}/src/wexample_filestate_python/utils/relocate_imports/python_parser_import_index.py +0 -0
  86. {wexample_filestate_python-6.4.5 → wexample_filestate_python-6.5.0}/src/wexample_filestate_python/utils/relocate_imports/python_runtime_symbol_collector.py +0 -0
  87. {wexample_filestate_python-6.4.5 → wexample_filestate_python-6.5.0}/src/wexample_filestate_python/utils/relocate_imports/python_usage_collector.py +0 -0
  88. {wexample_filestate_python-6.4.5 → wexample_filestate_python-6.5.0}/tests/tests/__init__.py +0 -0
  89. {wexample_filestate_python-6.4.5 → wexample_filestate_python-6.5.0}/tests/wexample_tests/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: wexample-filestate-python
3
- Version: 6.4.5
3
+ Version: 6.5.0
4
4
  Summary: Helpers for Python.
5
5
  Author-Email: weeger <contact@wexample.com>
6
6
  License: MIT
@@ -20,7 +20,7 @@ Requires-Dist: packaging
20
20
  Requires-Dist: pyupgrade
21
21
  Requires-Dist: tomli
22
22
  Requires-Dist: wexample-api>=6.1.0
23
- Requires-Dist: wexample-filestate>=7.3.0
23
+ Requires-Dist: wexample-filestate>=9.0.0
24
24
  Provides-Extra: dev
25
25
  Requires-Dist: pytest; extra == "dev"
26
26
  Requires-Dist: pytest-cov; extra == "dev"
@@ -28,7 +28,7 @@ Description-Content-Type: text/markdown
28
28
 
29
29
  # filestate_python
30
30
 
31
- Version: 6.4.5
31
+ Version: 6.5.0
32
32
 
33
33
  Helpers for Python.
34
34
 
@@ -119,7 +119,7 @@ Visit the [Wexample Suite documentation](https://docs.wexample.com) for the comp
119
119
  - pyupgrade:
120
120
  - tomli:
121
121
  - wexample-api: >=6.1.0
122
- - wexample-filestate: >=7.3.0
122
+ - wexample-filestate: >=9.0.0
123
123
 
124
124
  ## Versioning & Compatibility Policy
125
125
 
@@ -1,6 +1,6 @@
1
1
  # filestate_python
2
2
 
3
- Version: 6.4.5
3
+ Version: 6.5.0
4
4
 
5
5
  Helpers for Python.
6
6
 
@@ -91,7 +91,7 @@ Visit the [Wexample Suite documentation](https://docs.wexample.com) for the comp
91
91
  - pyupgrade:
92
92
  - tomli:
93
93
  - wexample-api: >=6.1.0
94
- - wexample-filestate: >=7.3.0
94
+ - wexample-filestate: >=9.0.0
95
95
 
96
96
  ## Versioning & Compatibility Policy
97
97
 
@@ -6,7 +6,7 @@ build-backend = "pdm.backend"
6
6
 
7
7
  [project]
8
8
  name = "wexample-filestate-python"
9
- version = "6.4.5"
9
+ version = "6.5.0"
10
10
  description = "Helpers for Python."
11
11
  authors = [
12
12
  { name = "weeger", email = "contact@wexample.com" },
@@ -29,7 +29,7 @@ dependencies = [
29
29
  "pyupgrade",
30
30
  "tomli",
31
31
  "wexample-api>=6.1.0",
32
- "wexample-filestate>=7.3.0",
32
+ "wexample-filestate>=9.0.0",
33
33
  ]
34
34
 
35
35
  [project.readme]
@@ -22,7 +22,14 @@ class AddReturnTypesOption(AbstractPythonFileContentOption):
22
22
  statements in a function agree on one of these literal types."""
23
23
  import libcst as cst
24
24
 
25
- src = target.get_local_file().read()
25
+ from wexample_filestate_python.utils.cst_cache import (
26
+ get_python_source_and_module,
27
+ )
28
+
29
+ try:
30
+ src, module = get_python_source_and_module(target)
31
+ except Exception:
32
+ return target.get_local_file().read()
26
33
 
27
34
  # We implement type inference and rewriting using LibCST to ensure
28
35
  # robust, formatting-preserving edits. We extend inference to:
@@ -251,12 +258,6 @@ class AddReturnTypesOption(AbstractPythonFileContentOption):
251
258
  )
252
259
  return updated_node
253
260
 
254
- try:
255
- module = cst.parse_module(src)
256
- except Exception:
257
- # If parsing fails for any reason, return the original source unchanged
258
- return src
259
-
260
261
  # Collect known simple type names from the module
261
262
  ktc = _KnownTypesCollector()
262
263
  module.visit(ktc)
@@ -24,14 +24,14 @@ class FixAttrsOption(AbstractPythonFileContentOption):
24
24
  - Ensure @attrs.define always uses kw_only=True
25
25
  - Ensure @attr.s always uses kw_only=True
26
26
  """
27
- import libcst as cst
28
-
27
+ from wexample_filestate_python.utils.cst_cache import (
28
+ get_python_source_and_module,
29
+ )
29
30
  from wexample_filestate_python.utils.python_attrs_utils import (
30
31
  fix_attrs_kw_only,
31
32
  )
32
33
 
33
- src = target.get_local_file().read()
34
- module = cst.parse_module(src)
34
+ src, module = get_python_source_and_module(target)
35
35
 
36
36
  modified = fix_attrs_kw_only(module)
37
37
  return modified.code
@@ -34,14 +34,14 @@ class FixBlankLinesOption(AbstractPythonFileContentOption):
34
34
 
35
35
  Note: Module-level spacing (between classes/functions/imports) is handled by Black.
36
36
  """
37
- import libcst as cst
38
-
37
+ from wexample_filestate_python.utils.cst_cache import (
38
+ get_python_source_and_module,
39
+ )
39
40
  from wexample_filestate_python.utils.python_blank_lines_utils import (
40
41
  fix_function_blank_lines,
41
42
  )
42
43
 
43
- src = target.get_local_file().read()
44
- module = cst.parse_module(src)
44
+ src, module = get_python_source_and_module(target)
45
45
 
46
46
  modified = fix_function_blank_lines(module)
47
47
  return modified.code
@@ -0,0 +1,48 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, ClassVar
4
+
5
+ from wexample_filestate.option.mixin.with_batch_option_mixin import (
6
+ WithBatchOptionMixin,
7
+ )
8
+ from wexample_helpers.decorator.base_class import base_class
9
+
10
+ from .abstract_python_file_content_option import AbstractPythonFileContentOption
11
+
12
+ if TYPE_CHECKING:
13
+ from pathlib import Path
14
+
15
+ from wexample_filestate.const.types_state_items import TargetFileOrDirectoryType
16
+
17
+
18
+ @base_class
19
+ class FormatOption(WithBatchOptionMixin, AbstractPythonFileContentOption):
20
+ _line_length: ClassVar[int] = 88
21
+
22
+ def get_description(self) -> str:
23
+ return "Format the Python file content using Black."
24
+
25
+ def _apply_content_change(self, target: TargetFileOrDirectoryType) -> str:
26
+ cache = self._get_or_build_batch_cache(target)
27
+ path_key = str(target.get_path())
28
+ if path_key in cache:
29
+ return cache[path_key]
30
+ return target.read_text()
31
+
32
+ def _run_batch_on_paths(
33
+ self,
34
+ reference_target: TargetFileOrDirectoryType,
35
+ paths: list[Path],
36
+ ) -> None:
37
+ import black
38
+
39
+ mode = black.Mode(line_length=self._line_length)
40
+ for path in paths:
41
+ src = path.read_text()
42
+ try:
43
+ formatted = black.format_file_contents(src, fast=False, mode=mode)
44
+ if formatted != src:
45
+ path.write_text(formatted)
46
+ except black.NothingChanged:
47
+ pass
48
+ return None
@@ -21,14 +21,14 @@ class OrderClassAttributesOption(AbstractPythonFileContentOption):
21
21
  Special include __slots__, __match_args__, and inner class Config. Operates on
22
22
  contiguous attribute blocks and preserves comments attached to each attribute.
23
23
  """
24
- import libcst as cst
25
-
24
+ from wexample_filestate_python.utils.cst_cache import (
25
+ get_python_source_and_module,
26
+ )
26
27
  from wexample_filestate_python.utils.python_class_attributes_utils import (
27
28
  ensure_order_class_attributes_in_module,
28
29
  )
29
30
 
30
- src = target.get_local_file().read()
31
- module = cst.parse_module(src)
31
+ src, module = get_python_source_and_module(target)
32
32
 
33
33
  modified = ensure_order_class_attributes_in_module(module)
34
34
  return modified.code
@@ -23,14 +23,14 @@ class OrderClassDocstringOption(AbstractPythonFileContentOption):
23
23
  decorators and the class header). Normalizes to double quotes. Avoids
24
24
  whitespace-only diffs when already correct.
25
25
  """
26
- import libcst as cst
27
-
26
+ from wexample_filestate_python.utils.cst_cache import (
27
+ get_python_source_and_module,
28
+ )
28
29
  from wexample_filestate_python.utils.python_class_docstring_utils import (
29
30
  ensure_all_classes_docstring_first,
30
31
  )
31
32
 
32
- src = target.get_local_file().read()
33
- module = cst.parse_module(src)
33
+ src, module = get_python_source_and_module(target)
34
34
 
35
35
  modified = ensure_all_classes_docstring_first(module)
36
36
  return modified.code
@@ -24,14 +24,14 @@ class OrderClassMethodsOption(AbstractPythonFileContentOption):
24
24
  - Properties grouped by base name (getter/setter/deleter together), groups A–Z
25
25
  - Instance methods: public A–Z, then private/protected A–Z
26
26
  """
27
- import libcst as cst
28
-
27
+ from wexample_filestate_python.utils.cst_cache import (
28
+ get_python_source_and_module,
29
+ )
29
30
  from wexample_filestate_python.utils.python_class_methods_utils import (
30
31
  ensure_order_class_methods_in_module,
31
32
  )
32
33
 
33
- src = target.get_local_file().read()
34
- module = cst.parse_module(src)
34
+ src, module = get_python_source_and_module(target)
35
35
 
36
36
  modified = ensure_order_class_methods_in_module(module)
37
37
  return modified.code
@@ -22,14 +22,14 @@ class OrderConstantsOption(AbstractPythonFileContentOption):
22
22
  A block is a contiguous sequence of simple UPPER_CASE assignments (no blank line between).
23
23
  Non-flagged constants and other contexts are ignored.
24
24
  """
25
- import libcst as cst
26
-
25
+ from wexample_filestate_python.utils.cst_cache import (
26
+ get_python_source_and_module,
27
+ )
27
28
  from wexample_filestate_python.utils.python_constants_utils import (
28
29
  reorder_flagged_constants_everywhere,
29
30
  )
30
31
 
31
- src = target.get_local_file().read()
32
- module = cst.parse_module(src)
32
+ src, module = get_python_source_and_module(target)
33
33
 
34
34
  modified = reorder_flagged_constants_everywhere(module, src)
35
35
  return modified.code
@@ -21,16 +21,16 @@ class OrderMainGuardOption(AbstractPythonFileContentOption):
21
21
  Moves any top-level main-guard blocks to be the last non-empty statement in the
22
22
  module (before trailing blank lines), preserving content and spacing as much as possible.
23
23
  """
24
- import libcst as cst
25
-
24
+ from wexample_filestate_python.utils.cst_cache import (
25
+ get_python_source_and_module,
26
+ )
26
27
  from wexample_filestate_python.utils.python_main_guard_utils import (
27
28
  find_main_guard_blocks,
28
29
  is_main_guard_at_end,
29
30
  move_main_guard_to_end,
30
31
  )
31
32
 
32
- src = target.get_local_file().read()
33
- module = cst.parse_module(src)
33
+ src, module = get_python_source_and_module(target)
34
34
 
35
35
  # No main guard present => nothing to do
36
36
  if not find_main_guard_blocks(module):
@@ -23,14 +23,16 @@ class OrderModuleDocstringOption(AbstractPythonFileContentOption):
23
23
  """
24
24
  import libcst as cst
25
25
 
26
+ from wexample_filestate_python.utils.cst_cache import (
27
+ get_python_source_and_module,
28
+ )
26
29
  from wexample_filestate_python.utils.python_docstring_utils import (
27
30
  find_module_docstring,
28
31
  is_module_docstring_at_top,
29
32
  move_docstring_to_top,
30
33
  )
31
34
 
32
- src = target.get_local_file().read()
33
- module = cst.parse_module(src)
35
+ src, module = get_python_source_and_module(target)
34
36
 
35
37
  # Check if there's a docstring and if it needs to be moved
36
38
  docstring_node, position = find_module_docstring(module)
@@ -21,15 +21,15 @@ class OrderModuleFunctionsOption(AbstractPythonFileContentOption):
21
21
  - Keeps @overload groups attached to their implementation.
22
22
  - Preserves spacing/comments by keeping each group's first function's leading_lines.
23
23
  """
24
- import libcst as cst
25
-
24
+ from wexample_filestate_python.utils.cst_cache import (
25
+ get_python_source_and_module,
26
+ )
26
27
  from wexample_filestate_python.utils.python_functions_utils import (
27
28
  module_functions_sorted_before_classes,
28
29
  reorder_module_functions,
29
30
  )
30
31
 
31
- src = target.get_local_file().read()
32
- module = cst.parse_module(src)
32
+ src, module = get_python_source_and_module(target)
33
33
 
34
34
  # Quick no-op detection: if there are no functions, or functions already sorted
35
35
  # and placed before classes, the transformation may be a noop.
@@ -23,16 +23,16 @@ class OrderModuleMetadataOption(AbstractPythonFileContentOption):
23
23
 
24
24
  Placement: after imports and `if TYPE_CHECKING:` blocks, before other module-level code.
25
25
  """
26
- import libcst as cst
27
-
26
+ from wexample_filestate_python.utils.cst_cache import (
27
+ get_python_source_and_module,
28
+ )
28
29
  from wexample_filestate_python.utils.python_module_metadata_utils import (
29
30
  find_module_metadata_statements,
30
31
  group_and_sort_module_metadata,
31
32
  target_index_for_module_metadata,
32
33
  )
33
34
 
34
- src = target.get_local_file().read()
35
- module = cst.parse_module(src)
35
+ src, module = get_python_source_and_module(target)
36
36
 
37
37
  found = find_module_metadata_statements(module)
38
38
  if not found:
@@ -22,16 +22,16 @@ class OrderTypeCheckingBlockOption(AbstractPythonFileContentOption):
22
22
  after the last regular import section (or after `from __future__ import ...`
23
23
  if no regular imports exist). Keeps spacing minimal and preserves content.
24
24
  """
25
- import libcst as cst
26
-
25
+ from wexample_filestate_python.utils.cst_cache import (
26
+ get_python_source_and_module,
27
+ )
27
28
  from wexample_filestate_python.utils.python_type_checking_utils import (
28
29
  find_type_checking_blocks,
29
30
  move_type_checking_blocks_after_imports,
30
31
  target_index_for_type_checking,
31
32
  )
32
33
 
33
- src = target.get_local_file().read()
34
- module = cst.parse_module(src)
34
+ src, module = get_python_source_and_module(target)
35
35
 
36
36
  blocks = find_type_checking_blocks(module)
37
37
  if not blocks:
@@ -35,6 +35,9 @@ class RelocateImportsOption(AbstractPythonFileContentOption):
35
35
 
36
36
  import libcst as cst
37
37
 
38
+ from wexample_filestate_python.utils.cst_cache import (
39
+ get_python_source_and_module,
40
+ )
38
41
  from wexample_filestate_python.utils.relocate_imports.python_import_rewriter import (
39
42
  PythonImportRewriter,
40
43
  )
@@ -51,8 +54,7 @@ class RelocateImportsOption(AbstractPythonFileContentOption):
51
54
  PythonUsageCollector,
52
55
  )
53
56
 
54
- src = target.get_local_file().read()
55
- module = cst.parse_module(src)
57
+ src, module = get_python_source_and_module(target)
56
58
 
57
59
  # Index current imports using shared utility
58
60
  idx = PythonParserImportIndex()
@@ -0,0 +1,38 @@
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 RemoveUnusedOption(AbstractPythonFileContentOption):
15
+ def get_description(self) -> str:
16
+ return "Remove unused imports from the Python file using autoflake."
17
+
18
+ def _apply_content_change(self, target: TargetFileOrDirectoryType) -> str:
19
+ """Remove unused Python imports using autoflake's in-process API.
20
+
21
+ Previously shelled out to `autoflake --stdout` per file, costing ~80ms
22
+ of subprocess + Python startup overhead each call (≈20s on a 268-file
23
+ project).
24
+ """
25
+ from autoflake import fix_code
26
+
27
+ src = target.get_local_file().read()
28
+ try:
29
+ return fix_code(
30
+ src,
31
+ remove_all_unused_imports=True,
32
+ remove_unused_variables=True,
33
+ expand_star_imports=True,
34
+ remove_duplicate_keys=True,
35
+ )
36
+ except Exception as e:
37
+ target.io.error(f"Autoflake error: {e}\n\n")
38
+ return src
@@ -21,7 +21,11 @@ class UnquoteAnnotationsOption(AbstractPythonFileContentOption):
21
21
 
22
22
  import libcst as cst
23
23
 
24
- src = target.get_local_file().read()
24
+ from wexample_filestate_python.utils.cst_cache import (
25
+ get_python_source_and_module,
26
+ )
27
+
28
+ src, module = get_python_source_and_module(target)
25
29
 
26
30
  class _Unquoter(cst.CSTTransformer):
27
31
  @staticmethod
@@ -80,6 +84,5 @@ class UnquoteAnnotationsOption(AbstractPythonFileContentOption):
80
84
  return updated_node.with_changes(annotation=expr)
81
85
  return updated_node
82
86
 
83
- module = cst.parse_module(src)
84
87
  new_mod = module.visit(_Unquoter())
85
88
  return new_mod.code
@@ -0,0 +1,37 @@
1
+ """Per-target libcst parse cache.
2
+
3
+ Within a single rectification scan, a Python file's disk content does not
4
+ change — yet historically each option parsed the libcst module independently,
5
+ multiplying the parse cost by the number of content options (~15). This
6
+ helper caches the `(src, module)` pair on the target item so all options on
7
+ the same file share a single parse.
8
+
9
+ The cache is naturally invalidated between rectification passes (a new
10
+ workdir means new target instances).
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from typing import TYPE_CHECKING
16
+
17
+ if TYPE_CHECKING:
18
+ import libcst as cst
19
+ from wexample_filestate.const.types_state_items import TargetFileOrDirectoryType
20
+
21
+
22
+ _CACHE_ATTR = "_cst_cache"
23
+
24
+
25
+ def get_python_source_and_module(
26
+ target: TargetFileOrDirectoryType,
27
+ ) -> tuple[str, cst.Module]:
28
+ cached = getattr(target, _CACHE_ATTR, None)
29
+ if cached is not None:
30
+ return cached
31
+
32
+ import libcst as cst
33
+
34
+ src = target.get_local_file().read()
35
+ module = cst.parse_module(src)
36
+ setattr(target, _CACHE_ATTR, (src, module))
37
+ return src, module
@@ -1,34 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from typing import TYPE_CHECKING, ClassVar
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 FormatOption(AbstractPythonFileContentOption):
15
- # Use ClassVar to avoid Pydantic treating it as a model field/private attr
16
- _line_length: ClassVar[int] = 88
17
-
18
- def get_description(self) -> str:
19
- return "Format the Python file content using Black."
20
-
21
- def _apply_content_change(self, target: TargetFileOrDirectoryType) -> str:
22
- """Format Python files using Black."""
23
- import black
24
-
25
- src = target.get_local_file().read()
26
- mode = black.Mode(line_length=self._line_length)
27
-
28
- try:
29
- formatted = black.format_file_contents(src, fast=False, mode=mode)
30
- return formatted
31
- except black.NothingChanged:
32
- return src
33
- except Exception as e:
34
- raise e
@@ -1,45 +0,0 @@
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 RemoveUnusedOption(AbstractPythonFileContentOption):
15
- def get_description(self) -> str:
16
- return "Remove unused imports from the Python file using autoflake."
17
-
18
- def _apply_content_change(self, target: TargetFileOrDirectoryType) -> str:
19
- """Remove unused Python imports using autoflake."""
20
- from wexample_helpers.helpers.shell import shell_run
21
- from wexample_helpers.helpers.system import system_get_venv_bin_path
22
-
23
- result = shell_run(
24
- cmd=[
25
- f"{system_get_venv_bin_path()}/autoflake",
26
- "--stdout",
27
- "--remove-all-unused-imports",
28
- "--remove-unused-variables",
29
- "--expand-star-imports",
30
- "--remove-duplicate-keys",
31
- str(target.get_path()),
32
- ],
33
- )
34
-
35
- if result.returncode != 0:
36
- # Double line return is important to keep message visible event last line is erased by parent process.
37
- target.io.error(f"Autoflake error: {result.stderr}\n\n")
38
- return target.get_local_file().read() # Return original content on error
39
-
40
- modified_content = result.stdout
41
-
42
- if not modified_content.strip():
43
- return target.get_local_file().read() # Return original content if empty
44
-
45
- return modified_content