uv-refresh 0.1.0__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.
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: uv-refresh
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Baut die pyproject.toml eines uv-Projekts neu auf, damit alle Dependencies frisch aufgelöst und geupdatet werden
|
|
5
|
+
Keywords: uv,pyproject,dependencies,upgrade
|
|
6
|
+
Author: Zora
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
Requires-Dist: packaging>=26.3
|
|
9
|
+
Requires-Python: >=3.11
|
|
10
|
+
Project-URL: Homepage, https://github.com/Zorakidd/uv-refresh
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
|
|
13
|
+
# uv-refresh
|
|
14
|
+
|
|
15
|
+
Baut die `pyproject.toml` eines uv-Projekts neu auf, damit alle Dependencies
|
|
16
|
+
frisch aufgelöst werden statt alte Versionsbindungen mitzuschleppen.
|
|
17
|
+
|
|
18
|
+
## Ablauf
|
|
19
|
+
|
|
20
|
+
1. `pyproject.toml` lesen, Dependencies ohne Versionsangabe einsammeln
|
|
21
|
+
(Extras und Environment-Marker bleiben erhalten; `include-group`-Einträge
|
|
22
|
+
aus `dependency-groups` werden dabei aufgelöst, nicht verworfen)
|
|
23
|
+
2. `pyproject.toml` und `uv.lock` nach `.uv-refresh-backup/<zeitstempel>/` sichern
|
|
24
|
+
3. `uv init --bare` + `uv add <pakete>` in einem Temp-Verzeichnis neben dem
|
|
25
|
+
Projekt ausführen -- die echte `pyproject.toml` bleibt dabei unangetastet
|
|
26
|
+
4. Ergebnis atomar an die Stelle der alten `pyproject.toml`/`uv.lock` setzen
|
|
27
|
+
|
|
28
|
+
Schlägt ein Schritt fehl -- auch per Ctrl+C --, wurde die echte
|
|
29
|
+
`pyproject.toml` nie verändert, weil der komplette Aufbau im
|
|
30
|
+
Temp-Verzeichnis geschah. Das Backup liegt zusätzlich als Referenz bereit.
|
|
31
|
+
|
|
32
|
+
## Installation
|
|
33
|
+
|
|
34
|
+
uv tool install git+https://github.com/Zorakidd/uv-refresh
|
|
35
|
+
|
|
36
|
+
Oder ohne Installation, direkt aus dem Repo:
|
|
37
|
+
|
|
38
|
+
uvx --from git+https://github.com/Zorakidd/uv-refresh uv-refresh --dry-run
|
|
39
|
+
|
|
40
|
+
## Benutzung
|
|
41
|
+
|
|
42
|
+
uv-refresh --dry-run # nur anzeigen, nichts verändern
|
|
43
|
+
uv-refresh # mit Rückfrage durchziehen
|
|
44
|
+
uv-refresh -y # ohne Rückfrage
|
|
45
|
+
|
|
46
|
+
## Optionen
|
|
47
|
+
|
|
48
|
+
| Flag | Wirkung |
|
|
49
|
+
| --- | --- |
|
|
50
|
+
| `--path PFAD` | anderes Projektverzeichnis (Default: aktuelles) |
|
|
51
|
+
| `--dry-run` | zeigt nur, was passieren würde |
|
|
52
|
+
| `-y`, `--yes` | keine Rückfrage |
|
|
53
|
+
| `-v`, `--verbose` | die komplette neue `pyproject.toml` am Ende ausgeben |
|
|
54
|
+
| `--timeout SEK` | Timeout je `uv`-Aufruf, Default 300s |
|
|
55
|
+
| `--raw` | Pakete ganz ohne Versionsgrenze eintragen |
|
|
56
|
+
| `--bounds {lower,major,minor,exact}` | Art der Versionsgrenze, die `uv add` setzt |
|
|
57
|
+
| `--keep-lock` | `uv.lock` behalten (dann bevorzugt uv die alten Versionen) |
|
|
58
|
+
| `--no-groups` | optional-dependencies und dependency-groups ignorieren |
|
|
59
|
+
| `--drop-extras` | `fastapi[standard]` zu `fastapi` eindampfen |
|
|
60
|
+
| `--drop-markers` | Environment-Marker verwerfen |
|
|
61
|
+
| `--version` | Version von uv-refresh anzeigen |
|
|
62
|
+
|
|
63
|
+
## Achtung
|
|
64
|
+
|
|
65
|
+
Alles außer `[project]` und `[dependency-groups]` geht verloren, also
|
|
66
|
+
`[build-system]`, `[tool.ruff]`, `[project.scripts]` und so weiter. Das Tool
|
|
67
|
+
warnt vorher und listet die betroffenen Blöcke auf. Zurückkopieren musst du
|
|
68
|
+
sie von Hand aus dem Backup.
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# uv-refresh
|
|
2
|
+
|
|
3
|
+
Baut die `pyproject.toml` eines uv-Projekts neu auf, damit alle Dependencies
|
|
4
|
+
frisch aufgelöst werden statt alte Versionsbindungen mitzuschleppen.
|
|
5
|
+
|
|
6
|
+
## Ablauf
|
|
7
|
+
|
|
8
|
+
1. `pyproject.toml` lesen, Dependencies ohne Versionsangabe einsammeln
|
|
9
|
+
(Extras und Environment-Marker bleiben erhalten; `include-group`-Einträge
|
|
10
|
+
aus `dependency-groups` werden dabei aufgelöst, nicht verworfen)
|
|
11
|
+
2. `pyproject.toml` und `uv.lock` nach `.uv-refresh-backup/<zeitstempel>/` sichern
|
|
12
|
+
3. `uv init --bare` + `uv add <pakete>` in einem Temp-Verzeichnis neben dem
|
|
13
|
+
Projekt ausführen -- die echte `pyproject.toml` bleibt dabei unangetastet
|
|
14
|
+
4. Ergebnis atomar an die Stelle der alten `pyproject.toml`/`uv.lock` setzen
|
|
15
|
+
|
|
16
|
+
Schlägt ein Schritt fehl -- auch per Ctrl+C --, wurde die echte
|
|
17
|
+
`pyproject.toml` nie verändert, weil der komplette Aufbau im
|
|
18
|
+
Temp-Verzeichnis geschah. Das Backup liegt zusätzlich als Referenz bereit.
|
|
19
|
+
|
|
20
|
+
## Installation
|
|
21
|
+
|
|
22
|
+
uv tool install git+https://github.com/Zorakidd/uv-refresh
|
|
23
|
+
|
|
24
|
+
Oder ohne Installation, direkt aus dem Repo:
|
|
25
|
+
|
|
26
|
+
uvx --from git+https://github.com/Zorakidd/uv-refresh uv-refresh --dry-run
|
|
27
|
+
|
|
28
|
+
## Benutzung
|
|
29
|
+
|
|
30
|
+
uv-refresh --dry-run # nur anzeigen, nichts verändern
|
|
31
|
+
uv-refresh # mit Rückfrage durchziehen
|
|
32
|
+
uv-refresh -y # ohne Rückfrage
|
|
33
|
+
|
|
34
|
+
## Optionen
|
|
35
|
+
|
|
36
|
+
| Flag | Wirkung |
|
|
37
|
+
| --- | --- |
|
|
38
|
+
| `--path PFAD` | anderes Projektverzeichnis (Default: aktuelles) |
|
|
39
|
+
| `--dry-run` | zeigt nur, was passieren würde |
|
|
40
|
+
| `-y`, `--yes` | keine Rückfrage |
|
|
41
|
+
| `-v`, `--verbose` | die komplette neue `pyproject.toml` am Ende ausgeben |
|
|
42
|
+
| `--timeout SEK` | Timeout je `uv`-Aufruf, Default 300s |
|
|
43
|
+
| `--raw` | Pakete ganz ohne Versionsgrenze eintragen |
|
|
44
|
+
| `--bounds {lower,major,minor,exact}` | Art der Versionsgrenze, die `uv add` setzt |
|
|
45
|
+
| `--keep-lock` | `uv.lock` behalten (dann bevorzugt uv die alten Versionen) |
|
|
46
|
+
| `--no-groups` | optional-dependencies und dependency-groups ignorieren |
|
|
47
|
+
| `--drop-extras` | `fastapi[standard]` zu `fastapi` eindampfen |
|
|
48
|
+
| `--drop-markers` | Environment-Marker verwerfen |
|
|
49
|
+
| `--version` | Version von uv-refresh anzeigen |
|
|
50
|
+
|
|
51
|
+
## Achtung
|
|
52
|
+
|
|
53
|
+
Alles außer `[project]` und `[dependency-groups]` geht verloren, also
|
|
54
|
+
`[build-system]`, `[tool.ruff]`, `[project.scripts]` und so weiter. Das Tool
|
|
55
|
+
warnt vorher und listet die betroffenen Blöcke auf. Zurückkopieren musst du
|
|
56
|
+
sie von Hand aus dem Backup.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "uv-refresh"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Baut die pyproject.toml eines uv-Projekts neu auf, damit alle Dependencies frisch aufgelöst und geupdatet werden"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
keywords = [
|
|
8
|
+
"uv",
|
|
9
|
+
"pyproject",
|
|
10
|
+
"dependencies",
|
|
11
|
+
"upgrade",
|
|
12
|
+
]
|
|
13
|
+
requires-python = ">=3.11"
|
|
14
|
+
dependencies = ["packaging>=26.3"]
|
|
15
|
+
|
|
16
|
+
[[project.authors]]
|
|
17
|
+
name = "Zora"
|
|
18
|
+
|
|
19
|
+
[project.urls]
|
|
20
|
+
Homepage = "https://github.com/Zorakidd/uv-refresh"
|
|
21
|
+
|
|
22
|
+
[project.scripts]
|
|
23
|
+
uv-refresh = "uv_refresh.cli:main"
|
|
24
|
+
|
|
25
|
+
[build-system]
|
|
26
|
+
requires = ["uv_build>=0.12.5,<0.13.0"]
|
|
27
|
+
build-backend = "uv_build"
|
|
28
|
+
|
|
29
|
+
[dependency-groups]
|
|
30
|
+
dev = [
|
|
31
|
+
"pytest>=9.1.1",
|
|
32
|
+
"ruff>=0.16.3",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
[tool.ruff]
|
|
36
|
+
target-version = "py311"
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "uv-refresh"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Baut die pyproject.toml eines uv-Projekts neu auf, damit alle Dependencies frisch aufgelöst und geupdatet werden"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
authors = [{ name = "Zora" }]
|
|
8
|
+
keywords = ["uv", "pyproject", "dependencies", "upgrade"]
|
|
9
|
+
requires-python = ">=3.11"
|
|
10
|
+
dependencies = [
|
|
11
|
+
"packaging>=26.3",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
[project.urls]
|
|
15
|
+
Homepage = "https://github.com/Zorakidd/uv-refresh"
|
|
16
|
+
|
|
17
|
+
[project.scripts]
|
|
18
|
+
uv-refresh = "uv_refresh.cli:main"
|
|
19
|
+
|
|
20
|
+
[build-system]
|
|
21
|
+
requires = ["uv_build>=0.12.5,<0.13.0"]
|
|
22
|
+
build-backend = "uv_build"
|
|
23
|
+
|
|
24
|
+
[dependency-groups]
|
|
25
|
+
dev = [
|
|
26
|
+
"pytest>=9.1.1",
|
|
27
|
+
"ruff>=0.16.3",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
[tool.ruff]
|
|
31
|
+
target-version = "py311"
|
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
"""uv-refresh
|
|
2
|
+
==========
|
|
3
|
+
|
|
4
|
+
Baut die pyproject.toml eines uv-Projekts neu auf, damit alle Dependencies
|
|
5
|
+
frisch aufgeloest werden statt alte Versionsbindungen mitzuschleppen.
|
|
6
|
+
|
|
7
|
+
Ablauf:
|
|
8
|
+
1. pyproject.toml lesen und die Dependencies ohne Versionsangabe einsammeln
|
|
9
|
+
(Extras und Environment-Marker bleiben erhalten, siehe --drop-extras)
|
|
10
|
+
2. pyproject.toml + uv.lock in einen Backup-Ordner sichern
|
|
11
|
+
3. uv init --bare + uv add <namen> in einem Temp-Verzeichnis neben dem
|
|
12
|
+
Projekt ausfuehren -- die echte pyproject.toml bleibt dabei unangetastet
|
|
13
|
+
4. Ergebnis atomar an die Stelle der alten pyproject.toml/uv.lock setzen
|
|
14
|
+
|
|
15
|
+
Schlaegt irgendein Schritt fehl (auch per Ctrl+C), wurde die echte
|
|
16
|
+
pyproject.toml nie veraendert -- der komplette Aufbau geschah im
|
|
17
|
+
Temp-Verzeichnis. Das Backup bleibt zusaetzlich als Referenz liegen.
|
|
18
|
+
|
|
19
|
+
Benutzung:
|
|
20
|
+
uv-refresh # im Projektverzeichnis, mit Rueckfrage
|
|
21
|
+
uv-refresh --dry-run # nur zeigen, nichts anfassen
|
|
22
|
+
uv-refresh --raw # ganz ohne Versionsangabe eintragen
|
|
23
|
+
uv-refresh --path ../other # anderes Projektverzeichnis
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import argparse
|
|
29
|
+
import contextlib
|
|
30
|
+
import os
|
|
31
|
+
import re
|
|
32
|
+
import shutil
|
|
33
|
+
import subprocess
|
|
34
|
+
import sys
|
|
35
|
+
import tempfile
|
|
36
|
+
import tomllib
|
|
37
|
+
from datetime import UTC, datetime
|
|
38
|
+
from pathlib import Path
|
|
39
|
+
from typing import NoReturn
|
|
40
|
+
|
|
41
|
+
from . import __version__
|
|
42
|
+
|
|
43
|
+
try: # bevorzugt der offizielle PEP-508-Parser
|
|
44
|
+
from packaging.requirements import InvalidRequirement, Requirement
|
|
45
|
+
except ModuleNotFoundError: # Fallback, damit das Skript auch nackt laeuft
|
|
46
|
+
Requirement = None # type: ignore[assignment]
|
|
47
|
+
InvalidRequirement = ValueError # type: ignore[assignment,misc]
|
|
48
|
+
|
|
49
|
+
_SPEC_RE = re.compile(r"^\s*(?P<name>[A-Za-z0-9][A-Za-z0-9._-]*)\s*(?P<extras>\[[^\]]*\])?")
|
|
50
|
+
_VERSION_LINE_RE = re.compile(r'(?m)^version\s*=\s*"[^"]*"')
|
|
51
|
+
|
|
52
|
+
C_OK, C_WARN, C_ERR, C_DIM, C_OFF = "\033[32m", "\033[33m", "\033[31m", "\033[2m", "\033[0m"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _color_enabled(stream) -> bool:
|
|
56
|
+
return stream.isatty() and os.environ.get("NO_COLOR") is None
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def say(msg: str, color: str = "") -> None:
|
|
60
|
+
# Fehler/Warnungen auf stderr: sonst verschwinden sie beim Umleiten von
|
|
61
|
+
# stdout (z. B. 'uv-refresh --dry-run > log.txt') spurlos.
|
|
62
|
+
stream = sys.stderr if color in (C_WARN, C_ERR) else sys.stdout
|
|
63
|
+
text = f"{color}{msg}{C_OFF}" if color and _color_enabled(stream) else msg
|
|
64
|
+
print(text, file=stream)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def die(msg: str) -> NoReturn:
|
|
68
|
+
say(f"FEHLER: {msg}", C_ERR)
|
|
69
|
+
sys.exit(1)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def strip_version(spec: str, keep_extras: bool = True, keep_markers: bool = True) -> str | None:
|
|
73
|
+
"""Schneidet NUR die Versionsangabe ab.
|
|
74
|
+
|
|
75
|
+
'fastapi[standard]>=0.110; python_version<"3.13"'
|
|
76
|
+
-> 'fastapi[standard]; python_version < "3.13"'
|
|
77
|
+
|
|
78
|
+
Extras und Marker sind keine Versionsinformation: Extras bestimmen, WAS
|
|
79
|
+
installiert wird, Marker WO. Beides bleibt darum per Default erhalten.
|
|
80
|
+
Direkte Quellen ('paket @ git+https://...') bleiben komplett unveraendert,
|
|
81
|
+
sonst waere das Package hinterher nicht mehr auffindbar.
|
|
82
|
+
"""
|
|
83
|
+
spec = spec.strip()
|
|
84
|
+
if not spec:
|
|
85
|
+
return None
|
|
86
|
+
|
|
87
|
+
if Requirement is not None:
|
|
88
|
+
try:
|
|
89
|
+
req = Requirement(spec)
|
|
90
|
+
except InvalidRequirement:
|
|
91
|
+
req = None
|
|
92
|
+
if req is not None:
|
|
93
|
+
if req.url:
|
|
94
|
+
return spec
|
|
95
|
+
out = req.name
|
|
96
|
+
if keep_extras and req.extras:
|
|
97
|
+
out += "[" + ",".join(sorted(req.extras)) + "]"
|
|
98
|
+
if keep_markers and req.marker:
|
|
99
|
+
out += f"; {req.marker}"
|
|
100
|
+
return out
|
|
101
|
+
|
|
102
|
+
# Fallback, falls packaging fehlt
|
|
103
|
+
head, sep, marker = spec.partition(";")
|
|
104
|
+
if "@" in head:
|
|
105
|
+
return spec
|
|
106
|
+
m = _SPEC_RE.match(head)
|
|
107
|
+
if not m:
|
|
108
|
+
return None
|
|
109
|
+
out = m.group("name")
|
|
110
|
+
if keep_extras and m.group("extras"):
|
|
111
|
+
out += m.group("extras").replace(" ", "")
|
|
112
|
+
if keep_markers and sep and marker.strip():
|
|
113
|
+
out += f"; {marker.strip()}"
|
|
114
|
+
return out
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def specs_from(entries: list, keep_extras: bool, keep_markers: bool) -> list[str]:
|
|
118
|
+
out: list[str] = []
|
|
119
|
+
seen: set[str] = set()
|
|
120
|
+
for entry in entries:
|
|
121
|
+
if not isinstance(entry, str): # z. B. {include-group = "..."}
|
|
122
|
+
say(f" uebersprungen (kein PEP-508-String): {entry!r}", C_WARN)
|
|
123
|
+
continue
|
|
124
|
+
spec = strip_version(entry, keep_extras, keep_markers)
|
|
125
|
+
if not spec or spec.lower() in seen:
|
|
126
|
+
continue
|
|
127
|
+
seen.add(spec.lower())
|
|
128
|
+
if "@" in spec:
|
|
129
|
+
say(f" {spec}: direkte Quelle, bleibt unveraendert", C_WARN)
|
|
130
|
+
out.append(spec)
|
|
131
|
+
return out
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def resolve_groups(raw_groups: dict, keep_extras: bool, keep_markers: bool) -> dict[str, list[str]]:
|
|
135
|
+
"""Loest 'include-group'-Eintraege (PEP 735) auf, bevor Specs eingesammelt werden.
|
|
136
|
+
|
|
137
|
+
Ohne das hier wuerde specs_from() jeden {include-group = "..."}-Eintrag nur
|
|
138
|
+
ueberspringen (mit Warnung) und die darueber eingebundenen Pakete
|
|
139
|
+
stillschweigend aus der neuen Gruppe verlieren.
|
|
140
|
+
"""
|
|
141
|
+
flat: dict[str, list] = {}
|
|
142
|
+
|
|
143
|
+
def expand(grp: str, chain: tuple[str, ...] = ()) -> list:
|
|
144
|
+
if grp in flat:
|
|
145
|
+
return flat[grp]
|
|
146
|
+
if grp in chain:
|
|
147
|
+
raise RuntimeError(f"dependency-groups: Zirkel bei include-group '{grp}'")
|
|
148
|
+
if grp not in raw_groups:
|
|
149
|
+
raise RuntimeError(f"dependency-groups: include-group '{grp}' existiert nicht")
|
|
150
|
+
out: list = []
|
|
151
|
+
for entry in raw_groups[grp]:
|
|
152
|
+
if isinstance(entry, dict) and set(entry) == {"include-group"}:
|
|
153
|
+
out += expand(entry["include-group"], (*chain, grp))
|
|
154
|
+
else:
|
|
155
|
+
out.append(entry)
|
|
156
|
+
flat[grp] = out
|
|
157
|
+
return out
|
|
158
|
+
|
|
159
|
+
return {grp: specs_from(expand(grp), keep_extras, keep_markers) for grp in raw_groups}
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def ensure_backup_ignored(root: Path) -> None:
|
|
163
|
+
"""Traegt '.uv-refresh-backup/' in die .gitignore ein, falls root ein Git-Repo ist.
|
|
164
|
+
|
|
165
|
+
Das Backup kann unveraendert uebernommene Direktquellen enthalten, z. B.
|
|
166
|
+
'pkg @ git+https://user:token@...' (siehe strip_version) -- ohne Eintrag
|
|
167
|
+
landet das leicht im naechsten 'git add .'.
|
|
168
|
+
"""
|
|
169
|
+
if not (root / ".git").is_dir():
|
|
170
|
+
return
|
|
171
|
+
entry = ".uv-refresh-backup/"
|
|
172
|
+
gitignore = root / ".gitignore"
|
|
173
|
+
text = gitignore.read_text(encoding="utf-8") if gitignore.is_file() else ""
|
|
174
|
+
if entry in text.splitlines():
|
|
175
|
+
return
|
|
176
|
+
with gitignore.open("a", encoding="utf-8") as f:
|
|
177
|
+
if text and not text.endswith("\n"):
|
|
178
|
+
f.write("\n")
|
|
179
|
+
f.write(f"{entry}\n")
|
|
180
|
+
say(f" {entry} zur .gitignore hinzugefuegt (Backup kann Zugangsdaten enthalten)", C_WARN)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def build_init_cmd(name: str | None, requires_python: str | None, description: str | None) -> list[str]:
|
|
184
|
+
"""Baut den 'uv init'-Aufruf.
|
|
185
|
+
|
|
186
|
+
Bindet Werte mit '=' statt als eigenes Argv-Token: eine description, die
|
|
187
|
+
mit '-' beginnt (z. B. '--experimental'), ist sonst nicht von einem Flag
|
|
188
|
+
zu unterscheiden und uv/clap lehnt den Aufruf mit 'unexpected argument' ab.
|
|
189
|
+
"""
|
|
190
|
+
cmd = ["uv", "init", "--bare", "--no-workspace"]
|
|
191
|
+
if name:
|
|
192
|
+
cmd.append(f"--name={name}")
|
|
193
|
+
if requires_python:
|
|
194
|
+
cmd.append(f"--python={requires_python}")
|
|
195
|
+
if description:
|
|
196
|
+
cmd.append(f"--description={description}")
|
|
197
|
+
return cmd
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def run(cmd: list[str], cwd: Path, dry: bool, timeout: float | None = None) -> None:
|
|
201
|
+
say(f" $ {' '.join(cmd)}", C_DIM)
|
|
202
|
+
if dry:
|
|
203
|
+
return
|
|
204
|
+
try:
|
|
205
|
+
result = subprocess.run(cmd, cwd=cwd, check=False, timeout=timeout)
|
|
206
|
+
except subprocess.TimeoutExpired as e:
|
|
207
|
+
raise RuntimeError(
|
|
208
|
+
f"Befehl lief laenger als {timeout:.0f}s und wurde abgebrochen: {' '.join(cmd)}"
|
|
209
|
+
) from e
|
|
210
|
+
if result.returncode != 0:
|
|
211
|
+
raise RuntimeError(f"Befehl fehlgeschlagen: {' '.join(cmd)}")
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _toml_string(value: str) -> str:
|
|
215
|
+
return '"' + value.replace("\\", "\\\\").replace('"', '\\"') + '"'
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def restore_fields(text: str, version: str | None, readme: str | None) -> str:
|
|
219
|
+
"""Setzt version/readme zurueck, die 'uv init --bare' nicht uebernimmt.
|
|
220
|
+
|
|
221
|
+
uv init setzt version IMMER auf "0.1.0" (kein --version-Flag vorhanden)
|
|
222
|
+
und schreibt readme nie (nur als Datei, nicht als Feld). Ohne das hier
|
|
223
|
+
wuerde eine bestehende Versionsnummer sonst still auf 0.1.0 zurueckfallen.
|
|
224
|
+
|
|
225
|
+
Ersetzt ueber eine Lambda-Funktion statt einen Text-String: re.sub()
|
|
226
|
+
interpretiert String-Replacements selbst wieder als Muster (\\1, \\g<...>);
|
|
227
|
+
eine Lambda gibt ihren Rueckgabewert dagegen immer woertlich ein. Und
|
|
228
|
+
.subn() statt .sub(): passt das Muster in einer kuenftigen uv-Version
|
|
229
|
+
nicht mehr (anderes Anfuehrungszeichen, andere Formatierung), gibt
|
|
230
|
+
re.sub() den Text sonst kommentarlos unveraendert zurueck -- die
|
|
231
|
+
Versionsnummer waere still auf 0.1.0 zurueckgefallen, ohne jede Meldung.
|
|
232
|
+
"""
|
|
233
|
+
if version:
|
|
234
|
+
text, n = _VERSION_LINE_RE.subn(lambda _m: f"version = {_toml_string(version)}", text, count=1)
|
|
235
|
+
if n == 0:
|
|
236
|
+
say(" WARNUNG: version-Feld konnte nicht wiederhergestellt werden "
|
|
237
|
+
"(unerwartetes Format in der neuen pyproject.toml)", C_WARN)
|
|
238
|
+
if readme:
|
|
239
|
+
text, n = re.subn(r"(?m)^(version\s*=.*)$",
|
|
240
|
+
lambda m: f"{m.group(1)}\nreadme = {_toml_string(readme)}",
|
|
241
|
+
text, count=1)
|
|
242
|
+
if n == 0:
|
|
243
|
+
say(" WARNUNG: readme-Feld konnte nicht wiederhergestellt werden "
|
|
244
|
+
"(unerwartetes Format in der neuen pyproject.toml)", C_WARN)
|
|
245
|
+
return text
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def main() -> int:
|
|
249
|
+
p = argparse.ArgumentParser(prog="uv-refresh", description=__doc__,
|
|
250
|
+
formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
251
|
+
p.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
|
|
252
|
+
p.add_argument("--path", default=".", help="Projektverzeichnis (Default: aktuelles)")
|
|
253
|
+
p.add_argument("--dry-run", action="store_true", help="nur anzeigen, nichts veraendern")
|
|
254
|
+
p.add_argument("--yes", "-y", action="store_true", help="ohne Rueckfrage durchziehen")
|
|
255
|
+
p.add_argument("--verbose", "-v", action="store_true",
|
|
256
|
+
help="komplette neue pyproject.toml am Ende ausgeben")
|
|
257
|
+
p.add_argument("--timeout", type=float, default=300.0,
|
|
258
|
+
help="Timeout in Sekunden je uv-Aufruf (Default: 300)")
|
|
259
|
+
p.add_argument("--keep-lock", action="store_true",
|
|
260
|
+
help="uv.lock behalten (dann bevorzugt uv die alten Versionen!)")
|
|
261
|
+
p.add_argument("--no-groups", action="store_true",
|
|
262
|
+
help="optional-dependencies und dependency-groups nicht uebernehmen")
|
|
263
|
+
p.add_argument("--drop-extras", action="store_true",
|
|
264
|
+
help="Extras verwerfen: fastapi[standard] wird zu fastapi (selten sinnvoll!)")
|
|
265
|
+
p.add_argument("--drop-markers", action="store_true",
|
|
266
|
+
help="Environment-Marker verwerfen, z. B. ; sys_platform == 'win32'")
|
|
267
|
+
bounds_group = p.add_mutually_exclusive_group()
|
|
268
|
+
bounds_group.add_argument("--raw", action="store_true",
|
|
269
|
+
help="Namen komplett ohne Versionsgrenze eintragen (uv add --raw)")
|
|
270
|
+
bounds_group.add_argument("--bounds", choices=["lower", "major", "minor", "exact"],
|
|
271
|
+
help="Art der Versionsgrenze, die uv add setzt (Preview-Feature von uv)")
|
|
272
|
+
args = p.parse_args()
|
|
273
|
+
|
|
274
|
+
root = Path(args.path).resolve()
|
|
275
|
+
pyproject = root / "pyproject.toml"
|
|
276
|
+
lock = root / "uv.lock"
|
|
277
|
+
|
|
278
|
+
if not shutil.which("uv"):
|
|
279
|
+
die("uv ist nicht im PATH.")
|
|
280
|
+
if not pyproject.is_file():
|
|
281
|
+
die(f"Keine pyproject.toml in {root}")
|
|
282
|
+
|
|
283
|
+
# ---- 1. lesen ---------------------------------------------------------
|
|
284
|
+
try:
|
|
285
|
+
with pyproject.open("rb") as f:
|
|
286
|
+
data = tomllib.load(f)
|
|
287
|
+
except tomllib.TOMLDecodeError as e:
|
|
288
|
+
die(f"pyproject.toml ist kein gueltiges TOML: {e}")
|
|
289
|
+
except UnicodeDecodeError as e:
|
|
290
|
+
die(f"pyproject.toml ist nicht UTF-8-kodiert: {e}")
|
|
291
|
+
|
|
292
|
+
project = data.get("project", {})
|
|
293
|
+
if not project:
|
|
294
|
+
die("Kein [project]-Abschnitt gefunden. Ist das ein uv-/PEP-621-Projekt?")
|
|
295
|
+
|
|
296
|
+
name = project.get("name")
|
|
297
|
+
requires_python = project.get("requires-python")
|
|
298
|
+
description = project.get("description")
|
|
299
|
+
version = project.get("version")
|
|
300
|
+
readme = project.get("readme")
|
|
301
|
+
keep_extras, keep_markers = not args.drop_extras, not args.drop_markers
|
|
302
|
+
main_deps = specs_from(project.get("dependencies", []), keep_extras, keep_markers)
|
|
303
|
+
|
|
304
|
+
extras: dict[str, list[str]] = {}
|
|
305
|
+
groups: dict[str, list[str]] = {}
|
|
306
|
+
if not args.no_groups:
|
|
307
|
+
for grp, specs in (project.get("optional-dependencies") or {}).items():
|
|
308
|
+
extras[grp] = specs_from(specs, keep_extras, keep_markers)
|
|
309
|
+
raw_groups = data.get("dependency-groups") or {}
|
|
310
|
+
if raw_groups:
|
|
311
|
+
try:
|
|
312
|
+
groups = resolve_groups(raw_groups, keep_extras, keep_markers)
|
|
313
|
+
except RuntimeError as e:
|
|
314
|
+
die(str(e))
|
|
315
|
+
|
|
316
|
+
if not main_deps and not extras and not groups:
|
|
317
|
+
die("Keine Dependencies gefunden. Nichts zu tun.")
|
|
318
|
+
|
|
319
|
+
say(f"\nProjekt: {name or '(kein Name)'} [{root}]", C_OK)
|
|
320
|
+
say(f" dependencies : {', '.join(main_deps) or '-'}")
|
|
321
|
+
for g, n in extras.items():
|
|
322
|
+
say(f" optional [{g}] : {', '.join(n)}")
|
|
323
|
+
for g, n in groups.items():
|
|
324
|
+
say(f" group [{g}] : {', '.join(n)}")
|
|
325
|
+
|
|
326
|
+
# ---- Was verloren geht ------------------------------------------------
|
|
327
|
+
# version/description werden aktiv wiederhergestellt (s. u.), readme nur
|
|
328
|
+
# als einfacher String -- als Tabelle ({file=..., text=...}) nicht.
|
|
329
|
+
safe_keys = {"name", "version", "description", "requires-python",
|
|
330
|
+
"dependencies", "optional-dependencies"}
|
|
331
|
+
if isinstance(readme, str):
|
|
332
|
+
safe_keys.add("readme")
|
|
333
|
+
lost = [k for k in project if k not in safe_keys]
|
|
334
|
+
other_tables = [k for k in data if k not in {"project", "dependency-groups"}]
|
|
335
|
+
if lost or other_tables:
|
|
336
|
+
say("\nACHTUNG, diese Konfiguration wird NICHT wiederhergestellt:", C_WARN)
|
|
337
|
+
for k in lost:
|
|
338
|
+
say(f" [project].{k}", C_WARN)
|
|
339
|
+
for k in other_tables:
|
|
340
|
+
say(f" [{k}]", C_WARN)
|
|
341
|
+
say(" Das Backup liegt daneben, du musst diese Bloecke von Hand zurueckkopieren.", C_WARN)
|
|
342
|
+
|
|
343
|
+
if args.dry_run:
|
|
344
|
+
say("\n--dry-run: ab hier wuerde passieren:", C_DIM)
|
|
345
|
+
elif not args.yes:
|
|
346
|
+
try:
|
|
347
|
+
answer = input("\nWirklich neu aufbauen? [j/y/N] ").strip().lower()
|
|
348
|
+
except EOFError:
|
|
349
|
+
die("Keine Eingabe moeglich (kein Terminal). Mit --yes ohne Rueckfrage ausfuehren.")
|
|
350
|
+
if answer not in {"j", "y"}:
|
|
351
|
+
say("Abgebrochen.", C_DIM)
|
|
352
|
+
return 1
|
|
353
|
+
|
|
354
|
+
# ---- 2. Backup --------------------------------------------------------
|
|
355
|
+
stamp = datetime.now(UTC).astimezone().strftime("%Y%m%d-%H%M%S")
|
|
356
|
+
backup = root / ".uv-refresh-backup" / stamp
|
|
357
|
+
say(f"\nBackup -> {backup}", C_DIM)
|
|
358
|
+
say("Aufbau erfolgt in einem Temp-Verzeichnis; deine echte pyproject.toml/"
|
|
359
|
+
"uv.lock bleiben bis zum letzten Schritt unangetastet.", C_DIM)
|
|
360
|
+
|
|
361
|
+
build_ctx = (tempfile.TemporaryDirectory(dir=root, prefix=".uv-refresh-tmp-")
|
|
362
|
+
if not args.dry_run else contextlib.nullcontext(root))
|
|
363
|
+
|
|
364
|
+
try:
|
|
365
|
+
if not args.dry_run:
|
|
366
|
+
ensure_backup_ignored(root)
|
|
367
|
+
backup.mkdir(parents=True, exist_ok=True)
|
|
368
|
+
try:
|
|
369
|
+
os.chmod(backup, 0o700)
|
|
370
|
+
except OSError:
|
|
371
|
+
pass
|
|
372
|
+
shutil.copy2(pyproject, backup / "pyproject.toml")
|
|
373
|
+
if lock.is_file():
|
|
374
|
+
shutil.copy2(lock, backup / "uv.lock")
|
|
375
|
+
|
|
376
|
+
with build_ctx as build_dir_raw:
|
|
377
|
+
build_dir = Path(build_dir_raw)
|
|
378
|
+
if not args.dry_run and args.keep_lock and lock.is_file():
|
|
379
|
+
shutil.copy2(lock, build_dir / "uv.lock")
|
|
380
|
+
|
|
381
|
+
# ---- 3. uv init -----------------------------------------------
|
|
382
|
+
init = build_init_cmd(name, requires_python, description)
|
|
383
|
+
try:
|
|
384
|
+
run(init, build_dir, args.dry_run, args.timeout)
|
|
385
|
+
except RuntimeError:
|
|
386
|
+
if not any(f.startswith("--python=") for f in init):
|
|
387
|
+
raise
|
|
388
|
+
say(" uv init mit --python fehlgeschlagen, versuche es ohne", C_WARN)
|
|
389
|
+
run([f for f in init if not f.startswith("--python=")],
|
|
390
|
+
build_dir, args.dry_run, args.timeout)
|
|
391
|
+
|
|
392
|
+
# ---- 4. uv add --------------------------------------------------
|
|
393
|
+
flags: list[str] = []
|
|
394
|
+
if args.raw:
|
|
395
|
+
flags.append("--raw")
|
|
396
|
+
if args.bounds:
|
|
397
|
+
flags += ["--bounds", args.bounds]
|
|
398
|
+
|
|
399
|
+
if main_deps:
|
|
400
|
+
run(["uv", "add", *flags, *main_deps], build_dir, args.dry_run, args.timeout)
|
|
401
|
+
for grp, deps in extras.items():
|
|
402
|
+
if deps:
|
|
403
|
+
run(["uv", "add", "--optional", grp, *flags, *deps],
|
|
404
|
+
build_dir, args.dry_run, args.timeout)
|
|
405
|
+
for grp, deps in groups.items():
|
|
406
|
+
if deps:
|
|
407
|
+
run(["uv", "add", "--group", grp, *flags, *deps],
|
|
408
|
+
build_dir, args.dry_run, args.timeout)
|
|
409
|
+
|
|
410
|
+
# ---- 5. version/readme zuruecksetzen -----------------------------
|
|
411
|
+
if not args.dry_run and (version or isinstance(readme, str)):
|
|
412
|
+
text = restore_fields((build_dir / "pyproject.toml").read_text(encoding="utf-8"),
|
|
413
|
+
version, readme if isinstance(readme, str) else None)
|
|
414
|
+
(build_dir / "pyproject.toml").write_text(text, encoding="utf-8")
|
|
415
|
+
|
|
416
|
+
# ---- 6. atomarer Tausch -------------------------------------------
|
|
417
|
+
# root blieb bis hierher unveraendert: schlaegt oben etwas fehl
|
|
418
|
+
# (auch per Ctrl+C), gibt es nichts zurueckzuholen.
|
|
419
|
+
if not args.dry_run:
|
|
420
|
+
os.replace(build_dir / "pyproject.toml", pyproject)
|
|
421
|
+
new_lock = build_dir / "uv.lock"
|
|
422
|
+
if new_lock.is_file():
|
|
423
|
+
os.replace(new_lock, lock)
|
|
424
|
+
else:
|
|
425
|
+
say(" $ (pyproject.toml/uv.lock wuerden jetzt atomar ersetzt)", C_DIM)
|
|
426
|
+
|
|
427
|
+
except (Exception, KeyboardInterrupt) as e: # noqa: BLE001 -- Notbremse: bei
|
|
428
|
+
# JEDEM Fehler (uv, Dateisystem, Interrupt, ...) klar melden. Der Aufbau
|
|
429
|
+
# geschah in einem Temp-Verzeichnis, root ist daher normalerweise
|
|
430
|
+
# unveraendert; das Backup bleibt als zusaetzliches Netz trotzdem liegen.
|
|
431
|
+
say(f"\n{e}", C_ERR)
|
|
432
|
+
if not args.dry_run:
|
|
433
|
+
say(f"pyproject.toml unveraendert. Backup liegt in {backup}.", C_WARN)
|
|
434
|
+
return 1
|
|
435
|
+
|
|
436
|
+
if args.dry_run:
|
|
437
|
+
say("\n--dry-run: nichts wurde veraendert.", C_OK)
|
|
438
|
+
return 0
|
|
439
|
+
|
|
440
|
+
say("\nFertig.", C_OK)
|
|
441
|
+
if args.verbose and pyproject.is_file():
|
|
442
|
+
say(pyproject.read_text(encoding="utf-8"), C_DIM)
|
|
443
|
+
return 0
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
if __name__ == "__main__":
|
|
447
|
+
sys.exit(main())
|