Skip to content

Commit eeb2965

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

File tree

3 files changed

+99
-27
lines changed

3 files changed

+99
-27
lines changed

crates/uv-python/src/installation.rs

Lines changed: 80 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -107,66 +107,122 @@ 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 should hint why and return the original
162+
// error.
163+
// TODO(zanieb): Attach these as actual hints to the error, instead of just logging.
164+
if !downloads_enabled {
165+
match python_downloads {
166+
PythonDownloads::Automatic => {}
167+
PythonDownloads::Manual => {
168+
return Err(err.with_missing_python_hint(format!(
169+
"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"
170+
)));
171+
}
172+
PythonDownloads::Never => {
173+
return Err(err.with_missing_python_hint(format!(
174+
"A managed Python download is available for {request}, but Python downloads are set to 'never'"
175+
)));
176+
}
177+
}
178+
179+
match preference {
180+
PythonPreference::OnlySystem => {
181+
return Err(err.with_missing_python_hint(format!(
182+
"A managed Python download is available for {request}, but the Python preference is set to 'only system'"
183+
)));
184+
}
185+
PythonPreference::Managed
186+
| PythonPreference::OnlyManaged
187+
| PythonPreference::System => {}
188+
}
189+
190+
if !client_builder.connectivity.is_online() {
191+
return Err(err.with_missing_python_hint(format!(
192+
"A managed Python download is available for request `{request}`, but uv is set to offline mode"
193+
)));
194+
}
195+
196+
return Err(err);
197+
}
198+
199+
Self::fetch(
200+
download,
135201
client_builder,
136202
cache,
137203
reporter,
138204
python_install_mirror,
139205
pypy_install_mirror,
140-
python_downloads_json_url,
141206
preview,
142207
)
143208
.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-
}
151209
}
152210

153211
/// Download and install the requested installation.
154212
pub async fn fetch(
155-
request: PythonDownloadRequest,
213+
download: &'static ManagedPythonDownload,
156214
client_builder: &BaseClientBuilder<'_>,
157215
cache: &Cache,
158216
reporter: Option<&dyn Reporter>,
159217
python_install_mirror: Option<&str>,
160218
pypy_install_mirror: Option<&str>,
161-
python_downloads_json_url: Option<&str>,
162219
preview: PreviewMode,
163220
) -> Result<Self, Error> {
164221
let installations = ManagedPythonInstallations::from_settings(None)?.init()?;
165222
let installations_dir = installations.root();
166223
let scratch_dir = installations.scratch();
167224
let _lock = installations.lock().await?;
168225

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

172228
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)