Skip to content

Commit 586bab3

Browse files
authored
Update uv python install --reinstall to reinstall all previous versions (#11072)
Since we're shipping substantive updates to Python versions frequently, I want to lower the bar for reinstalling with the latest distributions. There's a follow-up task that's documented in a test case at https://github.com/astral-sh/uv/pull/11072/files#diff-f499c776e1d8cc5e55d7620786e32e8732b675abd98e246c0971130f5de9ed50R157-R158
1 parent d517b1c commit 586bab3

File tree

3 files changed

+164
-23
lines changed

3 files changed

+164
-23
lines changed

crates/uv-python/src/downloads.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ use crate::implementation::{
2828
};
2929
use crate::installation::PythonInstallationKey;
3030
use crate::libc::LibcDetectionError;
31+
use crate::managed::ManagedPythonInstallation;
3132
use crate::platform::{self, Arch, Libc, Os};
3233
use crate::{Interpreter, PythonRequest, PythonVersion, VersionRequest};
3334

@@ -344,6 +345,23 @@ impl PythonDownloadRequest {
344345
}
345346
}
346347

348+
impl From<&ManagedPythonInstallation> for PythonDownloadRequest {
349+
fn from(installation: &ManagedPythonInstallation) -> Self {
350+
let key = installation.key();
351+
Self::new(
352+
Some(VersionRequest::from(&key.version())),
353+
match &key.implementation {
354+
LenientImplementationName::Known(implementation) => Some(*implementation),
355+
LenientImplementationName::Unknown(name) => unreachable!("Managed Python installations are expected to always have known implementation names, found {name}"),
356+
},
357+
Some(key.arch),
358+
Some(key.os),
359+
Some(key.libc),
360+
Some(key.prerelease.is_some()),
361+
)
362+
}
363+
}
364+
347365
impl Display for PythonDownloadRequest {
348366
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
349367
let mut parts = Vec::new();

crates/uv/src/commands/python/install.rs

Lines changed: 69 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use std::borrow::Cow;
12
use std::fmt::Write;
23
use std::io::ErrorKind;
34
use std::path::{Path, PathBuf};
@@ -164,7 +165,12 @@ pub(crate) async fn install(
164165
.unwrap_or_else(|| {
165166
// If no version file is found and no requests were made
166167
is_default_install = true;
167-
vec![PythonRequest::Default]
168+
vec![if reinstall {
169+
// On bare `--reinstall`, reinstall all Python versions
170+
PythonRequest::Any
171+
} else {
172+
PythonRequest::Default
173+
}]
168174
})
169175
.into_iter()
170176
.map(InstallRequest::new)
@@ -193,35 +199,76 @@ pub(crate) async fn install(
193199

194200
// Find requests that are already satisfied
195201
let mut changelog = Changelog::default();
196-
let (satisfied, unsatisfied): (Vec<_>, Vec<_>) = requests.iter().partition_map(|request| {
197-
if let Some(installation) = existing_installations
198-
.iter()
199-
.find(|installation| request.matches_installation(installation))
200-
{
201-
changelog.existing.insert(installation.key().clone());
202-
if reinstall {
203-
debug!(
204-
"Ignoring match `{}` for request `{}` due to `--reinstall` flag",
205-
installation.key().green(),
206-
request.cyan()
207-
);
202+
let (satisfied, unsatisfied): (Vec<_>, Vec<_>) = if reinstall {
203+
// In the reinstall case, we want to iterate over all matching installations instead of
204+
// stopping at the first match.
208205

209-
Either::Right(request)
210-
} else {
206+
let mut unsatisfied: Vec<Cow<InstallRequest>> =
207+
Vec::with_capacity(existing_installations.len() + requests.len());
208+
209+
for request in &requests {
210+
if existing_installations.is_empty() {
211+
debug!("No installation found for request `{}`", request.cyan());
212+
unsatisfied.push(Cow::Borrowed(request));
213+
}
214+
215+
for installation in existing_installations
216+
.iter()
217+
.filter(|installation| request.matches_installation(installation))
218+
{
219+
changelog.existing.insert(installation.key().clone());
220+
if matches!(&request.request, &PythonRequest::Any) {
221+
// Construct a install request matching the existing installation
222+
match InstallRequest::new(PythonRequest::Key(installation.into())) {
223+
Ok(request) => {
224+
debug!("Will reinstall `{}`", installation.key().green());
225+
unsatisfied.push(Cow::Owned(request));
226+
}
227+
Err(err) => {
228+
// This shouldn't really happen, but maybe a new version of uv dropped
229+
// support for a key we previously supported
230+
warn_user!(
231+
"Failed to create reinstall request for existing installation `{}`: {err}",
232+
installation.key().green()
233+
);
234+
}
235+
}
236+
} else {
237+
// TODO(zanieb): This isn't really right! But we need `--upgrade` or similar
238+
// to handle this case correctly without causing a breaking change.
239+
240+
// If we have real requests, just ignore the existing installation
241+
debug!(
242+
"Ignoring match `{}` for request `{}` due to `--reinstall` flag",
243+
installation.key().green(),
244+
request.cyan()
245+
);
246+
unsatisfied.push(Cow::Borrowed(request));
247+
break;
248+
}
249+
}
250+
}
251+
252+
(vec![], unsatisfied)
253+
} else {
254+
// If we can find one existing installation that matches the request, it is satisfied
255+
requests.iter().partition_map(|request| {
256+
if let Some(installation) = existing_installations
257+
.iter()
258+
.find(|installation| request.matches_installation(installation))
259+
{
211260
debug!(
212261
"Found `{}` for request `{}`",
213262
installation.key().green(),
214263
request.cyan(),
215264
);
216-
217265
Either::Left(installation)
266+
} else {
267+
debug!("No installation found for request `{}`", request.cyan());
268+
Either::Right(Cow::Borrowed(request))
218269
}
219-
} else {
220-
debug!("No installation found for request `{}`", request.cyan());
221-
222-
Either::Right(request)
223-
}
224-
});
270+
})
271+
};
225272

226273
// Check if Python downloads are banned
227274
if matches!(python_downloads, PythonDownloads::Never) && !unsatisfied.is_empty() {

crates/uv/tests/it/python_install.rs

Lines changed: 77 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ fn python_install() {
5454
"###);
5555

5656
// You can opt-in to a reinstall
57-
uv_snapshot!(context.filters(), context.python_install().arg("--reinstall"), @r###"
57+
uv_snapshot!(context.filters(), context.python_install().arg("3.13").arg("--reinstall"), @r###"
5858
success: true
5959
exit_code: 0
6060
----- stdout -----
@@ -91,6 +91,82 @@ fn python_install() {
9191
"###);
9292
}
9393

94+
#[test]
95+
fn python_reinstall() {
96+
let context: TestContext = TestContext::new_with_versions(&[])
97+
.with_filtered_python_keys()
98+
.with_filtered_exe_suffix()
99+
.with_managed_python_dirs();
100+
101+
// Install a couple versions
102+
uv_snapshot!(context.filters(), context.python_install().arg("3.12").arg("3.13"), @r###"
103+
success: true
104+
exit_code: 0
105+
----- stdout -----
106+
107+
----- stderr -----
108+
Installed 2 versions in [TIME]
109+
+ cpython-3.12.8-[PLATFORM]
110+
+ cpython-3.13.1-[PLATFORM]
111+
"###);
112+
113+
// Reinstall a single version
114+
uv_snapshot!(context.filters(), context.python_install().arg("3.13").arg("--reinstall"), @r###"
115+
success: true
116+
exit_code: 0
117+
----- stdout -----
118+
119+
----- stderr -----
120+
Installed Python 3.13.1 in [TIME]
121+
~ cpython-3.13.1-[PLATFORM]
122+
"###);
123+
124+
// Reinstall multiple versions
125+
uv_snapshot!(context.filters(), context.python_install().arg("--reinstall"), @r###"
126+
success: true
127+
exit_code: 0
128+
----- stdout -----
129+
130+
----- stderr -----
131+
Installed 2 versions in [TIME]
132+
~ cpython-3.12.8-[PLATFORM]
133+
~ cpython-3.13.1-[PLATFORM]
134+
"###);
135+
}
136+
137+
#[test]
138+
fn python_reinstall_patch() {
139+
let context: TestContext = TestContext::new_with_versions(&[])
140+
.with_filtered_python_keys()
141+
.with_filtered_exe_suffix()
142+
.with_managed_python_dirs();
143+
144+
// Install a couple patch versions
145+
uv_snapshot!(context.filters(), context.python_install().arg("3.12.6").arg("3.12.7"), @r###"
146+
success: true
147+
exit_code: 0
148+
----- stdout -----
149+
150+
----- stderr -----
151+
Installed 2 versions in [TIME]
152+
+ cpython-3.12.6-[PLATFORM]
153+
+ cpython-3.12.7-[PLATFORM]
154+
"###);
155+
156+
// Reinstall all "3.12" versions
157+
// TODO(zanieb): This doesn't work today, because we need this to install the "latest" as there
158+
// is no workflow for `--upgrade` yet
159+
uv_snapshot!(context.filters(), context.python_install().arg("3.12").arg("--reinstall"), @r###"
160+
success: true
161+
exit_code: 0
162+
----- stdout -----
163+
164+
----- stderr -----
165+
Installed Python 3.12.8 in [TIME]
166+
+ cpython-3.12.8-[PLATFORM]
167+
"###);
168+
}
169+
94170
#[test]
95171
fn python_install_automatic() {
96172
let context: TestContext = TestContext::new_with_versions(&[])

0 commit comments

Comments
 (0)