diff --git a/crates/uv-python/src/discovery.rs b/crates/uv-python/src/discovery.rs index e53e9e81baa2..d376b3c13ef8 100644 --- a/crates/uv-python/src/discovery.rs +++ b/crates/uv-python/src/discovery.rs @@ -36,12 +36,14 @@ use crate::{Interpreter, PythonVersion}; /// See [`PythonRequest::from_str`]. #[derive(Debug, Clone, PartialEq, Eq, Default)] pub enum PythonRequest { - /// Use an appropriate default Python installation + /// An appropriate default Python installation /// /// This may skip some Python installations, such as pre-release versions or alternative /// implementations. #[default] Default, + /// Any Python installation + Any, /// A Python version without an implementation name e.g. `3.10` or `>=3.12,<3.13` Version(VersionRequest), /// A path to a directory containing a Python installation, e.g. `.venv` @@ -137,6 +139,8 @@ pub enum VersionRequest { /// Allow an appropriate default Python version. #[default] Default, + /// Allow any Python version. + Any, Major(u8), MajorMinor(u8, u8), MajorMinorPatch(u8, u8, u8), @@ -488,7 +492,10 @@ fn find_all_minor( dir: &Path, ) -> impl Iterator { match version_request { - VersionRequest::Default | VersionRequest::Major(_) | VersionRequest::Range(_) => { + &VersionRequest::Any + | VersionRequest::Default + | VersionRequest::Major(_) + | VersionRequest::Range(_) => { let regex = if let Some(implementation) = implementation { Regex::new(&format!( r"^({}|python3)\.(?\d\d?){}$", @@ -780,9 +787,31 @@ pub fn find_python_installations<'a>( )))) } } + PythonRequest::Any => Box::new({ + debug!("Searching for any Python interpreter in {preference}"); + python_interpreters( + Some(&VersionRequest::Any), + None, + environments, + preference, + cache, + ) + .map(|result| { + result + .map(PythonInstallation::from_tuple) + .map(FindPythonResult::Ok) + }) + }), PythonRequest::Default => Box::new({ - debug!("Searching for Python interpreter in {preference}"); - python_interpreters(None, None, environments, preference, cache).map(|result| { + debug!("Searching for default Python interpreter in {preference}"); + python_interpreters( + Some(&VersionRequest::Default), + None, + environments, + preference, + cache, + ) + .map(|result| { result .map(PythonInstallation::from_tuple) .map(FindPythonResult::Ok) @@ -979,7 +1008,7 @@ pub fn find_best_python_installation( } // If a Python version was requested but cannot be fulfilled, just take any version - debug!("Looking for Python installation with any version"); + debug!("Looking for a default Python installation"); let request = PythonRequest::Default; Ok( find_python_installation(&request, environments, preference, cache)?.map_err(|err| { @@ -1144,8 +1173,11 @@ impl PythonRequest { /// /// This cannot fail, which means weird inputs will be parsed as [`PythonRequest::File`] or [`PythonRequest::ExecutableName`]. pub fn parse(value: &str) -> Self { - // e.g. `any` + // Literals, e.g. `any` or `default` if value.eq_ignore_ascii_case("any") { + return Self::Any; + } + if value.eq_ignore_ascii_case("default") { return Self::Default; } @@ -1250,7 +1282,7 @@ impl PythonRequest { } match self { - PythonRequest::Default => true, + PythonRequest::Default | PythonRequest::Any => true, PythonRequest::Version(version_request) => { version_request.matches_interpreter(interpreter) } @@ -1335,6 +1367,7 @@ impl PythonRequest { pub(crate) fn allows_prereleases(&self) -> bool { match self { Self::Default => false, + Self::Any => true, Self::Version(version) => version.allows_prereleases(), Self::Directory(_) | Self::File(_) | Self::ExecutableName(_) => true, Self::Implementation(_) => false, @@ -1352,7 +1385,8 @@ impl PythonRequest { /// [`Self::parse`] should always return the same request when given the output of this method. pub fn to_canonical_string(&self) -> String { match self { - Self::Default => "any".to_string(), + Self::Any => "any".to_string(), + Self::Default => "default".to_string(), Self::Version(version) => version.to_string(), Self::Directory(path) => path.display().to_string(), Self::File(path) => path.display().to_string(), @@ -1453,7 +1487,7 @@ impl VersionRequest { }; match self { - Self::Default | Self::Range(_) => [Some(python3), Some(python), None, None], + Self::Any | Self::Default | Self::Range(_) => [Some(python3), Some(python), None, None], Self::Major(major) => [ Some(Cow::Owned(format!("python{major}{extension}"))), Some(python), @@ -1504,7 +1538,9 @@ impl VersionRequest { }; match self { - Self::Default | Self::Range(_) => [Some(python3), Some(python), None, None], + Self::Any | Self::Default | Self::Range(_) => { + [Some(python3), Some(python), None, None] + } Self::Major(major) => [ Some(Cow::Owned(format!("{name}{major}{extension}"))), Some(python), @@ -1541,7 +1577,7 @@ impl VersionRequest { pub(crate) fn check_supported(&self) -> Result<(), String> { match self { - Self::Default => (), + Self::Any | Self::Default => (), Self::Major(major) => { if *major < 3 { return Err(format!( @@ -1580,7 +1616,7 @@ impl VersionRequest { /// Check if a interpreter matches the requested Python version. pub(crate) fn matches_interpreter(&self, interpreter: &Interpreter) -> bool { match self { - Self::Default => true, + Self::Any | Self::Default => true, Self::Major(major) => interpreter.python_major() == *major, Self::MajorMinor(major, minor) => { (interpreter.python_major(), interpreter.python_minor()) == (*major, *minor) @@ -1612,7 +1648,7 @@ impl VersionRequest { pub(crate) fn matches_version(&self, version: &PythonVersion) -> bool { match self { - Self::Default => true, + Self::Any | Self::Default => true, Self::Major(major) => version.major() == *major, Self::MajorMinor(major, minor) => { (version.major(), version.minor()) == (*major, *minor) @@ -1631,7 +1667,7 @@ impl VersionRequest { fn matches_major_minor(&self, major: u8, minor: u8) -> bool { match self { - Self::Default => true, + Self::Any | Self::Default => true, Self::Major(self_major) => *self_major == major, Self::MajorMinor(self_major, self_minor) => { (*self_major, *self_minor) == (major, minor) @@ -1650,7 +1686,7 @@ impl VersionRequest { pub(crate) fn matches_major_minor_patch(&self, major: u8, minor: u8, patch: u8) -> bool { match self { - Self::Default => true, + Self::Any | Self::Default => true, Self::Major(self_major) => *self_major == major, Self::MajorMinor(self_major, self_minor) => { (*self_major, *self_minor) == (major, minor) @@ -1673,7 +1709,7 @@ impl VersionRequest { /// Return true if a patch version is present in the request. fn has_patch(&self) -> bool { match self { - Self::Default => false, + Self::Any | Self::Default => false, Self::Major(..) => false, Self::MajorMinor(..) => false, Self::MajorMinorPatch(..) => true, @@ -1689,6 +1725,7 @@ impl VersionRequest { fn without_patch(self) -> Self { match self { Self::Default => Self::Default, + Self::Any => Self::Any, Self::Major(major) => Self::Major(major), Self::MajorMinor(major, minor) => Self::MajorMinor(major, minor), Self::MajorMinorPatch(major, minor, _) => Self::MajorMinor(major, minor), @@ -1703,6 +1740,7 @@ impl VersionRequest { pub(crate) fn allows_prereleases(&self) -> bool { match self { Self::Default => false, + Self::Any => true, Self::Major(_) => true, Self::MajorMinor(..) => true, Self::MajorMinorPatch(..) => true, @@ -1788,6 +1826,7 @@ impl From<&PythonVersion> for VersionRequest { impl fmt::Display for VersionRequest { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { match self { + Self::Any => f.write_str("any"), Self::Default => f.write_str("default"), Self::Major(major) => write!(f, "{major}"), Self::MajorMinor(major, minor) => write!(f, "{major}.{minor}"), @@ -1805,7 +1844,8 @@ impl fmt::Display for VersionRequest { impl fmt::Display for PythonRequest { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { match self { - Self::Default => write!(f, "any Python"), + Self::Default => write!(f, "a default Python"), + Self::Any => write!(f, "any Python"), Self::Version(version) => write!(f, "Python {version}"), Self::Directory(path) => write!(f, "directory `{}`", path.user_display()), Self::File(path) => write!(f, "path `{}`", path.user_display()), @@ -1887,7 +1927,7 @@ impl fmt::Display for PythonNotFound { }; match self.request { - PythonRequest::Default => { + PythonRequest::Default | PythonRequest::Any => { write!(f, "No interpreter found in {sources}") } _ => { @@ -1965,7 +2005,8 @@ mod tests { #[test] fn interpreter_request_from_str() { - assert_eq!(PythonRequest::parse("any"), PythonRequest::Default); + assert_eq!(PythonRequest::parse("any"), PythonRequest::Any); + assert_eq!(PythonRequest::parse("default"), PythonRequest::Default); assert_eq!( PythonRequest::parse("3.12"), PythonRequest::Version(VersionRequest::from_str("3.12").unwrap()) @@ -2127,7 +2168,8 @@ mod tests { #[test] fn interpreter_request_to_canonical_string() { - assert_eq!(PythonRequest::Default.to_canonical_string(), "any"); + assert_eq!(PythonRequest::Default.to_canonical_string(), "default"); + assert_eq!(PythonRequest::Any.to_canonical_string(), "any"); assert_eq!( PythonRequest::Version(VersionRequest::from_str("3.12").unwrap()).to_canonical_string(), "3.12" diff --git a/crates/uv-python/src/downloads.rs b/crates/uv-python/src/downloads.rs index 174d48707e67..6b8f4565f103 100644 --- a/crates/uv-python/src/downloads.rs +++ b/crates/uv-python/src/downloads.rs @@ -173,7 +173,7 @@ impl PythonDownloadRequest { .with_version(version.clone()), ), PythonRequest::Key(request) => Some(request.clone()), - PythonRequest::Default => Some(Self::default()), + PythonRequest::Default | PythonRequest::Any => Some(Self::default()), // We can't download a managed installation for these request kinds PythonRequest::Directory(_) | PythonRequest::ExecutableName(_) diff --git a/crates/uv-python/src/environment.rs b/crates/uv-python/src/environment.rs index ec1eafd53662..a6dcfd614411 100644 --- a/crates/uv-python/src/environment.rs +++ b/crates/uv-python/src/environment.rs @@ -80,7 +80,7 @@ impl fmt::Display for EnvironmentNotFound { EnvironmentPreference::OnlyVirtual => SearchType::Virtual, }; - if matches!(self.request, PythonRequest::Default) { + if matches!(self.request, PythonRequest::Default | PythonRequest::Any) { write!(f, "No {search_type} found")?; } else { write!(f, "No {search_type} found for {}", self.request)?; diff --git a/crates/uv-python/src/managed.rs b/crates/uv-python/src/managed.rs index 1122f6772f0f..7f5d538ff454 100644 --- a/crates/uv-python/src/managed.rs +++ b/crates/uv-python/src/managed.rs @@ -305,7 +305,7 @@ impl ManagedPythonInstallation { pub fn satisfies(&self, request: &PythonRequest) -> bool { match request { PythonRequest::File(path) => self.executable() == *path, - PythonRequest::Default => true, + PythonRequest::Default | PythonRequest::Any => true, PythonRequest::Directory(path) => self.path() == *path, PythonRequest::ExecutableName(name) => self .executable() diff --git a/crates/uv/src/commands/python/list.rs b/crates/uv/src/commands/python/list.rs index 65700b3ae9ec..b8f089cf3ef1 100644 --- a/crates/uv/src/commands/python/list.rs +++ b/crates/uv/src/commands/python/list.rs @@ -66,7 +66,7 @@ pub(crate) async fn list( }; let installed = find_python_installations( - &PythonRequest::Default, + &PythonRequest::Any, EnvironmentPreference::OnlySystem, python_preference, cache,