mirror of
https://github.com/astral-sh/uv.git
synced 2025-10-26 18:06:45 +00:00
Add hint when Python downloads are disabled (#14522)
Follow-up to https://github.com/astral-sh/uv/pull/14509 to provide the _reason_ downloads are disabled and surface it as a hint rather than a debug log. e.g., ``` ❯ cargo run -q -- run --no-managed-python -p 3.13.4 python error: No interpreter found for Python 3.13.4 in virtual environments or search path hint: A managed Python download is available for Python 3.13.4, but the Python preference is set to 'only system' ```
This commit is contained in:
parent
1dff18897a
commit
02345a5a7d
9 changed files with 187 additions and 59 deletions
|
|
@ -107,18 +107,9 @@ impl PythonInstallation {
|
|||
Err(err) => err,
|
||||
};
|
||||
|
||||
let downloads_enabled = preference.allows_managed()
|
||||
&& python_downloads.is_automatic()
|
||||
&& client_builder.connectivity.is_online();
|
||||
|
||||
if !downloads_enabled {
|
||||
debug!("Python downloads are disabled. Skipping check for available downloads...");
|
||||
return Err(err);
|
||||
}
|
||||
|
||||
match err {
|
||||
// If Python is missing, we should attempt a download
|
||||
Error::MissingPython(_) => {}
|
||||
Error::MissingPython(..) => {}
|
||||
// If we raised a non-critical error, we should attempt a download
|
||||
Error::Discovery(ref err) if !err.is_critical() => {}
|
||||
// Otherwise, this is fatal
|
||||
|
|
@ -126,40 +117,109 @@ impl PythonInstallation {
|
|||
}
|
||||
|
||||
// If we can't convert the request to a download, throw the original error
|
||||
let Some(request) = PythonDownloadRequest::from_request(request) else {
|
||||
let Some(download_request) = PythonDownloadRequest::from_request(request) else {
|
||||
return Err(err);
|
||||
};
|
||||
|
||||
debug!("Requested Python not found, checking for available download...");
|
||||
match Self::fetch(
|
||||
request.fill()?,
|
||||
let downloads_enabled = preference.allows_managed()
|
||||
&& python_downloads.is_automatic()
|
||||
&& client_builder.connectivity.is_online();
|
||||
|
||||
let download = download_request.clone().fill().map(|request| {
|
||||
ManagedPythonDownload::from_request(&request, python_downloads_json_url)
|
||||
});
|
||||
|
||||
// Regardless of whether downloads are enabled, we want to determine if the download is
|
||||
// available to power error messages. However, if downloads aren't enabled, we don't want to
|
||||
// report any errors related to them.
|
||||
let download = match download {
|
||||
Ok(Ok(download)) => Some(download),
|
||||
// If the download cannot be found, return the _original_ discovery error
|
||||
Ok(Err(downloads::Error::NoDownloadFound(_))) => {
|
||||
if downloads_enabled {
|
||||
debug!("No downloads are available for {request}");
|
||||
return Err(err);
|
||||
}
|
||||
None
|
||||
}
|
||||
Err(err) | Ok(Err(err)) => {
|
||||
if downloads_enabled {
|
||||
// We failed to determine the platform information
|
||||
return Err(err.into());
|
||||
}
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
let Some(download) = download else {
|
||||
// N.B. We should only be in this case when downloads are disabled; when downloads are
|
||||
// enabled, we should fail eagerly when something goes wrong with the download.
|
||||
debug_assert!(!downloads_enabled);
|
||||
return Err(err);
|
||||
};
|
||||
|
||||
// If the download is available, but not usable, we attach a hint to the original error.
|
||||
if !downloads_enabled {
|
||||
let for_request = match request {
|
||||
PythonRequest::Default | PythonRequest::Any => String::new(),
|
||||
_ => format!(" for {request}"),
|
||||
};
|
||||
|
||||
match python_downloads {
|
||||
PythonDownloads::Automatic => {}
|
||||
PythonDownloads::Manual => {
|
||||
return Err(err.with_missing_python_hint(format!(
|
||||
"A managed Python download is available{for_request}, but Python downloads are set to 'manual', use `uv python install {}` to install the required version",
|
||||
request.to_canonical_string(),
|
||||
)));
|
||||
}
|
||||
PythonDownloads::Never => {
|
||||
return Err(err.with_missing_python_hint(format!(
|
||||
"A managed Python download is available{for_request}, but Python downloads are set to 'never'"
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
match preference {
|
||||
PythonPreference::OnlySystem => {
|
||||
return Err(err.with_missing_python_hint(format!(
|
||||
"A managed Python download is available{for_request}, but the Python preference is set to 'only system'"
|
||||
)));
|
||||
}
|
||||
PythonPreference::Managed
|
||||
| PythonPreference::OnlyManaged
|
||||
| PythonPreference::System => {}
|
||||
}
|
||||
|
||||
if !client_builder.connectivity.is_online() {
|
||||
return Err(err.with_missing_python_hint(format!(
|
||||
"A managed Python download is available{for_request}, but uv is set to offline mode"
|
||||
)));
|
||||
}
|
||||
|
||||
return Err(err);
|
||||
}
|
||||
|
||||
Self::fetch(
|
||||
download,
|
||||
client_builder,
|
||||
cache,
|
||||
reporter,
|
||||
python_install_mirror,
|
||||
pypy_install_mirror,
|
||||
python_downloads_json_url,
|
||||
preview,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(installation) => Ok(installation),
|
||||
// Throw the original error if we couldn't find a download
|
||||
Err(Error::Download(downloads::Error::NoDownloadFound(_))) => Err(err),
|
||||
// But if the download failed, throw that error
|
||||
Err(err) => Err(err),
|
||||
}
|
||||
}
|
||||
|
||||
/// Download and install the requested installation.
|
||||
pub async fn fetch(
|
||||
request: PythonDownloadRequest,
|
||||
download: &'static ManagedPythonDownload,
|
||||
client_builder: &BaseClientBuilder<'_>,
|
||||
cache: &Cache,
|
||||
reporter: Option<&dyn Reporter>,
|
||||
python_install_mirror: Option<&str>,
|
||||
pypy_install_mirror: Option<&str>,
|
||||
python_downloads_json_url: Option<&str>,
|
||||
preview: PreviewMode,
|
||||
) -> Result<Self, Error> {
|
||||
let installations = ManagedPythonInstallations::from_settings(None)?.init()?;
|
||||
|
|
@ -167,7 +227,6 @@ impl PythonInstallation {
|
|||
let scratch_dir = installations.scratch();
|
||||
let _lock = installations.lock().await?;
|
||||
|
||||
let download = ManagedPythonDownload::from_request(&request, python_downloads_json_url)?;
|
||||
let client = client_builder.build();
|
||||
|
||||
info!("Fetching requested Python...");
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
//! Find requested Python interpreters and query interpreters for information.
|
||||
use owo_colors::OwoColorize;
|
||||
use thiserror::Error;
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
@ -93,8 +94,8 @@ pub enum Error {
|
|||
#[error(transparent)]
|
||||
KeyError(#[from] installation::PythonInstallationKeyError),
|
||||
|
||||
#[error(transparent)]
|
||||
MissingPython(#[from] PythonNotFound),
|
||||
#[error("{}{}", .0, if let Some(hint) = .1 { format!("\n\n{}{} {hint}", "hint".bold().cyan(), ":".bold()) } else { String::new() })]
|
||||
MissingPython(PythonNotFound, Option<String>),
|
||||
|
||||
#[error(transparent)]
|
||||
MissingEnvironment(#[from] environment::EnvironmentNotFound),
|
||||
|
|
@ -103,6 +104,21 @@ pub enum Error {
|
|||
InvalidEnvironment(#[from] environment::InvalidEnvironment),
|
||||
}
|
||||
|
||||
impl Error {
|
||||
pub(crate) fn with_missing_python_hint(self, hint: String) -> Self {
|
||||
match self {
|
||||
Error::MissingPython(err, _) => Error::MissingPython(err, Some(hint)),
|
||||
_ => self,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PythonNotFound> for Error {
|
||||
fn from(err: PythonNotFound) -> Self {
|
||||
Error::MissingPython(err, None)
|
||||
}
|
||||
}
|
||||
|
||||
// The mock interpreters are not valid on Windows so we don't have unit test coverage there
|
||||
// TODO(zanieb): We should write a mock interpreter script that works on Windows
|
||||
#[cfg(all(test, unix))]
|
||||
|
|
|
|||
|
|
@ -129,7 +129,8 @@ pub(crate) async fn pin(
|
|||
{
|
||||
Ok(python) => Some(python),
|
||||
// If no matching Python version is found, don't fail unless `resolved` was requested
|
||||
Err(uv_python::Error::MissingPython(err)) if !resolved => {
|
||||
Err(uv_python::Error::MissingPython(err, ..)) if !resolved => {
|
||||
// N.B. We omit the hint and just show the inner error message
|
||||
warn_user_once!("{err}");
|
||||
None
|
||||
}
|
||||
|
|
|
|||
|
|
@ -195,6 +195,12 @@ impl TestContext {
|
|||
"managed installations, search path, or registry".to_string(),
|
||||
"[PYTHON SOURCES]".to_string(),
|
||||
));
|
||||
self.filters.push((
|
||||
"registry or search path".to_string(),
|
||||
"[PYTHON SOURCES]".to_string(),
|
||||
));
|
||||
self.filters
|
||||
.push(("search path".to_string(), "[PYTHON SOURCES]".to_string()));
|
||||
self
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4318,14 +4318,16 @@ fn lock_requires_python() -> Result<()> {
|
|||
|
||||
// Install from the lockfile.
|
||||
// Note we need to disable Python fetches or we'll just download 3.12
|
||||
uv_snapshot!(context_unsupported.filters(), context_unsupported.sync().arg("--frozen").arg("--no-python-downloads"), @r###"
|
||||
uv_snapshot!(context_unsupported.filters(), context_unsupported.sync().arg("--frozen").arg("--no-python-downloads"), @r"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
error: No interpreter found for Python >=3.12 in [PYTHON SOURCES]
|
||||
"###);
|
||||
|
||||
hint: A managed Python download is available for Python >=3.12, but Python downloads are set to 'never'
|
||||
");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -873,6 +873,8 @@ fn python_find_script_python_not_found() {
|
|||
|
||||
----- stderr -----
|
||||
No interpreter found in [PYTHON SOURCES]
|
||||
|
||||
hint: A managed Python download is available, but Python downloads are set to 'never'
|
||||
");
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -196,14 +196,16 @@ fn python_install_automatic() {
|
|||
uv_snapshot!(context.filters(), context.run()
|
||||
.env_remove("VIRTUAL_ENV")
|
||||
.arg("--no-python-downloads")
|
||||
.arg("python").arg("-c").arg("import sys; print(sys.version_info[:2])"), @r###"
|
||||
.arg("python").arg("-c").arg("import sys; print(sys.version_info[:2])"), @r"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
error: No interpreter found in [PYTHON SOURCES]
|
||||
"###);
|
||||
|
||||
hint: A managed Python download is available, but Python downloads are set to 'never'
|
||||
");
|
||||
|
||||
// Otherwise, we should fetch the latest Python version
|
||||
uv_snapshot!(context.filters(), context.run()
|
||||
|
|
|
|||
|
|
@ -164,7 +164,7 @@ fn python_pin() {
|
|||
// (skip on Windows because the snapshot is different and the behavior is not platform dependent)
|
||||
#[cfg(unix)]
|
||||
{
|
||||
uv_snapshot!(context.filters(), context.python_pin().arg("pypy"), @r###"
|
||||
uv_snapshot!(context.filters(), context.python_pin().arg("pypy"), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
|
@ -172,7 +172,7 @@ fn python_pin() {
|
|||
|
||||
----- stderr -----
|
||||
warning: No interpreter found for PyPy in managed installations or search path
|
||||
"###);
|
||||
");
|
||||
|
||||
let python_version = context.read(PYTHON_VERSION_FILENAME);
|
||||
assert_snapshot!(python_version, @r###"
|
||||
|
|
@ -361,7 +361,7 @@ fn python_pin_global_creates_parent_dirs() {
|
|||
fn python_pin_no_python() {
|
||||
let context: TestContext = TestContext::new_with_versions(&[]);
|
||||
|
||||
uv_snapshot!(context.filters(), context.python_pin().arg("3.12"), @r###"
|
||||
uv_snapshot!(context.filters(), context.python_pin().arg("3.12"), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
|
@ -369,7 +369,7 @@ fn python_pin_no_python() {
|
|||
|
||||
----- stderr -----
|
||||
warning: No interpreter found for Python 3.12 in managed installations or search path
|
||||
"###);
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -448,7 +448,7 @@ fn python_pin_compatible_with_requires_python() -> Result<()> {
|
|||
"###);
|
||||
|
||||
// Request a version that is compatible and uses a Python variant
|
||||
uv_snapshot!(context.filters(), context.python_pin().arg("3.13t"), @r###"
|
||||
uv_snapshot!(context.filters(), context.python_pin().arg("3.13t"), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
|
@ -456,7 +456,7 @@ fn python_pin_compatible_with_requires_python() -> Result<()> {
|
|||
|
||||
----- stderr -----
|
||||
warning: No interpreter found for Python 3.13t in [PYTHON SOURCES]
|
||||
"###);
|
||||
");
|
||||
|
||||
// Request a implementation version that is compatible
|
||||
uv_snapshot!(context.filters(), context.python_pin().arg("cpython@3.11"), @r###"
|
||||
|
|
@ -587,27 +587,17 @@ fn warning_pinned_python_version_not_installed() -> Result<()> {
|
|||
/// We do need a Python interpreter for `--resolved` pins
|
||||
#[test]
|
||||
fn python_pin_resolve_no_python() {
|
||||
let context: TestContext = TestContext::new_with_versions(&[]);
|
||||
let context: TestContext = TestContext::new_with_versions(&[]).with_filtered_python_sources();
|
||||
uv_snapshot!(context.filters(), context.python_pin().arg("--resolved").arg("3.12"), @r"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
if cfg!(windows) {
|
||||
uv_snapshot!(context.filters(), context.python_pin().arg("--resolved").arg("3.12"), @r###"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
----- stderr -----
|
||||
error: No interpreter found for Python 3.12 in [PYTHON SOURCES]
|
||||
|
||||
----- stderr -----
|
||||
error: No interpreter found for Python 3.12 in managed installations, search path, or registry
|
||||
"###);
|
||||
} else {
|
||||
uv_snapshot!(context.filters(), context.python_pin().arg("--resolved").arg("3.12"), @r###"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
error: No interpreter found for Python 3.12 in managed installations or search path
|
||||
"###);
|
||||
}
|
||||
hint: A managed Python download is available for Python 3.12, but Python downloads are set to 'never'
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -741,14 +731,16 @@ fn python_pin_resolve() {
|
|||
// Request an implementation that is not installed
|
||||
// (skip on Windows because the snapshot is different and the behavior is not platform dependent)
|
||||
#[cfg(unix)]
|
||||
uv_snapshot!(context.filters(), context.python_pin().arg("--resolved").arg("pypy"), @r###"
|
||||
uv_snapshot!(context.filters(), context.python_pin().arg("--resolved").arg("pypy"), @r"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
error: No interpreter found for PyPy in managed installations or search path
|
||||
"###);
|
||||
|
||||
hint: A managed Python download is available for PyPy, but Python downloads are set to 'never'
|
||||
");
|
||||
|
||||
let python_version = context.read(PYTHON_VERSION_FILENAME);
|
||||
insta::with_settings!({
|
||||
|
|
|
|||
|
|
@ -2049,6 +2049,54 @@ fn tool_run_python_at_version() {
|
|||
"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tool_run_hint_version_not_available() {
|
||||
let context = TestContext::new_with_versions(&[])
|
||||
.with_filtered_counts()
|
||||
.with_filtered_python_sources();
|
||||
|
||||
uv_snapshot!(context.filters(), context.tool_run()
|
||||
.arg("python@3.12")
|
||||
.env(EnvVars::UV_PYTHON_DOWNLOADS, "never"), @r"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
error: No interpreter found for Python 3.12 in [PYTHON SOURCES]
|
||||
|
||||
hint: A managed Python download is available for Python 3.12, but Python downloads are set to 'never'
|
||||
");
|
||||
|
||||
uv_snapshot!(context.filters(), context.tool_run()
|
||||
.arg("python@3.12")
|
||||
.env(EnvVars::UV_PYTHON_DOWNLOADS, "auto")
|
||||
.env(EnvVars::UV_OFFLINE, "true"), @r"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
error: No interpreter found for Python 3.12 in [PYTHON SOURCES]
|
||||
|
||||
hint: A managed Python download is available for Python 3.12, but uv is set to offline mode
|
||||
");
|
||||
|
||||
uv_snapshot!(context.filters(), context.tool_run()
|
||||
.arg("python@3.12")
|
||||
.env(EnvVars::UV_PYTHON_DOWNLOADS, "auto")
|
||||
.env(EnvVars::UV_NO_MANAGED_PYTHON, "true"), @r"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
error: No interpreter found for Python 3.12 in [PYTHON SOURCES]
|
||||
|
||||
hint: A managed Python download is available for Python 3.12, but the Python preference is set to 'only system'
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tool_run_python_from() {
|
||||
let context = TestContext::new_with_versions(&["3.12", "3.11"])
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue