wexample-filestate-python 0.0.40__tar.gz → 0.0.41__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 (33) hide show
  1. {wexample_filestate_python-0.0.40 → wexample_filestate_python-0.0.41}/PKG-INFO +5 -22
  2. {wexample_filestate_python-0.0.40 → wexample_filestate_python-0.0.41}/README.md +2 -11
  3. {wexample_filestate_python-0.0.40 → wexample_filestate_python-0.0.41}/pyproject.toml +4 -12
  4. wexample_filestate_python-0.0.41/src/wexample_filestate_python/common/pipy_gateway.py +18 -0
  5. wexample_filestate_python-0.0.41/src/wexample_filestate_python/config_option/python_config_option.py +20 -0
  6. wexample_filestate_python-0.0.41/src/wexample_filestate_python/const/__init__.py +0 -0
  7. wexample_filestate_python-0.0.41/src/wexample_filestate_python/const/name_pattern.py +3 -0
  8. wexample_filestate_python-0.0.41/src/wexample_filestate_python/file/__init__.py +0 -0
  9. wexample_filestate_python-0.0.41/src/wexample_filestate_python/file/python_file.py +12 -0
  10. wexample_filestate_python-0.0.41/src/wexample_filestate_python/helpers/__init__.py +0 -0
  11. wexample_filestate_python-0.0.41/src/wexample_filestate_python/helpers/package.py +136 -0
  12. wexample_filestate_python-0.0.41/src/wexample_filestate_python/helpers/toml.py +105 -0
  13. wexample_filestate_python-0.0.41/src/wexample_filestate_python/operation/__init__.py +0 -0
  14. wexample_filestate_python-0.0.41/src/wexample_filestate_python/operation/abstract_python_file_operation.py +39 -0
  15. wexample_filestate_python-0.0.41/src/wexample_filestate_python/operation/python_add_future_annotations_operation.py +101 -0
  16. wexample_filestate_python-0.0.41/src/wexample_filestate_python/operation/python_add_return_types_operation.py +283 -0
  17. wexample_filestate_python-0.0.41/src/wexample_filestate_python/operation/python_format_operation.py +49 -0
  18. wexample_filestate_python-0.0.41/src/wexample_filestate_python/operation/python_fstringify_operation.py +42 -0
  19. wexample_filestate_python-0.0.41/src/wexample_filestate_python/operation/python_modernize_typing_operation.py +41 -0
  20. wexample_filestate_python-0.0.41/src/wexample_filestate_python/operation/python_remove_unused_imports_operation.py +42 -0
  21. wexample_filestate_python-0.0.41/src/wexample_filestate_python/operation/python_sort_imports_operation.py +39 -0
  22. wexample_filestate_python-0.0.41/src/wexample_filestate_python/operation/python_unquote_annotations_operation.py +98 -0
  23. wexample_filestate_python-0.0.41/src/wexample_filestate_python/operations_provider/__init__.py +0 -0
  24. wexample_filestate_python-0.0.41/src/wexample_filestate_python/operations_provider/python_operations_provider.py +50 -0
  25. wexample_filestate_python-0.0.41/src/wexample_filestate_python/options_provider/__init__.py +0 -0
  26. wexample_filestate_python-0.0.41/src/wexample_filestate_python/options_provider/python_options_provider.py +23 -0
  27. wexample_filestate_python-0.0.41/src/wexample_filestate_python/py.typed +0 -0
  28. wexample_filestate_python-0.0.41/src/wexample_filestate_python/workdir/__init__.py +0 -0
  29. wexample_filestate_python-0.0.41/tests/tests/__init__.py +0 -0
  30. wexample_filestate_python-0.0.41/tests/wexample_tests/__init__.py +0 -0
  31. {wexample_filestate_python-0.0.40/tests/tests → wexample_filestate_python-0.0.41/src/wexample_filestate_python}/__init__.py +0 -0
  32. {wexample_filestate_python-0.0.40/tests/wexample_tests → wexample_filestate_python-0.0.41/src/wexample_filestate_python/common}/__init__.py +0 -0
  33. /wexample_filestate_python-0.0.40/src/wexample_filestate_python/py.typed → /wexample_filestate_python-0.0.41/src/wexample_filestate_python/config_option/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: wexample-filestate-python
3
- Version: 0.0.40
3
+ Version: 0.0.41
4
4
  Summary: Helpers for Python.
5
5
  Author-Email: weeger <contact@wexample.com>
6
6
  License: MIT
@@ -9,17 +9,9 @@ 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: autoflake
13
- Requires-Dist: black
14
- Requires-Dist: flynt
15
- Requires-Dist: isort
16
- Requires-Dist: networkx
17
- Requires-Dist: packaging
18
12
  Requires-Dist: pydantic<3,>=2
19
- Requires-Dist: python-dotenv
20
- Requires-Dist: pyupgrade
21
- Requires-Dist: tomlkit
22
- Requires-Dist: wexample-filestate-git==0.0.38
13
+ Requires-Dist: wexample-filestate==0.0.51
14
+ Requires-Dist: wexample-helpers-api==0.0.32
23
15
  Provides-Extra: dev
24
16
  Requires-Dist: pytest; extra == "dev"
25
17
  Description-Content-Type: text/markdown
@@ -28,7 +20,7 @@ Description-Content-Type: text/markdown
28
20
 
29
21
  Helpers for Python.
30
22
 
31
- Version: 0.0.40
23
+ Version: 0.0.41
32
24
 
33
25
  ## Requirements
34
26
 
@@ -36,17 +28,8 @@ Version: 0.0.40
36
28
 
37
29
  ## Dependencies
38
30
 
39
- - autoflake
40
- - black
41
- - flynt
42
- - isort
43
- - networkx
44
- - packaging
45
31
  - pydantic>=2,<3
46
- - python-dotenv
47
- - pyupgrade
48
- - tomlkit
49
- - wexample-filestate-git==0.0.38
32
+ - wexample-filestate==0.0.51
50
33
 
51
34
  ## Installation
52
35
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  Helpers for Python.
4
4
 
5
- Version: 0.0.40
5
+ Version: 0.0.41
6
6
 
7
7
  ## Requirements
8
8
 
@@ -10,17 +10,8 @@ Version: 0.0.40
10
10
 
11
11
  ## Dependencies
12
12
 
13
- - autoflake
14
- - black
15
- - flynt
16
- - isort
17
- - networkx
18
- - packaging
19
13
  - pydantic>=2,<3
20
- - python-dotenv
21
- - pyupgrade
22
- - tomlkit
23
- - wexample-filestate-git==0.0.38
14
+ - wexample-filestate==0.0.51
24
15
 
25
16
  ## Installation
26
17
 
@@ -6,7 +6,7 @@ build-backend = "pdm.backend"
6
6
 
7
7
  [project]
8
8
  name = "wexample-filestate-python"
9
- version = "0.0.40"
9
+ version = "0.0.41"
10
10
  description = "Helpers for Python."
11
11
  authors = [
12
12
  { name = "weeger", email = "contact@wexample.com" },
@@ -18,17 +18,9 @@ classifiers = [
18
18
  "Operating System :: OS Independent",
19
19
  ]
20
20
  dependencies = [
21
- "autoflake",
22
- "black",
23
- "flynt",
24
- "isort",
25
- "networkx",
26
- "packaging",
27
21
  "pydantic>=2,<3",
28
- "python-dotenv",
29
- "pyupgrade",
30
- "tomlkit",
31
- "wexample-filestate-git==0.0.38",
22
+ "wexample-filestate==0.0.51",
23
+ "wexample-helpers-api==0.0.32",
32
24
  ]
33
25
 
34
26
  [project.readme]
@@ -51,7 +43,7 @@ distribution = true
51
43
 
52
44
  [tool.pdm.build]
53
45
  includes = [
54
- "src/wexample_filestate_python/py.typed",
46
+ "src/wexample_filestate_python/*",
55
47
  ]
56
48
  package-dir = "src"
57
49
  packages = [
@@ -0,0 +1,18 @@
1
+ from __future__ import annotations
2
+
3
+ from pydantic import Field
4
+ from wexample_helpers_api.common.abstract_gateway import AbstractGateway
5
+
6
+
7
+ class PipyGateway(AbstractGateway):
8
+ base_url: str | None = Field(
9
+ default="https://pypi.org/", description="Base Pipy API URL"
10
+ )
11
+
12
+ def package_release_exists(self, package_name: str, version: str) -> bool:
13
+ response = self.make_request(f"pypi/{package_name}/json")
14
+ # Package exists
15
+ if response.status_code == 200:
16
+ return bool(response.json().get("releases", {}).get(version))
17
+
18
+ return False
@@ -0,0 +1,20 @@
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
+ OPTION_NAME_FORMAT: ClassVar[str] = "format"
8
+ OPTION_NAME_SORT_IMPORTS: ClassVar[str] = "sort_imports"
9
+ OPTION_NAME_ADD_RETURN_TYPES: ClassVar[str] = "add_return_types"
10
+ OPTION_NAME_MODERNIZE_TYPING: ClassVar[str] = "modernize_typing"
11
+ OPTION_NAME_FSTRINGIFY: ClassVar[str] = "fstringify"
12
+ OPTION_NAME_REMOVE_UNUSED: ClassVar[str] = "remove_unused"
13
+ # New preferred option name to add `from __future__ import annotations`
14
+ OPTION_NAME_ADD_FUTURE_ANNOTATIONS: ClassVar[str] = "add_future_annotations"
15
+ # New policy: unquote annotations (remove string annotations)
16
+ OPTION_NAME_UNQUOTE_ANNOTATIONS: ClassVar[str] = "unquote_annotations"
17
+
18
+ @staticmethod
19
+ def get_raw_value_allowed_type() -> Any:
20
+ return list[str]
@@ -0,0 +1,3 @@
1
+ from __future__ import annotations
2
+
3
+ NAME_PATTERN_PYTHON_NOT_PYCACHE = "^(?!__pycache__$).+$"
@@ -0,0 +1,12 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import ClassVar
4
+
5
+ from wexample_filestate.item.item_target_file import ItemTargetFile
6
+
7
+
8
+ class PythonFile(ItemTargetFile):
9
+ EXTENSION_ENV: ClassVar[str] = "py"
10
+
11
+ def _expected_file_name_extension(self) -> str | None:
12
+ return PythonFile.EXTENSION_ENV
@@ -0,0 +1,136 @@
1
+ from __future__ import annotations
2
+
3
+ import ast
4
+ from pathlib import Path
5
+
6
+ import tomli
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)
75
+
76
+
77
+ def package_get_dependencies(root_dir: str | Path) -> dict[str, set[str]]:
78
+ """
79
+ Get dependencies between packages in a directory.
80
+ """
81
+ packages_root = Path(root_dir)
82
+ if not packages_root.exists() or not packages_root.is_dir():
83
+ raise ValueError(f"Error: {packages_root} does not exist or is not a directory")
84
+
85
+ dependencies = {}
86
+
87
+ # First pass: collect all local packages
88
+ for package_dir in packages_root.iterdir():
89
+ if not package_dir.is_dir():
90
+ continue
91
+
92
+ package_info = package_get_info(package_dir)
93
+ if package_info:
94
+ name, _ = package_info
95
+ dependencies[name] = set()
96
+
97
+ # Second pass: analyze dependencies
98
+ for package_dir in packages_root.iterdir():
99
+ if not package_dir.is_dir():
100
+ continue
101
+
102
+ package_info = package_get_info(package_dir)
103
+ if package_info:
104
+ name, deps = package_info
105
+ if name in dependencies:
106
+ # Only keep dependencies that are local packages
107
+ dependencies[name] = {dep for dep in deps if dep in dependencies}
108
+
109
+ return dependencies
110
+
111
+
112
+ def package_list_sorted(root_dir: str | Path) -> list[str]:
113
+ """
114
+ Get a list of package names sorted by dependency order.
115
+ """
116
+ from wexample_filestate.helpers.dependencies import dependencies_sort
117
+
118
+ dependencies = package_get_dependencies(root_dir)
119
+ if not dependencies:
120
+ return []
121
+
122
+ # Convert dependencies dict to list for sorting
123
+ packages = list(dependencies.keys())
124
+
125
+ def get_deps(pkg: str) -> set[str]:
126
+ return dependencies[pkg]
127
+
128
+ return dependencies_sort(packages, get_deps)
129
+
130
+
131
+ def package_normalize_name(val: str) -> str:
132
+ import re as _re
133
+
134
+ # strip extras, versions, markers
135
+ base = _re.split(r"[\s<>=!~;\[]", val, maxsplit=1)[0]
136
+ return base.strip().lower()
@@ -0,0 +1,105 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from tomlkit import array as _tk_array
6
+ from tomlkit import table as _tk_table
7
+ from tomlkit.items import Array, String
8
+
9
+
10
+ def toml_sort_string_array(arr: Any) -> bool:
11
+ """
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.
16
+ """
17
+ # Validate array type
18
+ if not isinstance(arr, Array):
19
+ return False
20
+
21
+ items = list(arr)
22
+ if not items or not all(isinstance(i, String) for i in items):
23
+ return False
24
+
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
+
31
+ if items == sorted_items:
32
+ return False
33
+
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
43
+
44
+
45
+ def toml_ensure_table(doc: Any, path: list[str]) -> tuple[Any, bool]:
46
+ """
47
+ Ensure a nested TOML table exists and return (table, changed).
48
+ Path example: ["tool", "pdm", "build"]. Uses tomlkit.table() for missing parts.
49
+ """
50
+ if not isinstance(path, list) or not path:
51
+ raise ValueError("path must be a non-empty list of keys")
52
+
53
+ changed = False
54
+ current = doc
55
+ for key in path:
56
+ tbl = current.get(key) if isinstance(current, dict) else None
57
+ if not tbl or not isinstance(tbl, dict):
58
+ tbl = _tk_table()
59
+ current[key] = tbl
60
+ changed = True
61
+ current = tbl
62
+ return current, changed
63
+
64
+
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
+ def toml_get_string_value(item: Any) -> str:
79
+ """Return the string content of a tomlkit String or generic item as str."""
80
+ if isinstance(item, String):
81
+ return item.value
82
+ return str(item)
83
+
84
+
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
+ def toml_set_array_multiline(tbl: Any, key: str, values: list[Any]) -> Array:
98
+ """
99
+ Replace tbl[key] with a tomlkit array built from values and set multiline(True).
100
+ Returns the created Array instance.
101
+ """
102
+ arr = _tk_array(values)
103
+ arr.multiline(True)
104
+ tbl[key] = arr
105
+ return arr
@@ -0,0 +1,39 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ from wexample_config.config_option.abstract_config_option import AbstractConfigOption
6
+ from wexample_filestate.enum.scopes import Scope
7
+ from wexample_filestate.operation.abstract_existing_file_operation import (
8
+ AbstractExistingFileOperation,
9
+ )
10
+ from wexample_filestate_python.config_option.python_config_option import (
11
+ PythonConfigOption,
12
+ )
13
+
14
+ if TYPE_CHECKING:
15
+ pass
16
+
17
+
18
+ class AbstractPythonFileOperation(AbstractExistingFileOperation):
19
+ @classmethod
20
+ def get_scope(cls) -> Scope:
21
+ return Scope.CONTENT
22
+
23
+ @classmethod
24
+ def get_option_name(cls) -> str:
25
+ raise NotImplementedError
26
+
27
+ def applicable_for_option(self, option: AbstractConfigOption) -> bool:
28
+ """Generic applicability for Python file transforms controlled by a single option name."""
29
+ # Option type
30
+ if not isinstance(option, PythonConfigOption):
31
+ return False
32
+
33
+ # Option value must contain our specific option name
34
+ value = option.get_value()
35
+ if value is None or not value.has_item_in_list(self.get_option_name()):
36
+ return False
37
+
38
+ # Delegate change detection to the base helper
39
+ return self.source_need_change(self.target)
@@ -0,0 +1,101 @@
1
+ from wexample_filestate.const.types_state_items import TargetFileOrDirectoryType
2
+
3
+ from .abstract_python_file_operation import AbstractPythonFileOperation
4
+
5
+
6
+ class PythonAddFutureAnnotationsOperation(AbstractPythonFileOperation):
7
+ """Ensure `from __future__ import annotations` is present at module top.
8
+
9
+ Triggered by: {"python": ["add_future_annotations"]}
10
+
11
+ Note: This operation historically removed __future__ imports; we repurpose it
12
+ to add the annotations future consistently across the codebase, per new policy.
13
+ """
14
+
15
+ @classmethod
16
+ def get_option_name(cls) -> str:
17
+ from wexample_filestate_python.config_option.python_config_option import (
18
+ PythonConfigOption,
19
+ )
20
+
21
+ return PythonConfigOption.OPTION_NAME_ADD_FUTURE_ANNOTATIONS
22
+
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
+ @classmethod
33
+ def preview_source_change(cls, target: TargetFileOrDirectoryType) -> str | None:
34
+ """Return source with a `from __future__ import annotations` inserted
35
+ in the correct place if it is not already present.
36
+
37
+ Rules:
38
+ - Keep shebang on first line intact.
39
+ - Keep encoding cookie (PEP 263) within first two lines intact.
40
+ - If module docstring exists, insert after the docstring statement.
41
+ - Otherwise, insert after shebang/encoding header block.
42
+ - If the future import already exists anywhere, return the original source.
43
+ """
44
+ src = cls._read_current_str_or_fail(target)
45
+
46
+ # Fast path: already present
47
+ if "from __future__ import annotations" in src:
48
+ return src
49
+
50
+ import ast
51
+ import re
52
+
53
+ lines = src.splitlines(keepends=True)
54
+
55
+ # Detect shebang and encoding cookie positions
56
+ idx = 0
57
+ if lines and lines[0].startswith("#!"):
58
+ idx = 1
59
+ # Encoding cookie can be on first or second line (after shebang)
60
+ enc_re = re.compile(r"^#.*coding[:=]\\s*([-_.a-zA-Z0-9]+)")
61
+ for i in range(idx, min(idx + 2, len(lines))):
62
+ if enc_re.match(lines[i]):
63
+ idx = i + 1
64
+
65
+ # Parse to find module docstring span
66
+ try:
67
+ tree = ast.parse(src)
68
+ except SyntaxError:
69
+ # If parsing fails, be conservative: insert after header block
70
+ insert_at = idx
71
+ else:
72
+ body = getattr(tree, "body", [])
73
+ if (
74
+ body
75
+ and isinstance(body[0], ast.Expr)
76
+ and isinstance(getattr(body[0], "value", None), ast.Constant)
77
+ and isinstance(body[0].value.value, str)
78
+ ):
79
+ # Module docstring present; insert after its end_lineno
80
+ doc_end = getattr(body[0], "end_lineno", body[0].lineno) # 1-based
81
+ insert_at = max(idx, doc_end) # line number where next stmt starts
82
+ else:
83
+ insert_at = idx
84
+
85
+ # lines is 0-based; insert_at is 1-based line count from AST
86
+ insert_index = max(0, min(len(lines), insert_at))
87
+
88
+ # Ensure there is a newline after the inserted import if needed
89
+ future_line = "from __future__ import annotations\n"
90
+
91
+ # Avoid inserting duplicate blank lines: if previous line is not blank and not newline, keep
92
+ # If there are existing future imports right after docstring, we can insert alongside them; no special handling needed
93
+
94
+ lines.insert(insert_index, future_line)
95
+ # If there isn't a blank line after the future import and next line is not blank, add one for readability
96
+ j = insert_index + 1
97
+ if j < len(lines):
98
+ if lines[j].strip() != "":
99
+ lines.insert(j, "\n")
100
+
101
+ return "".join(lines)
@@ -0,0 +1,283 @@
1
+ from __future__ import annotations
2
+
3
+ from wexample_filestate.const.types_state_items import TargetFileOrDirectoryType
4
+
5
+ from .abstract_python_file_operation import AbstractPythonFileOperation
6
+
7
+
8
+ class PythonAddReturnTypesOperation(AbstractPythonFileOperation):
9
+ """Annotate return types for functions lacking them when trivially inferable.
10
+
11
+ Phase 1: annotate -> None, -> bool, -> str, -> int, -> float when all return
12
+ statements in a function agree on one of these literal types.
13
+
14
+ Triggered by config: { "python": ["add_return_types"] }.
15
+ """
16
+
17
+ @classmethod
18
+ def get_option_name(cls) -> str:
19
+ from wexample_filestate_python.config_option.python_config_option import (
20
+ PythonConfigOption,
21
+ )
22
+
23
+ return PythonConfigOption.OPTION_NAME_ADD_RETURN_TYPES
24
+
25
+ @classmethod
26
+ def preview_source_change(cls, target: TargetFileOrDirectoryType) -> str | None:
27
+ """Add a return annotation to def lines where a simple literal type is inferable.
28
+
29
+ Logic is inlined here (previously in helpers.source.source_annotate_simple_returns).
30
+ """
31
+ import libcst as cst
32
+
33
+ src = cls._read_current_str_or_fail(target)
34
+
35
+ # We implement type inference and rewriting using LibCST to ensure
36
+ # robust, formatting-preserving edits. We extend inference to:
37
+ # - simple literals (None, bool, str, int, float)
38
+ # - simple class instantiation returns (MyClass() or via a variable assigned once)
39
+
40
+ def _infer_literal_type(expr: cst.BaseExpression) -> str | None:
41
+ if isinstance(expr, cst.Name):
42
+ if expr.value in ("True", "False"):
43
+ return "bool"
44
+ if expr.value == "None":
45
+ return "None"
46
+ return None
47
+ if isinstance(expr, cst.SimpleString):
48
+ return "str"
49
+ if isinstance(expr, cst.Integer):
50
+ return "int"
51
+ if isinstance(expr, cst.Float):
52
+ return "float"
53
+ return None
54
+
55
+ class _ReturnCollector(cst.CSTVisitor):
56
+ def __init__(self) -> None:
57
+ self.returns: list[cst.Return] = []
58
+
59
+ # Do not descend into nested scopes that could have their own returns
60
+ def visit_FunctionDef(self, node: cst.FunctionDef) -> bool: # type: ignore[override]
61
+ return False
62
+
63
+ def visit_AsyncFunctionDef(self, node: cst.AsyncFunctionDef) -> bool: # type: ignore[override]
64
+ return False
65
+
66
+ def visit_Lambda(self, node: cst.Lambda) -> bool: # type: ignore[override]
67
+ return False
68
+
69
+ def visit_ClassDef(self, node: cst.ClassDef) -> bool: # type: ignore[override]
70
+ return False
71
+
72
+ def visit_Return(self, node: cst.Return) -> None: # type: ignore[override]
73
+ self.returns.append(node)
74
+
75
+ class _KnownTypesCollector(cst.CSTVisitor):
76
+ """Collect known simple type names from class defs and from-imports.
77
+
78
+ We stay conservative: only names directly available in the module namespace
79
+ (class definitions and `from x import Name [as Alias]`).
80
+ """
81
+
82
+ def __init__(self) -> None:
83
+ self.known: set[str] = set()
84
+
85
+ def visit_ClassDef(self, node: cst.ClassDef) -> None: # type: ignore[override]
86
+ # Record class name as a potential return type
87
+ self.known.add(node.name.value)
88
+
89
+ def visit_ImportFrom(self, node: cst.ImportFrom) -> None: # type: ignore[override]
90
+ # from pkg import A as B -> record B (or A if no alias)
91
+ for n in node.names:
92
+ if isinstance(n, cst.ImportAlias):
93
+ asname = n.asname.name.value if n.asname else None
94
+ name = n.name.value if isinstance(n.name, cst.Name) else None
95
+ if name:
96
+ self.known.add(asname or name)
97
+
98
+ class _FunctionAssignCollector(cst.CSTVisitor):
99
+ """Collect simple var -> class-call assignments in a function body.
100
+
101
+ Only records the first simple assignment `x = MyClass(...)` where x is a Name
102
+ and call target resolves to a known type. If a variable is assigned multiple
103
+ times to different types, it is discarded.
104
+ """
105
+
106
+ def __init__(self, known_types: set[str]) -> None:
107
+ self.known_types = known_types
108
+ self.var_type: dict[str, str] = {}
109
+ self.discarded: set[str] = set()
110
+
111
+ def _infer_call_type(self, call: cst.Call) -> str | None:
112
+ func = call.func
113
+ if isinstance(func, cst.Name):
114
+ # Only keep conservative matches: known type names
115
+ if func.value in self.known_types and func.value[:1].isupper():
116
+ return func.value
117
+ elif isinstance(func, cst.Attribute):
118
+ # module.MyClass(...) -> infer MyClass if it's a known type
119
+ if isinstance(func.attr, cst.Name):
120
+ attr_name = func.attr.value
121
+ if attr_name in self.known_types and attr_name[:1].isupper():
122
+ return attr_name
123
+ return None
124
+
125
+ def _record_assignment(
126
+ self, target: cst.BaseExpression, value: cst.BaseExpression
127
+ ) -> None:
128
+ if not isinstance(target, cst.Name):
129
+ return
130
+ var = target.value
131
+ if var in self.discarded:
132
+ return
133
+ if not isinstance(value, cst.Call):
134
+ return
135
+ rtype = self._infer_call_type(value)
136
+ if rtype is None:
137
+ return
138
+ existing = self.var_type.get(var)
139
+ if existing is None:
140
+ self.var_type[var] = rtype
141
+ elif existing != rtype:
142
+ # conflicting assignments -> discard
143
+ self.discarded.add(var)
144
+ self.var_type.pop(var, None)
145
+
146
+ # Stop at nested scopes
147
+ def visit_FunctionDef(self, node: cst.FunctionDef) -> bool: # type: ignore[override]
148
+ return False
149
+
150
+ def visit_AsyncFunctionDef(self, node: cst.AsyncFunctionDef) -> bool: # type: ignore[override]
151
+ return False
152
+
153
+ def visit_ClassDef(self, node: cst.ClassDef) -> bool: # type: ignore[override]
154
+ return False
155
+
156
+ def visit_Lambda(self, node: cst.Lambda) -> bool: # type: ignore[override]
157
+ return False
158
+
159
+ def visit_Assign(self, node: cst.Assign) -> None: # type: ignore[override]
160
+ # Handle simple form: a = Call(...)
161
+ if len(node.targets) != 1:
162
+ return
163
+ target = node.targets[0].target
164
+ self._record_assignment(target, node.value)
165
+
166
+ class AddReturnTypesTransformer(cst.CSTTransformer):
167
+ def __init__(self, known_types: set[str]) -> None:
168
+ super().__init__()
169
+ self.known_types = known_types
170
+
171
+ def _infer_return_expr_type(
172
+ self, expr: cst.BaseExpression, var_types: dict[str, str]
173
+ ) -> str | None:
174
+ # Literal simple types
175
+ lit = _infer_literal_type(expr)
176
+ if lit is not None:
177
+ return lit
178
+
179
+ # Call to a known class
180
+ if isinstance(expr, cst.Call):
181
+ func = expr.func
182
+ if (
183
+ isinstance(func, cst.Name)
184
+ and func.value in self.known_types
185
+ and func.value[:1].isupper()
186
+ ):
187
+ return func.value
188
+ if isinstance(func, cst.Attribute) and isinstance(
189
+ func.attr, cst.Name
190
+ ):
191
+ attr_name = func.attr.value
192
+ if attr_name in self.known_types and attr_name[:1].isupper():
193
+ return attr_name
194
+ return None
195
+
196
+ # Variable referring to a previously inferred var type
197
+ if isinstance(expr, cst.Name):
198
+ return var_types.get(expr.value)
199
+
200
+ return None
201
+
202
+ def _infer_for_function(self, func_node: cst.BaseFunctionDef) -> str | None:
203
+ # Collect returns in the function body (non-nested)
204
+ collector = _ReturnCollector()
205
+ # Visit only the immediate body of the function
206
+ if isinstance(func_node, cst.FunctionDef):
207
+ func_node.body.visit(collector)
208
+ elif isinstance(func_node, cst.AsyncFunctionDef):
209
+ func_node.body.visit(collector)
210
+ else:
211
+ return None
212
+ # If no return statements -> None
213
+ if not collector.returns:
214
+ return "None"
215
+
216
+ # Build a simple assignment map within the function
217
+ fac = _FunctionAssignCollector(self.known_types)
218
+ if isinstance(func_node, cst.FunctionDef):
219
+ func_node.body.visit(fac)
220
+ elif isinstance(func_node, cst.AsyncFunctionDef):
221
+ func_node.body.visit(fac)
222
+
223
+ kinds: set[str] = set()
224
+ for r in collector.returns:
225
+ if r.value is None:
226
+ kinds.add("None")
227
+ continue
228
+ inferred = self._infer_return_expr_type(r.value, fac.var_type)
229
+ if inferred is None:
230
+ return None
231
+ kinds.add(inferred)
232
+
233
+ if len(kinds) == 1:
234
+ return next(iter(kinds))
235
+ return None
236
+
237
+ def leave_FunctionDef(
238
+ self,
239
+ original_node: cst.FunctionDef,
240
+ updated_node: cst.FunctionDef,
241
+ ) -> cst.FunctionDef:
242
+ if updated_node.returns is None:
243
+ rtype = self._infer_for_function(original_node)
244
+ if rtype is not None:
245
+ return updated_node.with_changes(
246
+ returns=cst.Annotation(annotation=cst.Name(rtype))
247
+ )
248
+ return updated_node
249
+
250
+ def leave_AsyncFunctionDef(
251
+ self,
252
+ original_node: cst.AsyncFunctionDef,
253
+ updated_node: cst.AsyncFunctionDef,
254
+ ) -> cst.AsyncFunctionDef:
255
+ if updated_node.returns is None:
256
+ rtype = self._infer_for_function(original_node)
257
+ if rtype is not None:
258
+ return updated_node.with_changes(
259
+ returns=cst.Annotation(annotation=cst.Name(rtype))
260
+ )
261
+ return updated_node
262
+
263
+ try:
264
+ module = cst.parse_module(src)
265
+ except Exception:
266
+ # If parsing fails for any reason, return the original source unchanged
267
+ return src
268
+
269
+ # Collect known simple type names from the module
270
+ ktc = _KnownTypesCollector()
271
+ module.visit(ktc)
272
+
273
+ new_module = module.visit(AddReturnTypesTransformer(ktc.known))
274
+ return new_module.code
275
+
276
+ def describe_before(self) -> str:
277
+ return "Some Python functions are missing obvious return type annotations."
278
+
279
+ def describe_after(self) -> str:
280
+ return "Functions have been annotated with simple return types where obvious."
281
+
282
+ def description(self) -> str:
283
+ return "Add simple return type annotations (None/bool/str/int/float) when trivially inferable."
@@ -0,0 +1,49 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import ClassVar
4
+
5
+ from wexample_filestate.const.types_state_items import TargetFileOrDirectoryType
6
+
7
+ from .abstract_python_file_operation import AbstractPythonFileOperation
8
+
9
+
10
+ class PythonFormatOperation(AbstractPythonFileOperation):
11
+ """Format Python files using Black.
12
+
13
+ Triggered by config: { "python": ["format"] }
14
+ """
15
+
16
+ # Use ClassVar to avoid Pydantic treating it as a model field/private attr
17
+ _line_length: ClassVar[int] = 88
18
+
19
+ @classmethod
20
+ def get_option_name(cls) -> str:
21
+ from wexample_filestate_python.config_option.python_config_option import (
22
+ PythonConfigOption,
23
+ )
24
+
25
+ return PythonConfigOption.OPTION_NAME_FORMAT
26
+
27
+ @classmethod
28
+ def preview_source_change(cls, target: TargetFileOrDirectoryType) -> str | None:
29
+ import black
30
+
31
+ src = cls._read_current_str_or_fail(target)
32
+ mode = black.Mode(line_length=cls._line_length)
33
+
34
+ try:
35
+ formatted = black.format_file_contents(src, fast=False, mode=mode)
36
+ return formatted
37
+ except black.NothingChanged:
38
+ return src
39
+ except Exception as e:
40
+ raise e
41
+
42
+ def describe_before(self) -> str:
43
+ return "The Python file is not formatted according to Black's rules."
44
+
45
+ def describe_after(self) -> str:
46
+ return "The Python file has been formatted with Black."
47
+
48
+ def description(self) -> str:
49
+ return "Format the Python file content using Black."
@@ -0,0 +1,42 @@
1
+ from __future__ import annotations
2
+
3
+ from wexample_filestate.const.types_state_items import TargetFileOrDirectoryType
4
+
5
+ from .abstract_python_file_operation import AbstractPythonFileOperation
6
+
7
+
8
+ class PythonFStringifyOperation(AbstractPythonFileOperation):
9
+ """Convert string formatting to f-strings using flynt.
10
+
11
+ Triggered by: {"python": ["fstringify"]}
12
+ """
13
+
14
+ @classmethod
15
+ def get_option_name(cls) -> str:
16
+ from wexample_filestate_python.config_option.python_config_option import (
17
+ PythonConfigOption,
18
+ )
19
+
20
+ return PythonConfigOption.OPTION_NAME_FSTRINGIFY
21
+
22
+ @classmethod
23
+ def preview_source_change(cls, target: TargetFileOrDirectoryType) -> str | None:
24
+ from flynt.api import fstringify_code # type: ignore
25
+ from flynt.state import State # type: ignore
26
+
27
+ src = cls._read_current_str_or_fail(target)
28
+ state = State(aggressive=False, multiline=False, len_limit=120)
29
+ result = fstringify_code(src, state=state)
30
+
31
+ if result is None:
32
+ return src
33
+ return result.content
34
+
35
+ def describe_before(self) -> str:
36
+ return "The file uses legacy string formatting ('%'/format) that can be upgraded to f-strings."
37
+
38
+ def describe_after(self) -> str:
39
+ return "String formatting has been converted to modern f-strings."
40
+
41
+ def description(self) -> str:
42
+ return "Convert old-style string formatting to f-strings using flynt."
@@ -0,0 +1,41 @@
1
+ from __future__ import annotations
2
+
3
+ from wexample_filestate.const.types_state_items import TargetFileOrDirectoryType
4
+
5
+ from .abstract_python_file_operation import AbstractPythonFileOperation
6
+
7
+
8
+ class PythonModernizeTypingOperation(AbstractPythonFileOperation):
9
+ """Modernize typing syntax (PEP 585/604) to Python 3.12 style.
10
+
11
+ Triggered by: {"python": ["modernize_typing"]}
12
+ """
13
+
14
+ @classmethod
15
+ def get_option_name(cls) -> str:
16
+ from wexample_filestate_python.config_option.python_config_option import (
17
+ PythonConfigOption,
18
+ )
19
+
20
+ return PythonConfigOption.OPTION_NAME_MODERNIZE_TYPING
21
+
22
+ @classmethod
23
+ def preview_source_change(cls, target: TargetFileOrDirectoryType) -> str | None:
24
+ from pyupgrade._main import Settings, _fix_plugins # type: ignore
25
+
26
+ src = cls._read_current_str_or_fail(target)
27
+ settings = Settings(min_version=(3, 12))
28
+ updated = _fix_plugins(src, settings=settings)
29
+ # _fix_plugins returns a string; return as-is
30
+ return updated
31
+
32
+ def describe_before(self) -> str:
33
+ return (
34
+ "The file uses legacy typing syntax that can be modernized for Python 3.12."
35
+ )
36
+
37
+ def describe_after(self) -> str:
38
+ return "Typing syntax has been modernized to Python 3.12 style (PEP 585/604)."
39
+
40
+ def description(self) -> str:
41
+ return "Modernize typing syntax (PEP 585/604) using pyupgrade for Python 3.12."
@@ -0,0 +1,42 @@
1
+ from __future__ import annotations
2
+
3
+ from wexample_filestate.const.types_state_items import TargetFileOrDirectoryType
4
+
5
+ from .abstract_python_file_operation import AbstractPythonFileOperation
6
+
7
+
8
+ class PythonRemoveUnusedOperation(AbstractPythonFileOperation):
9
+ """Remove unused Python imports using autoflake.
10
+
11
+ Triggered by config: { "python": ["remove_unused_imports"] }
12
+ """
13
+
14
+ @classmethod
15
+ def get_option_name(cls) -> str:
16
+ from wexample_filestate_python.config_option.python_config_option import (
17
+ PythonConfigOption,
18
+ )
19
+
20
+ return PythonConfigOption.OPTION_NAME_REMOVE_UNUSED
21
+
22
+ @classmethod
23
+ def preview_source_change(cls, target: TargetFileOrDirectoryType) -> str | None:
24
+ from autoflake import fix_code
25
+
26
+ src = cls._read_current_str_or_fail(target)
27
+ return fix_code(
28
+ src,
29
+ remove_all_unused_imports=True,
30
+ expand_star_imports=True,
31
+ remove_duplicate_keys=True,
32
+ remove_unused_variables=True,
33
+ )
34
+
35
+ def describe_before(self) -> str:
36
+ return "The Python file contains unused imports."
37
+
38
+ def describe_after(self) -> str:
39
+ return "Unused imports have been removed with autoflake."
40
+
41
+ def description(self) -> str:
42
+ return "Remove unused imports from the Python file using autoflake."
@@ -0,0 +1,39 @@
1
+ from __future__ import annotations
2
+
3
+ from wexample_filestate.const.types_state_items import TargetFileOrDirectoryType
4
+
5
+ from .abstract_python_file_operation import AbstractPythonFileOperation
6
+
7
+
8
+ class PythonSortImportsOperation(AbstractPythonFileOperation):
9
+ """Sort Python imports using isort.
10
+
11
+ Triggered by config: { "python": ["sort_imports"] }
12
+ """
13
+
14
+ @classmethod
15
+ def get_option_name(cls) -> str:
16
+ from wexample_filestate_python.config_option.python_config_option import (
17
+ PythonConfigOption,
18
+ )
19
+
20
+ return PythonConfigOption.OPTION_NAME_SORT_IMPORTS
21
+
22
+ @classmethod
23
+ def preview_source_change(cls, target: TargetFileOrDirectoryType) -> str | None:
24
+ from isort import code as isort_code
25
+ from isort.settings import Config
26
+
27
+ src = cls._read_current_str_or_fail(target)
28
+ config = Config(profile="black")
29
+ formatted = isort_code(src, config=config)
30
+ return formatted
31
+
32
+ def describe_before(self) -> str:
33
+ return "The Python file has unsorted or poorly grouped imports."
34
+
35
+ def describe_after(self) -> str:
36
+ return "The Python imports have been sorted and grouped by isort."
37
+
38
+ def description(self) -> str:
39
+ return "Sort and group Python imports using isort."
@@ -0,0 +1,98 @@
1
+ from __future__ import annotations
2
+
3
+ from wexample_filestate.const.types_state_items import TargetFileOrDirectoryType
4
+
5
+ from .abstract_python_file_operation import AbstractPythonFileOperation
6
+
7
+
8
+ class PythonUnquoteAnnotationsOperation(AbstractPythonFileOperation):
9
+ """Remove quotes around type annotations by turning stringized annotations back into expressions.
10
+
11
+ Triggered by config: { "python": ["unquote_annotations"] }
12
+ """
13
+
14
+ @classmethod
15
+ def get_option_name(cls) -> str:
16
+ from wexample_filestate_python.config_option.python_config_option import (
17
+ PythonConfigOption,
18
+ )
19
+
20
+ return PythonConfigOption.OPTION_NAME_UNQUOTE_ANNOTATIONS
21
+
22
+ @classmethod
23
+ def preview_source_change(cls, target: TargetFileOrDirectoryType) -> str | None:
24
+ import json
25
+
26
+ import libcst as cst
27
+
28
+ src = cls._read_current_str_or_fail(target)
29
+
30
+ class _Unquoter(cst.CSTTransformer):
31
+ @staticmethod
32
+ def _unquote_expr(s: cst.SimpleString) -> cst.BaseExpression | None:
33
+ try:
34
+ code = json.loads(s.value)
35
+ except Exception:
36
+ return None
37
+ try:
38
+ return cst.parse_expression(code)
39
+ except Exception:
40
+ return None
41
+
42
+ @staticmethod
43
+ def _process_annotation(
44
+ ann: cst.Annotation | None,
45
+ ) -> cst.Annotation | None:
46
+ if ann is None:
47
+ return None
48
+ node = ann.annotation
49
+ if isinstance(node, cst.SimpleString):
50
+ expr = _Unquoter._unquote_expr(node)
51
+ if expr is not None:
52
+ return cst.Annotation(annotation=expr)
53
+ return ann
54
+
55
+ def leave_Param(
56
+ self, original_node: cst.Param, updated_node: cst.Param
57
+ ) -> cst.Param:
58
+ return updated_node.with_changes(
59
+ annotation=self._process_annotation(updated_node.annotation)
60
+ )
61
+
62
+ def leave_FunctionDef(
63
+ self, original_node: cst.FunctionDef, updated_node: cst.FunctionDef
64
+ ) -> cst.FunctionDef:
65
+ return updated_node.with_changes(
66
+ returns=self._process_annotation(updated_node.returns)
67
+ )
68
+
69
+ def leave_AnnAssign(
70
+ self, original_node: cst.AnnAssign, updated_node: cst.AnnAssign
71
+ ) -> cst.AnnAssign:
72
+ return updated_node.with_changes(
73
+ annotation=self._process_annotation(updated_node.annotation)
74
+ )
75
+
76
+ def leave_TypeAlias(
77
+ self, original_node: cst.TypeAlias, updated_node: cst.TypeAlias
78
+ ) -> cst.TypeAlias:
79
+ # Python 3.12 'type X = ...' syntax
80
+ ann = updated_node.annotation
81
+ if isinstance(ann, cst.SimpleString):
82
+ expr = self._unquote_expr(ann)
83
+ if expr is not None:
84
+ return updated_node.with_changes(annotation=expr)
85
+ return updated_node
86
+
87
+ module = cst.parse_module(src)
88
+ new_mod = module.visit(_Unquoter())
89
+ return new_mod.code
90
+
91
+ def describe_before(self) -> str:
92
+ return "The Python file contains stringized (quoted) type annotations."
93
+
94
+ def describe_after(self) -> str:
95
+ return "Quoted type annotations have been converted back to expressions."
96
+
97
+ def description(self) -> str:
98
+ return "Unquote type annotations (arguments, returns, variables) using LibCST."
@@ -0,0 +1,50 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ from wexample_filestate.operations_provider.abstract_operations_provider import (
6
+ AbstractOperationsProvider,
7
+ )
8
+
9
+ if TYPE_CHECKING:
10
+ from wexample_filestate.operation.abstract_operation import AbstractOperation
11
+
12
+
13
+ class PythonOperationsProvider(AbstractOperationsProvider):
14
+ @staticmethod
15
+ def get_operations() -> list[type[AbstractOperation]]:
16
+ from wexample_filestate_python.operation.python_add_future_annotations_operation import (
17
+ PythonAddFutureAnnotationsOperation,
18
+ )
19
+ from wexample_filestate_python.operation.python_add_return_types_operation import (
20
+ PythonAddReturnTypesOperation,
21
+ )
22
+ from wexample_filestate_python.operation.python_format_operation import (
23
+ PythonFormatOperation,
24
+ )
25
+ from wexample_filestate_python.operation.python_fstringify_operation import (
26
+ PythonFStringifyOperation,
27
+ )
28
+ from wexample_filestate_python.operation.python_modernize_typing_operation import (
29
+ PythonModernizeTypingOperation,
30
+ )
31
+ from wexample_filestate_python.operation.python_remove_unused_imports_operation import (
32
+ PythonRemoveUnusedOperation,
33
+ )
34
+ from wexample_filestate_python.operation.python_sort_imports_operation import (
35
+ PythonSortImportsOperation,
36
+ )
37
+ from wexample_filestate_python.operation.python_unquote_annotations_operation import (
38
+ PythonUnquoteAnnotationsOperation,
39
+ )
40
+
41
+ return [
42
+ PythonFormatOperation,
43
+ PythonSortImportsOperation,
44
+ PythonAddReturnTypesOperation,
45
+ PythonModernizeTypingOperation,
46
+ PythonFStringifyOperation,
47
+ PythonRemoveUnusedOperation,
48
+ PythonAddFutureAnnotationsOperation,
49
+ PythonUnquoteAnnotationsOperation,
50
+ ]
@@ -0,0 +1,23 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ from wexample_config.options_provider.abstract_options_provider import (
6
+ AbstractOptionsProvider,
7
+ )
8
+ from wexample_filestate_python.config_option.python_config_option import (
9
+ PythonConfigOption,
10
+ )
11
+
12
+ if TYPE_CHECKING:
13
+ from wexample_config.config_option.abstract_config_option import (
14
+ AbstractConfigOption,
15
+ )
16
+
17
+
18
+ class PythonOptionsProvider(AbstractOptionsProvider):
19
+ @classmethod
20
+ def get_options(cls) -> list[type[AbstractConfigOption]]:
21
+ return [
22
+ PythonConfigOption,
23
+ ]