diff --git a/crates/uv-python/src/environment.rs b/crates/uv-python/src/environment.rs index a49e93afd076..95437e41ccb7 100644 --- a/crates/uv-python/src/environment.rs +++ b/crates/uv-python/src/environment.rs @@ -1,9 +1,9 @@ +use owo_colors::OwoColorize; use std::borrow::Cow; use std::env; use std::fmt; use std::path::{Path, PathBuf}; use std::sync::Arc; - use uv_cache::Cache; use uv_fs::{LockedFile, Simplified}; @@ -45,27 +45,56 @@ impl From for EnvironmentNotFound { impl fmt::Display for EnvironmentNotFound { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - let environment = match self.preference { - EnvironmentPreference::Any => "virtual or system environment", + #[derive(Debug, Copy, Clone)] + enum SearchType { + /// Only virtual environments were searched. + Virtual, + /// Only system installations were searched. + System, + /// Both virtual and system installations were searched. + VirtualOrSystem, + } + + impl fmt::Display for SearchType { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Self::Virtual => write!(f, "virtual environment"), + Self::System => write!(f, "system Python installation"), + Self::VirtualOrSystem => { + write!(f, "virtual environment or system Python installation") + } + } + } + } + + let search_type = match self.preference { + EnvironmentPreference::Any => SearchType::VirtualOrSystem, EnvironmentPreference::ExplicitSystem => { if self.request.is_explicit_system() { - "virtual or system environment" + SearchType::VirtualOrSystem } else { - // TODO(zanieb): We could add a hint to use the `--system` flag here - "virtual environment" + SearchType::Virtual } } - EnvironmentPreference::OnlySystem => "system environment", - EnvironmentPreference::OnlyVirtual => "virtual environment", + EnvironmentPreference::OnlySystem => SearchType::System, + EnvironmentPreference::OnlyVirtual => SearchType::Virtual, }; - match self.request { - PythonRequest::Any => { - write!(f, "No {environment} found") - } - _ => { - write!(f, "No {environment} found for {}", self.request) - } + + if matches!(self.request, PythonRequest::Any) { + write!(f, "No {search_type} found")?; + } else { + write!(f, "No {search_type} found for {}", self.request)?; } + + match search_type { + // This error message assumes that the relevant API accepts the `--system` flag. This + // is true of the callsites today, since the project APIs never surface this error. + SearchType::Virtual => write!(f, "; run `{}` to create an environment, or pass `{}` to install into a non-virtual environment", "uv venv".green(), "--system".green())?, + SearchType::VirtualOrSystem => write!(f, "; run `{}` to create an environment", "uv venv".green())?, + SearchType::System => {} + } + + Ok(()) } } diff --git a/crates/uv/tests/pip_sync.rs b/crates/uv/tests/pip_sync.rs index 4cbbcf34a31d..eceb18943855 100644 --- a/crates/uv/tests/pip_sync.rs +++ b/crates/uv/tests/pip_sync.rs @@ -65,7 +65,7 @@ fn missing_venv() -> Result<()> { ----- stdout ----- ----- stderr ----- - error: No virtual environment found + error: No virtual environment found; run `uv venv` to create an environment, or pass `--system` to install into a non-virtual environment "###); assert!(predicates::path::missing().eval(&context.venv)); @@ -73,6 +73,24 @@ fn missing_venv() -> Result<()> { Ok(()) } +#[test] +fn missing_system() -> Result<()> { + let context = TestContext::new_with_versions(&[]); + let requirements = context.temp_dir.child("requirements.txt"); + requirements.write_str("anyio")?; + + uv_snapshot!(context.filters(), context.pip_sync().arg("requirements.txt").arg("--system"), @r###" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: No system Python installation found + "###); + + Ok(()) +} + /// Install a package into a virtual environment using the default link semantics. (On macOS, /// this using `clone` semantics.) #[test]