mirror of
https://github.com/astral-sh/uv.git
synced 2025-07-07 13:25:00 +00:00
Merge c07e98de74
into f609e1ddaf
This commit is contained in:
commit
51a53fed84
5 changed files with 147 additions and 23 deletions
|
@ -52,6 +52,8 @@ pub enum PythonRequest {
|
|||
/// Any Python installation
|
||||
Any,
|
||||
/// A Python version without an implementation name e.g. `3.10` or `>=3.12,<3.13`
|
||||
Latest,
|
||||
// Newest resolvable, stable Python version.
|
||||
Version(VersionRequest),
|
||||
/// A path to a directory containing a Python installation, e.g. `.venv`
|
||||
Directory(PathBuf),
|
||||
|
@ -181,6 +183,7 @@ pub enum VersionRequest {
|
|||
Default,
|
||||
/// Allow any Python version.
|
||||
Any,
|
||||
Latest,
|
||||
Major(u8, PythonVariant),
|
||||
MajorMinor(u8, u8, PythonVariant),
|
||||
MajorMinorPatch(u8, u8, u8, PythonVariant),
|
||||
|
@ -624,6 +627,7 @@ fn find_all_minor(
|
|||
match version_request {
|
||||
&VersionRequest::Any
|
||||
| VersionRequest::Default
|
||||
| VersionRequest::Latest
|
||||
| VersionRequest::Major(_, _)
|
||||
| VersionRequest::Range(_, _) => {
|
||||
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)))
|
||||
})
|
||||
}
|
||||
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.
|
||||
if let Some(after_at) = rest.strip_prefix('@') {
|
||||
if after_at == "latest" {
|
||||
// Handle `@latest` as a special case. It's still an error for now, but we plan to
|
||||
// support it. TODO(zanieb): Add `PythonRequest::Latest`
|
||||
return Err(Error::LatestVersionRequest);
|
||||
return Ok(Some(VersionRequest::Latest));
|
||||
}
|
||||
return after_at.parse().map(Some);
|
||||
}
|
||||
|
@ -1710,7 +1725,7 @@ impl PythonRequest {
|
|||
}
|
||||
|
||||
match self {
|
||||
PythonRequest::Default | PythonRequest::Any => true,
|
||||
PythonRequest::Default | PythonRequest::Any | PythonRequest::Latest => true,
|
||||
PythonRequest::Version(version_request) => {
|
||||
version_request.matches_interpreter(interpreter)
|
||||
}
|
||||
|
@ -1799,7 +1814,7 @@ impl PythonRequest {
|
|||
/// Whether this request opts-in to a pre-release Python version.
|
||||
pub(crate) fn allows_prereleases(&self) -> bool {
|
||||
match self {
|
||||
Self::Default => false,
|
||||
Self::Default | Self::Latest => false,
|
||||
Self::Any => true,
|
||||
Self::Version(version) => version.allows_prereleases(),
|
||||
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.
|
||||
pub(crate) fn allows_alternative_implementations(&self) -> bool {
|
||||
match self {
|
||||
Self::Default => false,
|
||||
Self::Default | Self::Latest => false,
|
||||
Self::Any => true,
|
||||
Self::Version(_) => false,
|
||||
Self::Directory(_) | Self::File(_) | Self::ExecutableName(_) => true,
|
||||
|
@ -1833,6 +1848,7 @@ impl PythonRequest {
|
|||
match self {
|
||||
Self::Any => "any".to_string(),
|
||||
Self::Default => "default".to_string(),
|
||||
Self::Latest => "latest".to_string(),
|
||||
Self::Version(version) => version.to_string(),
|
||||
Self::Directory(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.
|
||||
pub(crate) fn major(&self) -> Option<u8> {
|
||||
match self {
|
||||
Self::Any | Self::Default | Self::Range(_, _) => None,
|
||||
Self::Any | Self::Default | Self::Latest | Self::Range(_, _) => None,
|
||||
Self::Major(major, _) => Some(*major),
|
||||
Self::MajorMinor(major, _, _) => Some(*major),
|
||||
Self::MajorMinorPatch(major, _, _, _) => Some(*major),
|
||||
|
@ -2249,7 +2265,7 @@ impl VersionRequest {
|
|||
/// Return the minor version segment of the request, if any.
|
||||
pub(crate) fn minor(&self) -> Option<u8> {
|
||||
match self {
|
||||
Self::Any | Self::Default | Self::Range(_, _) => None,
|
||||
Self::Any | Self::Default | Self::Latest | Self::Range(_, _) => None,
|
||||
Self::Major(_, _) => None,
|
||||
Self::MajorMinor(_, minor, _) => Some(*minor),
|
||||
Self::MajorMinorPatch(_, minor, _, _) => Some(*minor),
|
||||
|
@ -2260,7 +2276,7 @@ impl VersionRequest {
|
|||
/// Return the patch version segment of the request, if any.
|
||||
pub(crate) fn patch(&self) -> Option<u8> {
|
||||
match self {
|
||||
Self::Any | Self::Default | Self::Range(_, _) => None,
|
||||
Self::Any | Self::Default | Self::Latest | Self::Range(_, _) => None,
|
||||
Self::Major(_, _) => None,
|
||||
Self::MajorMinor(_, _, _) => None,
|
||||
Self::MajorMinorPatch(_, _, patch, _) => Some(*patch),
|
||||
|
@ -2273,7 +2289,7 @@ impl VersionRequest {
|
|||
/// If not, an `Err` is returned with an explanatory message.
|
||||
pub(crate) fn check_supported(&self) -> Result<(), String> {
|
||||
match self {
|
||||
Self::Any | Self::Default => (),
|
||||
Self::Any | Self::Latest | Self::Default => (),
|
||||
Self::Major(major, _) => {
|
||||
if *major < 3 {
|
||||
return Err(format!(
|
||||
|
@ -2348,6 +2364,7 @@ impl VersionRequest {
|
|||
pub(crate) fn matches_interpreter(&self, interpreter: &Interpreter) -> bool {
|
||||
match self {
|
||||
Self::Any => true,
|
||||
Self::Latest => interpreter.python_version().pre().is_none(),
|
||||
// Do not use free-threaded interpreters by default
|
||||
Self::Default => PythonVariant::Default.matches_interpreter(interpreter),
|
||||
Self::Major(major, variant) => {
|
||||
|
@ -2391,6 +2408,7 @@ impl VersionRequest {
|
|||
pub(crate) fn matches_version(&self, version: &PythonVersion) -> bool {
|
||||
match self {
|
||||
Self::Any | Self::Default => true,
|
||||
Self::Latest => version.pre().is_none(),
|
||||
Self::Major(major, _) => version.major() == *major,
|
||||
Self::MajorMinor(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.
|
||||
fn matches_major_minor(&self, major: u8, minor: u8) -> bool {
|
||||
match self {
|
||||
Self::Any | Self::Default => true,
|
||||
Self::Any | Self::Latest | Self::Default => true,
|
||||
Self::Major(self_major, _) => *self_major == major,
|
||||
Self::MajorMinor(self_major, self_minor, _) => {
|
||||
(*self_major, *self_minor) == (major, minor)
|
||||
|
@ -2460,6 +2478,7 @@ impl VersionRequest {
|
|||
) -> bool {
|
||||
match self {
|
||||
Self::Any | Self::Default => true,
|
||||
Self::Latest => prerelease.is_none(),
|
||||
Self::Major(self_major, _) => *self_major == major,
|
||||
Self::MajorMinor(self_major, self_minor, _) => {
|
||||
(*self_major, *self_minor) == (major, minor)
|
||||
|
@ -2482,7 +2501,7 @@ impl VersionRequest {
|
|||
/// Whether a patch version segment is present in the request.
|
||||
fn has_patch(&self) -> bool {
|
||||
match self {
|
||||
Self::Any | Self::Default => false,
|
||||
Self::Any | Self::Default | Self::Latest => false,
|
||||
Self::Major(..) => false,
|
||||
Self::MajorMinor(..) => false,
|
||||
Self::MajorMinorPatch(..) => true,
|
||||
|
@ -2499,6 +2518,7 @@ impl VersionRequest {
|
|||
match self {
|
||||
Self::Default => Self::Default,
|
||||
Self::Any => Self::Any,
|
||||
Self::Latest => Self::Latest,
|
||||
Self::Major(major, variant) => Self::Major(major, variant),
|
||||
Self::MajorMinor(major, minor, variant) => Self::MajorMinor(major, minor, variant),
|
||||
Self::MajorMinorPatch(major, minor, _, variant) => {
|
||||
|
@ -2514,7 +2534,7 @@ impl VersionRequest {
|
|||
/// Whether this request should allow selection of pre-release versions.
|
||||
pub(crate) fn allows_prereleases(&self) -> bool {
|
||||
match self {
|
||||
Self::Default => false,
|
||||
Self::Default | Self::Latest => false,
|
||||
Self::Any => true,
|
||||
Self::Major(..) => false,
|
||||
Self::MajorMinor(..) => false,
|
||||
|
@ -2527,7 +2547,7 @@ impl VersionRequest {
|
|||
/// Whether this request is for a free-threaded Python variant.
|
||||
pub(crate) fn is_freethreaded(&self) -> bool {
|
||||
match self {
|
||||
Self::Any | Self::Default => false,
|
||||
Self::Any | Self::Default | Self::Latest => false,
|
||||
Self::Major(_, variant)
|
||||
| Self::MajorMinor(_, _, 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
|
||||
// without using `VersionRequest::to_string`.
|
||||
match self {
|
||||
Self::Any | Self::Default => self,
|
||||
Self::Any | Self::Default | Self::Latest => self,
|
||||
Self::Major(major, _) => Self::Major(major, PythonVariant::Default),
|
||||
Self::MajorMinor(major, minor, _) => {
|
||||
Self::MajorMinor(major, minor, PythonVariant::Default)
|
||||
|
@ -2563,7 +2583,7 @@ impl VersionRequest {
|
|||
pub(crate) fn variant(&self) -> Option<PythonVariant> {
|
||||
match self {
|
||||
Self::Any => None,
|
||||
Self::Default => Some(PythonVariant::Default),
|
||||
Self::Default | Self::Latest => Some(PythonVariant::Default),
|
||||
Self::Major(_, variant)
|
||||
| Self::MajorMinor(_, _, variant)
|
||||
| Self::MajorMinorPatch(_, _, _, variant)
|
||||
|
@ -2578,7 +2598,7 @@ impl FromStr for VersionRequest {
|
|||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
// 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) {
|
||||
return Err(Error::InvalidVersionRequest(s.to_string()));
|
||||
}
|
||||
|
@ -2718,6 +2738,7 @@ impl fmt::Display for VersionRequest {
|
|||
match self {
|
||||
Self::Any => f.write_str("any"),
|
||||
Self::Default => f.write_str("default"),
|
||||
Self::Latest => f.write_str("latest"),
|
||||
Self::Major(major, PythonVariant::Default) => write!(f, "{major}"),
|
||||
Self::Major(major, PythonVariant::Freethreaded) => write!(f, "{major}t"),
|
||||
Self::MajorMinor(major, minor, PythonVariant::Default) => write!(f, "{major}.{minor}"),
|
||||
|
@ -2746,6 +2767,7 @@ impl fmt::Display for PythonRequest {
|
|||
match self {
|
||||
Self::Default => write!(f, "a default Python"),
|
||||
Self::Any => write!(f, "any Python"),
|
||||
Self::Latest => write!(f, "latest stable 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()),
|
||||
|
|
|
@ -316,7 +316,7 @@ impl PythonDownloadRequest {
|
|||
prereleases: Some(true), // Explicitly allow pre-releases for PythonRequest::Any
|
||||
..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
|
||||
PythonRequest::Directory(_)
|
||||
| PythonRequest::ExecutableName(_)
|
||||
|
|
|
@ -1061,6 +1061,106 @@ mod tests {
|
|||
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]
|
||||
fn find_python_from_conda_prefix() -> Result<()> {
|
||||
let context = TestContext::new()?;
|
||||
|
|
|
@ -473,6 +473,7 @@ impl ManagedPythonInstallation {
|
|||
}
|
||||
PythonRequest::Version(version) => version.matches_version(&self.version()),
|
||||
PythonRequest::Key(request) => request.satisfied_by_key(self.key()),
|
||||
PythonRequest::Latest => true,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -2039,14 +2039,15 @@ fn tool_run_python_at_version() {
|
|||
// Request `@latest` (not yet supported)
|
||||
uv_snapshot!(context.filters(), context.tool_run()
|
||||
.arg("python@latest")
|
||||
.arg("--version"), @r###"
|
||||
success: false
|
||||
exit_code: 2
|
||||
.arg("--version"), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
Python 3.12.[X]
|
||||
|
||||
----- stderr -----
|
||||
error: Requesting the 'latest' Python version is not yet supported
|
||||
"###);
|
||||
Resolved in [TIME]
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue