This commit is contained in:
Max Mynter 2025-07-06 14:07:00 +02:00 committed by GitHub
commit 51a53fed84
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 147 additions and 23 deletions

View file

@ -52,6 +52,8 @@ pub enum PythonRequest {
/// Any Python installation /// Any Python installation
Any, 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`
Latest,
// Newest resolvable, stable Python version.
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`
Directory(PathBuf), Directory(PathBuf),
@ -181,6 +183,7 @@ pub enum VersionRequest {
Default, Default,
/// Allow any Python version. /// Allow any Python version.
Any, Any,
Latest,
Major(u8, PythonVariant), Major(u8, PythonVariant),
MajorMinor(u8, u8, PythonVariant), MajorMinor(u8, u8, PythonVariant),
MajorMinorPatch(u8, u8, u8, PythonVariant), MajorMinorPatch(u8, u8, u8, PythonVariant),
@ -624,6 +627,7 @@ fn find_all_minor(
match version_request { match version_request {
&VersionRequest::Any &VersionRequest::Any
| VersionRequest::Default | VersionRequest::Default
| VersionRequest::Latest
| VersionRequest::Major(_, _) | VersionRequest::Major(_, _)
| VersionRequest::Range(_, _) => { | VersionRequest::Range(_, _) => {
let regex = if let Some(implementation) = implementation { let regex = if let Some(implementation) = implementation {
@ -1135,6 +1139,19 @@ pub fn find_python_installations<'a>(
.map_ok(|tuple| Ok(PythonInstallation::from_tuple(tuple))) .map_ok(|tuple| Ok(PythonInstallation::from_tuple(tuple)))
}) })
} }
PythonRequest::Latest => Box::new({
debug!("Searching for latest Python interpreter in {sources}");
python_interpreters(&VersionRequest::Any, None, environments, preference, cache)
.filter_ok(|(_source, interpreter)| interpreter.python_version().pre().is_none())
.max_by(|a, b| match (a.as_ref(), b.as_ref()) {
(Ok((_, interpreter_a)), Ok((_, interpreter_b))) => {
interpreter_a.key().cmp(&interpreter_b.key())
}
_ => std::cmp::Ordering::Equal,
})
.into_iter()
.map_ok(|tuple| Ok(PythonInstallation::from_tuple(tuple)))
}),
} }
} }
@ -1673,9 +1690,7 @@ impl PythonRequest {
// parsing errors are raised to the caller. // parsing errors are raised to the caller.
if let Some(after_at) = rest.strip_prefix('@') { if let Some(after_at) = rest.strip_prefix('@') {
if after_at == "latest" { if after_at == "latest" {
// Handle `@latest` as a special case. It's still an error for now, but we plan to return Ok(Some(VersionRequest::Latest));
// support it. TODO(zanieb): Add `PythonRequest::Latest`
return Err(Error::LatestVersionRequest);
} }
return after_at.parse().map(Some); return after_at.parse().map(Some);
} }
@ -1710,7 +1725,7 @@ impl PythonRequest {
} }
match self { match self {
PythonRequest::Default | PythonRequest::Any => true, PythonRequest::Default | PythonRequest::Any | PythonRequest::Latest => true,
PythonRequest::Version(version_request) => { PythonRequest::Version(version_request) => {
version_request.matches_interpreter(interpreter) version_request.matches_interpreter(interpreter)
} }
@ -1799,7 +1814,7 @@ impl PythonRequest {
/// Whether this request opts-in to a pre-release Python version. /// Whether this request opts-in to a pre-release Python version.
pub(crate) fn allows_prereleases(&self) -> bool { pub(crate) fn allows_prereleases(&self) -> bool {
match self { match self {
Self::Default => false, Self::Default | Self::Latest => false,
Self::Any => true, 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,
@ -1812,7 +1827,7 @@ impl PythonRequest {
/// Whether this request opts-in to an alternative Python implementation, e.g., PyPy. /// Whether this request opts-in to an alternative Python implementation, e.g., PyPy.
pub(crate) fn allows_alternative_implementations(&self) -> bool { pub(crate) fn allows_alternative_implementations(&self) -> bool {
match self { match self {
Self::Default => false, Self::Default | Self::Latest => false,
Self::Any => true, Self::Any => true,
Self::Version(_) => false, Self::Version(_) => false,
Self::Directory(_) | Self::File(_) | Self::ExecutableName(_) => true, Self::Directory(_) | Self::File(_) | Self::ExecutableName(_) => true,
@ -1833,6 +1848,7 @@ impl PythonRequest {
match self { match self {
Self::Any => "any".to_string(), Self::Any => "any".to_string(),
Self::Default => "default".to_string(), Self::Default => "default".to_string(),
Self::Latest => "latest".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(),
@ -2238,7 +2254,7 @@ impl VersionRequest {
/// Return the major version segment of the request, if any. /// Return the major version segment of the request, if any.
pub(crate) fn major(&self) -> Option<u8> { pub(crate) fn major(&self) -> Option<u8> {
match self { match self {
Self::Any | Self::Default | Self::Range(_, _) => None, Self::Any | Self::Default | Self::Latest | Self::Range(_, _) => None,
Self::Major(major, _) => Some(*major), Self::Major(major, _) => Some(*major),
Self::MajorMinor(major, _, _) => Some(*major), Self::MajorMinor(major, _, _) => Some(*major),
Self::MajorMinorPatch(major, _, _, _) => Some(*major), Self::MajorMinorPatch(major, _, _, _) => Some(*major),
@ -2249,7 +2265,7 @@ impl VersionRequest {
/// Return the minor version segment of the request, if any. /// Return the minor version segment of the request, if any.
pub(crate) fn minor(&self) -> Option<u8> { pub(crate) fn minor(&self) -> Option<u8> {
match self { match self {
Self::Any | Self::Default | Self::Range(_, _) => None, Self::Any | Self::Default | Self::Latest | Self::Range(_, _) => None,
Self::Major(_, _) => None, Self::Major(_, _) => None,
Self::MajorMinor(_, minor, _) => Some(*minor), Self::MajorMinor(_, minor, _) => Some(*minor),
Self::MajorMinorPatch(_, minor, _, _) => Some(*minor), Self::MajorMinorPatch(_, minor, _, _) => Some(*minor),
@ -2260,7 +2276,7 @@ impl VersionRequest {
/// Return the patch version segment of the request, if any. /// Return the patch version segment of the request, if any.
pub(crate) fn patch(&self) -> Option<u8> { pub(crate) fn patch(&self) -> Option<u8> {
match self { match self {
Self::Any | Self::Default | Self::Range(_, _) => None, Self::Any | Self::Default | Self::Latest | Self::Range(_, _) => None,
Self::Major(_, _) => None, Self::Major(_, _) => None,
Self::MajorMinor(_, _, _) => None, Self::MajorMinor(_, _, _) => None,
Self::MajorMinorPatch(_, _, patch, _) => Some(*patch), Self::MajorMinorPatch(_, _, patch, _) => Some(*patch),
@ -2273,7 +2289,7 @@ impl VersionRequest {
/// If not, an `Err` is returned with an explanatory message. /// If not, an `Err` is returned with an explanatory message.
pub(crate) fn check_supported(&self) -> Result<(), String> { pub(crate) fn check_supported(&self) -> Result<(), String> {
match self { match self {
Self::Any | Self::Default => (), Self::Any | Self::Latest | Self::Default => (),
Self::Major(major, _) => { Self::Major(major, _) => {
if *major < 3 { if *major < 3 {
return Err(format!( return Err(format!(
@ -2348,6 +2364,7 @@ impl VersionRequest {
pub(crate) fn matches_interpreter(&self, interpreter: &Interpreter) -> bool { pub(crate) fn matches_interpreter(&self, interpreter: &Interpreter) -> bool {
match self { match self {
Self::Any => true, Self::Any => true,
Self::Latest => interpreter.python_version().pre().is_none(),
// Do not use free-threaded interpreters by default // Do not use free-threaded interpreters by default
Self::Default => PythonVariant::Default.matches_interpreter(interpreter), Self::Default => PythonVariant::Default.matches_interpreter(interpreter),
Self::Major(major, variant) => { Self::Major(major, variant) => {
@ -2391,6 +2408,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::Any | Self::Default => true, Self::Any | Self::Default => true,
Self::Latest => version.pre().is_none(),
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)
@ -2413,7 +2431,7 @@ impl VersionRequest {
/// avoid querying interpreters if it's clear it cannot fulfill the request. /// avoid querying interpreters if it's clear it cannot fulfill the request.
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::Any | Self::Default => true, Self::Any | Self::Latest | 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)
@ -2460,6 +2478,7 @@ impl VersionRequest {
) -> bool { ) -> bool {
match self { match self {
Self::Any | Self::Default => true, Self::Any | Self::Default => true,
Self::Latest => prerelease.is_none(),
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)
@ -2482,7 +2501,7 @@ impl VersionRequest {
/// Whether a patch version segment is present in the request. /// Whether a patch version segment is present in the request.
fn has_patch(&self) -> bool { fn has_patch(&self) -> bool {
match self { match self {
Self::Any | Self::Default => false, Self::Any | Self::Default | Self::Latest => false,
Self::Major(..) => false, Self::Major(..) => false,
Self::MajorMinor(..) => false, Self::MajorMinor(..) => false,
Self::MajorMinorPatch(..) => true, Self::MajorMinorPatch(..) => true,
@ -2499,6 +2518,7 @@ impl VersionRequest {
match self { match self {
Self::Default => Self::Default, Self::Default => Self::Default,
Self::Any => Self::Any, Self::Any => Self::Any,
Self::Latest => Self::Latest,
Self::Major(major, variant) => Self::Major(major, variant), Self::Major(major, variant) => Self::Major(major, variant),
Self::MajorMinor(major, minor, variant) => Self::MajorMinor(major, minor, variant), Self::MajorMinor(major, minor, variant) => Self::MajorMinor(major, minor, variant),
Self::MajorMinorPatch(major, minor, _, variant) => { Self::MajorMinorPatch(major, minor, _, variant) => {
@ -2514,7 +2534,7 @@ impl VersionRequest {
/// Whether this request should allow selection of pre-release versions. /// Whether this request should allow selection of pre-release versions.
pub(crate) fn allows_prereleases(&self) -> bool { pub(crate) fn allows_prereleases(&self) -> bool {
match self { match self {
Self::Default => false, Self::Default | Self::Latest => false,
Self::Any => true, Self::Any => true,
Self::Major(..) => false, Self::Major(..) => false,
Self::MajorMinor(..) => false, Self::MajorMinor(..) => false,
@ -2527,7 +2547,7 @@ impl VersionRequest {
/// Whether this request is for a free-threaded Python variant. /// Whether this request is for a free-threaded Python variant.
pub(crate) fn is_freethreaded(&self) -> bool { pub(crate) fn is_freethreaded(&self) -> bool {
match self { match self {
Self::Any | Self::Default => false, Self::Any | Self::Default | Self::Latest => false,
Self::Major(_, variant) Self::Major(_, variant)
| Self::MajorMinor(_, _, variant) | Self::MajorMinor(_, _, variant)
| Self::MajorMinorPatch(_, _, _, variant) | Self::MajorMinorPatch(_, _, _, variant)
@ -2544,7 +2564,7 @@ impl VersionRequest {
// TODO(zanieb): Replace this entire function with a utility that casts this to a version // TODO(zanieb): Replace this entire function with a utility that casts this to a version
// without using `VersionRequest::to_string`. // without using `VersionRequest::to_string`.
match self { match self {
Self::Any | Self::Default => self, Self::Any | Self::Default | Self::Latest => self,
Self::Major(major, _) => Self::Major(major, PythonVariant::Default), Self::Major(major, _) => Self::Major(major, PythonVariant::Default),
Self::MajorMinor(major, minor, _) => { Self::MajorMinor(major, minor, _) => {
Self::MajorMinor(major, minor, PythonVariant::Default) Self::MajorMinor(major, minor, PythonVariant::Default)
@ -2563,7 +2583,7 @@ impl VersionRequest {
pub(crate) fn variant(&self) -> Option<PythonVariant> { pub(crate) fn variant(&self) -> Option<PythonVariant> {
match self { match self {
Self::Any => None, Self::Any => None,
Self::Default => Some(PythonVariant::Default), Self::Default | Self::Latest => Some(PythonVariant::Default),
Self::Major(_, variant) Self::Major(_, variant)
| Self::MajorMinor(_, _, variant) | Self::MajorMinor(_, _, variant)
| Self::MajorMinorPatch(_, _, _, variant) | Self::MajorMinorPatch(_, _, _, variant)
@ -2578,7 +2598,7 @@ impl FromStr for VersionRequest {
fn from_str(s: &str) -> Result<Self, Self::Err> { fn from_str(s: &str) -> Result<Self, Self::Err> {
// Stripping the 't' suffix produces awkward error messages if the user tries a version // Stripping the 't' suffix produces awkward error messages if the user tries a version
// like "latest". HACK: If the version is all letters, don't even try to parse it further. // like "oldest". HACK: If the version is all letters, don't even try to parse it further.
if s.chars().all(char::is_alphabetic) { if s.chars().all(char::is_alphabetic) {
return Err(Error::InvalidVersionRequest(s.to_string())); return Err(Error::InvalidVersionRequest(s.to_string()));
} }
@ -2718,6 +2738,7 @@ impl fmt::Display for VersionRequest {
match self { match self {
Self::Any => f.write_str("any"), Self::Any => f.write_str("any"),
Self::Default => f.write_str("default"), Self::Default => f.write_str("default"),
Self::Latest => f.write_str("latest"),
Self::Major(major, PythonVariant::Default) => write!(f, "{major}"), Self::Major(major, PythonVariant::Default) => write!(f, "{major}"),
Self::Major(major, PythonVariant::Freethreaded) => write!(f, "{major}t"), Self::Major(major, PythonVariant::Freethreaded) => write!(f, "{major}t"),
Self::MajorMinor(major, minor, PythonVariant::Default) => write!(f, "{major}.{minor}"), Self::MajorMinor(major, minor, PythonVariant::Default) => write!(f, "{major}.{minor}"),
@ -2746,6 +2767,7 @@ impl fmt::Display for PythonRequest {
match self { match self {
Self::Default => write!(f, "a default Python"), Self::Default => write!(f, "a default Python"),
Self::Any => write!(f, "any Python"), Self::Any => write!(f, "any Python"),
Self::Latest => write!(f, "latest stable 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()),

View file

@ -316,7 +316,7 @@ impl PythonDownloadRequest {
prereleases: Some(true), // Explicitly allow pre-releases for PythonRequest::Any prereleases: Some(true), // Explicitly allow pre-releases for PythonRequest::Any
..Self::default() ..Self::default()
}), }),
PythonRequest::Default => Some(Self::default()), PythonRequest::Default | PythonRequest::Latest => 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

@ -1061,6 +1061,106 @@ mod tests {
Ok(()) Ok(())
} }
#[test]
fn find_python_latest() -> Result<()> {
let mut context = TestContext::new()?;
context.add_python_versions(&["3.8.10", "3.11.5", "3.9.18", "3.10.12"])?;
let python = context.run(|| {
find_python_installation(
&PythonRequest::Latest,
EnvironmentPreference::Any,
PythonPreference::OnlySystem,
&context.cache,
)
})??;
assert!(
matches!(
python,
PythonInstallation {
source: PythonSource::SearchPath,
interpreter: _
}
),
"We should find a python; got {python:?}"
);
assert_eq!(
&python.interpreter().python_full_version().to_string(),
"3.11.5",
"We should find the latest version (3.11.5)"
);
Ok(())
}
#[test]
fn find_python_latest_with_prereleases() -> Result<()> {
let mut context = TestContext::new()?;
context.add_python_versions(&["3.10.1", "3.11.2", "3.12.0rc1", "3.13.0a1"])?;
let python = context.run(|| {
find_python_installation(
&PythonRequest::Latest,
EnvironmentPreference::Any,
PythonPreference::OnlySystem,
&context.cache,
)
})??;
assert_eq!(
&python.interpreter().python_full_version().to_string(),
"3.11.2",
"Latest should find the highest stable version"
);
Ok(())
}
#[test]
fn find_python_latest_no_pythons() -> Result<()> {
let context = TestContext::new()?;
// Don't add any Python versions
let result = context.run(|| {
find_python_installation(
&PythonRequest::Latest,
EnvironmentPreference::Any,
PythonPreference::OnlySystem,
&context.cache,
)
})?;
assert!(
matches!(result, Err(PythonNotFound { .. })),
"We should not find any python when none are available; got {result:?}"
);
Ok(())
}
#[test]
fn find_python_latest_only_prereleases() -> Result<()> {
let mut context = TestContext::new()?;
context.add_python_versions(&["3.12.0rc1", "3.13.0a1", "3.11.0b2"])?;
let result = context.run(|| {
find_python_installation(
&PythonRequest::Latest,
EnvironmentPreference::Any,
PythonPreference::OnlySystem,
&context.cache,
)
})?;
assert!(
matches!(result, Err(PythonNotFound { .. })),
"We should not find any python when only prereleases are available; got {result:?}"
);
Ok(())
}
#[test] #[test]
fn find_python_from_conda_prefix() -> Result<()> { fn find_python_from_conda_prefix() -> Result<()> {
let context = TestContext::new()?; let context = TestContext::new()?;

View file

@ -473,6 +473,7 @@ impl ManagedPythonInstallation {
} }
PythonRequest::Version(version) => version.matches_version(&self.version()), PythonRequest::Version(version) => version.matches_version(&self.version()),
PythonRequest::Key(request) => request.satisfied_by_key(self.key()), PythonRequest::Key(request) => request.satisfied_by_key(self.key()),
PythonRequest::Latest => true,
} }
} }

View file

@ -2039,14 +2039,15 @@ fn tool_run_python_at_version() {
// Request `@latest` (not yet supported) // Request `@latest` (not yet supported)
uv_snapshot!(context.filters(), context.tool_run() uv_snapshot!(context.filters(), context.tool_run()
.arg("python@latest") .arg("python@latest")
.arg("--version"), @r###" .arg("--version"), @r"
success: false success: true
exit_code: 2 exit_code: 0
----- stdout ----- ----- stdout -----
Python 3.12.[X]
----- stderr ----- ----- stderr -----
error: Requesting the 'latest' Python version is not yet supported Resolved in [TIME]
"###); ");
} }
#[test] #[test]