wexample-filestate-python 0.0.40__tar.gz → 0.0.43__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.43}/PKG-INFO +10 -24
- {wexample_filestate_python-0.0.40 → wexample_filestate_python-0.0.43}/README.md +5 -12
- {wexample_filestate_python-0.0.40 → wexample_filestate_python-0.0.43}/pyproject.toml +5 -15
- wexample_filestate_python-0.0.43/src/wexample_filestate_python/common/pipy_gateway.py +20 -0
- wexample_filestate_python-0.0.43/src/wexample_filestate_python/config_option/python_config_option.py +49 -0
- wexample_filestate_python-0.0.43/src/wexample_filestate_python/const/__init__.py +0 -0
- wexample_filestate_python-0.0.43/src/wexample_filestate_python/const/name_pattern.py +4 -0
- wexample_filestate_python-0.0.43/src/wexample_filestate_python/file/__init__.py +0 -0
- wexample_filestate_python-0.0.43/src/wexample_filestate_python/file/python_file.py +12 -0
- wexample_filestate_python-0.0.43/src/wexample_filestate_python/helpers/__init__.py +0 -0
- wexample_filestate_python-0.0.43/src/wexample_filestate_python/helpers/package.py +141 -0
- wexample_filestate_python-0.0.43/src/wexample_filestate_python/helpers/toml.py +116 -0
- wexample_filestate_python-0.0.43/src/wexample_filestate_python/operation/__init__.py +0 -0
- wexample_filestate_python-0.0.43/src/wexample_filestate_python/operation/abstract_python_file_operation.py +84 -0
- wexample_filestate_python-0.0.43/src/wexample_filestate_python/operation/python_add_future_annotations_operation.py +105 -0
- wexample_filestate_python-0.0.43/src/wexample_filestate_python/operation/python_add_return_types_operation.py +285 -0
- wexample_filestate_python-0.0.43/src/wexample_filestate_python/operation/python_fix_attrs_operation.py +53 -0
- wexample_filestate_python-0.0.43/src/wexample_filestate_python/operation/python_fix_blank_lines_operation.py +62 -0
- wexample_filestate_python-0.0.43/src/wexample_filestate_python/operation/python_format_operation.py +50 -0
- wexample_filestate_python-0.0.43/src/wexample_filestate_python/operation/python_fstringify_operation.py +49 -0
- wexample_filestate_python-0.0.43/src/wexample_filestate_python/operation/python_modernize_typing_operation.py +44 -0
- wexample_filestate_python-0.0.43/src/wexample_filestate_python/operation/python_order_class_attributes_operation.py +50 -0
- wexample_filestate_python-0.0.43/src/wexample_filestate_python/operation/python_order_class_docstring_operation.py +54 -0
- wexample_filestate_python-0.0.43/src/wexample_filestate_python/operation/python_order_class_methods_operation.py +53 -0
- wexample_filestate_python-0.0.43/src/wexample_filestate_python/operation/python_order_constants_operation.py +51 -0
- wexample_filestate_python-0.0.43/src/wexample_filestate_python/operation/python_order_iterable_items_operation.py +48 -0
- wexample_filestate_python-0.0.43/src/wexample_filestate_python/operation/python_order_main_guard_operation.py +58 -0
- wexample_filestate_python-0.0.43/src/wexample_filestate_python/operation/python_order_module_docstring_operation.py +87 -0
- wexample_filestate_python-0.0.43/src/wexample_filestate_python/operation/python_order_module_functions_operation.py +58 -0
- wexample_filestate_python-0.0.43/src/wexample_filestate_python/operation/python_order_module_metadata_operation.py +76 -0
- wexample_filestate_python-0.0.43/src/wexample_filestate_python/operation/python_order_type_checking_block_operation.py +65 -0
- wexample_filestate_python-0.0.43/src/wexample_filestate_python/operation/python_relocate_imports_operation.py +203 -0
- wexample_filestate_python-0.0.43/src/wexample_filestate_python/operation/python_remove_unused_imports_operation.py +60 -0
- wexample_filestate_python-0.0.43/src/wexample_filestate_python/operation/python_sort_imports_operation.py +42 -0
- wexample_filestate_python-0.0.43/src/wexample_filestate_python/operation/python_unquote_annotations_operation.py +101 -0
- wexample_filestate_python-0.0.43/src/wexample_filestate_python/operation/utils/__init__.py +0 -0
- wexample_filestate_python-0.0.43/src/wexample_filestate_python/operation/utils/python_attrs_utils.py +112 -0
- wexample_filestate_python-0.0.43/src/wexample_filestate_python/operation/utils/python_blank_lines_utils.py +597 -0
- wexample_filestate_python-0.0.43/src/wexample_filestate_python/operation/utils/python_class_attributes_utils.py +261 -0
- wexample_filestate_python-0.0.43/src/wexample_filestate_python/operation/utils/python_class_docstring_utils.py +85 -0
- wexample_filestate_python-0.0.43/src/wexample_filestate_python/operation/utils/python_class_methods_utils.py +230 -0
- wexample_filestate_python-0.0.43/src/wexample_filestate_python/operation/utils/python_constants_utils.py +299 -0
- wexample_filestate_python-0.0.43/src/wexample_filestate_python/operation/utils/python_docstring_utils.py +117 -0
- wexample_filestate_python-0.0.43/src/wexample_filestate_python/operation/utils/python_functions_utils.py +212 -0
- wexample_filestate_python-0.0.43/src/wexample_filestate_python/operation/utils/python_iterable_utils.py +131 -0
- wexample_filestate_python-0.0.43/src/wexample_filestate_python/operation/utils/python_main_guard_utils.py +80 -0
- wexample_filestate_python-0.0.43/src/wexample_filestate_python/operation/utils/python_module_metadata_utils.py +146 -0
- wexample_filestate_python-0.0.43/src/wexample_filestate_python/operation/utils/python_type_checking_utils.py +113 -0
- wexample_filestate_python-0.0.43/src/wexample_filestate_python/operation/utils/relocate_imports/__init__.py +7 -0
- wexample_filestate_python-0.0.43/src/wexample_filestate_python/operation/utils/relocate_imports/python_import_rewriter.py +400 -0
- wexample_filestate_python-0.0.43/src/wexample_filestate_python/operation/utils/relocate_imports/python_localize_runtime_imports.py +249 -0
- wexample_filestate_python-0.0.43/src/wexample_filestate_python/operation/utils/relocate_imports/python_parser_import_index.py +54 -0
- wexample_filestate_python-0.0.43/src/wexample_filestate_python/operation/utils/relocate_imports/python_runtime_symbol_collector.py +33 -0
- wexample_filestate_python-0.0.43/src/wexample_filestate_python/operation/utils/relocate_imports/python_usage_collector.py +360 -0
- wexample_filestate_python-0.0.43/src/wexample_filestate_python/operations_provider/__init__.py +0 -0
- wexample_filestate_python-0.0.43/src/wexample_filestate_python/operations_provider/python_operations_provider.py +103 -0
- wexample_filestate_python-0.0.43/src/wexample_filestate_python/options_provider/__init__.py +0 -0
- wexample_filestate_python-0.0.43/src/wexample_filestate_python/options_provider/python_options_provider.py +24 -0
- wexample_filestate_python-0.0.43/src/wexample_filestate_python/py.typed +0 -0
- wexample_filestate_python-0.0.43/src/wexample_filestate_python/workdir/__init__.py +0 -0
- wexample_filestate_python-0.0.43/tests/tests/__init__.py +0 -0
- wexample_filestate_python-0.0.43/tests/wexample_tests/__init__.py +0 -0
- {wexample_filestate_python-0.0.40/tests/tests → wexample_filestate_python-0.0.43/src/wexample_filestate_python}/__init__.py +0 -0
- {wexample_filestate_python-0.0.40/tests/wexample_tests → wexample_filestate_python-0.0.43/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.43/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.43
|
|
4
4
|
Summary: Helpers for Python.
|
|
5
5
|
Author-Email: weeger <contact@wexample.com>
|
|
6
6
|
License: MIT
|
|
@@ -9,17 +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:
|
|
15
|
-
Requires-Dist:
|
|
16
|
-
Requires-Dist: networkx
|
|
17
|
-
Requires-Dist: packaging
|
|
18
|
-
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
|
|
12
|
+
Requires-Dist: attrs>=23.1.0
|
|
13
|
+
Requires-Dist: cattrs>=23.1.0
|
|
14
|
+
Requires-Dist: wexample-filestate==0.0.53
|
|
15
|
+
Requires-Dist: wexample-helpers-api==0.0.34
|
|
23
16
|
Provides-Extra: dev
|
|
24
17
|
Requires-Dist: pytest; extra == "dev"
|
|
25
18
|
Description-Content-Type: text/markdown
|
|
@@ -28,7 +21,7 @@ Description-Content-Type: text/markdown
|
|
|
28
21
|
|
|
29
22
|
Helpers for Python.
|
|
30
23
|
|
|
31
|
-
Version: 0.0.
|
|
24
|
+
Version: 0.0.42
|
|
32
25
|
|
|
33
26
|
## Requirements
|
|
34
27
|
|
|
@@ -36,17 +29,10 @@ Version: 0.0.40
|
|
|
36
29
|
|
|
37
30
|
## Dependencies
|
|
38
31
|
|
|
39
|
-
-
|
|
40
|
-
-
|
|
41
|
-
-
|
|
42
|
-
-
|
|
43
|
-
- networkx
|
|
44
|
-
- packaging
|
|
45
|
-
- pydantic>=2,<3
|
|
46
|
-
- python-dotenv
|
|
47
|
-
- pyupgrade
|
|
48
|
-
- tomlkit
|
|
49
|
-
- wexample-filestate-git==0.0.38
|
|
32
|
+
- attrs>=23.1.0
|
|
33
|
+
- cattrs>=23.1.0
|
|
34
|
+
- wexample-filestate==0.0.52
|
|
35
|
+
- wexample-helpers-api==0.0.33
|
|
50
36
|
|
|
51
37
|
## Installation
|
|
52
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,17 +10,10 @@ Version: 0.0.40
|
|
|
10
10
|
|
|
11
11
|
## Dependencies
|
|
12
12
|
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
- networkx
|
|
18
|
-
- packaging
|
|
19
|
-
- pydantic>=2,<3
|
|
20
|
-
- python-dotenv
|
|
21
|
-
- pyupgrade
|
|
22
|
-
- tomlkit
|
|
23
|
-
- wexample-filestate-git==0.0.38
|
|
13
|
+
- attrs>=23.1.0
|
|
14
|
+
- cattrs>=23.1.0
|
|
15
|
+
- wexample-filestate==0.0.52
|
|
16
|
+
- wexample-helpers-api==0.0.33
|
|
24
17
|
|
|
25
18
|
## Installation
|
|
26
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.43"
|
|
10
10
|
description = "Helpers for Python."
|
|
11
11
|
authors = [
|
|
12
12
|
{ name = "weeger", email = "contact@wexample.com" },
|
|
@@ -18,17 +18,10 @@ classifiers = [
|
|
|
18
18
|
"Operating System :: OS Independent",
|
|
19
19
|
]
|
|
20
20
|
dependencies = [
|
|
21
|
-
"
|
|
22
|
-
"
|
|
23
|
-
"
|
|
24
|
-
"
|
|
25
|
-
"networkx",
|
|
26
|
-
"packaging",
|
|
27
|
-
"pydantic>=2,<3",
|
|
28
|
-
"python-dotenv",
|
|
29
|
-
"pyupgrade",
|
|
30
|
-
"tomlkit",
|
|
31
|
-
"wexample-filestate-git==0.0.38",
|
|
21
|
+
"attrs>=23.1.0",
|
|
22
|
+
"cattrs>=23.1.0",
|
|
23
|
+
"wexample-filestate==0.0.53",
|
|
24
|
+
"wexample-helpers-api==0.0.34",
|
|
32
25
|
]
|
|
33
26
|
|
|
34
27
|
[project.readme]
|
|
@@ -50,9 +43,6 @@ dev = [
|
|
|
50
43
|
distribution = true
|
|
51
44
|
|
|
52
45
|
[tool.pdm.build]
|
|
53
|
-
includes = [
|
|
54
|
-
"src/wexample_filestate_python/py.typed",
|
|
55
|
-
]
|
|
56
46
|
package-dir = "src"
|
|
57
47
|
packages = [
|
|
58
48
|
{ include = "wexample_filestate_python", from = "src" },
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from wexample_helpers.classes.field import public_field
|
|
4
|
+
from wexample_helpers_api.common.abstract_gateway import AbstractGateway
|
|
5
|
+
from wexample_helpers.decorator.base_class import base_class
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@base_class
|
|
9
|
+
class PipyGateway(AbstractGateway):
|
|
10
|
+
base_url: str | None = public_field(
|
|
11
|
+
default="https://pypi.org/", description="Base Pipy API URL"
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
def package_release_exists(self, package_name: str, version: str) -> bool:
|
|
15
|
+
response = self.make_request(f"pypi/{package_name}/json")
|
|
16
|
+
# Package exists
|
|
17
|
+
if response.status_code == 200:
|
|
18
|
+
return bool(response.json().get("releases", {}).get(version))
|
|
19
|
+
|
|
20
|
+
return False
|
wexample_filestate_python-0.0.43/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]
|
|
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,141 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
import tomli
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def package_get_dependencies(root_dir: str | Path) -> dict[str, set[str]]:
|
|
13
|
+
"""
|
|
14
|
+
Get dependencies between packages in a directory.
|
|
15
|
+
"""
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
packages_root = Path(root_dir)
|
|
19
|
+
if not packages_root.exists() or not packages_root.is_dir():
|
|
20
|
+
raise ValueError(f"Error: {packages_root} does not exist or is not a directory")
|
|
21
|
+
|
|
22
|
+
dependencies = {}
|
|
23
|
+
|
|
24
|
+
# First pass: collect all local packages
|
|
25
|
+
for package_dir in packages_root.iterdir():
|
|
26
|
+
if not package_dir.is_dir():
|
|
27
|
+
continue
|
|
28
|
+
|
|
29
|
+
package_info = package_get_info(package_dir)
|
|
30
|
+
if package_info:
|
|
31
|
+
name, _ = package_info
|
|
32
|
+
dependencies[name] = set()
|
|
33
|
+
|
|
34
|
+
# Second pass: analyze dependencies
|
|
35
|
+
for package_dir in packages_root.iterdir():
|
|
36
|
+
if not package_dir.is_dir():
|
|
37
|
+
continue
|
|
38
|
+
|
|
39
|
+
package_info = package_get_info(package_dir)
|
|
40
|
+
if package_info:
|
|
41
|
+
name, deps = package_info
|
|
42
|
+
if name in dependencies:
|
|
43
|
+
# Only keep dependencies that are local packages
|
|
44
|
+
dependencies[name] = {dep for dep in deps if dep in dependencies}
|
|
45
|
+
|
|
46
|
+
return dependencies
|
|
47
|
+
|
|
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
|
+
|
|
73
|
+
def package_list_sorted(root_dir: str | Path) -> list[str]:
|
|
74
|
+
"""
|
|
75
|
+
Get a list of package names sorted by dependency order.
|
|
76
|
+
"""
|
|
77
|
+
from wexample_filestate.helpers.dependencies import dependencies_sort
|
|
78
|
+
|
|
79
|
+
dependencies = package_get_dependencies(root_dir)
|
|
80
|
+
if not dependencies:
|
|
81
|
+
return []
|
|
82
|
+
|
|
83
|
+
# Convert dependencies dict to list for sorting
|
|
84
|
+
packages = list(dependencies.keys())
|
|
85
|
+
|
|
86
|
+
def get_deps(pkg: str) -> set[str]:
|
|
87
|
+
return dependencies[pkg]
|
|
88
|
+
|
|
89
|
+
return dependencies_sort(packages, get_deps)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def package_normalize_name(val: str) -> str:
|
|
93
|
+
import re as _re
|
|
94
|
+
|
|
95
|
+
# strip extras, versions, markers
|
|
96
|
+
base = _re.split(r"[\s<>=!~;\[]", val, maxsplit=1)[0]
|
|
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 {}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Any
|
|
4
|
+
|
|
5
|
+
if TYPE_CHECKING:
|
|
6
|
+
from tomlkit.items import Array
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def toml_ensure_array(tbl: Any, key: str) -> tuple[Any, bool]:
|
|
10
|
+
"""
|
|
11
|
+
Ensure an array exists at tbl[key] and return (array, changed).
|
|
12
|
+
Uses tomlkit.array() for creation.
|
|
13
|
+
"""
|
|
14
|
+
from tomlkit import array
|
|
15
|
+
|
|
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
|
|
22
|
+
|
|
23
|
+
|
|
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
|
|
30
|
+
|
|
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
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def toml_ensure_table(doc: Any, path: list[str]) -> tuple[Any, bool]:
|
|
39
|
+
"""
|
|
40
|
+
Ensure a nested TOML table exists and return (table, changed).
|
|
41
|
+
Path example: ["tool", "pdm", "build"]. Uses tomlkit.table() for missing parts.
|
|
42
|
+
"""
|
|
43
|
+
from tomlkit import table
|
|
44
|
+
|
|
45
|
+
if not isinstance(path, list) or not path:
|
|
46
|
+
raise ValueError("path must be a non-empty list of keys")
|
|
47
|
+
|
|
48
|
+
changed = False
|
|
49
|
+
current = doc
|
|
50
|
+
for key in path:
|
|
51
|
+
tbl = current.get(key) if isinstance(current, dict) else None
|
|
52
|
+
if not tbl or not isinstance(tbl, dict):
|
|
53
|
+
tbl = table()
|
|
54
|
+
current[key] = tbl
|
|
55
|
+
changed = True
|
|
56
|
+
current = tbl
|
|
57
|
+
return current, changed
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def toml_get_string_value(item: Any) -> str:
|
|
61
|
+
"""Return the string content of a tomlkit String or generic item as str."""
|
|
62
|
+
from tomlkit.items import String
|
|
63
|
+
|
|
64
|
+
if isinstance(item, String):
|
|
65
|
+
return item.value
|
|
66
|
+
return str(item)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def toml_set_array_multiline(tbl: Any, key: str, values: list[Any]) -> Array:
|
|
70
|
+
"""
|
|
71
|
+
Replace tbl[key] with a tomlkit array built from values and set multiline(True).
|
|
72
|
+
Returns the created Array instance.
|
|
73
|
+
"""
|
|
74
|
+
from tomlkit import array
|
|
75
|
+
|
|
76
|
+
arr = array(values)
|
|
77
|
+
arr.multiline(True)
|
|
78
|
+
tbl[key] = arr
|
|
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
|
|
File without changes
|
|
@@ -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)
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
from typing import TYPE_CHECKING
|
|
2
|
+
|
|
3
|
+
from wexample_filestate.const.types_state_items import TargetFileOrDirectoryType
|
|
4
|
+
|
|
5
|
+
from .abstract_python_file_operation import AbstractPythonFileOperation
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from wexample_filestate.const.types_state_items import TargetFileOrDirectoryType
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class PythonAddFutureAnnotationsOperation(AbstractPythonFileOperation):
|
|
12
|
+
"""Ensure `from __future__ import annotations` is present at module top.
|
|
13
|
+
|
|
14
|
+
Triggered by: {"python": ["add_future_annotations"]}
|
|
15
|
+
|
|
16
|
+
Note: This operation historically removed __future__ imports; we repurpose it
|
|
17
|
+
to add the annotations future consistently across the codebase, per new policy.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
@classmethod
|
|
21
|
+
def get_option_name(cls) -> str:
|
|
22
|
+
from wexample_filestate_python.config_option.python_config_option import (
|
|
23
|
+
PythonConfigOption,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
return PythonConfigOption.OPTION_NAME_ADD_FUTURE_ANNOTATIONS
|
|
27
|
+
|
|
28
|
+
@classmethod
|
|
29
|
+
def preview_source_change(cls, target: TargetFileOrDirectoryType) -> str | None:
|
|
30
|
+
"""Return source with a `from __future__ import annotations` inserted
|
|
31
|
+
in the correct place if it is not already present.
|
|
32
|
+
|
|
33
|
+
Rules:
|
|
34
|
+
- Keep shebang on first line intact.
|
|
35
|
+
- Keep encoding cookie (PEP 263) within first two lines intact.
|
|
36
|
+
- If module docstring exists, insert after the docstring statement.
|
|
37
|
+
- Otherwise, insert after shebang/encoding header block.
|
|
38
|
+
- If the future import already exists anywhere, return the original source.
|
|
39
|
+
"""
|
|
40
|
+
src = cls._read_current_str_or_fail(target)
|
|
41
|
+
|
|
42
|
+
# Fast path: already present
|
|
43
|
+
if "from __future__ import annotations" in src:
|
|
44
|
+
return src
|
|
45
|
+
|
|
46
|
+
import ast
|
|
47
|
+
import re
|
|
48
|
+
|
|
49
|
+
lines = src.splitlines(keepends=True)
|
|
50
|
+
|
|
51
|
+
# Detect shebang and encoding cookie positions
|
|
52
|
+
idx = 0
|
|
53
|
+
if lines and lines[0].startswith("#!"):
|
|
54
|
+
idx = 1
|
|
55
|
+
# Encoding cookie can be on first or second line (after shebang)
|
|
56
|
+
enc_re = re.compile(r"^#.*coding[:=]\\s*([-_.a-zA-Z0-9]+)")
|
|
57
|
+
for i in range(idx, min(idx + 2, len(lines))):
|
|
58
|
+
if enc_re.match(lines[i]):
|
|
59
|
+
idx = i + 1
|
|
60
|
+
|
|
61
|
+
# Parse to find module docstring span
|
|
62
|
+
try:
|
|
63
|
+
tree = ast.parse(src)
|
|
64
|
+
except SyntaxError:
|
|
65
|
+
# If parsing fails, be conservative: insert after header block
|
|
66
|
+
insert_at = idx
|
|
67
|
+
else:
|
|
68
|
+
body = getattr(tree, "body", [])
|
|
69
|
+
if (
|
|
70
|
+
body
|
|
71
|
+
and isinstance(body[0], ast.Expr)
|
|
72
|
+
and isinstance(getattr(body[0], "value", None), ast.Constant)
|
|
73
|
+
and isinstance(body[0].value.value, str)
|
|
74
|
+
):
|
|
75
|
+
# Module docstring present; insert after its end_lineno
|
|
76
|
+
doc_end = getattr(body[0], "end_lineno", body[0].lineno) # 1-based
|
|
77
|
+
insert_at = max(idx, doc_end) # line number where next stmt starts
|
|
78
|
+
else:
|
|
79
|
+
insert_at = idx
|
|
80
|
+
|
|
81
|
+
# lines is 0-based; insert_at is 1-based line count from AST
|
|
82
|
+
insert_index = max(0, min(len(lines), insert_at))
|
|
83
|
+
|
|
84
|
+
# Ensure there is a newline after the inserted import if needed
|
|
85
|
+
future_line = "from __future__ import annotations\n"
|
|
86
|
+
|
|
87
|
+
# Avoid inserting duplicate blank lines: if previous line is not blank and not newline, keep
|
|
88
|
+
# If there are existing future imports right after docstring, we can insert alongside them; no special handling needed
|
|
89
|
+
lines.insert(insert_index, future_line)
|
|
90
|
+
# If there isn't a blank line after the future import and next line is not blank, add one for readability
|
|
91
|
+
j = insert_index + 1
|
|
92
|
+
if j < len(lines):
|
|
93
|
+
if lines[j].strip() != "":
|
|
94
|
+
lines.insert(j, "\n")
|
|
95
|
+
|
|
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)."
|