Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,18 @@ jobs:

strategy:
matrix:
python-version: ['3.9', '3.10', '3.11', '3.12']
python-version: ['3.10', '3.11', '3.12', '3.13']

steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}

- run: pip install -U pip tox
- run: tox -e py
- uses: astral-sh/setup-uv@v5
- run: uv venv
- run: uv pip install -U tox-uv
- run: .venv/bin/tox -e py
- uses: codecov/codecov-action@v4
with:
files: .tox/test-reports/coverage.xml
Expand All @@ -34,7 +36,7 @@ jobs:

strategy:
matrix:
python-version: ['3.6', '3.7', '3.8']
python-version: ['3.6', '3.7', '3.8', '3.9']

steps:
- uses: actions/checkout@v4
Expand Down
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: Implementation :: CPython",
"Topic :: Software Development :: Build Tools",
"Topic :: System :: Installation/Setup",
Expand Down
3 changes: 1 addition & 2 deletions src/pickley/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@
"version",
"version_check_delay",
}
KNOWN_ENTRYPOINTS = {bstrap.PICKLEY: (bstrap.PICKLEY,), "tox": ("tox",), "uv": ("uv", "uvx")}
PLATFORM = platform.system().lower()


Expand Down Expand Up @@ -750,7 +749,7 @@ def configured_entrypoints(self, canonical_name) -> Optional[list]:
if value:
return value

return KNOWN_ENTRYPOINTS.get(canonical_name)
return bstrap.KNOWN_ENTRYPOINTS.get(canonical_name)

def require_bootstrap(self):
"""
Expand Down
49 changes: 36 additions & 13 deletions src/pickley/bstrap.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import argparse
import json
import os
import re
import shutil
import subprocess
import sys
Expand All @@ -27,6 +28,7 @@
CURRENT_PYTHON_MM = sys.version_info[:2]
UV_CUTOFF = (3, 8)
USE_UV = CURRENT_PYTHON_MM >= UV_CUTOFF # Default to `uv` for python versions >= this
KNOWN_ENTRYPOINTS = {PICKLEY: (PICKLEY,), "tox": ("tox",), "uv": ("uv", "uvx")}


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

def bootstrap_pickley(self):
"""Run `pickley bootstrap` in a temporary venv"""
Expand Down Expand Up @@ -167,12 +168,36 @@ def __init__(self, pickley_base):
def auto_bootstrap_uv(self):
self.freshly_bootstrapped = self.bootstrap_reason()
if self.freshly_bootstrapped:
Reporter.trace(f"Auto-bootstrapping uv, reason: {self.freshly_bootstrapped}")
Reporter.inform(f"Auto-bootstrapping uv, reason: {self.freshly_bootstrapped}")
uv_tmp = self.download_uv()
shutil.move(uv_tmp / "uv", self.pickley_base / "uv")
shutil.move(uv_tmp / "uvx", self.pickley_base / "uvx")
shutil.rmtree(uv_tmp, ignore_errors=True)

# Touch cooldown file to let pickley know no need to check for uv upgrade for a while
cooldown_relative_path = f"{DOT_META}/.cache/uv.cooldown"
cooldown_path = self.pickley_base / cooldown_relative_path
ensure_folder(cooldown_path.parent, dryrun=False)
cooldown_path.write_text("")
Reporter.debug(f"[bootstrap] Touched {cooldown_relative_path}")

# Let pickley know which version of uv is installed
uv_version = run_program(self.uv_path, "--version", fatal=False, dryrun=False)
if uv_version:
m = re.search(r"(\d+\.\d+\.\d+)", uv_version)
if m:
uv_version = m.group(1)
manifest_relative_path = f"{DOT_META}/.manifest/uv.manifest.json"
manifest_path = self.pickley_base / manifest_relative_path
manifest = {
"entrypoints": KNOWN_ENTRYPOINTS["uv"],
"tracked_settings": {"auto_upgrade_spec": "uv"},
"version": uv_version,
}
ensure_folder(manifest_path.parent, dryrun=False)
manifest_path.write_text(json.dumps(manifest))
Reporter.debug(f"[bootstrap] Saved {manifest_relative_path}")

def bootstrap_reason(self):
if not self.uv_path.exists():
return "uv not present"
Expand Down Expand Up @@ -210,8 +235,7 @@ def download_uv(self, version=None, dryrun=False):
def built_in_download(target, url):
request = Request(url)
response = urlopen(request, timeout=10)
with open(target, "wb") as fh:
fh.write(response.read())
target.write_bytes(response.read())


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

else:
stdout = stderr = subprocess.PIPE
Expand Down Expand Up @@ -350,13 +374,12 @@ def seed_mirror(mirror, path, section):
msg = f"{short(config_path)} with {mirror}"
if not hdry(f"Would seed {msg}"):
Reporter.inform(f"Seeding {msg}")
with open(config_path, "wt") as fh:
if section == "pip" and not mirror.startswith('"'):
# This assumes user passed a reasonable URL as --mirror, no further validation is done
# We only ensure the URL is quoted, as uv.toml requires it
mirror = f'"{mirror}"'
if section == "pip" and not mirror.startswith('"'):
# This assumes user passed a reasonable URL as --mirror, no further validation is done
# We only ensure the URL is quoted, as uv.toml requires it
mirror = f'"{mirror}"'

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

except Exception as e:
Reporter.inform(f"Seeding {path} failed: {e}")
Expand Down
20 changes: 12 additions & 8 deletions src/pickley/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -303,13 +303,16 @@ def auto_upgrade_uv(cooldown_hours=12):
cooldown_hours : int
Cooldown period in hours, auto-upgrade won't be attempted any more frequently than that.
"""
cooldown_path = CFG.cache / "uv.cooldown"
if not cooldown_hours or not runez.file.is_younger(cooldown_path, cooldown_hours * runez.date.SECONDS_IN_ONE_HOUR):
runez.touch(cooldown_path)
settings = TrackedSettings()
settings.auto_upgrade_spec = "uv"
pspec = PackageSpec("uv", settings=settings)
perform_upgrade(pspec)
if not CFG.uv_bootstrap.freshly_bootstrapped:
cooldown_path = CFG.cache / "uv.cooldown"
if not cooldown_hours or not runez.file.is_younger(cooldown_path, cooldown_hours * runez.date.SECONDS_IN_ONE_HOUR):
runez.touch(cooldown_path)
settings = TrackedSettings()
settings.auto_upgrade_spec = "uv"
pspec = PackageSpec("uv", settings=settings)

# Automatic background upgrade of `uv` is not treated as fatal, for more resilience
perform_upgrade(pspec, fatal=False)


@main.command()
Expand Down Expand Up @@ -401,7 +404,7 @@ def bootstrap(base_folder, pickley_spec):
runez.Anchored.add(CFG.base)
setup_audit_log()
if bstrap.USE_UV:
auto_upgrade_uv(cooldown_hours=0)
auto_upgrade_uv()

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

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

Expand Down
22 changes: 13 additions & 9 deletions tests/test_bootstrap.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,31 +9,35 @@
from pickley.cli import CFG


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

# Simulate an old uv semi-venv present
runez.touch(".local/bin/.pk/uv-0.0.1/bin/uv", logger=None)
runez.ensure_folder(".local/bin", logger=None)
cli.run("--no-color", "-vv", "bootstrap", ".local/bin", cli.project_folder)
assert cli.succeeded
assert "Saved .pk/.manifest/.bootstrap.json" in cli.logged
assert "Installed pickley v" in cli.logged
assert CFG.program_version(".local/bin/pickley")
if bstrap.USE_UV:
assert CFG._uv_bootstrap.freshly_bootstrapped == "uv not present"
assert "Deleted .pk/uv-0.0.1" in cli.logged
assert "Auto-bootstrapping uv, reason: uv not present" in cli.logged
assert "Saved .pk/.manifest/uv.manifest.json" in cli.logged
assert "[bootstrap] Saved .pk/.manifest/uv.manifest.json" in cli.logged
assert CFG.program_version(".local/bin/uv")

# Simulate an old uv semi-venv present
runez.touch(".local/bin/.pk/uv-0.0.1/bin/uv", logger=None)
monkeypatch.setenv("PICKLEY_ROOT", ".local/bin")
cli.run("-vv", "install", "-f", "uv")
assert cli.succeeded
assert "Deleted .pk/uv-0.0.1" in cli.logged

else:
# Verify that no uv bootstrap took place
assert "/uv" not in cli.logged
assert CFG._uv_bootstrap is None

assert "Installed pickley v" in cli.logged
assert CFG.program_version(".local/bin/pickley")


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

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

# Simulate bogus mirror, verify that we fail bootstrap in that case
cli.run("-nvv", cli.project_folder, "-mhttp://localhost:12345")
Expand Down
4 changes: 4 additions & 0 deletions tests/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,10 @@ def test_good_config(cli):
assert cli.succeeded
assert "Would wrap mgit -> .pk/mgit-1.2.1/bin/mgit" in cli.logged

cli.run("-n install bundle:foo")
assert cli.failed
assert "Can't install 'bundle:foo', not configured" in cli.logged


def test_despecced():
assert CFG.despecced("mgit") == ("mgit", None)
Expand Down
Loading