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.
- {wexample_filestate_python-0.0.40 → wexample_filestate_python-0.0.41}/PKG-INFO +5 -22
- {wexample_filestate_python-0.0.40 → wexample_filestate_python-0.0.41}/README.md +2 -11
- {wexample_filestate_python-0.0.40 → wexample_filestate_python-0.0.41}/pyproject.toml +4 -12
- wexample_filestate_python-0.0.41/src/wexample_filestate_python/common/pipy_gateway.py +18 -0
- wexample_filestate_python-0.0.41/src/wexample_filestate_python/config_option/python_config_option.py +20 -0
- wexample_filestate_python-0.0.41/src/wexample_filestate_python/const/__init__.py +0 -0
- wexample_filestate_python-0.0.41/src/wexample_filestate_python/const/name_pattern.py +3 -0
- wexample_filestate_python-0.0.41/src/wexample_filestate_python/file/__init__.py +0 -0
- wexample_filestate_python-0.0.41/src/wexample_filestate_python/file/python_file.py +12 -0
- wexample_filestate_python-0.0.41/src/wexample_filestate_python/helpers/__init__.py +0 -0
- wexample_filestate_python-0.0.41/src/wexample_filestate_python/helpers/package.py +136 -0
- wexample_filestate_python-0.0.41/src/wexample_filestate_python/helpers/toml.py +105 -0
- wexample_filestate_python-0.0.41/src/wexample_filestate_python/operation/__init__.py +0 -0
- wexample_filestate_python-0.0.41/src/wexample_filestate_python/operation/abstract_python_file_operation.py +39 -0
- wexample_filestate_python-0.0.41/src/wexample_filestate_python/operation/python_add_future_annotations_operation.py +101 -0
- wexample_filestate_python-0.0.41/src/wexample_filestate_python/operation/python_add_return_types_operation.py +283 -0
- wexample_filestate_python-0.0.41/src/wexample_filestate_python/operation/python_format_operation.py +49 -0
- wexample_filestate_python-0.0.41/src/wexample_filestate_python/operation/python_fstringify_operation.py +42 -0
- wexample_filestate_python-0.0.41/src/wexample_filestate_python/operation/python_modernize_typing_operation.py +41 -0
- wexample_filestate_python-0.0.41/src/wexample_filestate_python/operation/python_remove_unused_imports_operation.py +42 -0
- wexample_filestate_python-0.0.41/src/wexample_filestate_python/operation/python_sort_imports_operation.py +39 -0
- wexample_filestate_python-0.0.41/src/wexample_filestate_python/operation/python_unquote_annotations_operation.py +98 -0
- wexample_filestate_python-0.0.41/src/wexample_filestate_python/operations_provider/__init__.py +0 -0
- wexample_filestate_python-0.0.41/src/wexample_filestate_python/operations_provider/python_operations_provider.py +50 -0
- wexample_filestate_python-0.0.41/src/wexample_filestate_python/options_provider/__init__.py +0 -0
- wexample_filestate_python-0.0.41/src/wexample_filestate_python/options_provider/python_options_provider.py +23 -0
- wexample_filestate_python-0.0.41/src/wexample_filestate_python/py.typed +0 -0
- wexample_filestate_python-0.0.41/src/wexample_filestate_python/workdir/__init__.py +0 -0
- wexample_filestate_python-0.0.41/tests/tests/__init__.py +0 -0
- wexample_filestate_python-0.0.41/tests/wexample_tests/__init__.py +0 -0
- {wexample_filestate_python-0.0.40/tests/tests → wexample_filestate_python-0.0.41/src/wexample_filestate_python}/__init__.py +0 -0
- {wexample_filestate_python-0.0.40/tests/wexample_tests → wexample_filestate_python-0.0.41/src/wexample_filestate_python/common}/__init__.py +0 -0
- /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.
|
|
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:
|
|
20
|
-
Requires-Dist:
|
|
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.
|
|
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
|
-
-
|
|
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.
|
|
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
|
-
-
|
|
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.
|
|
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
|
-
"
|
|
29
|
-
"
|
|
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
|
|
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
|
wexample_filestate_python-0.0.41/src/wexample_filestate_python/config_option/python_config_option.py
ADDED
|
@@ -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]
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import ClassVar
|
|
4
|
+
|
|
5
|
+
from wexample_filestate.item.item_target_file import ItemTargetFile
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class PythonFile(ItemTargetFile):
|
|
9
|
+
EXTENSION_ENV: ClassVar[str] = "py"
|
|
10
|
+
|
|
11
|
+
def _expected_file_name_extension(self) -> str | None:
|
|
12
|
+
return PythonFile.EXTENSION_ENV
|
|
File without changes
|
|
@@ -0,0 +1,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
|
|
File without changes
|
|
@@ -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."
|
wexample_filestate_python-0.0.41/src/wexample_filestate_python/operation/python_format_operation.py
ADDED
|
@@ -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."
|
wexample_filestate_python-0.0.41/src/wexample_filestate_python/operations_provider/__init__.py
ADDED
|
File without changes
|
|
@@ -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
|
+
]
|
|
File without changes
|
|
@@ -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
|
+
]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|