Skip to content

Commit

Permalink
Add new PythonRequest::Any and VersionRequest::Any variants (#7517)
Browse files Browse the repository at this point in the history
Follow-up to #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 #7508
to avoid querying a bunch of alternative interpreters unnecessarily in
the non-list case.
  • Loading branch information
zanieb committed Sep 19, 2024
1 parent 0f9a2e3 commit 09a2ebc
Show file tree
Hide file tree
Showing 5 changed files with 66 additions and 24 deletions.
82 changes: 62 additions & 20 deletions crates/uv-python/src/discovery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -488,7 +492,10 @@ fn find_all_minor(
dir: &Path,
) -> impl Iterator<Item = PathBuf> {
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)\.(?<minor>\d\d?){}$",
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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| {
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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,
Expand All @@ -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(),
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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!(
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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,
Expand All @@ -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),
Expand All @@ -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,
Expand Down Expand Up @@ -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}"),
Expand All @@ -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()),
Expand Down Expand Up @@ -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}")
}
_ => {
Expand Down Expand Up @@ -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())
Expand Down Expand Up @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion crates/uv-python/src/downloads.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(_)
Expand Down
2 changes: 1 addition & 1 deletion crates/uv-python/src/environment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)?;
Expand Down
2 changes: 1 addition & 1 deletion crates/uv-python/src/managed.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
2 changes: 1 addition & 1 deletion crates/uv/src/commands/python/list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ pub(crate) async fn list(
};

let installed = find_python_installations(
&PythonRequest::Default,
&PythonRequest::Any,
EnvironmentPreference::OnlySystem,
python_preference,
cache,
Expand Down

0 comments on commit 09a2ebc

Please sign in to comment.