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