Skip to content

Commit 0924e07

Browse files
zsimicZoran Simic
andauthored
More resilient uv auto-upgrade (#52)
* More resilient uv auto-upgrade * Create uv venv in GH actions * Corrected uv usage * Corrected comment * Bootstrap uv only once on first run * Test with py3.13 as well * Ensure exit code != 0 when unknown bundle is referenced --------- Co-authored-by: Zoran Simic <zsimic@netflix.com>
1 parent 593c7d3 commit 0924e07

File tree

7 files changed

+73
-36
lines changed

7 files changed

+73
-36
lines changed

.github/workflows/tests.yml

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,16 +13,18 @@ jobs:
1313

1414
strategy:
1515
matrix:
16-
python-version: ['3.9', '3.10', '3.11', '3.12']
16+
python-version: ['3.10', '3.11', '3.12', '3.13']
1717

1818
steps:
1919
- uses: actions/checkout@v4
2020
- uses: actions/setup-python@v5
2121
with:
2222
python-version: ${{ matrix.python-version }}
2323

24-
- run: pip install -U pip tox
25-
- run: tox -e py
24+
- uses: astral-sh/setup-uv@v5
25+
- run: uv venv
26+
- run: uv pip install -U tox-uv
27+
- run: .venv/bin/tox -e py
2628
- uses: codecov/codecov-action@v4
2729
with:
2830
files: .tox/test-reports/coverage.xml
@@ -34,7 +36,7 @@ jobs:
3436

3537
strategy:
3638
matrix:
37-
python-version: ['3.6', '3.7', '3.8']
39+
python-version: ['3.6', '3.7', '3.8', '3.9']
3840

3941
steps:
4042
- uses: actions/checkout@v4

setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
"Programming Language :: Python :: 3.10",
2929
"Programming Language :: Python :: 3.11",
3030
"Programming Language :: Python :: 3.12",
31+
"Programming Language :: Python :: 3.13",
3132
"Programming Language :: Python :: Implementation :: CPython",
3233
"Topic :: Software Development :: Build Tools",
3334
"Topic :: System :: Installation/Setup",

src/pickley/__init__.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@
3030
"version",
3131
"version_check_delay",
3232
}
33-
KNOWN_ENTRYPOINTS = {bstrap.PICKLEY: (bstrap.PICKLEY,), "tox": ("tox",), "uv": ("uv", "uvx")}
3433
PLATFORM = platform.system().lower()
3534

3635

@@ -750,7 +749,7 @@ def configured_entrypoints(self, canonical_name) -> Optional[list]:
750749
if value:
751750
return value
752751

753-
return KNOWN_ENTRYPOINTS.get(canonical_name)
752+
return bstrap.KNOWN_ENTRYPOINTS.get(canonical_name)
754753

755754
def require_bootstrap(self):
756755
"""

src/pickley/bstrap.py

Lines changed: 36 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import argparse
99
import json
1010
import os
11+
import re
1112
import shutil
1213
import subprocess
1314
import sys
@@ -27,6 +28,7 @@
2728
CURRENT_PYTHON_MM = sys.version_info[:2]
2829
UV_CUTOFF = (3, 8)
2930
USE_UV = CURRENT_PYTHON_MM >= UV_CUTOFF # Default to `uv` for python versions >= this
31+
KNOWN_ENTRYPOINTS = {PICKLEY: (PICKLEY,), "tox": ("tox",), "uv": ("uv", "uvx")}
3032

3133

3234
class _Reporter:
@@ -84,9 +86,8 @@ def seed_pickley_config(self, desired_cfg):
8486
if not hdry(f"Would seed {msg}"):
8587
Reporter.inform(f"Seeding {msg}")
8688
ensure_folder(pickley_config.parent)
87-
with open(pickley_config, "wt") as fh:
88-
json.dump(desired_cfg, fh, sort_keys=True, indent=2)
89-
fh.write("\n")
89+
payload = json.dumps(desired_cfg, sort_keys=True, indent=2)
90+
pickley_config.write_text(f"{payload}\n")
9091

9192
def bootstrap_pickley(self):
9293
"""Run `pickley bootstrap` in a temporary venv"""
@@ -167,12 +168,36 @@ def __init__(self, pickley_base):
167168
def auto_bootstrap_uv(self):
168169
self.freshly_bootstrapped = self.bootstrap_reason()
169170
if self.freshly_bootstrapped:
170-
Reporter.trace(f"Auto-bootstrapping uv, reason: {self.freshly_bootstrapped}")
171+
Reporter.inform(f"Auto-bootstrapping uv, reason: {self.freshly_bootstrapped}")
171172
uv_tmp = self.download_uv()
172173
shutil.move(uv_tmp / "uv", self.pickley_base / "uv")
173174
shutil.move(uv_tmp / "uvx", self.pickley_base / "uvx")
174175
shutil.rmtree(uv_tmp, ignore_errors=True)
175176

177+
# Touch cooldown file to let pickley know no need to check for uv upgrade for a while
178+
cooldown_relative_path = f"{DOT_META}/.cache/uv.cooldown"
179+
cooldown_path = self.pickley_base / cooldown_relative_path
180+
ensure_folder(cooldown_path.parent, dryrun=False)
181+
cooldown_path.write_text("")
182+
Reporter.debug(f"[bootstrap] Touched {cooldown_relative_path}")
183+
184+
# Let pickley know which version of uv is installed
185+
uv_version = run_program(self.uv_path, "--version", fatal=False, dryrun=False)
186+
if uv_version:
187+
m = re.search(r"(\d+\.\d+\.\d+)", uv_version)
188+
if m:
189+
uv_version = m.group(1)
190+
manifest_relative_path = f"{DOT_META}/.manifest/uv.manifest.json"
191+
manifest_path = self.pickley_base / manifest_relative_path
192+
manifest = {
193+
"entrypoints": KNOWN_ENTRYPOINTS["uv"],
194+
"tracked_settings": {"auto_upgrade_spec": "uv"},
195+
"version": uv_version,
196+
}
197+
ensure_folder(manifest_path.parent, dryrun=False)
198+
manifest_path.write_text(json.dumps(manifest))
199+
Reporter.debug(f"[bootstrap] Saved {manifest_relative_path}")
200+
176201
def bootstrap_reason(self):
177202
if not self.uv_path.exists():
178203
return "uv not present"
@@ -210,8 +235,7 @@ def download_uv(self, version=None, dryrun=False):
210235
def built_in_download(target, url):
211236
request = Request(url)
212237
response = urlopen(request, timeout=10)
213-
with open(target, "wb") as fh:
214-
fh.write(response.read())
238+
target.write_bytes(response.read())
215239

216240

217241
def clean_env_vars(keys=("__PYVENV_LAUNCHER__", "CLICOLOR_FORCE", "PYTHONPATH")):
@@ -320,9 +344,9 @@ def run_program(program, *args, **kwargs):
320344
description = " ".join(short(x) for x in args)
321345
description = f"{short(program)} {description}"
322346
if not hdry(f"Would run: {description}", dryrun=kwargs.pop("dryrun", None)):
347+
Reporter.inform(f"Running: {description}")
323348
if fatal:
324349
stdout = stderr = None
325-
Reporter.debug(f"Running: {description}")
326350

327351
else:
328352
stdout = stderr = subprocess.PIPE
@@ -350,13 +374,12 @@ def seed_mirror(mirror, path, section):
350374
msg = f"{short(config_path)} with {mirror}"
351375
if not hdry(f"Would seed {msg}"):
352376
Reporter.inform(f"Seeding {msg}")
353-
with open(config_path, "wt") as fh:
354-
if section == "pip" and not mirror.startswith('"'):
355-
# This assumes user passed a reasonable URL as --mirror, no further validation is done
356-
# We only ensure the URL is quoted, as uv.toml requires it
357-
mirror = f'"{mirror}"'
377+
if section == "pip" and not mirror.startswith('"'):
378+
# This assumes user passed a reasonable URL as --mirror, no further validation is done
379+
# We only ensure the URL is quoted, as uv.toml requires it
380+
mirror = f'"{mirror}"'
358381

359-
fh.write(f"[{section}]\nindex-url = {mirror}\n")
382+
config_path.write_text(f"[{section}]\nindex-url = {mirror}\n")
360383

361384
except Exception as e:
362385
Reporter.inform(f"Seeding {path} failed: {e}")

src/pickley/cli.py

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -303,13 +303,16 @@ def auto_upgrade_uv(cooldown_hours=12):
303303
cooldown_hours : int
304304
Cooldown period in hours, auto-upgrade won't be attempted any more frequently than that.
305305
"""
306-
cooldown_path = CFG.cache / "uv.cooldown"
307-
if not cooldown_hours or not runez.file.is_younger(cooldown_path, cooldown_hours * runez.date.SECONDS_IN_ONE_HOUR):
308-
runez.touch(cooldown_path)
309-
settings = TrackedSettings()
310-
settings.auto_upgrade_spec = "uv"
311-
pspec = PackageSpec("uv", settings=settings)
312-
perform_upgrade(pspec)
306+
if not CFG.uv_bootstrap.freshly_bootstrapped:
307+
cooldown_path = CFG.cache / "uv.cooldown"
308+
if not cooldown_hours or not runez.file.is_younger(cooldown_path, cooldown_hours * runez.date.SECONDS_IN_ONE_HOUR):
309+
runez.touch(cooldown_path)
310+
settings = TrackedSettings()
311+
settings.auto_upgrade_spec = "uv"
312+
pspec = PackageSpec("uv", settings=settings)
313+
314+
# Automatic background upgrade of `uv` is not treated as fatal, for more resilience
315+
perform_upgrade(pspec, fatal=False)
313316

314317

315318
@main.command()
@@ -401,7 +404,7 @@ def bootstrap(base_folder, pickley_spec):
401404
runez.Anchored.add(CFG.base)
402405
setup_audit_log()
403406
if bstrap.USE_UV:
404-
auto_upgrade_uv(cooldown_hours=0)
407+
auto_upgrade_uv()
405408

406409
bootstrap_marker = CFG.manifests / ".bootstrap.json"
407410
if not bootstrap_marker.exists():
@@ -526,6 +529,7 @@ def install(force, packages):
526529

527530
setup_audit_log()
528531
specs = CFG.package_specs(packages, authoritative=True)
532+
runez.abort_if(not specs, f"Can't install '{runez.joined(packages)}', not configured")
529533
for pspec in specs:
530534
perform_install(pspec)
531535

tests/test_bootstrap.py

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,31 +9,35 @@
99
from pickley.cli import CFG
1010

1111

12-
def test_bootstrap_command(cli):
12+
def test_bootstrap_command(cli, monkeypatch):
1313
cli.run("-n", "bootstrap", ".local/bin", cli.project_folder)
1414
assert cli.failed
1515
assert "Folder .local/bin does not exist" in cli.logged
1616

17-
# Simulate an old uv semi-venv present
18-
runez.touch(".local/bin/.pk/uv-0.0.1/bin/uv", logger=None)
17+
runez.ensure_folder(".local/bin", logger=None)
1918
cli.run("--no-color", "-vv", "bootstrap", ".local/bin", cli.project_folder)
2019
assert cli.succeeded
2120
assert "Saved .pk/.manifest/.bootstrap.json" in cli.logged
21+
assert "Installed pickley v" in cli.logged
22+
assert CFG.program_version(".local/bin/pickley")
2223
if bstrap.USE_UV:
2324
assert CFG._uv_bootstrap.freshly_bootstrapped == "uv not present"
24-
assert "Deleted .pk/uv-0.0.1" in cli.logged
2525
assert "Auto-bootstrapping uv, reason: uv not present" in cli.logged
26-
assert "Saved .pk/.manifest/uv.manifest.json" in cli.logged
26+
assert "[bootstrap] Saved .pk/.manifest/uv.manifest.json" in cli.logged
2727
assert CFG.program_version(".local/bin/uv")
2828

29+
# Simulate an old uv semi-venv present
30+
runez.touch(".local/bin/.pk/uv-0.0.1/bin/uv", logger=None)
31+
monkeypatch.setenv("PICKLEY_ROOT", ".local/bin")
32+
cli.run("-vv", "install", "-f", "uv")
33+
assert cli.succeeded
34+
assert "Deleted .pk/uv-0.0.1" in cli.logged
35+
2936
else:
3037
# Verify that no uv bootstrap took place
3138
assert "/uv" not in cli.logged
3239
assert CFG._uv_bootstrap is None
3340

34-
assert "Installed pickley v" in cli.logged
35-
assert CFG.program_version(".local/bin/pickley")
36-
3741

3842
def test_bootstrap_script(cli, monkeypatch):
3943
# Ensure changes to bstrap.py globals are restored
@@ -54,7 +58,7 @@ def test_bootstrap_script(cli, monkeypatch):
5458

5559
# Verify that uv is seeded even in dryrun mode
5660
uv_path = CFG.resolved_path(".local/bin/uv")
57-
assert not runez.is_executable(uv_path) # Not seed by conftest.py (it seeds ./uv)
61+
assert not runez.is_executable(uv_path) # Not seeded by conftest.py (it seeds ./uv)
5862

5963
# Simulate bogus mirror, verify that we fail bootstrap in that case
6064
cli.run("-nvv", cli.project_folder, "-mhttp://localhost:12345")

tests/test_config.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,10 @@ def test_good_config(cli):
8585
assert cli.succeeded
8686
assert "Would wrap mgit -> .pk/mgit-1.2.1/bin/mgit" in cli.logged
8787

88+
cli.run("-n install bundle:foo")
89+
assert cli.failed
90+
assert "Can't install 'bundle:foo', not configured" in cli.logged
91+
8892

8993
def test_despecced():
9094
assert CFG.despecced("mgit") == ("mgit", None)

0 commit comments

Comments
 (0)