Add new PythonRequest::Any and VersionRequest::Any variants (#7517)

Follow-up to https://github.com/astral-sh/uv/pull/7514 and #7526 adding
the `Any` variants again with slightly different semantics (i.e., they
allow pre-releases)

We'll be able to use this in https://github.com/astral-sh/uv/pull/7508
to avoid querying a bunch of alternative interpreters unnecessarily in
the non-list case.
This commit is contained in:
Zanie Blue 2024-09-19 06:19:13 -05:00 committed by GitHub
parent 0f9a2e3870
commit 09a2ebca8b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 66 additions and 24 deletions

View file

@ -36,12 +36,14 @@ use crate::{Interpreter, PythonVersion};
/// See [`PythonRequest::from_str`]. /// See [`PythonRequest::from_str`].
#[derive(Debug, Clone, PartialEq, Eq, Default)] #[derive(Debug, Clone, PartialEq, Eq, Default)]
pub enum PythonRequest { 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 /// This may skip some Python installations, such as pre-release versions or alternative
/// implementations. /// implementations.
#[default] #[default]
Default, Default,
/// Any Python installation
Any,
/// A Python version without an implementation name e.g. `3.10` or `>=3.12,<3.13` /// A Python version without an implementation name e.g. `3.10` or `>=3.12,<3.13`
Version(VersionRequest), Version(VersionRequest),
/// A path to a directory containing a Python installation, e.g. `.venv` /// 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. /// Allow an appropriate default Python version.
#[default] #[default]
Default, Default,
/// Allow any Python version.
Any,
Major(u8), Major(u8),
MajorMinor(u8, u8), MajorMinor(u8, u8),
MajorMinorPatch(u8, u8, u8), MajorMinorPatch(u8, u8, u8),
@ -488,7 +492,10 @@ fn find_all_minor(
dir: &Path, dir: &Path,
) -> impl Iterator<Item = PathBuf> { ) -> impl Iterator<Item = PathBuf> {
match version_request { match version_request {
VersionRequest::Default | VersionRequest::Major(_) | VersionRequest::Range(_) => { &VersionRequest::Any
| VersionRequest::Default
| VersionRequest::Major(_)
| VersionRequest::Range(_) => {
let regex = if let Some(implementation) = implementation { let regex = if let Some(implementation) = implementation {
Regex::new(&format!( Regex::new(&format!(
r"^({}|python3)\.(?<minor>\d\d?){}$", r"^({}|python3)\.(?<minor>\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({ PythonRequest::Default => Box::new({
debug!("Searching for Python interpreter in {preference}"); debug!("Searching for default Python interpreter in {preference}");
python_interpreters(None, None, environments, preference, cache).map(|result| { python_interpreters(
Some(&VersionRequest::Default),
None,
environments,
preference,
cache,
)
.map(|result| {
result result
.map(PythonInstallation::from_tuple) .map(PythonInstallation::from_tuple)
.map(FindPythonResult::Ok) .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 // 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; let request = PythonRequest::Default;
Ok( Ok(
find_python_installation(&request, environments, preference, cache)?.map_err(|err| { 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`]. /// This cannot fail, which means weird inputs will be parsed as [`PythonRequest::File`] or [`PythonRequest::ExecutableName`].
pub fn parse(value: &str) -> Self { pub fn parse(value: &str) -> Self {
// e.g. `any` // Literals, e.g. `any` or `default`
if value.eq_ignore_ascii_case("any") { if value.eq_ignore_ascii_case("any") {
return Self::Any;
}
if value.eq_ignore_ascii_case("default") {
return Self::Default; return Self::Default;
} }
@ -1250,7 +1282,7 @@ impl PythonRequest {
} }
match self { match self {
PythonRequest::Default => true, PythonRequest::Default | PythonRequest::Any => true,
PythonRequest::Version(version_request) => { PythonRequest::Version(version_request) => {
version_request.matches_interpreter(interpreter) version_request.matches_interpreter(interpreter)
} }
@ -1335,6 +1367,7 @@ impl PythonRequest {
pub(crate) fn allows_prereleases(&self) -> bool { pub(crate) fn allows_prereleases(&self) -> bool {
match self { match self {
Self::Default => false, Self::Default => false,
Self::Any => true,
Self::Version(version) => version.allows_prereleases(), Self::Version(version) => version.allows_prereleases(),
Self::Directory(_) | Self::File(_) | Self::ExecutableName(_) => true, Self::Directory(_) | Self::File(_) | Self::ExecutableName(_) => true,
Self::Implementation(_) => false, Self::Implementation(_) => false,
@ -1352,7 +1385,8 @@ impl PythonRequest {
/// [`Self::parse`] should always return the same request when given the output of this method. /// [`Self::parse`] should always return the same request when given the output of this method.
pub fn to_canonical_string(&self) -> String { pub fn to_canonical_string(&self) -> String {
match self { match self {
Self::Default => "any".to_string(), Self::Any => "any".to_string(),
Self::Default => "default".to_string(),
Self::Version(version) => version.to_string(), Self::Version(version) => version.to_string(),
Self::Directory(path) => path.display().to_string(), Self::Directory(path) => path.display().to_string(),
Self::File(path) => path.display().to_string(), Self::File(path) => path.display().to_string(),
@ -1453,7 +1487,7 @@ impl VersionRequest {
}; };
match self { 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) => [ Self::Major(major) => [
Some(Cow::Owned(format!("python{major}{extension}"))), Some(Cow::Owned(format!("python{major}{extension}"))),
Some(python), Some(python),
@ -1504,7 +1538,9 @@ impl VersionRequest {
}; };
match self { 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) => [ Self::Major(major) => [
Some(Cow::Owned(format!("{name}{major}{extension}"))), Some(Cow::Owned(format!("{name}{major}{extension}"))),
Some(python), Some(python),
@ -1541,7 +1577,7 @@ impl VersionRequest {
pub(crate) fn check_supported(&self) -> Result<(), String> { pub(crate) fn check_supported(&self) -> Result<(), String> {
match self { match self {
Self::Default => (), Self::Any | Self::Default => (),
Self::Major(major) => { Self::Major(major) => {
if *major < 3 { if *major < 3 {
return Err(format!( return Err(format!(
@ -1580,7 +1616,7 @@ impl VersionRequest {
/// Check if a interpreter matches the requested Python version. /// Check if a interpreter matches the requested Python version.
pub(crate) fn matches_interpreter(&self, interpreter: &Interpreter) -> bool { pub(crate) fn matches_interpreter(&self, interpreter: &Interpreter) -> bool {
match self { match self {
Self::Default => true, Self::Any | Self::Default => true,
Self::Major(major) => interpreter.python_major() == *major, Self::Major(major) => interpreter.python_major() == *major,
Self::MajorMinor(major, minor) => { Self::MajorMinor(major, minor) => {
(interpreter.python_major(), interpreter.python_minor()) == (*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 { pub(crate) fn matches_version(&self, version: &PythonVersion) -> bool {
match self { match self {
Self::Default => true, Self::Any | Self::Default => true,
Self::Major(major) => version.major() == *major, Self::Major(major) => version.major() == *major,
Self::MajorMinor(major, minor) => { Self::MajorMinor(major, minor) => {
(version.major(), version.minor()) == (*major, *minor) (version.major(), version.minor()) == (*major, *minor)
@ -1631,7 +1667,7 @@ impl VersionRequest {
fn matches_major_minor(&self, major: u8, minor: u8) -> bool { fn matches_major_minor(&self, major: u8, minor: u8) -> bool {
match self { match self {
Self::Default => true, Self::Any | Self::Default => true,
Self::Major(self_major) => *self_major == major, Self::Major(self_major) => *self_major == major,
Self::MajorMinor(self_major, self_minor) => { Self::MajorMinor(self_major, self_minor) => {
(*self_major, *self_minor) == (major, 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 { pub(crate) fn matches_major_minor_patch(&self, major: u8, minor: u8, patch: u8) -> bool {
match self { match self {
Self::Default => true, Self::Any | Self::Default => true,
Self::Major(self_major) => *self_major == major, Self::Major(self_major) => *self_major == major,
Self::MajorMinor(self_major, self_minor) => { Self::MajorMinor(self_major, self_minor) => {
(*self_major, *self_minor) == (major, minor) (*self_major, *self_minor) == (major, minor)
@ -1673,7 +1709,7 @@ impl VersionRequest {
/// Return true if a patch version is present in the request. /// Return true if a patch version is present in the request.
fn has_patch(&self) -> bool { fn has_patch(&self) -> bool {
match self { match self {
Self::Default => false, Self::Any | Self::Default => false,
Self::Major(..) => false, Self::Major(..) => false,
Self::MajorMinor(..) => false, Self::MajorMinor(..) => false,
Self::MajorMinorPatch(..) => true, Self::MajorMinorPatch(..) => true,
@ -1689,6 +1725,7 @@ impl VersionRequest {
fn without_patch(self) -> Self { fn without_patch(self) -> Self {
match self { match self {
Self::Default => Self::Default, Self::Default => Self::Default,
Self::Any => Self::Any,
Self::Major(major) => Self::Major(major), Self::Major(major) => Self::Major(major),
Self::MajorMinor(major, minor) => Self::MajorMinor(major, minor), Self::MajorMinor(major, minor) => Self::MajorMinor(major, minor),
Self::MajorMinorPatch(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 { pub(crate) fn allows_prereleases(&self) -> bool {
match self { match self {
Self::Default => false, Self::Default => false,
Self::Any => true,
Self::Major(_) => true, Self::Major(_) => true,
Self::MajorMinor(..) => true, Self::MajorMinor(..) => true,
Self::MajorMinorPatch(..) => true, Self::MajorMinorPatch(..) => true,
@ -1788,6 +1826,7 @@ impl From<&PythonVersion> for VersionRequest {
impl fmt::Display for VersionRequest { impl fmt::Display for VersionRequest {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self { match self {
Self::Any => f.write_str("any"),
Self::Default => f.write_str("default"), Self::Default => f.write_str("default"),
Self::Major(major) => write!(f, "{major}"), Self::Major(major) => write!(f, "{major}"),
Self::MajorMinor(major, minor) => write!(f, "{major}.{minor}"), Self::MajorMinor(major, minor) => write!(f, "{major}.{minor}"),
@ -1805,7 +1844,8 @@ impl fmt::Display for VersionRequest {
impl fmt::Display for PythonRequest { impl fmt::Display for PythonRequest {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self { 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::Version(version) => write!(f, "Python {version}"),
Self::Directory(path) => write!(f, "directory `{}`", path.user_display()), Self::Directory(path) => write!(f, "directory `{}`", path.user_display()),
Self::File(path) => write!(f, "path `{}`", path.user_display()), Self::File(path) => write!(f, "path `{}`", path.user_display()),
@ -1887,7 +1927,7 @@ impl fmt::Display for PythonNotFound {
}; };
match self.request { match self.request {
PythonRequest::Default => { PythonRequest::Default | PythonRequest::Any => {
write!(f, "No interpreter found in {sources}") write!(f, "No interpreter found in {sources}")
} }
_ => { _ => {
@ -1965,7 +2005,8 @@ mod tests {
#[test] #[test]
fn interpreter_request_from_str() { 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!( assert_eq!(
PythonRequest::parse("3.12"), PythonRequest::parse("3.12"),
PythonRequest::Version(VersionRequest::from_str("3.12").unwrap()) PythonRequest::Version(VersionRequest::from_str("3.12").unwrap())
@ -2127,7 +2168,8 @@ mod tests {
#[test] #[test]
fn interpreter_request_to_canonical_string() { 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!( assert_eq!(
PythonRequest::Version(VersionRequest::from_str("3.12").unwrap()).to_canonical_string(), PythonRequest::Version(VersionRequest::from_str("3.12").unwrap()).to_canonical_string(),
"3.12" "3.12"

View file

@ -173,7 +173,7 @@ impl PythonDownloadRequest {
.with_version(version.clone()), .with_version(version.clone()),
), ),
PythonRequest::Key(request) => Some(request.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 // We can't download a managed installation for these request kinds
PythonRequest::Directory(_) PythonRequest::Directory(_)
| PythonRequest::ExecutableName(_) | PythonRequest::ExecutableName(_)

View file

@ -80,7 +80,7 @@ impl fmt::Display for EnvironmentNotFound {
EnvironmentPreference::OnlyVirtual => SearchType::Virtual, EnvironmentPreference::OnlyVirtual => SearchType::Virtual,
}; };
if matches!(self.request, PythonRequest::Default) { if matches!(self.request, PythonRequest::Default | PythonRequest::Any) {
write!(f, "No {search_type} found")?; write!(f, "No {search_type} found")?;
} else { } else {
write!(f, "No {search_type} found for {}", self.request)?; write!(f, "No {search_type} found for {}", self.request)?;

View file

@ -305,7 +305,7 @@ impl ManagedPythonInstallation {
pub fn satisfies(&self, request: &PythonRequest) -> bool { pub fn satisfies(&self, request: &PythonRequest) -> bool {
match request { match request {
PythonRequest::File(path) => self.executable() == *path, PythonRequest::File(path) => self.executable() == *path,
PythonRequest::Default => true, PythonRequest::Default | PythonRequest::Any => true,
PythonRequest::Directory(path) => self.path() == *path, PythonRequest::Directory(path) => self.path() == *path,
PythonRequest::ExecutableName(name) => self PythonRequest::ExecutableName(name) => self
.executable() .executable()

View file

@ -66,7 +66,7 @@ pub(crate) async fn list(
}; };
let installed = find_python_installations( let installed = find_python_installations(
&PythonRequest::Default, &PythonRequest::Any,
EnvironmentPreference::OnlySystem, EnvironmentPreference::OnlySystem,
python_preference, python_preference,
cache, cache,