Skip to content

Commit d75fbc6

Browse files
committed
Add hint when Python downloads are disabled
1 parent b1dc2b7 commit d75fbc6

File tree

3 files changed

+97
-27
lines changed

3 files changed

+97
-27
lines changed

crates/uv-python/src/installation.rs

Lines changed: 78 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -107,66 +107,120 @@ impl PythonInstallation {
107107
Err(err) => err,
108108
};
109109

110-
let downloads_enabled = preference.allows_managed()
111-
&& python_downloads.is_automatic()
112-
&& client_builder.connectivity.is_online();
113-
114-
if !downloads_enabled {
115-
return Err(err);
116-
}
117-
118110
match err {
119111
// If Python is missing, we should attempt a download
120-
Error::MissingPython(_) => {}
112+
Error::MissingPython(..) => {}
121113
// If we raised a non-critical error, we should attempt a download
122114
Error::Discovery(ref err) if !err.is_critical() => {}
123115
// Otherwise, this is fatal
124116
_ => return Err(err),
125117
}
126118

127119
// If we can't convert the request to a download, throw the original error
128-
let Some(request) = PythonDownloadRequest::from_request(request) else {
120+
let Some(download_request) = PythonDownloadRequest::from_request(request) else {
121+
return Err(err);
122+
};
123+
124+
let downloads_enabled = preference.allows_managed()
125+
&& python_downloads.is_automatic()
126+
&& client_builder.connectivity.is_online();
127+
128+
let download = download_request.clone().fill().map(|request| {
129+
ManagedPythonDownload::from_request(&request, python_downloads_json_url)
130+
});
131+
132+
// Regardless of whether downloads are enabled, we want to determine if the download is
133+
// available to power error messages. However, if downloads aren't enabled, we don't want to
134+
// report any errors related to them.
135+
let download = match download {
136+
Ok(Ok(download)) => Some(download),
137+
// If the download cannot be found, return the _original_ discovery error
138+
Ok(Err(downloads::Error::NoDownloadFound(_))) => {
139+
if downloads_enabled {
140+
debug!("No downloads are available for {request}");
141+
return Err(err);
142+
}
143+
None
144+
}
145+
Err(err) | Ok(Err(err)) => {
146+
if downloads_enabled {
147+
// We failed to determine the platform information
148+
return Err(err.into());
149+
}
150+
None
151+
}
152+
};
153+
154+
let Some(download) = download else {
155+
// N.B. We should only be in this case when downloads are disabled; when downloads are
156+
// enabled, we should fail eagerly when something goes wrong with the download.
157+
debug_assert!(!downloads_enabled);
129158
return Err(err);
130159
};
131160

132-
debug!("Requested Python not found, checking for available download...");
133-
match Self::fetch(
134-
request.fill()?,
161+
// If the download is available, but not usable, we attach a hint to the original error.
162+
if !downloads_enabled {
163+
match python_downloads {
164+
PythonDownloads::Automatic => {}
165+
PythonDownloads::Manual => {
166+
return Err(err.with_missing_python_hint(format!(
167+
"A managed Python download is available for {request}, but Python downloads are set to 'manual', use `uv python install {request}` to install the required version"
168+
)));
169+
}
170+
PythonDownloads::Never => {
171+
return Err(err.with_missing_python_hint(format!(
172+
"A managed Python download is available for {request}, but Python downloads are set to 'never'"
173+
)));
174+
}
175+
}
176+
177+
match preference {
178+
PythonPreference::OnlySystem => {
179+
return Err(err.with_missing_python_hint(format!(
180+
"A managed Python download is available for {request}, but the Python preference is set to 'only system'"
181+
)));
182+
}
183+
PythonPreference::Managed
184+
| PythonPreference::OnlyManaged
185+
| PythonPreference::System => {}
186+
}
187+
188+
if !client_builder.connectivity.is_online() {
189+
return Err(err.with_missing_python_hint(format!(
190+
"A managed Python download is available for request `{request}`, but uv is set to offline mode"
191+
)));
192+
}
193+
194+
return Err(err);
195+
}
196+
197+
Self::fetch(
198+
download,
135199
client_builder,
136200
cache,
137201
reporter,
138202
python_install_mirror,
139203
pypy_install_mirror,
140-
python_downloads_json_url,
141204
preview,
142205
)
143206
.await
144-
{
145-
Ok(installation) => Ok(installation),
146-
// Throw the original error if we couldn't find a download
147-
Err(Error::Download(downloads::Error::NoDownloadFound(_))) => Err(err),
148-
// But if the download failed, throw that error
149-
Err(err) => Err(err),
150-
}
151207
}
152208

153209
/// Download and install the requested installation.
154210
pub async fn fetch(
155-
request: PythonDownloadRequest,
211+
download: &'static ManagedPythonDownload,
156212
client_builder: &BaseClientBuilder<'_>,
157213
cache: &Cache,
158214
reporter: Option<&dyn Reporter>,
159215
python_install_mirror: Option<&str>,
160216
pypy_install_mirror: Option<&str>,
161-
python_downloads_json_url: Option<&str>,
162217
preview: PreviewMode,
163218
) -> Result<Self, Error> {
164219
let installations = ManagedPythonInstallations::from_settings(None)?.init()?;
165220
let installations_dir = installations.root();
166221
let scratch_dir = installations.scratch();
167222
let _lock = installations.lock().await?;
168223

169-
let download = ManagedPythonDownload::from_request(&request, python_downloads_json_url)?;
170224
let client = client_builder.build();
171225

172226
info!("Fetching requested Python...");

crates/uv-python/src/lib.rs

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
//! Find requested Python interpreters and query interpreters for information.
2+
use owo_colors::OwoColorize;
23
use thiserror::Error;
34

45
#[cfg(test)]
@@ -93,8 +94,8 @@ pub enum Error {
9394
#[error(transparent)]
9495
KeyError(#[from] installation::PythonInstallationKeyError),
9596

96-
#[error(transparent)]
97-
MissingPython(#[from] PythonNotFound),
97+
#[error("{}{}", .0, if let Some(hint) = .1 { format!("\n\n{}{} {hint}", "hint".bold().cyan(), ":".bold()) } else { String::new() })]
98+
MissingPython(PythonNotFound, Option<String>),
9899

99100
#[error(transparent)]
100101
MissingEnvironment(#[from] environment::EnvironmentNotFound),
@@ -103,6 +104,21 @@ pub enum Error {
103104
InvalidEnvironment(#[from] environment::InvalidEnvironment),
104105
}
105106

107+
impl Error {
108+
pub(crate) fn with_missing_python_hint(self, hint: String) -> Self {
109+
match self {
110+
Error::MissingPython(err, _) => Error::MissingPython(err, Some(hint)),
111+
_ => self,
112+
}
113+
}
114+
}
115+
116+
impl From<PythonNotFound> for Error {
117+
fn from(err: PythonNotFound) -> Self {
118+
Error::MissingPython(err, None)
119+
}
120+
}
121+
106122
// The mock interpreters are not valid on Windows so we don't have unit test coverage there
107123
// TODO(zanieb): We should write a mock interpreter script that works on Windows
108124
#[cfg(all(test, unix))]

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ pub(crate) async fn pin(
129129
{
130130
Ok(python) => Some(python),
131131
// If no matching Python version is found, don't fail unless `resolved` was requested
132-
Err(uv_python::Error::MissingPython(err)) if !resolved => {
132+
Err(err @ uv_python::Error::MissingPython(..)) if !resolved => {
133133
warn_user_once!("{err}");
134134
None
135135
}

0 commit comments

Comments
 (0)