Skip to content

Commit 161e364

Browse files
authored
Merge pull request #42 from DavidCEllis/use-local-uv
Use user's installation of `uv` if it exists
2 parents e5b4fdb + 4091d5c commit 161e364

File tree

3 files changed

+119
-59
lines changed

3 files changed

+119
-59
lines changed

src/ducktools/env/scripts/get_pip.py

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -62,17 +62,17 @@ def as_version(self):
6262

6363
# This is mostly kept for testing.
6464
PREVIOUS_PIP = PipZipapp(
65-
version_str="24.2",
66-
sha3_256="8dc4860613c47cb2e5e55c7e1ecf4046abe18edca083073d51f1720011bed6ea",
67-
source_url="zipapp/pip-24.2.pyz",
68-
)
69-
70-
LATEST_PIP = PipZipapp(
7165
version_str="24.3.1",
7266
sha3_256="6dd95ab685d00abada578f2744f2b235faf5876d3f3468f0756e2b78bce050f1",
7367
source_url="zipapp/pip-24.3.1.pyz"
7468
)
7569

70+
LATEST_PIP = PipZipapp(
71+
version_str="25.0.1",
72+
sha3_256="eff93cf5d562974c1e6fff03c3d48c4b08bf5c5be8ceb315dc8b6ffe21860cb4",
73+
source_url="zipapp/pip-25.0.1.pyz"
74+
)
75+
7676

7777
def is_pip_outdated(
7878
paths: ManagedPaths,
@@ -105,10 +105,13 @@ def download_pip(
105105
with _laz.urlopen(url) as f:
106106
data = f.read()
107107

108+
dl_hash = _laz.hashlib.sha3_256(data).hexdigest()
108109
# Check hash matches
109-
if _laz.hashlib.sha3_256(data).hexdigest() != latest_version.sha3_256:
110+
if dl_hash != latest_version.sha3_256:
110111
raise InvalidPipDownload(
111-
"The checksum of the downloaded PIP binary did not match the expected value."
112+
"The checksum of the downloaded PIP binary did not match the expected value.\n"
113+
f"Expected: {latest_version.sha3_256}\n"
114+
f"Received: {dl_hash}"
112115
)
113116

114117
# Make directory if it does not exist

src/ducktools/env/scripts/get_uv.py

Lines changed: 76 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,86 @@
2929
from ..platform_paths import ManagedPaths
3030

3131

32-
uv_versionspec = ">=0.4.25"
32+
uv_versionspec = ">=0.6.5"
33+
uv_versionre = r"^uv (?P<uv_ver>\d+\.\d+\.\d+)"
3334

3435
uv_download = "bin/uv.exe" if sys.platform == "win32" else "bin/uv"
3536

3637

38+
def download_uv(paths: ManagedPaths):
39+
# Just the code to download UV from PyPI if it is otherwise unavailable
40+
pip_install = retrieve_pip(paths=paths)
41+
42+
log("Downloading UV from PyPi")
43+
with paths.build_folder() as build_folder:
44+
45+
install_folder = os.path.join(build_folder, "uv")
46+
47+
uv_dl = os.path.join(install_folder, uv_download)
48+
49+
pip_command = [
50+
sys.executable,
51+
pip_install,
52+
"--disable-pip-version-check",
53+
"install",
54+
"-q",
55+
f"uv{uv_versionspec}",
56+
"--only-binary=:all:",
57+
"--target",
58+
install_folder,
59+
]
60+
61+
# Download UV with pip - handles getting the correct platform version
62+
try:
63+
_laz.subprocess.run(
64+
pip_command,
65+
check=True,
66+
)
67+
except _laz.subprocess.CalledProcessError as e:
68+
log(f"UV download failed: {e}")
69+
uv_path = None
70+
else:
71+
# Copy the executable out of the pip install
72+
_laz.shutil.copy(uv_dl, paths.uv_executable)
73+
uv_path = paths.uv_executable
74+
75+
version_command = [uv_path, "-V"]
76+
version_output = _laz.subprocess.run(version_command, capture_output=True, text=True)
77+
ver_match = _laz.re.match(uv_versionre, version_output.stdout.strip())
78+
if ver_match:
79+
uv_version = ver_match.group("uv_ver")
80+
with open(f"{uv_path}.version", 'w') as ver_file:
81+
ver_file.write(uv_version)
82+
else:
83+
log(f"Unexpected UV version output {version_output.stdout!r}")
84+
uv_path = None
85+
86+
return uv_path
87+
88+
89+
def get_local_uv():
90+
uv_path = _laz.shutil.which("uv")
91+
if uv_path:
92+
try:
93+
version_output = _laz.subprocess.run([uv_path, "-V"], capture_output=True, text=True)
94+
except (FileNotFoundError, _laz.subprocess.CalledProcessError):
95+
return None
96+
97+
ver_match = _laz.re.match(uv_versionre, version_output.stdout.strip())
98+
if ver_match:
99+
uv_version = ver_match.group("uv_ver")
100+
if uv_version not in _laz.SpecifierSet(uv_versionspec):
101+
log(
102+
f"Local uv install version {uv_version!r} "
103+
f"does not satisfy the ducktools.env specifier {uv_versionspec!r}"
104+
)
105+
return None
106+
107+
return uv_path
108+
109+
37110
def retrieve_uv(paths: ManagedPaths, reinstall: bool = False) -> str | None:
38-
uv_path = None
111+
uv_path = get_local_uv()
39112

40113
if os.path.exists(paths.uv_executable):
41114
uv_path = paths.uv_executable
@@ -48,46 +121,7 @@ def retrieve_uv(paths: ManagedPaths, reinstall: bool = False) -> str | None:
48121
uv_path = None
49122

50123
if uv_path is None:
51-
pip_install = retrieve_pip(paths=paths)
52-
53-
log("Downloading UV from PyPi")
54-
with paths.build_folder() as build_folder:
55-
56-
install_folder = os.path.join(build_folder, "uv")
57-
58-
uv_dl = os.path.join(install_folder, uv_download)
59-
60-
pip_command = [
61-
sys.executable,
62-
pip_install,
63-
"--disable-pip-version-check",
64-
"install",
65-
"-q",
66-
f"uv{uv_versionspec}",
67-
"--only-binary=:all:",
68-
"--target",
69-
install_folder,
70-
]
71-
72-
# Download UV with pip - handles getting the correct platform version
73-
try:
74-
_laz.subprocess.run(
75-
pip_command,
76-
check=True,
77-
)
78-
except _laz.subprocess.CalledProcessError as e:
79-
log(f"UV download failed: {e}")
80-
uv_path = None
81-
else:
82-
# Copy the executable out of the pip install
83-
_laz.shutil.copy(uv_dl, paths.uv_executable)
84-
uv_path = paths.uv_executable
85-
86-
version_command = [uv_path, "-V"]
87-
version_output = _laz.subprocess.run(version_command, capture_output=True, text=True)
88-
uv_version = version_output.stdout.split()[1]
89-
with open(f"{uv_path}.version", 'w') as ver_file:
90-
ver_file.write(uv_version)
124+
uv_path = download_uv(paths=paths)
91125

92126
return uv_path
93127

tests/test_get_uv.py

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -26,17 +26,40 @@
2626
import unittest.mock as mock
2727
from pathlib import Path
2828

29+
import pytest
30+
2931
import ducktools.env.scripts.get_uv as get_uv
3032
from ducktools.env.platform_paths import ManagedPaths
3133

3234
UV_PYTHON_LIST_OUTPUT = Path(__file__).parent / "data" / "uv_python_versions_list.txt"
3335

3436

37+
@pytest.fixture(scope="function")
38+
def block_local_uv():
39+
with mock.patch("shutil.which") as which_mock:
40+
which_mock.return_value = None
41+
yield
42+
43+
3544
class TestRetrieveUV:
3645
paths = ManagedPaths("ducktools_testing")
3746

47+
def test_local_uv(self):
48+
fake_uv_path = "/home/uname/.cargo/bin/uv"
49+
with (
50+
mock.patch("shutil.which") as which_mock,
51+
mock.patch("subprocess.run") as run_mock,
52+
):
53+
which_mock.return_value = fake_uv_path
54+
55+
uv_cmd_mock = mock.MagicMock()
56+
uv_cmd_mock.stdout = "uv 0.6.5"
57+
run_mock.return_value = uv_cmd_mock
58+
59+
assert get_uv.get_local_uv() == fake_uv_path
60+
assert get_uv.retrieve_uv(self.paths, reinstall=False) == fake_uv_path
3861

39-
def test_uv_install(self):
62+
def test_uv_install(self, block_local_uv):
4063
with (
4164
mock.patch("os.path.exists") as exists_mock,
4265
mock.patch("os.remove") as remove_mock,
@@ -52,7 +75,7 @@ def test_uv_install(self):
5275
open_mock.return_value.__enter__.return_value = writer_mock
5376

5477
uv_cmd_mock = mock.MagicMock()
55-
uv_cmd_mock.stdout = "uv 0.4.15 (0d81bfbc6 2024-09-21)"
78+
uv_cmd_mock.stdout = "uv 0.6.5"
5679
run_mock.return_value = uv_cmd_mock
5780

5881
build_path = "build/path"
@@ -94,14 +117,14 @@ def test_uv_install(self):
94117

95118
# One open call and write to store the UV version
96119
open_mock.assert_called_with(f"{self.paths.uv_executable}.version", "w")
97-
writer_mock.write.assert_called_with("0.4.15")
120+
writer_mock.write.assert_called_with("0.6.5")
98121

99122
# Remove should not have been called
100123
remove_mock.assert_not_called()
101124

102125
assert uv_path == self.paths.uv_executable
103126

104-
def test_uv_reinstall(self):
127+
def test_uv_reinstall(self, block_local_uv):
105128
with (
106129
mock.patch("os.path.exists") as exists_mock,
107130
mock.patch("os.remove") as remove_mock,
@@ -117,7 +140,7 @@ def test_uv_reinstall(self):
117140
open_mock.return_value.__enter__.return_value = writer_mock
118141

119142
uv_cmd_mock = mock.MagicMock()
120-
uv_cmd_mock.stdout = "uv 0.4.15 (0d81bfbc6 2024-09-21)"
143+
uv_cmd_mock.stdout = "uv 0.6.5"
121144
run_mock.return_value = uv_cmd_mock
122145

123146
build_path = "build/path"
@@ -159,7 +182,7 @@ def test_uv_reinstall(self):
159182

160183
# One open call and write to store the UV version
161184
open_mock.assert_called_with(f"{self.paths.uv_executable}.version", "w")
162-
writer_mock.write.assert_called_with("0.4.15")
185+
writer_mock.write.assert_called_with("0.6.5")
163186

164187
# Remove should have been called twice
165188
remove_mock.assert_has_calls(
@@ -171,7 +194,7 @@ def test_uv_reinstall(self):
171194

172195
assert uv_path == self.paths.uv_executable
173196

174-
def test_uv_install_failure(self):
197+
def test_uv_install_failure(self, block_local_uv):
175198
with (
176199
mock.patch("os.path.exists") as exists_mock,
177200
mock.patch("os.remove") as remove_mock,
@@ -218,14 +241,14 @@ def test_uv_install_failure(self):
218241
open_mock.assert_not_called()
219242
remove_mock.assert_not_called()
220243

221-
def test_uv_exists_keep(self):
244+
def test_uv_exists_keep(self, block_local_uv):
222245
with (
223246
mock.patch("os.path.exists") as exists_mock,
224247
mock.patch("os.remove") as remove_mock,
225248
mock.patch("subprocess.run") as run_mock,
226249
mock.patch.object(ManagedPaths, "get_uv_version") as uv_ver_mock
227250
):
228-
uv_ver_mock.return_value = "0.4.25"
251+
uv_ver_mock.return_value = "0.6.5"
229252
exists_mock.return_value = True
230253
uv_path = get_uv.retrieve_uv(paths=self.paths, reinstall=False)
231254

0 commit comments

Comments
 (0)