wexample-filestate-python 8.0.0__tar.gz → 8.1.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 (95) hide show
  1. {wexample_filestate_python-8.0.0 → wexample_filestate_python-8.1.0}/PKG-INFO +6 -6
  2. {wexample_filestate_python-8.0.0 → wexample_filestate_python-8.1.0}/README.md +3 -3
  3. {wexample_filestate_python-8.0.0 → wexample_filestate_python-8.1.0}/pyproject.toml +3 -3
  4. wexample_filestate_python-8.1.0/src/wexample_filestate_python/file/python_test_stub_file.py +128 -0
  5. {wexample_filestate_python-8.0.0 → wexample_filestate_python-8.1.0}/src/wexample_filestate_python/option/python/sort_imports_option.py +10 -6
  6. {wexample_filestate_python-8.0.0 → wexample_filestate_python-8.1.0}/src/wexample_filestate_python/option/python_option.py +21 -7
  7. {wexample_filestate_python-8.0.0 → wexample_filestate_python-8.1.0}/src/wexample_filestate_python/options_provider/python_options_provider.py +8 -9
  8. {wexample_filestate_python-8.0.0 → wexample_filestate_python-8.1.0}/src/wexample_filestate_python/utils/python_attrs_utils.py +1 -5
  9. {wexample_filestate_python-8.0.0 → wexample_filestate_python-8.1.0}/src/wexample_filestate_python/utils/python_blank_lines_utils.py +4 -4
  10. {wexample_filestate_python-8.0.0 → wexample_filestate_python-8.1.0}/src/wexample_filestate_python/utils/python_class_methods_utils.py +11 -5
  11. {wexample_filestate_python-8.0.0 → wexample_filestate_python-8.1.0}/src/wexample_filestate_python/utils/python_functions_utils.py +3 -9
  12. {wexample_filestate_python-8.0.0 → wexample_filestate_python-8.1.0}/src/wexample_filestate_python/utils/python_iterable_utils.py +6 -5
  13. {wexample_filestate_python-8.0.0/tests/wexample_tests → wexample_filestate_python-8.1.0/tests/unit}/__init__.py +0 -0
  14. wexample_filestate_python-8.1.0/tests/unit/helpers/__init__.py +0 -0
  15. wexample_filestate_python-8.1.0/tests/unit/helpers/test_package.py +16 -0
  16. wexample_filestate_python-8.1.0/tests/unit/helpers/test_toml.py +19 -0
  17. wexample_filestate_python-8.1.0/tests/wexample_tests/__init__.py +0 -0
  18. {wexample_filestate_python-8.0.0 → wexample_filestate_python-8.1.0}/src/wexample_filestate_python/__init__.py +0 -0
  19. {wexample_filestate_python-8.0.0 → wexample_filestate_python-8.1.0}/src/wexample_filestate_python/__pycache__/__init__.py +0 -0
  20. {wexample_filestate_python-8.0.0 → wexample_filestate_python-8.1.0}/src/wexample_filestate_python/common/__init__.py +0 -0
  21. {wexample_filestate_python-8.0.0 → wexample_filestate_python-8.1.0}/src/wexample_filestate_python/common/__pycache__/__init__.py +0 -0
  22. {wexample_filestate_python-8.0.0 → wexample_filestate_python-8.1.0}/src/wexample_filestate_python/common/pipy_gateway.py +0 -0
  23. {wexample_filestate_python-8.0.0 → wexample_filestate_python-8.1.0}/src/wexample_filestate_python/config_option/__init__.py +0 -0
  24. {wexample_filestate_python-8.0.0 → wexample_filestate_python-8.1.0}/src/wexample_filestate_python/config_option/__pycache__/__init__.py +0 -0
  25. {wexample_filestate_python-8.0.0 → wexample_filestate_python-8.1.0}/src/wexample_filestate_python/config_option/mixin/__init__.py +0 -0
  26. {wexample_filestate_python-8.0.0 → wexample_filestate_python-8.1.0}/src/wexample_filestate_python/config_option/mixin/__pycache__/__init__.py +0 -0
  27. {wexample_filestate_python-8.0.0 → wexample_filestate_python-8.1.0}/src/wexample_filestate_python/config_option/mixin/with_stdout_wrapping_mixin.py +0 -0
  28. {wexample_filestate_python-8.0.0 → wexample_filestate_python-8.1.0}/src/wexample_filestate_python/config_value/__init__.py +0 -0
  29. {wexample_filestate_python-8.0.0 → wexample_filestate_python-8.1.0}/src/wexample_filestate_python/config_value/__pycache__/__init__.py +0 -0
  30. {wexample_filestate_python-8.0.0 → wexample_filestate_python-8.1.0}/src/wexample_filestate_python/config_value/python_config_value.py +0 -0
  31. {wexample_filestate_python-8.0.0 → wexample_filestate_python-8.1.0}/src/wexample_filestate_python/const/__init__.py +0 -0
  32. {wexample_filestate_python-8.0.0 → wexample_filestate_python-8.1.0}/src/wexample_filestate_python/const/__pycache__/__init__.py +0 -0
  33. {wexample_filestate_python-8.0.0 → wexample_filestate_python-8.1.0}/src/wexample_filestate_python/const/name_pattern.py +0 -0
  34. {wexample_filestate_python-8.0.0 → wexample_filestate_python-8.1.0}/src/wexample_filestate_python/const/path.py +0 -0
  35. {wexample_filestate_python-8.0.0 → wexample_filestate_python-8.1.0}/src/wexample_filestate_python/const/python_file.py +0 -0
  36. {wexample_filestate_python-8.0.0 → wexample_filestate_python-8.1.0}/src/wexample_filestate_python/event/__init__.py +0 -0
  37. {wexample_filestate_python-8.0.0 → wexample_filestate_python-8.1.0}/src/wexample_filestate_python/event/src/__init__.py +0 -0
  38. {wexample_filestate_python-8.0.0 → wexample_filestate_python-8.1.0}/src/wexample_filestate_python/event/src/wexample_event/__init__.py +0 -0
  39. {wexample_filestate_python-8.0.0 → wexample_filestate_python-8.1.0}/src/wexample_filestate_python/file/__init__.py +0 -0
  40. {wexample_filestate_python-8.0.0 → wexample_filestate_python-8.1.0}/src/wexample_filestate_python/file/__pycache__/__init__.py +0 -0
  41. {wexample_filestate_python-8.0.0 → wexample_filestate_python-8.1.0}/src/wexample_filestate_python/file/python_file.py +0 -0
  42. {wexample_filestate_python-8.0.0 → wexample_filestate_python-8.1.0}/src/wexample_filestate_python/helpers/__init__.py +0 -0
  43. {wexample_filestate_python-8.0.0 → wexample_filestate_python-8.1.0}/src/wexample_filestate_python/helpers/__pycache__/__init__.py +0 -0
  44. {wexample_filestate_python-8.0.0 → wexample_filestate_python-8.1.0}/src/wexample_filestate_python/helpers/package.py +0 -0
  45. {wexample_filestate_python-8.0.0 → wexample_filestate_python-8.1.0}/src/wexample_filestate_python/helpers/python/__init__.py +0 -0
  46. {wexample_filestate_python-8.0.0 → wexample_filestate_python-8.1.0}/src/wexample_filestate_python/helpers/toml.py +0 -0
  47. {wexample_filestate_python-8.0.0 → wexample_filestate_python-8.1.0}/src/wexample_filestate_python/option/__init__.py +0 -0
  48. {wexample_filestate_python-8.0.0 → wexample_filestate_python-8.1.0}/src/wexample_filestate_python/option/__pycache__/__init__.py +0 -0
  49. {wexample_filestate_python-8.0.0 → wexample_filestate_python-8.1.0}/src/wexample_filestate_python/option/python/__init__.py +0 -0
  50. {wexample_filestate_python-8.0.0 → wexample_filestate_python-8.1.0}/src/wexample_filestate_python/option/python/abstract_python_file_content_option.py +0 -0
  51. {wexample_filestate_python-8.0.0 → wexample_filestate_python-8.1.0}/src/wexample_filestate_python/option/python/add_future_annotations_option.py +0 -0
  52. {wexample_filestate_python-8.0.0 → wexample_filestate_python-8.1.0}/src/wexample_filestate_python/option/python/add_return_types_option.py +0 -0
  53. {wexample_filestate_python-8.0.0 → wexample_filestate_python-8.1.0}/src/wexample_filestate_python/option/python/class_name_matches_file_name_option.py +0 -0
  54. {wexample_filestate_python-8.0.0 → wexample_filestate_python-8.1.0}/src/wexample_filestate_python/option/python/fix_attrs_option.py +0 -0
  55. {wexample_filestate_python-8.0.0 → wexample_filestate_python-8.1.0}/src/wexample_filestate_python/option/python/fix_blank_lines_option.py +0 -0
  56. {wexample_filestate_python-8.0.0 → wexample_filestate_python-8.1.0}/src/wexample_filestate_python/option/python/format_option.py +0 -0
  57. {wexample_filestate_python-8.0.0 → wexample_filestate_python-8.1.0}/src/wexample_filestate_python/option/python/fstringify_option.py +0 -0
  58. {wexample_filestate_python-8.0.0 → wexample_filestate_python-8.1.0}/src/wexample_filestate_python/option/python/modernize_typing_option.py +0 -0
  59. {wexample_filestate_python-8.0.0 → wexample_filestate_python-8.1.0}/src/wexample_filestate_python/option/python/order_class_attributes_option.py +0 -0
  60. {wexample_filestate_python-8.0.0 → wexample_filestate_python-8.1.0}/src/wexample_filestate_python/option/python/order_class_docstring_option.py +0 -0
  61. {wexample_filestate_python-8.0.0 → wexample_filestate_python-8.1.0}/src/wexample_filestate_python/option/python/order_class_methods_option.py +0 -0
  62. {wexample_filestate_python-8.0.0 → wexample_filestate_python-8.1.0}/src/wexample_filestate_python/option/python/order_constants_option.py +0 -0
  63. {wexample_filestate_python-8.0.0 → wexample_filestate_python-8.1.0}/src/wexample_filestate_python/option/python/order_iterable_items_option.py +0 -0
  64. {wexample_filestate_python-8.0.0 → wexample_filestate_python-8.1.0}/src/wexample_filestate_python/option/python/order_main_guard_option.py +0 -0
  65. {wexample_filestate_python-8.0.0 → wexample_filestate_python-8.1.0}/src/wexample_filestate_python/option/python/order_module_docstring_option.py +0 -0
  66. {wexample_filestate_python-8.0.0 → wexample_filestate_python-8.1.0}/src/wexample_filestate_python/option/python/order_module_functions_option.py +0 -0
  67. {wexample_filestate_python-8.0.0 → wexample_filestate_python-8.1.0}/src/wexample_filestate_python/option/python/order_module_metadata_option.py +0 -0
  68. {wexample_filestate_python-8.0.0 → wexample_filestate_python-8.1.0}/src/wexample_filestate_python/option/python/order_type_checking_block_option.py +0 -0
  69. {wexample_filestate_python-8.0.0 → wexample_filestate_python-8.1.0}/src/wexample_filestate_python/option/python/relocate_imports_option.py +0 -0
  70. {wexample_filestate_python-8.0.0 → wexample_filestate_python-8.1.0}/src/wexample_filestate_python/option/python/remove_unused_option.py +0 -0
  71. {wexample_filestate_python-8.0.0 → wexample_filestate_python-8.1.0}/src/wexample_filestate_python/option/python/unquote_annotations_option.py +0 -0
  72. {wexample_filestate_python-8.0.0 → wexample_filestate_python-8.1.0}/src/wexample_filestate_python/options_provider/__init__.py +0 -0
  73. {wexample_filestate_python-8.0.0 → wexample_filestate_python-8.1.0}/src/wexample_filestate_python/options_provider/__pycache__/__init__.py +0 -0
  74. {wexample_filestate_python-8.0.0 → wexample_filestate_python-8.1.0}/src/wexample_filestate_python/py.typed +0 -0
  75. {wexample_filestate_python-8.0.0 → wexample_filestate_python-8.1.0}/src/wexample_filestate_python/utils/__init__.py +0 -0
  76. {wexample_filestate_python-8.0.0 → wexample_filestate_python-8.1.0}/src/wexample_filestate_python/utils/__pycache__/__init__.py +0 -0
  77. {wexample_filestate_python-8.0.0 → wexample_filestate_python-8.1.0}/src/wexample_filestate_python/utils/cst_cache.py +0 -0
  78. {wexample_filestate_python-8.0.0 → wexample_filestate_python-8.1.0}/src/wexample_filestate_python/utils/python_class_attributes_utils.py +0 -0
  79. {wexample_filestate_python-8.0.0 → wexample_filestate_python-8.1.0}/src/wexample_filestate_python/utils/python_class_docstring_utils.py +0 -0
  80. {wexample_filestate_python-8.0.0 → wexample_filestate_python-8.1.0}/src/wexample_filestate_python/utils/python_constants_utils.py +0 -0
  81. {wexample_filestate_python-8.0.0 → wexample_filestate_python-8.1.0}/src/wexample_filestate_python/utils/python_docstring_utils.py +0 -0
  82. {wexample_filestate_python-8.0.0 → wexample_filestate_python-8.1.0}/src/wexample_filestate_python/utils/python_main_guard_utils.py +0 -0
  83. {wexample_filestate_python-8.0.0 → wexample_filestate_python-8.1.0}/src/wexample_filestate_python/utils/python_module_metadata_utils.py +0 -0
  84. {wexample_filestate_python-8.0.0 → wexample_filestate_python-8.1.0}/src/wexample_filestate_python/utils/python_type_checking_utils.py +0 -0
  85. {wexample_filestate_python-8.0.0 → wexample_filestate_python-8.1.0}/src/wexample_filestate_python/utils/relocate_imports/__init__.py +0 -0
  86. {wexample_filestate_python-8.0.0 → wexample_filestate_python-8.1.0}/src/wexample_filestate_python/utils/relocate_imports/__pycache__/__init__.py +0 -0
  87. {wexample_filestate_python-8.0.0 → wexample_filestate_python-8.1.0}/src/wexample_filestate_python/utils/relocate_imports/python_import_rewriter.py +0 -0
  88. {wexample_filestate_python-8.0.0 → wexample_filestate_python-8.1.0}/src/wexample_filestate_python/utils/relocate_imports/python_localize_runtime_imports.py +0 -0
  89. {wexample_filestate_python-8.0.0 → wexample_filestate_python-8.1.0}/src/wexample_filestate_python/utils/relocate_imports/python_parser_import_index.py +0 -0
  90. {wexample_filestate_python-8.0.0 → wexample_filestate_python-8.1.0}/src/wexample_filestate_python/utils/relocate_imports/python_runtime_symbol_collector.py +0 -0
  91. {wexample_filestate_python-8.0.0 → wexample_filestate_python-8.1.0}/src/wexample_filestate_python/utils/relocate_imports/python_usage_collector.py +0 -0
  92. {wexample_filestate_python-8.0.0 → wexample_filestate_python-8.1.0}/tests/helpers/__init__.py +0 -0
  93. {wexample_filestate_python-8.0.0 → wexample_filestate_python-8.1.0}/tests/helpers/test_package.py +0 -0
  94. {wexample_filestate_python-8.0.0 → wexample_filestate_python-8.1.0}/tests/helpers/test_toml.py +0 -0
  95. {wexample_filestate_python-8.0.0 → wexample_filestate_python-8.1.0}/tests/tests/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: wexample-filestate-python
3
- Version: 8.0.0
3
+ Version: 8.1.0
4
4
  Summary: Helpers for Python.
5
5
  Author-Email: weeger <contact@wexample.com>
6
6
  License: MIT
@@ -18,8 +18,8 @@ Requires-Dist: isort
18
18
  Requires-Dist: libcst
19
19
  Requires-Dist: packaging
20
20
  Requires-Dist: tomli
21
- Requires-Dist: wexample-api>=6.5.0
22
- Requires-Dist: wexample-filestate>=13.0.0
21
+ Requires-Dist: wexample-api>=6.6.0
22
+ Requires-Dist: wexample-filestate>=14.0.0
23
23
  Provides-Extra: dev
24
24
  Requires-Dist: pytest; extra == "dev"
25
25
  Requires-Dist: pytest-cov; extra == "dev"
@@ -27,7 +27,7 @@ Description-Content-Type: text/markdown
27
27
 
28
28
  # filestate_python
29
29
 
30
- Version: 8.0.0
30
+ Version: 8.1.0
31
31
 
32
32
  Helpers for Python.
33
33
 
@@ -116,8 +116,8 @@ Visit the [Wexample Suite documentation](https://docs.wexample.com) for the comp
116
116
  - libcst:
117
117
  - packaging:
118
118
  - tomli:
119
- - wexample-api: >=6.5.0
120
- - wexample-filestate: >=13.0.0
119
+ - wexample-api: >=6.6.0
120
+ - wexample-filestate: >=14.0.0
121
121
 
122
122
  ## Versioning & Compatibility Policy
123
123
 
@@ -1,6 +1,6 @@
1
1
  # filestate_python
2
2
 
3
- Version: 8.0.0
3
+ Version: 8.1.0
4
4
 
5
5
  Helpers for Python.
6
6
 
@@ -89,8 +89,8 @@ Visit the [Wexample Suite documentation](https://docs.wexample.com) for the comp
89
89
  - libcst:
90
90
  - packaging:
91
91
  - tomli:
92
- - wexample-api: >=6.5.0
93
- - wexample-filestate: >=13.0.0
92
+ - wexample-api: >=6.6.0
93
+ - wexample-filestate: >=14.0.0
94
94
 
95
95
  ## Versioning & Compatibility Policy
96
96
 
@@ -6,7 +6,7 @@ build-backend = "pdm.backend"
6
6
 
7
7
  [project]
8
8
  name = "wexample-filestate-python"
9
- version = "8.0.0"
9
+ version = "8.1.0"
10
10
  description = "Helpers for Python."
11
11
  authors = [
12
12
  { name = "weeger", email = "contact@wexample.com" },
@@ -27,8 +27,8 @@ dependencies = [
27
27
  "libcst",
28
28
  "packaging",
29
29
  "tomli",
30
- "wexample-api>=6.5.0",
31
- "wexample-filestate>=13.0.0",
30
+ "wexample-api>=6.6.0",
31
+ "wexample-filestate>=14.0.0",
32
32
  ]
33
33
 
34
34
  [project.readme]
@@ -0,0 +1,128 @@
1
+ from __future__ import annotations
2
+
3
+ from wexample_helpers.decorator.base_class import base_class
4
+
5
+ from wexample_filestate_python.file.python_file import PythonFile
6
+
7
+
8
+ def _is_skippable_class(class_name: str, class_node) -> bool:
9
+ import libcst as cst
10
+
11
+ if class_name.startswith("Abstract"):
12
+ return True
13
+ for decorator in class_node.decorators:
14
+ expr = decorator.decorator
15
+ if isinstance(expr, cst.Name) and expr.value in (
16
+ "abstract_class",
17
+ "abstractmethod",
18
+ ):
19
+ return True
20
+ if isinstance(expr, cst.Call) and isinstance(expr.func, cst.Name):
21
+ if expr.func.value == "abstract_class":
22
+ return True
23
+ return False
24
+
25
+
26
+ def _public_function_names(module) -> list[str]:
27
+ import libcst as cst
28
+
29
+ return [
30
+ node.name.value
31
+ for node in module.body
32
+ if isinstance(node, cst.FunctionDef) and not node.name.value.startswith("_")
33
+ ]
34
+
35
+
36
+ def _public_method_names(class_node) -> list[str]:
37
+ import libcst as cst
38
+
39
+ statements = getattr(class_node.body, "body", ())
40
+ return [
41
+ stmt.name.value
42
+ for stmt in statements
43
+ if isinstance(stmt, cst.FunctionDef) and not stmt.name.value.startswith("_")
44
+ ]
45
+
46
+
47
+ def _render_stub(primary_module_name: str, module) -> str:
48
+ import libcst as cst
49
+
50
+ lines: list[str] = [
51
+ "from __future__ import annotations",
52
+ "",
53
+ ]
54
+
55
+ classes_emitted = False
56
+ for node in module.body:
57
+ if isinstance(node, cst.ClassDef):
58
+ class_name = node.name.value
59
+ if _is_skippable_class(class_name, node):
60
+ continue
61
+ methods = _public_method_names(node)
62
+ if not methods:
63
+ continue
64
+ lines.append(f"class Test{class_name}:")
65
+ for method_name in methods:
66
+ lines.extend([
67
+ f" def test_{method_name}(self) -> None:",
68
+ " pass",
69
+ "",
70
+ ])
71
+ classes_emitted = True
72
+
73
+ func_names = _public_function_names(module)
74
+ if func_names:
75
+ if classes_emitted:
76
+ lines.append("")
77
+ for func_name in func_names:
78
+ lines.extend([
79
+ f"def test_{func_name}() -> None:",
80
+ " pass",
81
+ "",
82
+ ])
83
+
84
+ if not classes_emitted and not func_names:
85
+ # No public surface — emit a placeholder so the file is valid pytest
86
+ # and the human knows the stub generator ran but found nothing.
87
+ lines.append(f"# Auto-generated stub for {primary_module_name}.py.")
88
+ lines.append(
89
+ "# No public functions/classes detected — add tests manually."
90
+ )
91
+ lines.append("")
92
+
93
+ return "\n".join(lines)
94
+
95
+
96
+ @base_class
97
+ class PythonTestStubFile(PythonFile):
98
+ """A pytest stub generated as a sidecar of a Python module.
99
+
100
+ When used in a `sidecar_of` declaration as the `cls`, this class is
101
+ instantiated for each missing test file the framework needs to create.
102
+ `build_default_content()` is the hook `SidecarOfOption` wires into the
103
+ standard `default_content` flow — it reads the primary module via the
104
+ shared `cst_cache`, enumerates its public top-level functions and the
105
+ public methods of each top-level class, and emits a minimal pytest
106
+ skeleton (one `test_*` per public callable, wrapped in a
107
+ `TestPascalCase` class when the primary has classes).
108
+
109
+ Intentionally minimal:
110
+ - Generates `pass` bodies so the human fills them in.
111
+ - Skips private callables (`_*`) and dunders.
112
+ - Skips classes that look abstract (decorated `@abstract_class` or
113
+ prefixed `Abstract`) — they don't need direct tests.
114
+ """
115
+
116
+ def build_default_content(self) -> str:
117
+ primary = self.get_sidecar_primary()
118
+ primary_path = primary.get_path()
119
+
120
+ from wexample_filestate_python.utils.cst_cache import (
121
+ get_python_source_and_module,
122
+ )
123
+
124
+ _, module = get_python_source_and_module(primary)
125
+ return _render_stub(
126
+ primary_module_name=primary_path.stem,
127
+ module=module,
128
+ )
@@ -12,11 +12,16 @@ if TYPE_CHECKING:
12
12
 
13
13
 
14
14
  @lru_cache(maxsize=1)
15
- def _isort_black_config() -> Config:
16
- """Return a cached isort Config for the 'black' profile."""
15
+ def _isort_setup() -> tuple:
16
+ """Return cached (code_fn, Config) for isort with the 'black' profile.
17
+
18
+ Both objects are imported and constructed once; subsequent calls hit
19
+ the LRU cache, eliminating per-file import overhead for isort.code.
20
+ """
21
+ from isort import code as _code
17
22
  from isort.settings import Config
18
23
 
19
- return Config(profile="black")
24
+ return _code, Config(profile="black")
20
25
 
21
26
 
22
27
  @base_class
@@ -26,7 +31,6 @@ class SortImportsOption(AbstractPythonFileContentOption):
26
31
 
27
32
  def _apply_content_change(self, target: TargetFileOrDirectoryType) -> str:
28
33
  """Sort Python imports using isort."""
29
- from isort import code
30
-
34
+ _code, _config = _isort_setup()
31
35
  src = target.get_local_file().read()
32
- return code(src, config=_isort_black_config())
36
+ return _code(src, config=_config)
@@ -14,6 +14,12 @@ from wexample_helpers.decorator.base_class import base_class
14
14
  if TYPE_CHECKING:
15
15
  from wexample_filestate.const.types_state_items import TargetFileOrDirectoryType
16
16
 
17
+ # Module-level caches (safe per the @base_class note below; only instance-level
18
+ # annotations become attrs fields, not plain module variables).
19
+ _SCOPES: list[Scope] = [Scope.CONTENT]
20
+ _RAW_VALUE_ALLOWED_TYPE: Any = None
21
+ _ALLOWED_OPTIONS: list[type[AbstractConfigOption]] | None = None
22
+
17
23
 
18
24
  @base_class
19
25
  class PythonOption(OptionMixin, AbstractNestedConfigOption):
@@ -23,15 +29,18 @@ class PythonOption(OptionMixin, AbstractNestedConfigOption):
23
29
  # module-level constants or `functools.cache` on methods if needed.
24
30
  @classmethod
25
31
  def get_scopes(cls) -> list[Scope]:
26
- return [Scope.CONTENT]
32
+ return _SCOPES
27
33
 
28
34
  @staticmethod
29
35
  def get_raw_value_allowed_type() -> Any:
30
- from wexample_filestate_python.config_value.python_config_value import (
31
- PythonConfigValue,
32
- )
36
+ global _RAW_VALUE_ALLOWED_TYPE
37
+ if _RAW_VALUE_ALLOWED_TYPE is None:
38
+ from wexample_filestate_python.config_value.python_config_value import (
39
+ PythonConfigValue,
40
+ )
33
41
 
34
- return Union[list[str], dict, PythonConfigValue]
42
+ _RAW_VALUE_ALLOWED_TYPE = Union[list[str], dict, PythonConfigValue]
43
+ return _RAW_VALUE_ALLOWED_TYPE
35
44
 
36
45
  def create_required_operation(
37
46
  self, target: TargetFileOrDirectoryType, scopes: set[Scope]
@@ -39,6 +48,10 @@ class PythonOption(OptionMixin, AbstractNestedConfigOption):
39
48
  return self._create_child_required_operation(target=target, scopes=scopes)
40
49
 
41
50
  def get_allowed_options(self) -> list[type[AbstractConfigOption]]:
51
+ global _ALLOWED_OPTIONS
52
+ if _ALLOWED_OPTIONS is not None:
53
+ return _ALLOWED_OPTIONS
54
+
42
55
  # Import all the config options for each Python operation
43
56
  from wexample_filestate_python.option.python.add_future_annotations_option import (
44
57
  AddFutureAnnotationsOption,
@@ -105,7 +118,7 @@ class PythonOption(OptionMixin, AbstractNestedConfigOption):
105
118
  UnquoteAnnotationsOption,
106
119
  )
107
120
 
108
- return [
121
+ _ALLOWED_OPTIONS = [
109
122
  # filestate: python-iterable-sort
110
123
  AddFutureAnnotationsOption,
111
124
  AddReturnTypesOption,
@@ -130,10 +143,11 @@ class PythonOption(OptionMixin, AbstractNestedConfigOption):
130
143
  SortImportsOption,
131
144
  UnquoteAnnotationsOption,
132
145
  ]
146
+ return _ALLOWED_OPTIONS
133
147
 
134
148
  def set_value(self, raw_value: Any) -> None:
135
149
  # Convert list form to dict form for consistency
136
150
  if isinstance(raw_value, list):
137
- raw_value = {option_name: True for option_name in raw_value}
151
+ raw_value = dict.fromkeys(raw_value, True)
138
152
 
139
153
  super().set_value(raw_value=raw_value)
@@ -13,15 +13,14 @@ if TYPE_CHECKING:
13
13
 
14
14
 
15
15
  class PythonOptionsProvider(AbstractOptionsProvider):
16
- # Do NOT cache the returned list in a `cls._options_cache` class attribute.
17
- # The cache is inherited via MRO, so a write from the base shadows what
18
- # subclasses should return. Roadmap the optim instead.
19
16
  @classmethod
20
17
  def get_options(cls) -> list[type[AbstractConfigOption]]:
21
- from wexample_filestate_python.option.python_option import (
22
- PythonOption,
23
- )
18
+ # Use `cls.__dict__` (not `hasattr`) so each class in the hierarchy
19
+ # maintains its own cache without the MRO inheritance side-effect.
20
+ if "_options_cache" not in cls.__dict__:
21
+ from wexample_filestate_python.option.python_option import (
22
+ PythonOption,
23
+ )
24
24
 
25
- return [
26
- PythonOption,
27
- ]
25
+ cls._options_cache: list[type[AbstractConfigOption]] = [PythonOption]
26
+ return cls._options_cache
@@ -33,11 +33,7 @@ def _is_attrs_decorator(decorator: cst.Decorator) -> bool:
33
33
  base_decorator.value, cst.Name
34
34
  ):
35
35
  module_name = base_decorator.value.value
36
- attr_name = (
37
- base_decorator.attr.value
38
- if isinstance(base_decorator.attr, cst.Name)
39
- else None
40
- )
36
+ attr_name = base_decorator.attr.value
41
37
  return (module_name == "attrs" and attr_name in ("define", "frozen")) or (
42
38
  module_name == "attr" and attr_name == "s"
43
39
  )
@@ -190,6 +190,8 @@ def _is_class_property(node: cst.CSTNode) -> bool:
190
190
 
191
191
  def _is_dataclass(class_node: cst.ClassDef) -> bool:
192
192
  """Check if a class has @dataclass decorator."""
193
+ if not class_node.decorators:
194
+ return False
193
195
  for decorator in class_node.decorators:
194
196
  dec = decorator.decorator
195
197
  if isinstance(dec, cst.Name) and dec.value == "dataclass":
@@ -214,8 +216,7 @@ def _is_lowercase_property(node: cst.CSTNode) -> bool:
214
216
  if _is_class_property(node):
215
217
  assign = node.body[0]
216
218
  target = assign.targets[0].target
217
- if isinstance(target, cst.Name):
218
- return target.value.islower()
219
+ return target.value.islower() # _is_class_property already ensures cst.Name
219
220
  return False
220
221
 
221
222
 
@@ -264,8 +265,7 @@ def _is_uppercase_property(node: cst.CSTNode) -> bool:
264
265
  if _is_class_property(node):
265
266
  assign = node.body[0]
266
267
  target = assign.targets[0].target
267
- if isinstance(target, cst.Name):
268
- return target.value.isupper()
268
+ return target.value.isupper() # _is_class_property already ensures cst.Name
269
269
  return False
270
270
 
271
271
 
@@ -92,7 +92,7 @@ def reorder_class_methods(classdef: cst.ClassDef) -> cst.ClassDef:
92
92
  for m in dunders:
93
93
  (unknown if m["order"][0] == 999 else known).append(m)
94
94
  dunder_ordered = [m["node"] for m in sorted(known, key=lambda m: m["order"])]
95
- dunder_ordered += [m["node"] for m in sorted(unknown, key=lambda m: _sort_key_alpha(m["name"]))]
95
+ dunder_ordered += [m["node"] for m in sorted(unknown, key=_KEY_BY_NAME)]
96
96
 
97
97
  # Sort classmethods/staticmethods and instances: public first then private
98
98
  classmethods_ordered = _sort_by_visibility(classmethods)
@@ -188,6 +188,11 @@ def _is_dunder(name: str) -> bool:
188
188
  def _is_private(name: str) -> bool:
189
189
  return name.startswith("_") and not (name.startswith("__") and name.endswith("__"))
190
190
 
191
+ # Hoisted sort-key; avoids creating a new lambda/closure object on every
192
+ # call to _sort_by_visibility and reorder_class_methods.
193
+ def _KEY_BY_NAME(m: dict) -> tuple:
194
+ return _sort_key_alpha(m["name"])
195
+
191
196
 
192
197
  def _prop_group_to_nodes(g: dict[str, cst.FunctionDef | None]) -> list[cst.FunctionDef]:
193
198
  return [g[k] for k in _PROP_KINDS if g[k] is not None] # type: ignore
@@ -199,11 +204,12 @@ def _sort_by_visibility(items: list[dict]) -> list[cst.FunctionDef]:
199
204
  priv: list[dict] = []
200
205
  for i in items:
201
206
  (priv if _is_private(i["name"]) else pub).append(i)
202
- pub_sorted = [i["node"] for i in sorted(pub, key=lambda x: _sort_key_alpha(x["name"]))]
203
- priv_sorted = [i["node"] for i in sorted(priv, key=lambda x: _sort_key_alpha(x["name"]))]
207
+ pub_sorted = [i["node"] for i in sorted(pub, key=_KEY_BY_NAME)]
208
+ priv_sorted = [i["node"] for i in sorted(priv, key=_KEY_BY_NAME)]
204
209
  return pub_sorted + priv_sorted
205
210
 
206
211
 
207
212
  def _sort_key_alpha(name: str) -> tuple:
208
- # Case-insensitive, underscore after letters
209
- return (name.lstrip("_").lower(), name.startswith("_"))
213
+ # Case-insensitive, underscore after letters.
214
+ # name[0] == "_" avoids method-dispatch overhead; safe for non-empty identifiers.
215
+ return (name.lstrip("_").lower(), name[0] == "_")
@@ -152,15 +152,9 @@ def reorder_module_functions(module: cst.Module) -> cst.Module:
152
152
  # Build function nodes preserving each group's comments/spacing on first element
153
153
  rebuilt_functions: list[cst.CSTNode] = []
154
154
  for g in sorted_groups:
155
- # Preserve leading_lines of the original first node in the group
156
- original_first_leading = g.nodes[0].leading_lines
157
- for k, fn in enumerate(g.nodes):
158
- if k == 0:
159
- rebuilt_functions.append(
160
- fn.with_changes(leading_lines=original_first_leading)
161
- )
162
- else:
163
- rebuilt_functions.append(fn.with_changes(leading_lines=[]))
155
+ # First node keeps its original leading_lines (no-op with_changes avoided)
156
+ rebuilt_functions.append(g.nodes[0])
157
+ rebuilt_functions.extend(fn.with_changes(leading_lines=[]) for fn in g.nodes[1:])
164
158
 
165
159
  # Insert functions as a contiguous block
166
160
  new_body[insert_at:insert_at] = rebuilt_functions
@@ -35,9 +35,10 @@ def reorder_flagged_iterables(src: str) -> str:
35
35
  groups = _split_into_groups(block)
36
36
  # Compute keys once; reuse for both no-op check and sort order
37
37
  keys = [_group_key(g) for g in groups]
38
- sorted_keys = sorted(keys)
39
38
 
40
- if sorted_keys == keys:
39
+ # O(n) pairwise check instead of O(n log n) sorted(); short-circuits on first
40
+ # out-of-order pair so the already-sorted case is also fast.
41
+ if len(keys) < 2 or all(keys[i] <= keys[i + 1] for i in range(len(keys) - 1)):
41
42
  continue
42
43
 
43
44
  order = sorted(range(len(groups)), key=lambda i: keys[i])
@@ -110,9 +111,9 @@ def _split_into_groups(block_lines: list[str]) -> list[list[str]]:
110
111
  if ln.lstrip().startswith("#"):
111
112
  pending_comments.append(ln)
112
113
  continue
113
- # item line
114
- group = pending_comments + [ln]
115
- groups.append(group)
114
+ # item line: append in-place to avoid pending_comments + [ln] allocation
115
+ pending_comments.append(ln)
116
+ groups.append(pending_comments)
116
117
  pending_comments = []
117
118
  # Any trailing comments without item are ignored for sorting and left in place
118
119
  # (shouldn't occur in expected usage). If present, attach to last group to preserve.
@@ -0,0 +1,16 @@
1
+ from __future__ import annotations
2
+
3
+ def test_package_get_dependencies() -> None:
4
+ pass
5
+
6
+ def test_package_get_info() -> None:
7
+ pass
8
+
9
+ def test_package_normalize_name() -> None:
10
+ pass
11
+
12
+ def test_package_parse_setup() -> None:
13
+ pass
14
+
15
+ def test_package_parse_toml() -> None:
16
+ pass
@@ -0,0 +1,19 @@
1
+ from __future__ import annotations
2
+
3
+ def test_toml_ensure_array() -> None:
4
+ pass
5
+
6
+ def test_toml_ensure_array_multiline() -> None:
7
+ pass
8
+
9
+ def test_toml_ensure_table() -> None:
10
+ pass
11
+
12
+ def test_toml_get_string_value() -> None:
13
+ pass
14
+
15
+ def test_toml_set_array_multiline() -> None:
16
+ pass
17
+
18
+ def test_toml_sort_string_array() -> None:
19
+ pass