uv-upx 0.2.0__py3-none-any.whl
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.
- uv_upx/__init__.py +5 -0
- uv_upx/__main__.py +4 -0
- uv_upx/cli/__init__.py +0 -0
- uv_upx/cli/main.py +37 -0
- uv_upx/logging_custom/__init__.py +5 -0
- uv_upx/logging_custom/init_logging.py +8 -0
- uv_upx/main.py +7 -0
- uv_upx/models/__init__.py +0 -0
- uv_upx/models/dependency_parsed.py +36 -0
- uv_upx/py.typed +0 -0
- uv_upx/services/__init__.py +0 -0
- uv_upx/services/get_all_pyprojects.py +62 -0
- uv_upx/services/get_deps_from_project.py +141 -0
- uv_upx/services/get_pyproject_paths_by_globs.py +32 -0
- uv_upx/services/handle_groups.py +189 -0
- uv_upx/services/normalize_and_check_path_to_pyproject.py +41 -0
- uv_upx/services/parse_dependency.py +89 -0
- uv_upx/services/rollback_updater.py +25 -0
- uv_upx/services/run_updater.py +93 -0
- uv_upx/services/run_uv_lock.py +86 -0
- uv_upx/services/save_load_toml.py +18 -0
- uv_upx/services/update_dependencies.py +92 -0
- uv_upx-0.2.0.dist-info/METADATA +186 -0
- uv_upx-0.2.0.dist-info/RECORD +26 -0
- uv_upx-0.2.0.dist-info/WHEEL +4 -0
- uv_upx-0.2.0.dist-info/entry_points.txt +3 -0
uv_upx/__init__.py
ADDED
uv_upx/__main__.py
ADDED
uv_upx/cli/__init__.py
ADDED
|
File without changes
|
uv_upx/cli/main.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import pathlib # noqa: TC003
|
|
2
|
+
from typing import Annotated
|
|
3
|
+
|
|
4
|
+
import typer
|
|
5
|
+
|
|
6
|
+
from uv_upx.services.normalize_and_check_path_to_pyproject import normalize_and_check_path_to_project_root
|
|
7
|
+
from uv_upx.services.run_updater import run_updater
|
|
8
|
+
|
|
9
|
+
app = typer.Typer(
|
|
10
|
+
pretty_exceptions_enable=False,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@app.command()
|
|
15
|
+
def run(
|
|
16
|
+
*,
|
|
17
|
+
project_root_path: Annotated[
|
|
18
|
+
pathlib.Path | None,
|
|
19
|
+
typer.Option(
|
|
20
|
+
"--project",
|
|
21
|
+
"-p",
|
|
22
|
+
help="Path to project root directory. Use current working directory if not specified.",
|
|
23
|
+
),
|
|
24
|
+
] = None,
|
|
25
|
+
#
|
|
26
|
+
dry_run: Annotated[bool, typer.Option("--dry-run", help="Show changes without writing file")] = False,
|
|
27
|
+
#
|
|
28
|
+
verbose: Annotated[bool, typer.Option("--verbose", help="Show more output")] = False,
|
|
29
|
+
) -> None:
|
|
30
|
+
"""Update pyproject.toml dependencies to latest PyPI releases."""
|
|
31
|
+
path = normalize_and_check_path_to_project_root(project_root_path)
|
|
32
|
+
|
|
33
|
+
run_updater(
|
|
34
|
+
project_root_path=path,
|
|
35
|
+
dry_run=dry_run,
|
|
36
|
+
verbose=verbose,
|
|
37
|
+
)
|
uv_upx/main.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from pydantic import BaseModel
|
|
2
|
+
|
|
3
|
+
from uv_upx.services.get_deps_from_project import DependencyName
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class VersionConstraint(BaseModel):
|
|
7
|
+
operator: str
|
|
8
|
+
version: str
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class DependencyParsed(BaseModel):
|
|
12
|
+
# https://peps.python.org/pep-0508/
|
|
13
|
+
|
|
14
|
+
dependency_name: DependencyName
|
|
15
|
+
"""Dependency name (e.g., requests)"""
|
|
16
|
+
|
|
17
|
+
extras: list[str] | None
|
|
18
|
+
"""Extras (e.g., [dev])"""
|
|
19
|
+
|
|
20
|
+
version_constraints: list[VersionConstraint]
|
|
21
|
+
"""Version constraints (e.g., >=1.2.3, ==4.5.6)"""
|
|
22
|
+
|
|
23
|
+
marker: str | None
|
|
24
|
+
"""Environment marker (after ;)"""
|
|
25
|
+
|
|
26
|
+
def get_full_spec(self) -> str:
|
|
27
|
+
parts: list[str] = [self.dependency_name]
|
|
28
|
+
if self.extras:
|
|
29
|
+
extras_str = ",".join(self.extras)
|
|
30
|
+
parts.append(f"[{extras_str}]")
|
|
31
|
+
if self.version_constraints:
|
|
32
|
+
vc_strs = [f"{vc.operator}{vc.version}" for vc in self.version_constraints]
|
|
33
|
+
parts.append(",".join(vc_strs))
|
|
34
|
+
if self.marker:
|
|
35
|
+
parts.append(f"; {self.marker}")
|
|
36
|
+
return "".join(parts)
|
uv_upx/py.typed
ADDED
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import pathlib
|
|
2
|
+
from typing import Any, cast
|
|
3
|
+
|
|
4
|
+
from pydantic import BaseModel, ConfigDict
|
|
5
|
+
from tomlkit import TOMLDocument
|
|
6
|
+
|
|
7
|
+
from uv_upx.services.get_pyproject_paths_by_globs import get_pyproject_paths_by_globs
|
|
8
|
+
from uv_upx.services.normalize_and_check_path_to_pyproject import get_and_check_path_to_pyproject
|
|
9
|
+
from uv_upx.services.save_load_toml import load_toml
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class PyProjectWrapper(BaseModel):
|
|
13
|
+
path: pathlib.Path
|
|
14
|
+
data: TOMLDocument
|
|
15
|
+
|
|
16
|
+
model_config = ConfigDict(
|
|
17
|
+
arbitrary_types_allowed=True,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class PyProjectsRegistry(BaseModel):
|
|
22
|
+
items: list[PyProjectWrapper]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def get_all_pyprojects(
|
|
26
|
+
project_root_path: pathlib.Path,
|
|
27
|
+
) -> PyProjectsRegistry:
|
|
28
|
+
"""Find all pyproject.toml files in the project tree.
|
|
29
|
+
|
|
30
|
+
Use `workspaces` from uv.
|
|
31
|
+
"""
|
|
32
|
+
items: list[PyProjectWrapper] = []
|
|
33
|
+
|
|
34
|
+
root_pyproject_path = get_and_check_path_to_pyproject(project_root_path)
|
|
35
|
+
root_pyproject_data = load_toml(root_pyproject_path)
|
|
36
|
+
items.append(PyProjectWrapper(path=root_pyproject_path, data=root_pyproject_data))
|
|
37
|
+
|
|
38
|
+
# Get workspaces_config
|
|
39
|
+
# https://docs.astral.sh/uv/concepts/projects/workspaces/
|
|
40
|
+
|
|
41
|
+
# Example:
|
|
42
|
+
# [tool.uv.workspace]
|
|
43
|
+
# members = ["packages/*"]
|
|
44
|
+
# exclude = ["packages/seeds"]
|
|
45
|
+
|
|
46
|
+
# TODO: (?) Improve this place for efficiency
|
|
47
|
+
|
|
48
|
+
workspaces_config: dict[str, Any] = root_pyproject_data.get("tool", {}).get("uv", {}).get("workspace", {}) # pyright: ignore[reportUnknownVariableType, reportUnknownMemberType]
|
|
49
|
+
members_i_relative_glob_based: list[str] = cast("list[str]", workspaces_config.get("members", [])) # pyright: ignore[reportUnknownVariableType, reportUnknownMemberType]
|
|
50
|
+
exclude_i_relative_glob_based: list[str] = cast("list[str]", workspaces_config.get("exclude", [])) # pyright: ignore[reportUnknownVariableType, reportUnknownMemberType]
|
|
51
|
+
|
|
52
|
+
# Use glob-based pattern matching to find all pyproject.toml files in the workspace
|
|
53
|
+
members_paths_set = get_pyproject_paths_by_globs(project_root_path, members_i_relative_glob_based)
|
|
54
|
+
exclude_paths_set = get_pyproject_paths_by_globs(project_root_path, exclude_i_relative_glob_based)
|
|
55
|
+
|
|
56
|
+
result_paths_set = members_paths_set - exclude_paths_set
|
|
57
|
+
|
|
58
|
+
for path in result_paths_set:
|
|
59
|
+
data = load_toml(path)
|
|
60
|
+
items.append(PyProjectWrapper(path=path, data=data))
|
|
61
|
+
|
|
62
|
+
return PyProjectsRegistry(items=items)
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
from typing import TYPE_CHECKING, NewType
|
|
3
|
+
|
|
4
|
+
from uv_upx.services.normalize_and_check_path_to_pyproject import get_and_check_path_to_uv_lock
|
|
5
|
+
from uv_upx.services.save_load_toml import load_toml
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
import pathlib
|
|
9
|
+
|
|
10
|
+
DependencyName = NewType("DependencyName", str)
|
|
11
|
+
Version = NewType("Version", str)
|
|
12
|
+
|
|
13
|
+
type DependenciesRegistry = dict[DependencyName, Version]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def parse_from_pip(
|
|
17
|
+
workdir: pathlib.Path,
|
|
18
|
+
) -> DependenciesRegistry:
|
|
19
|
+
data = subprocess.run(
|
|
20
|
+
# uv pip list --outdated
|
|
21
|
+
["uv", "pip", "list", "--outdated"], # noqa: S607
|
|
22
|
+
check=True,
|
|
23
|
+
cwd=workdir,
|
|
24
|
+
capture_output=True,
|
|
25
|
+
text=True,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
# Note: It doesn't provide all necessary dependencies.
|
|
29
|
+
|
|
30
|
+
# Example output:
|
|
31
|
+
# Package Version Latest Type
|
|
32
|
+
# -------------- ------- ------ -----
|
|
33
|
+
# anyio 4.11.0 4.12.0 wheel
|
|
34
|
+
# basedpyright 1.34.0 1.35.0 wheel
|
|
35
|
+
# beautifulsoup4 4.14.2 4.14.3 wheel
|
|
36
|
+
|
|
37
|
+
dependencies: DependenciesRegistry = {}
|
|
38
|
+
# Note: skip the header line
|
|
39
|
+
for line in data.stdout.splitlines()[2:]:
|
|
40
|
+
package, _current_version, latest, _type = line.split()
|
|
41
|
+
dependencies[DependencyName(package)] = Version(latest)
|
|
42
|
+
|
|
43
|
+
return dependencies
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def parse_from_uv_tree(
|
|
47
|
+
workdir: pathlib.Path,
|
|
48
|
+
) -> DependenciesRegistry:
|
|
49
|
+
# Note: use this because it has better checks for outdated.
|
|
50
|
+
# For example, `uv pip list --outdated` may miss some packages.
|
|
51
|
+
# But we need extra parsing.
|
|
52
|
+
|
|
53
|
+
data = subprocess.run(
|
|
54
|
+
# uv tree --outdated --depth 1 --color never
|
|
55
|
+
[ # noqa: S607
|
|
56
|
+
"uv",
|
|
57
|
+
"tree",
|
|
58
|
+
"--outdated",
|
|
59
|
+
"--depth",
|
|
60
|
+
"1",
|
|
61
|
+
"--color",
|
|
62
|
+
"never",
|
|
63
|
+
],
|
|
64
|
+
check=True,
|
|
65
|
+
cwd=workdir,
|
|
66
|
+
capture_output=True,
|
|
67
|
+
text=True,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
# Example output:
|
|
71
|
+
# Resolved 129 packages in 0.61ms
|
|
72
|
+
# python-lab v0.1.0
|
|
73
|
+
# ├── jupyter v1.0.0 (latest: v1.1.1)
|
|
74
|
+
# ├── matplotlib v3.10.7
|
|
75
|
+
# ├── pydantic v2.12.5
|
|
76
|
+
# └── pytest v9.0.2 (group: dev)
|
|
77
|
+
|
|
78
|
+
# Parse
|
|
79
|
+
|
|
80
|
+
dependencies: DependenciesRegistry = {}
|
|
81
|
+
for line in data.stdout.splitlines():
|
|
82
|
+
line = line.strip() # noqa: PLW2901
|
|
83
|
+
if not line:
|
|
84
|
+
continue
|
|
85
|
+
|
|
86
|
+
if line[0].isalpha():
|
|
87
|
+
# Skip the root package line
|
|
88
|
+
continue
|
|
89
|
+
|
|
90
|
+
parts = line.split(maxsplit=3)
|
|
91
|
+
# Skip decorative prefix
|
|
92
|
+
parts.pop(0)
|
|
93
|
+
|
|
94
|
+
dependency_name = DependencyName(parts.pop(0))
|
|
95
|
+
|
|
96
|
+
# Get version
|
|
97
|
+
version_candidate = parts.pop(0)
|
|
98
|
+
|
|
99
|
+
if len(parts) == 1:
|
|
100
|
+
version_parts = parts.pop(0).split(") (")
|
|
101
|
+
|
|
102
|
+
# Check another version with the "latest:" marker
|
|
103
|
+
for part in version_parts:
|
|
104
|
+
part = part.strip("(").rstrip(")") # noqa: PLW2901
|
|
105
|
+
|
|
106
|
+
sub_parts = part.split(":", 1)
|
|
107
|
+
|
|
108
|
+
if len(sub_parts) == 2 and sub_parts[0] == "latest": # noqa: PLR2004
|
|
109
|
+
version_candidate = sub_parts[1]
|
|
110
|
+
|
|
111
|
+
elif len(parts) > 1:
|
|
112
|
+
msg = f"Unexpected line format: {line}"
|
|
113
|
+
raise ValueError(msg)
|
|
114
|
+
|
|
115
|
+
# Strip "v" prefix
|
|
116
|
+
version_candidate = version_candidate.strip().lstrip("v")
|
|
117
|
+
|
|
118
|
+
dependencies[dependency_name] = Version(version_candidate)
|
|
119
|
+
|
|
120
|
+
return dependencies
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def parse_from_uv_lock(
|
|
124
|
+
workdir: pathlib.Path,
|
|
125
|
+
) -> DependenciesRegistry:
|
|
126
|
+
uv_lock_path = get_and_check_path_to_uv_lock(workdir)
|
|
127
|
+
|
|
128
|
+
data = load_toml(uv_lock_path)
|
|
129
|
+
|
|
130
|
+
dependencies: DependenciesRegistry = {}
|
|
131
|
+
for package in data.get("package", []): # pyright: ignore[reportUnknownVariableType, reportUnknownMemberType]
|
|
132
|
+
dependencies[DependencyName(package["name"])] = Version(package["version"]) # pyright: ignore[reportUnknownArgumentType] # pyright: ignore[reportUnknownVariableType, reportUnknownMemberType]
|
|
133
|
+
|
|
134
|
+
return dependencies
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def get_deps_from_project(
|
|
138
|
+
workdir: pathlib.Path,
|
|
139
|
+
) -> DependenciesRegistry:
|
|
140
|
+
"""Get dependencies info."""
|
|
141
|
+
return parse_from_uv_lock(workdir=workdir)
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import TYPE_CHECKING
|
|
3
|
+
|
|
4
|
+
from uv_upx.services.normalize_and_check_path_to_pyproject import NAME_OF_PYPROJECT_FILE
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
import pathlib
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def get_pyproject_paths_by_globs(
|
|
11
|
+
base_path: pathlib.Path,
|
|
12
|
+
patterns: list[str],
|
|
13
|
+
) -> set[pathlib.Path]:
|
|
14
|
+
paths: set[pathlib.Path] = set()
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
for pattern in patterns:
|
|
19
|
+
for path in base_path.glob(pattern):
|
|
20
|
+
if path.is_file():
|
|
21
|
+
if path.name == NAME_OF_PYPROJECT_FILE:
|
|
22
|
+
paths.add(path)
|
|
23
|
+
elif path.is_dir():
|
|
24
|
+
path_candidate = path / NAME_OF_PYPROJECT_FILE
|
|
25
|
+
if path_candidate.exists():
|
|
26
|
+
paths.add(path_candidate)
|
|
27
|
+
else:
|
|
28
|
+
# sourcery skip: remove-redundant-pass
|
|
29
|
+
# Explicit skip for other cases
|
|
30
|
+
logger.warning(f"Skipping {path.as_uri()}: not a file or directory.")
|
|
31
|
+
|
|
32
|
+
return paths
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import TYPE_CHECKING, Any
|
|
3
|
+
|
|
4
|
+
from uv_upx.services.save_load_toml import save_toml
|
|
5
|
+
from uv_upx.services.update_dependencies import ChangesList, IncludedDependencyGroup, update_dependencies
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from tomlkit import TOMLDocument
|
|
9
|
+
|
|
10
|
+
from uv_upx.services.get_all_pyprojects import PyProjectsRegistry, PyProjectWrapper
|
|
11
|
+
from uv_upx.services.get_deps_from_project import DependenciesRegistry
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# https://docs.astral.sh/uv/concepts/projects/dependencies/
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def handle_main_dependency_group(
|
|
18
|
+
*,
|
|
19
|
+
data: TOMLDocument,
|
|
20
|
+
dependencies_registry: DependenciesRegistry,
|
|
21
|
+
verbose: bool = False,
|
|
22
|
+
) -> ChangesList:
|
|
23
|
+
"""Check the main dependencies group."""
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
project: dict[Any, Any] = data.get("project", {}) # pyright: ignore[reportUnknownMemberType, reportUnknownVariableType]
|
|
27
|
+
if not isinstance(project, dict):
|
|
28
|
+
logger.warning("No [project] table found in pyproject.toml")
|
|
29
|
+
return []
|
|
30
|
+
|
|
31
|
+
dependencies: list[str] = project.get("dependencies", []) # pyright: ignore[reportUnknownMemberType, reportUnknownVariableType]
|
|
32
|
+
if not isinstance(dependencies, list):
|
|
33
|
+
logger.warning("No [project].dependencies found in pyproject.toml")
|
|
34
|
+
return []
|
|
35
|
+
|
|
36
|
+
return update_dependencies(
|
|
37
|
+
deps_sequence_from_config=dependencies, # pyright: ignore[reportUnknownArgumentType]
|
|
38
|
+
dependencies_registry=dependencies_registry,
|
|
39
|
+
verbose=verbose,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def handle_dependency_groups(
|
|
44
|
+
*,
|
|
45
|
+
data: TOMLDocument,
|
|
46
|
+
dependencies_registry: DependenciesRegistry,
|
|
47
|
+
verbose: bool = False,
|
|
48
|
+
) -> ChangesList:
|
|
49
|
+
"""Check the dependency-groups table.
|
|
50
|
+
|
|
51
|
+
https://docs.astral.sh/uv/concepts/projects/dependencies/#development-dependencies
|
|
52
|
+
"""
|
|
53
|
+
return handle_dependency_groups_inner(
|
|
54
|
+
groups=data.get("dependency-groups", {}), # pyright: ignore[reportUnknownVariableType, reportUnknownMemberType]
|
|
55
|
+
dependencies_registry=dependencies_registry,
|
|
56
|
+
verbose=verbose,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def handle_optional_dependencies(
|
|
61
|
+
*,
|
|
62
|
+
data: TOMLDocument,
|
|
63
|
+
dependencies_registry: DependenciesRegistry,
|
|
64
|
+
verbose: bool = False,
|
|
65
|
+
) -> ChangesList:
|
|
66
|
+
"""Check the project.optional-dependencies table.
|
|
67
|
+
|
|
68
|
+
https://docs.astral.sh/uv/concepts/projects/dependencies/#optional-dependencies
|
|
69
|
+
"""
|
|
70
|
+
return handle_dependency_groups_inner(
|
|
71
|
+
groups=data.get("project", {}).get("optional-dependencies", {}), # pyright: ignore[reportUnknownVariableType, reportUnknownMemberType]
|
|
72
|
+
dependencies_registry=dependencies_registry,
|
|
73
|
+
verbose=verbose,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def handle_dependency_groups_inner(
|
|
78
|
+
*,
|
|
79
|
+
groups: Any, # noqa: ANN401
|
|
80
|
+
dependencies_registry: DependenciesRegistry,
|
|
81
|
+
verbose: bool = False,
|
|
82
|
+
) -> ChangesList:
|
|
83
|
+
"""Check the dependency-groups table.
|
|
84
|
+
|
|
85
|
+
https://docs.astral.sh/uv/concepts/projects/dependencies/#development-dependencies
|
|
86
|
+
|
|
87
|
+
https://docs.astral.sh/uv/concepts/projects/dependencies/#optional-dependencies
|
|
88
|
+
"""
|
|
89
|
+
logger = logging.getLogger(__name__)
|
|
90
|
+
|
|
91
|
+
if not isinstance(groups, dict):
|
|
92
|
+
return []
|
|
93
|
+
|
|
94
|
+
dependency_groups: dict[str, Any] = groups # pyright: ignore[reportUnknownVariableType]
|
|
95
|
+
|
|
96
|
+
changes: ChangesList = []
|
|
97
|
+
for _group_name, _group_val in dependency_groups.items():
|
|
98
|
+
if verbose:
|
|
99
|
+
logger.info(f"Checking group: {_group_name}")
|
|
100
|
+
|
|
101
|
+
if not isinstance(_group_val, list):
|
|
102
|
+
continue
|
|
103
|
+
|
|
104
|
+
group_val: list[str | IncludedDependencyGroup] = _group_val # pyright: ignore[reportUnknownVariableType]
|
|
105
|
+
|
|
106
|
+
changes_local = update_dependencies(
|
|
107
|
+
deps_sequence_from_config=group_val,
|
|
108
|
+
dependencies_registry=dependencies_registry,
|
|
109
|
+
verbose=verbose,
|
|
110
|
+
)
|
|
111
|
+
changes.extend(changes_local)
|
|
112
|
+
|
|
113
|
+
return changes
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def handle_py_project(
|
|
117
|
+
*,
|
|
118
|
+
dependencies_registry: DependenciesRegistry,
|
|
119
|
+
pyproject_wrapper: PyProjectWrapper,
|
|
120
|
+
#
|
|
121
|
+
dry_run: bool,
|
|
122
|
+
verbose: bool,
|
|
123
|
+
) -> ChangesList:
|
|
124
|
+
"""Handle a single pyproject.toml file."""
|
|
125
|
+
logger = logging.getLogger(__name__)
|
|
126
|
+
|
|
127
|
+
data = pyproject_wrapper.data
|
|
128
|
+
|
|
129
|
+
changes: ChangesList = []
|
|
130
|
+
|
|
131
|
+
changes.extend(
|
|
132
|
+
handle_main_dependency_group(
|
|
133
|
+
data=data,
|
|
134
|
+
dependencies_registry=dependencies_registry,
|
|
135
|
+
verbose=verbose,
|
|
136
|
+
),
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
changes.extend(
|
|
140
|
+
handle_dependency_groups(
|
|
141
|
+
data=data,
|
|
142
|
+
dependencies_registry=dependencies_registry,
|
|
143
|
+
verbose=verbose,
|
|
144
|
+
),
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
changes.extend(
|
|
148
|
+
handle_optional_dependencies(
|
|
149
|
+
data=data,
|
|
150
|
+
dependencies_registry=dependencies_registry,
|
|
151
|
+
verbose=verbose,
|
|
152
|
+
),
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
if changes:
|
|
156
|
+
if dry_run:
|
|
157
|
+
logger.info(f"[Dry Run] Changes detected in {pyproject_wrapper.path.as_uri()}, but not saving.")
|
|
158
|
+
else:
|
|
159
|
+
save_toml(pyproject_wrapper.path, data)
|
|
160
|
+
logger.info(f"Saved changes to {pyproject_wrapper.path.as_uri()}")
|
|
161
|
+
|
|
162
|
+
for change in changes:
|
|
163
|
+
logger.info(f" {change}")
|
|
164
|
+
|
|
165
|
+
return changes
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def handle_py_projects(
|
|
169
|
+
*,
|
|
170
|
+
dependencies_registry: DependenciesRegistry,
|
|
171
|
+
py_projects: PyProjectsRegistry,
|
|
172
|
+
#
|
|
173
|
+
dry_run: bool,
|
|
174
|
+
verbose: bool,
|
|
175
|
+
) -> ChangesList:
|
|
176
|
+
"""Handle multiple pyproject.toml files."""
|
|
177
|
+
changes: ChangesList = []
|
|
178
|
+
for py_project in py_projects.items:
|
|
179
|
+
changes_local = handle_py_project(
|
|
180
|
+
dependencies_registry=dependencies_registry,
|
|
181
|
+
pyproject_wrapper=py_project,
|
|
182
|
+
#
|
|
183
|
+
dry_run=dry_run,
|
|
184
|
+
verbose=verbose,
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
changes.extend(changes_local)
|
|
188
|
+
|
|
189
|
+
return changes
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import pathlib
|
|
2
|
+
from typing import Final
|
|
3
|
+
|
|
4
|
+
NAME_OF_PYPROJECT_FILE: Final[str] = "pyproject.toml"
|
|
5
|
+
|
|
6
|
+
NAME_OF_UV_LOCK_FILE: Final[str] = "uv.lock"
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def normalize_and_check_path_to_project_root(path: pathlib.Path | None) -> pathlib.Path:
|
|
10
|
+
if path is None:
|
|
11
|
+
path = pathlib.Path.cwd()
|
|
12
|
+
|
|
13
|
+
if not path.is_dir():
|
|
14
|
+
msg = f"Path {path} is not a directory."
|
|
15
|
+
raise NotADirectoryError(msg)
|
|
16
|
+
|
|
17
|
+
if not path.exists():
|
|
18
|
+
msg = f"Path {path} does not exist."
|
|
19
|
+
raise FileNotFoundError(msg)
|
|
20
|
+
|
|
21
|
+
return path
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def get_and_check_path_to_pyproject(path: pathlib.Path) -> pathlib.Path:
|
|
25
|
+
path = path / NAME_OF_PYPROJECT_FILE
|
|
26
|
+
|
|
27
|
+
if not path.exists():
|
|
28
|
+
msg = f"Path {path} does not exist."
|
|
29
|
+
raise FileNotFoundError(msg)
|
|
30
|
+
|
|
31
|
+
return path
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def get_and_check_path_to_uv_lock(path: pathlib.Path) -> pathlib.Path:
|
|
35
|
+
path = path / NAME_OF_UV_LOCK_FILE
|
|
36
|
+
|
|
37
|
+
if not path.exists():
|
|
38
|
+
msg = f"Path {path} does not exist."
|
|
39
|
+
raise FileNotFoundError(msg)
|
|
40
|
+
|
|
41
|
+
return path
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
from uv_upx.models.dependency_parsed import DependencyParsed, VersionConstraint
|
|
2
|
+
from uv_upx.services.get_deps_from_project import DependencyName
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def parse_dependency( # noqa: C901, PLR0912, PLR0915
|
|
6
|
+
dependency_string: str,
|
|
7
|
+
) -> DependencyParsed: # sourcery skip: low-code-quality
|
|
8
|
+
dependency_string = dependency_string.strip()
|
|
9
|
+
if not dependency_string:
|
|
10
|
+
msg = "Empty dependency string"
|
|
11
|
+
raise ValueError(msg)
|
|
12
|
+
|
|
13
|
+
# split off environment marker (after ';')
|
|
14
|
+
parts = dependency_string.split(";", 1)
|
|
15
|
+
main = parts[0].strip()
|
|
16
|
+
marker = parts[1].strip() if len(parts) > 1 else None
|
|
17
|
+
|
|
18
|
+
# parse name and extras
|
|
19
|
+
name = main
|
|
20
|
+
extras: list[str] | None = None
|
|
21
|
+
version_part = ""
|
|
22
|
+
|
|
23
|
+
if "[" in main:
|
|
24
|
+
i = main.find("[")
|
|
25
|
+
j = main.find("]", i)
|
|
26
|
+
if j == -1:
|
|
27
|
+
msg = f"Invalid dependency string: {main}"
|
|
28
|
+
raise ValueError(msg)
|
|
29
|
+
name = main[:i].strip()
|
|
30
|
+
extras_str = main[i + 1 : j].strip()
|
|
31
|
+
extras = [e.strip() for e in extras_str.split(",") if e.strip()] or None
|
|
32
|
+
version_part = main[j + 1 :].strip()
|
|
33
|
+
else:
|
|
34
|
+
# try to locate the start of version/operator tokens
|
|
35
|
+
ops = ["===", "==", "~=", "!=", "<=", ">=", "<", ">", "@"]
|
|
36
|
+
first_pos = None
|
|
37
|
+
for op in ops:
|
|
38
|
+
idx = main.find(op)
|
|
39
|
+
if idx != -1 and (first_pos is None or idx < first_pos):
|
|
40
|
+
first_pos = idx
|
|
41
|
+
if first_pos is not None:
|
|
42
|
+
name = main[:first_pos].strip()
|
|
43
|
+
version_part = main[first_pos:].strip()
|
|
44
|
+
else:
|
|
45
|
+
name = main.strip()
|
|
46
|
+
version_part = ""
|
|
47
|
+
|
|
48
|
+
if not name:
|
|
49
|
+
msg = f"Invalid dependency string: {main}"
|
|
50
|
+
raise ValueError(msg)
|
|
51
|
+
|
|
52
|
+
# parse version constraints (comma separated)
|
|
53
|
+
version_constraints: list[VersionConstraint] = []
|
|
54
|
+
if version_part:
|
|
55
|
+
# do not attempt to parse direct URL / VCS refs (start with '@')
|
|
56
|
+
if version_part.startswith("@"):
|
|
57
|
+
msg = f"Invalid dependency string: {main}"
|
|
58
|
+
raise ValueError(msg)
|
|
59
|
+
|
|
60
|
+
raw_parts = [p.strip() for p in version_part.split(",") if p.strip()]
|
|
61
|
+
op_candidates = ["===", "==", "~=", "!=", "<=", ">=", "<", ">"]
|
|
62
|
+
for rp in raw_parts:
|
|
63
|
+
token = rp.lstrip()
|
|
64
|
+
matched = False
|
|
65
|
+
for op in op_candidates:
|
|
66
|
+
if token.startswith(op):
|
|
67
|
+
ver = token[len(op) :].strip().strip("\"'")
|
|
68
|
+
version_constraints.append(VersionConstraint(operator=op, version=ver))
|
|
69
|
+
matched = True
|
|
70
|
+
break
|
|
71
|
+
if not matched:
|
|
72
|
+
# allow a bit of tolerance for leading space before the operator
|
|
73
|
+
stripped = token.lstrip()
|
|
74
|
+
for op in op_candidates:
|
|
75
|
+
if stripped.startswith(op):
|
|
76
|
+
ver = stripped[len(op) :].strip().strip("\"'")
|
|
77
|
+
version_constraints.append(VersionConstraint(operator=op, version=ver))
|
|
78
|
+
matched = True
|
|
79
|
+
break
|
|
80
|
+
if not matched:
|
|
81
|
+
msg = f"Invalid dependency string: {main}"
|
|
82
|
+
raise ValueError(msg)
|
|
83
|
+
|
|
84
|
+
return DependencyParsed(
|
|
85
|
+
dependency_name=DependencyName(name),
|
|
86
|
+
extras=extras,
|
|
87
|
+
version_constraints=version_constraints,
|
|
88
|
+
marker=marker,
|
|
89
|
+
)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from typing import TYPE_CHECKING
|
|
2
|
+
|
|
3
|
+
from uv_upx.services.run_uv_lock import run_uv_sync_frozen
|
|
4
|
+
from uv_upx.services.save_load_toml import save_toml
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
import pathlib
|
|
8
|
+
|
|
9
|
+
from tomlkit import TOMLDocument
|
|
10
|
+
|
|
11
|
+
from uv_upx.services.get_all_pyprojects import PyProjectsRegistry
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def rollback_updater(
|
|
15
|
+
*,
|
|
16
|
+
uv_lock_path: pathlib.Path,
|
|
17
|
+
uv_lock_data: TOMLDocument,
|
|
18
|
+
#
|
|
19
|
+
py_projects: PyProjectsRegistry,
|
|
20
|
+
) -> None:
|
|
21
|
+
save_toml(uv_lock_path, uv_lock_data)
|
|
22
|
+
for py_project in py_projects.items:
|
|
23
|
+
save_toml(py_project.path, py_project.data)
|
|
24
|
+
|
|
25
|
+
run_uv_sync_frozen(workdir=uv_lock_path.parent)
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import copy
|
|
2
|
+
import logging
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
from uv_upx.services.get_all_pyprojects import get_all_pyprojects
|
|
6
|
+
from uv_upx.services.get_deps_from_project import get_deps_from_project
|
|
7
|
+
from uv_upx.services.handle_groups import handle_py_projects
|
|
8
|
+
from uv_upx.services.normalize_and_check_path_to_pyproject import (
|
|
9
|
+
get_and_check_path_to_uv_lock,
|
|
10
|
+
)
|
|
11
|
+
from uv_upx.services.rollback_updater import rollback_updater
|
|
12
|
+
from uv_upx.services.run_uv_lock import (
|
|
13
|
+
run_uv_sync,
|
|
14
|
+
run_uv_sync_upgrade,
|
|
15
|
+
)
|
|
16
|
+
from uv_upx.services.save_load_toml import load_toml
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
import pathlib
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def run_updater(
|
|
23
|
+
*,
|
|
24
|
+
project_root_path: pathlib.Path,
|
|
25
|
+
dry_run: bool = False,
|
|
26
|
+
verbose: bool = False,
|
|
27
|
+
) -> None:
|
|
28
|
+
logger = logging.getLogger(__name__)
|
|
29
|
+
|
|
30
|
+
uv_lock_path = get_and_check_path_to_uv_lock(project_root_path)
|
|
31
|
+
uv_lock_data = load_toml(uv_lock_path)
|
|
32
|
+
uv_lock_data_copy = copy.deepcopy(uv_lock_data)
|
|
33
|
+
|
|
34
|
+
py_projects = get_all_pyprojects(project_root_path)
|
|
35
|
+
if verbose:
|
|
36
|
+
logger.info(f"Found {len(py_projects.items)} pyproject.toml files in the workspace.")
|
|
37
|
+
for py_project in py_projects.items:
|
|
38
|
+
logger.info(f" {py_project.path.as_uri()}")
|
|
39
|
+
|
|
40
|
+
py_projects_copy = copy.deepcopy(py_projects)
|
|
41
|
+
|
|
42
|
+
is_rollback_needed = dry_run
|
|
43
|
+
rollback_message = "Rolling back to previous state because dry run is enabled."
|
|
44
|
+
|
|
45
|
+
try:
|
|
46
|
+
# Because we want to check build problems also.
|
|
47
|
+
run_uv_sync_upgrade(workdir=project_root_path)
|
|
48
|
+
|
|
49
|
+
dependencies_registry = get_deps_from_project(workdir=project_root_path)
|
|
50
|
+
|
|
51
|
+
if handle_py_projects(
|
|
52
|
+
py_projects=py_projects,
|
|
53
|
+
dependencies_registry=dependencies_registry,
|
|
54
|
+
#
|
|
55
|
+
dry_run=dry_run,
|
|
56
|
+
verbose=verbose,
|
|
57
|
+
):
|
|
58
|
+
logger.info("Updated pyproject.toml files successfully.")
|
|
59
|
+
|
|
60
|
+
if dry_run:
|
|
61
|
+
logger.info("Dry run. No changes were made.")
|
|
62
|
+
else:
|
|
63
|
+
# run_uv_lock(workdir=project_root_path)
|
|
64
|
+
# logger.info("Updated dependencies successfully.")
|
|
65
|
+
|
|
66
|
+
# Because we want to re-check that all is ok.
|
|
67
|
+
run_uv_sync(workdir=project_root_path)
|
|
68
|
+
logger.info("Synced dependencies successfully.")
|
|
69
|
+
|
|
70
|
+
else:
|
|
71
|
+
msg = "No important changes detected. Rolling back to previous state."
|
|
72
|
+
logger.info(msg)
|
|
73
|
+
is_rollback_needed = True
|
|
74
|
+
rollback_message = msg
|
|
75
|
+
|
|
76
|
+
except Exception as e: # noqa: BLE001
|
|
77
|
+
msg = f"Failed to update dependencies: '{e}' Rolling back to previous state."
|
|
78
|
+
logger.error(msg) # noqa: TRY400
|
|
79
|
+
is_rollback_needed = True
|
|
80
|
+
rollback_message = msg
|
|
81
|
+
|
|
82
|
+
try:
|
|
83
|
+
if is_rollback_needed:
|
|
84
|
+
rollback_updater(
|
|
85
|
+
uv_lock_path=uv_lock_path,
|
|
86
|
+
uv_lock_data=uv_lock_data_copy,
|
|
87
|
+
#
|
|
88
|
+
py_projects=py_projects_copy,
|
|
89
|
+
)
|
|
90
|
+
logger.info(rollback_message)
|
|
91
|
+
except Exception as e: # noqa: BLE001
|
|
92
|
+
msg = f"Failed to rollback: '{e}'"
|
|
93
|
+
logger.error(msg) # noqa: TRY400
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
from typing import TYPE_CHECKING
|
|
3
|
+
|
|
4
|
+
if TYPE_CHECKING:
|
|
5
|
+
import pathlib
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class UnresolvedDependencyError(Exception):
|
|
9
|
+
pass
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def run_uv_lock_upgrade(workdir: pathlib.Path) -> None:
|
|
13
|
+
try:
|
|
14
|
+
subprocess.run(
|
|
15
|
+
# uv lock --upgrade
|
|
16
|
+
["uv", "lock", "--upgrade"], # noqa: S607
|
|
17
|
+
check=True,
|
|
18
|
+
cwd=workdir,
|
|
19
|
+
)
|
|
20
|
+
except subprocess.CalledProcessError as e:
|
|
21
|
+
msg = "Failed to resolve dependencies with 'uv lock'. Please check your dependency specifications."
|
|
22
|
+
raise UnresolvedDependencyError(
|
|
23
|
+
msg,
|
|
24
|
+
) from e
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def run_uv_lock(workdir: pathlib.Path) -> None:
|
|
28
|
+
try:
|
|
29
|
+
subprocess.run(
|
|
30
|
+
# uv lock
|
|
31
|
+
["uv", "lock"], # noqa: S607
|
|
32
|
+
check=True,
|
|
33
|
+
cwd=workdir,
|
|
34
|
+
)
|
|
35
|
+
except subprocess.CalledProcessError as e:
|
|
36
|
+
msg = "Failed to resolve dependencies with 'uv lock'. Please check your dependency specifications."
|
|
37
|
+
raise UnresolvedDependencyError(
|
|
38
|
+
msg,
|
|
39
|
+
) from e
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def run_uv_sync(workdir: pathlib.Path) -> None:
|
|
43
|
+
try:
|
|
44
|
+
subprocess.run(
|
|
45
|
+
# uv sync --all-groups --all-extras --all-packages
|
|
46
|
+
["uv", "sync", "--all-groups", "--all-extras", "--all-packages"], # noqa: S607
|
|
47
|
+
check=True,
|
|
48
|
+
cwd=workdir,
|
|
49
|
+
)
|
|
50
|
+
except subprocess.CalledProcessError as e:
|
|
51
|
+
msg = "Failed to sync dependencies with 'uv sync'. Please check your dependency specifications."
|
|
52
|
+
raise UnresolvedDependencyError(
|
|
53
|
+
msg,
|
|
54
|
+
) from e
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def run_uv_sync_upgrade(workdir: pathlib.Path) -> None:
|
|
58
|
+
# Note: Better to use this for new dependencies.
|
|
59
|
+
# Because it uses a better resolver for conflicts.
|
|
60
|
+
try:
|
|
61
|
+
subprocess.run(
|
|
62
|
+
# uv sync --all-groups --all-extras --all-packages --upgrade
|
|
63
|
+
["uv", "sync", "--all-groups", "--all-extras", "--all-packages", "--upgrade"], # noqa: S607
|
|
64
|
+
check=True,
|
|
65
|
+
cwd=workdir,
|
|
66
|
+
)
|
|
67
|
+
except subprocess.CalledProcessError as e:
|
|
68
|
+
msg = "Failed to sync dependencies with 'uv sync'. Please check your dependency specifications."
|
|
69
|
+
raise UnresolvedDependencyError(
|
|
70
|
+
msg,
|
|
71
|
+
) from e
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def run_uv_sync_frozen(workdir: pathlib.Path) -> None:
|
|
75
|
+
try:
|
|
76
|
+
subprocess.run(
|
|
77
|
+
# uv sync --all-groups --all-extras --all-packages --frozen
|
|
78
|
+
["uv", "sync", "--all-groups", "--all-extras", "--all-packages", "--frozen"], # noqa: S607
|
|
79
|
+
check=True,
|
|
80
|
+
cwd=workdir,
|
|
81
|
+
)
|
|
82
|
+
except subprocess.CalledProcessError as e:
|
|
83
|
+
msg = "Failed to sync dependencies with 'uv sync'. Please check your dependency specifications."
|
|
84
|
+
raise UnresolvedDependencyError(
|
|
85
|
+
msg,
|
|
86
|
+
) from e
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from typing import TYPE_CHECKING
|
|
2
|
+
|
|
3
|
+
import tomlkit
|
|
4
|
+
from tomlkit import TOMLDocument
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
# TODO: (?) Use also "https://github.com/galactixx/tomlkit-extras"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def load_toml(path: Path) -> TOMLDocument:
|
|
13
|
+
return tomlkit.parse(path.read_text(encoding="utf-8"))
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def save_toml(path: Path, data: TOMLDocument) -> None:
|
|
17
|
+
text = tomlkit.dumps(data) # pyright: ignore[reportUnknownMemberType]
|
|
18
|
+
path.write_text(text, encoding="utf-8")
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import copy
|
|
2
|
+
import logging
|
|
3
|
+
from typing import TYPE_CHECKING, Final
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel
|
|
6
|
+
|
|
7
|
+
from uv_upx.models.dependency_parsed import DependencyParsed
|
|
8
|
+
from uv_upx.services.parse_dependency import parse_dependency
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from uv_upx.services.get_deps_from_project import DependenciesRegistry
|
|
12
|
+
|
|
13
|
+
type IncludedDependencyGroup = dict[str, str]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ChangesItem(BaseModel):
|
|
17
|
+
from_item: DependencyParsed
|
|
18
|
+
to_item: DependencyParsed
|
|
19
|
+
|
|
20
|
+
def __str__(self) -> str:
|
|
21
|
+
return f"{self.from_item.get_full_spec()} -> {self.to_item.get_full_spec()}"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
type ChangesList = list[ChangesItem]
|
|
25
|
+
|
|
26
|
+
VERSION_OPERATORS_I_PUT_IF_DIFFERENT: Final[set[str]] = {">="}
|
|
27
|
+
|
|
28
|
+
VERSION_OPERATORS_I_EXPLICIT_IGNORE: Final[set[str]] = {"=="}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def update_dependencies( # noqa: C901, PLR0912
|
|
32
|
+
*,
|
|
33
|
+
deps_sequence_from_config: list[str] | list[str | IncludedDependencyGroup],
|
|
34
|
+
dependencies_registry: DependenciesRegistry,
|
|
35
|
+
verbose: bool = False,
|
|
36
|
+
) -> ChangesList:
|
|
37
|
+
"""Update the list of dependencies.
|
|
38
|
+
|
|
39
|
+
Update it in-place.
|
|
40
|
+
Because we want to preserve the comments.
|
|
41
|
+
"""
|
|
42
|
+
logger = logging.getLogger(__name__)
|
|
43
|
+
|
|
44
|
+
changes: ChangesList = []
|
|
45
|
+
for _index, dep in enumerate(deps_sequence_from_config):
|
|
46
|
+
if verbose:
|
|
47
|
+
logger.info(f"Parsing dependency: {dep}")
|
|
48
|
+
|
|
49
|
+
if not isinstance(dep, str):
|
|
50
|
+
if verbose:
|
|
51
|
+
# https://docs.astral.sh/uv/concepts/projects/dependencies/#nesting-groups
|
|
52
|
+
logger.warning(f"Skipping non-string dependency: {dep}")
|
|
53
|
+
continue
|
|
54
|
+
|
|
55
|
+
parsed = parse_dependency(dep)
|
|
56
|
+
|
|
57
|
+
if verbose:
|
|
58
|
+
logger.info(f"Parsed dependency: {parsed}")
|
|
59
|
+
|
|
60
|
+
try:
|
|
61
|
+
version_new = dependencies_registry[parsed.dependency_name]
|
|
62
|
+
except KeyError as e:
|
|
63
|
+
# Note: raise error, because it now we have all the dependencies in the registry.
|
|
64
|
+
msg = f"Dependency not found in the registry: {parsed.dependency_name}"
|
|
65
|
+
raise ValueError(msg) from e
|
|
66
|
+
|
|
67
|
+
parsed_original = copy.deepcopy(parsed)
|
|
68
|
+
|
|
69
|
+
is_has_changes = False
|
|
70
|
+
for version_constraint in parsed.version_constraints:
|
|
71
|
+
if version_constraint.operator in VERSION_OPERATORS_I_PUT_IF_DIFFERENT:
|
|
72
|
+
# TODO: (?) Implement better version comparison logic here
|
|
73
|
+
if version_new != version_constraint.version:
|
|
74
|
+
version_constraint.version = version_new
|
|
75
|
+
is_has_changes = True
|
|
76
|
+
|
|
77
|
+
elif version_constraint.operator in VERSION_OPERATORS_I_EXPLICIT_IGNORE:
|
|
78
|
+
if verbose:
|
|
79
|
+
msg = f"Explicitly ignoring version constraint {version_constraint}. Dependency: {dep}"
|
|
80
|
+
logger.info(msg)
|
|
81
|
+
else:
|
|
82
|
+
if verbose:
|
|
83
|
+
msg = f"Operator {version_constraint.operator} is not supported yet. Skip. Dependency: {dep}"
|
|
84
|
+
logger.warning(msg)
|
|
85
|
+
|
|
86
|
+
continue
|
|
87
|
+
|
|
88
|
+
if is_has_changes:
|
|
89
|
+
deps_sequence_from_config[_index] = parsed.get_full_spec()
|
|
90
|
+
changes.append(ChangesItem(from_item=parsed_original, to_item=parsed))
|
|
91
|
+
|
|
92
|
+
return changes
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: uv-upx
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Upgrade versions of dependencies in pyproject.toml files in uv-managed projects.
|
|
5
|
+
Keywords: uv,upgrade,dependencies,pyproject.toml,automation,cli
|
|
6
|
+
Author: Alirex
|
|
7
|
+
Author-email: Alirex <alirex.prime@gmail.com>
|
|
8
|
+
Requires-Dist: pydantic>=2.12.5
|
|
9
|
+
Requires-Dist: tomlkit>=0.13.3
|
|
10
|
+
Requires-Dist: typer>=0.20.0
|
|
11
|
+
Requires-Python: >=3.14
|
|
12
|
+
Project-URL: Homepage, https://github.com/Alirex/uv_upgrade
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
|
|
15
|
+
# Uv upgrade
|
|
16
|
+
|
|
17
|
+
Upgrade dependencies in `pyproject.toml` files with `uv`.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
# Install
|
|
22
|
+
|
|
23
|
+
For end-users.
|
|
24
|
+
|
|
25
|
+
Note: For developers, see the [dev](docs_extra/dev.md) section.
|
|
26
|
+
|
|
27
|
+
## Uv install or update
|
|
28
|
+
|
|
29
|
+
You need this for both usage and development.
|
|
30
|
+
|
|
31
|
+
https://docs.astral.sh/uv/getting-started/installation/
|
|
32
|
+
|
|
33
|
+
On Windows it can be better to use `Git Bash` terminal.
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
if ! command -v uv &> /dev/null; then
|
|
37
|
+
curl -LsSf https://astral.sh/uv/install.sh | sh;
|
|
38
|
+
else
|
|
39
|
+
uv self update;
|
|
40
|
+
fi
|
|
41
|
+
|
|
42
|
+
uv --version
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Install or update a project for system-wide usage
|
|
46
|
+
|
|
47
|
+
Run it from any directory, except the project directory.
|
|
48
|
+
|
|
49
|
+
```shell
|
|
50
|
+
if ! command -v uv-upgrade &> /dev/null; then
|
|
51
|
+
uv tool install git+https://github.com/Alirex/uv_upgrade
|
|
52
|
+
else
|
|
53
|
+
uv tool upgrade uv-upx
|
|
54
|
+
fi
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Or run with uvx:
|
|
58
|
+
|
|
59
|
+
```shell
|
|
60
|
+
uvx uv-upx
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Dev installation: [see here](docs_extra/dev.md#install-app-system-wide-in-development-mode)
|
|
64
|
+
|
|
65
|
+
## Check that project in the list
|
|
66
|
+
|
|
67
|
+
```shell
|
|
68
|
+
uv tool list --show-python
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Remove the project with all data
|
|
72
|
+
|
|
73
|
+
If you need to remove the project with all data, run this command from the project directory:
|
|
74
|
+
|
|
75
|
+
```shell
|
|
76
|
+
uv tool uninstall uv-upx
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
# Usage
|
|
82
|
+
|
|
83
|
+
## Run the tool in the folder with the pyproject.toml
|
|
84
|
+
|
|
85
|
+
After installation, you can run the tool from any directory.
|
|
86
|
+
|
|
87
|
+
```shell
|
|
88
|
+
uv-upgrade
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Also, you can run it with the `--help` flag with more details.
|
|
92
|
+
|
|
93
|
+
## Notes
|
|
94
|
+
|
|
95
|
+
### Uv as a source of truth
|
|
96
|
+
|
|
97
|
+
Run `uv sync --all-groups --all-extras --all-packages --upgrade`.
|
|
98
|
+
|
|
99
|
+
Get dependencies versions from `uv.lock`.
|
|
100
|
+
|
|
101
|
+
Put them in `pyproject.toml` files in related groups.
|
|
102
|
+
|
|
103
|
+
So, the main responsibility for dependencies resolution is on `uv`.
|
|
104
|
+
|
|
105
|
+
### Workspace support
|
|
106
|
+
|
|
107
|
+
It works with workspaces. At least for some basic cases.
|
|
108
|
+
|
|
109
|
+
### Updates simple dependencies
|
|
110
|
+
|
|
111
|
+
Updates `>=` dependencies. Like `bla>=2.0.0`.
|
|
112
|
+
|
|
113
|
+
### Skip pinned versions
|
|
114
|
+
|
|
115
|
+
It doesn't touch pinned versions. Like `bla==2.0.0`.
|
|
116
|
+
|
|
117
|
+
### Respect simple ranges
|
|
118
|
+
|
|
119
|
+
It respects simple dependency ranges. Like `bla>=1.0.0,<2.0.0`.
|
|
120
|
+
|
|
121
|
+
It moves the lower bound to the new version.
|
|
122
|
+
|
|
123
|
+
### Respect extras
|
|
124
|
+
|
|
125
|
+
It respects extras. Like `bla[dev]>=1.0.0;python_version>="3.14"`.
|
|
126
|
+
|
|
127
|
+
### Skip unhandled constraints
|
|
128
|
+
|
|
129
|
+
It skips unhandled and complex constraints. Like:
|
|
130
|
+
|
|
131
|
+
- `bla<=1.0.0`
|
|
132
|
+
- `bla>=2.0.0,!=1.5.0,<3.0.0`
|
|
133
|
+
|
|
134
|
+
### Style preservation
|
|
135
|
+
|
|
136
|
+
It preserves comments in the `pyproject.toml` file. Like here:
|
|
137
|
+
|
|
138
|
+
```toml
|
|
139
|
+
dependencies = [
|
|
140
|
+
# Better classes and data validation
|
|
141
|
+
"pydantic>=2.12.5",
|
|
142
|
+
# TOML parser and writer with preserved formatting
|
|
143
|
+
"tomlkit>=0.13.3",
|
|
144
|
+
# CLI app framework
|
|
145
|
+
"typer>=0.20.0",
|
|
146
|
+
]
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### Rollback
|
|
150
|
+
|
|
151
|
+
If something goes wrong, it rolls back the changes to the `pyproject.toml` and `uv.lock` files.
|
|
152
|
+
|
|
153
|
+
### Why?
|
|
154
|
+
|
|
155
|
+
I needed this for my own projects.
|
|
156
|
+
|
|
157
|
+
I know these issues:
|
|
158
|
+
|
|
159
|
+
- [Upgrade dependencies in pyproject.toml (uv upgrade)](https://github.com/astral-sh/uv/issues/6692)
|
|
160
|
+
- [What is the intended workflow for updating dependencies with uv?](ttps://github.com/astral-sh/uv/issues/6794)
|
|
161
|
+
|
|
162
|
+
But I didn't see enough progress.
|
|
163
|
+
|
|
164
|
+
So, I implemented this tool for my own usage.
|
|
165
|
+
|
|
166
|
+
Maybe it will be useful for someone else.
|
|
167
|
+
|
|
168
|
+
### Why not in Rust?
|
|
169
|
+
|
|
170
|
+
Because it's a temporary Proof-of-Concept.
|
|
171
|
+
|
|
172
|
+
Maybe it will be replaced by Rust in the future.
|
|
173
|
+
|
|
174
|
+
It uses type annotations anyway. For simpler migration.
|
|
175
|
+
|
|
176
|
+
---
|
|
177
|
+
|
|
178
|
+
# Dev
|
|
179
|
+
|
|
180
|
+
[Dev](docs_extra/dev.md)
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
# Other similar tools comparison
|
|
185
|
+
|
|
186
|
+
[Other similar tools comparison](docs_extra/other_similar_tools_comparison.md)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
uv_upx/__init__.py,sha256=rbBerQaGd0D2iPapShvgQ6lQlt88AQ872iv4obRBOi8,50
|
|
2
|
+
uv_upx/__main__.py,sha256=-BgNoaEyq21dKa_PHEODSvEnAq-IRAexGZR-xehbQNU,68
|
|
3
|
+
uv_upx/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
uv_upx/cli/main.py,sha256=DGpfzBxi9CP7J9VfnZcX4bg1Ol4H7aT25r7RHKlehg0,1044
|
|
5
|
+
uv_upx/logging_custom/__init__.py,sha256=IF9q4lMvyP24yXnj30ivZzX5RIw48RBRxhcWcQpmin4,74
|
|
6
|
+
uv_upx/logging_custom/init_logging.py,sha256=VlLhtOHd3YxfuDkWIptUYOlcfiyEW877yjqIA3xik60,176
|
|
7
|
+
uv_upx/main.py,sha256=eRHt38xqNaaWWYq-AAy3FXLq7pQF7PeFEjizWzZnVrw,130
|
|
8
|
+
uv_upx/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
9
|
+
uv_upx/models/dependency_parsed.py,sha256=z0jiVn_0FLxyjbcvBy1lmbmqcAWyPNk5LWkE-uTnkqQ,1026
|
|
10
|
+
uv_upx/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
11
|
+
uv_upx/services/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
12
|
+
uv_upx/services/get_all_pyprojects.py,sha256=vwHEKrQ1-QrgQL4L0Mr3BEOaY-BychbLxBhDPT2QHtQ,2346
|
|
13
|
+
uv_upx/services/get_deps_from_project.py,sha256=B5Fx0N2u7tGn-9Zt8qD4MIAdzeDLjQeSMeFJm2aiKuc,4057
|
|
14
|
+
uv_upx/services/get_pyproject_paths_by_globs.py,sha256=Es0rETkVZQUyB5P2SVFRKOll1mp_XOLN9dKvRfi-q44,977
|
|
15
|
+
uv_upx/services/handle_groups.py,sha256=EpRbK2rDo_CN0KS68_k_deLGS8Qhns6uo-9kwUilhpw,5647
|
|
16
|
+
uv_upx/services/normalize_and_check_path_to_pyproject.py,sha256=7VXmR1Sq9I5sTQdRV9pP82nHshqj6vfQTLCLsJWncvA,1007
|
|
17
|
+
uv_upx/services/parse_dependency.py,sha256=_Xgrys9oSyHPgGG3dV0tRWL7ihno1KTnCWkdCB_mpKA,3374
|
|
18
|
+
uv_upx/services/rollback_updater.py,sha256=0kl7uIt16NtYoOv86tmFyFzaaTAU9PlJKOIuKG-2SqE,630
|
|
19
|
+
uv_upx/services/run_updater.py,sha256=dLknXdYqXSaTEAQ2_DHtGTGz2KLUdyNbPjpNvwUCbRQ,3117
|
|
20
|
+
uv_upx/services/run_uv_lock.py,sha256=tPIjOEfYkpBb9RToNn2HHy4ozU0dg-z_Gw8joil6MNM,2786
|
|
21
|
+
uv_upx/services/save_load_toml.py,sha256=5GK0_CsqxAC8uSmqd-hkQGZnHYhuKeZNPi5JeJWgLgY,478
|
|
22
|
+
uv_upx/services/update_dependencies.py,sha256=yM_8mccwiQ42uLlfrgehSO7ROAnWWWTcs1aXTFm-r34,3150
|
|
23
|
+
uv_upx-0.2.0.dist-info/WHEEL,sha256=xDCZ-UyfvkGuEHPeI7BcJzYKIZzdqN8A8o1M5Om8IyA,79
|
|
24
|
+
uv_upx-0.2.0.dist-info/entry_points.txt,sha256=9zUzK7AuGeOfnZbapni_IeaqbXn6c3RfFDE9MHe4HFo,44
|
|
25
|
+
uv_upx-0.2.0.dist-info/METADATA,sha256=-R5INUX-wHGUtFUzir-rLA_PEO3Y5w5Bdh1E2LLLGLQ,3880
|
|
26
|
+
uv_upx-0.2.0.dist-info/RECORD,,
|