Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 46 additions & 7 deletions crates/uv-python/src/discovery.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use itertools::{Either, Itertools};
use regex::Regex;
use reqwest_retry::policies::ExponentialBackoff;
use rustc_hash::{FxBuildHasher, FxHashSet};
use same_file::is_same_file;
use std::borrow::Cow;
Expand All @@ -10,6 +11,7 @@ use std::{path::Path, path::PathBuf, str::FromStr};
use thiserror::Error;
use tracing::{debug, instrument, trace};
use uv_cache::Cache;
use uv_client::BaseClient;
use uv_fs::Simplified;
use uv_fs::which::is_executable;
use uv_pep440::{
Expand All @@ -21,7 +23,7 @@ use uv_static::EnvVars;
use uv_warnings::warn_user_once;
use which::{which, which_all};

use crate::downloads::{PlatformRequest, PythonDownloadRequest};
use crate::downloads::{ManagedPythonDownloadList, PlatformRequest, PythonDownloadRequest};
use crate::implementation::ImplementationName;
use crate::installation::PythonInstallation;
use crate::interpreter::Error as InterpreterError;
Expand Down Expand Up @@ -1445,13 +1447,20 @@ pub(crate) fn find_python_installation(
///
/// See [`find_python_installation`] for more details on installation discovery.
#[instrument(skip_all, fields(request))]
pub(crate) fn find_best_python_installation(
pub(crate) async fn find_best_python_installation(
request: &PythonRequest,
environments: EnvironmentPreference,
preference: PythonPreference,
downloads_enabled: bool,
download_list: &ManagedPythonDownloadList,
client: &BaseClient,
retry_policy: &ExponentialBackoff,
cache: &Cache,
reporter: Option<&dyn crate::downloads::Reporter>,
python_install_mirror: Option<&str>,
pypy_install_mirror: Option<&str>,
preview: Preview,
) -> Result<FindPythonResult, Error> {
) -> Result<FindPythonResult, crate::Error> {
debug!("Starting Python discovery for {}", request);

// First, check for an exact match (or the first available version if no Python version was provided)
Expand All @@ -1465,11 +1474,41 @@ pub(crate) fn find_best_python_installation(
// Continue if we can't find a matching Python and ignore non-critical discovery errors
Ok(Err(_)) => {}
Err(ref err) if !err.is_critical() => {}
_ => return result,
_ => return Ok(result?),
}

// Attempt to download the version if downloads are enabled
if downloads_enabled
&& let Some(download_request) = PythonDownloadRequest::from_request(request)
{
let download = download_request
.clone()
.fill()
.map(|request| download_list.find(&request));
if let Some(download) = match download {
Ok(Ok(download)) => Some(download),
Ok(Err(crate::downloads::Error::NoDownloadFound(_))) => None,
Ok(Err(error)) => return Err(error.into()),
Err(error) => return Err(error.into()),
Comment thread
EliteTK marked this conversation as resolved.
Outdated
} {
let installation = PythonInstallation::fetch(
download,
client,
retry_policy,
cache,
reporter,
python_install_mirror,
pypy_install_mirror,
preview,
)
.await?;

return Ok(Ok(installation));
}
}

// If that fails, and a specific patch version was requested try again allowing a
// different patch version
// If both approaches fail, and a specific patch version was requested try
// again allowing a different patch version
Comment thread
EliteTK marked this conversation as resolved.
Outdated
if let Some(request) = match request {
PythonRequest::Version(version) => {
if version.has_patch() {
Expand All @@ -1493,7 +1532,7 @@ pub(crate) fn find_best_python_installation(
// Continue if we can't find a matching Python and ignore non-critical discovery errors
Ok(Err(_)) => {}
Err(ref err) if !err.is_critical() => {}
_ => return result,
_ => return Ok(result?),
}
}

Expand Down
35 changes: 30 additions & 5 deletions crates/uv-python/src/installation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,17 +75,42 @@ impl PythonInstallation {

/// Find an installed [`PythonInstallation`] that satisfies a requested version, if the request cannot
/// be satisfied, fallback to the best available Python installation.
pub fn find_best(
pub async fn find_best(
request: &PythonRequest,
environments: EnvironmentPreference,
preference: PythonPreference,
download_list: &ManagedPythonDownloadList,
python_downloads: PythonDownloads,
client_builder: &BaseClientBuilder<'_>,
cache: &Cache,
reporter: Option<&dyn Reporter>,
python_install_mirror: Option<&str>,
pypy_install_mirror: Option<&str>,
python_downloads_json_url: Option<&str>,
preview: Preview,
) -> Result<Self, Error> {
let installation =
find_best_python_installation(request, environments, preference, cache, preview)??;
installation.warn_if_outdated_prerelease(request, download_list);
let retry_policy = client_builder.retry_policy();
let client = client_builder.clone().retries(0).build();
let download_list =
ManagedPythonDownloadList::new(&client, python_downloads_json_url).await?;
let downloads_enabled = preference.allows_managed()
&& python_downloads.is_automatic()
&& client_builder.connectivity.is_online();
let installation = find_best_python_installation(
request,
environments,
preference,
downloads_enabled,
&download_list,
&client,
&retry_policy,
cache,
reporter,
python_install_mirror,
pypy_install_mirror,
preview,
)
.await??;
installation.warn_if_outdated_prerelease(request, &download_list);
Ok(installation)
}

Expand Down
43 changes: 37 additions & 6 deletions crates/uv-python/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -135,15 +135,17 @@ mod tests {
use indoc::{formatdoc, indoc};
use temp_env::with_vars;
use test_log::test;
use uv_client::BaseClientBuilder;
use uv_preview::Preview;
use uv_static::EnvVars;

use uv_cache::Cache;

use crate::{
PythonNotFound, PythonRequest, PythonSource, PythonVersion,
implementation::ImplementationName, installation::PythonInstallation,
managed::ManagedPythonInstallations, virtualenv::virtualenv_python_executable,
downloads::ManagedPythonDownloadList, implementation::ImplementationName,
installation::PythonInstallation, managed::ManagedPythonInstallations,
virtualenv::virtualenv_python_executable,
};
use crate::{
PythonPreference,
Expand Down Expand Up @@ -989,13 +991,42 @@ mod tests {
Ok(())
}

fn find_best_python_installation_no_download(
request: &PythonRequest,
environments: EnvironmentPreference,
preference: PythonPreference,
cache: &Cache,
preview: Preview,
) -> Result<Result<PythonInstallation, PythonNotFound>, crate::Error> {
let client_builder = BaseClientBuilder::default();
let download_list = ManagedPythonDownloadList::new_only_embedded()?;
tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("Failed to build runtime")
.block_on(find_best_python_installation(
request,
environments,
preference,
false,
&download_list,
&client_builder.clone().retries(0).build(),
&client_builder.retry_policy(),
cache,
None,
None,
None,
preview,
))
}

#[test]
fn find_best_python_version_patch_exact() -> Result<()> {
let mut context = TestContext::new()?;
context.add_python_versions(&["3.10.1", "3.11.2", "3.11.4", "3.11.3", "3.12.5"])?;

let python = context.run(|| {
find_best_python_installation(
find_best_python_installation_no_download(
&PythonRequest::parse("3.11.3"),
EnvironmentPreference::Any,
PythonPreference::OnlySystem,
Expand Down Expand Up @@ -1029,7 +1060,7 @@ mod tests {
context.add_python_versions(&["3.10.1", "3.11.2", "3.11.4", "3.11.3", "3.12.5"])?;

let python = context.run(|| {
find_best_python_installation(
find_best_python_installation_no_download(
&PythonRequest::parse("3.11.11"),
EnvironmentPreference::Any,
PythonPreference::OnlySystem,
Expand Down Expand Up @@ -1066,7 +1097,7 @@ mod tests {

let python =
context.run_with_vars(&[(EnvVars::VIRTUAL_ENV, Some(venv.as_os_str()))], || {
find_best_python_installation(
find_best_python_installation_no_download(
&PythonRequest::parse("3.10"),
EnvironmentPreference::Any,
PythonPreference::OnlySystem,
Expand Down Expand Up @@ -1097,7 +1128,7 @@ mod tests {

let python =
context.run_with_vars(&[(EnvVars::VIRTUAL_ENV, Some(venv.as_os_str()))], || {
find_best_python_installation(
find_best_python_installation_no_download(
&PythonRequest::parse("3.10.2"),
EnvironmentPreference::Any,
PythonPreference::OnlySystem,
Expand Down
18 changes: 8 additions & 10 deletions crates/uv/src/commands/pip/compile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@ use owo_colors::OwoColorize;
use rustc_hash::FxHashSet;
use tracing::debug;

use uv_python::downloads::ManagedPythonDownloadList;

use uv_cache::Cache;
use uv_client::{BaseClientBuilder, FlatIndexClient, RegistryClientBuilder};
use uv_configuration::{
Expand Down Expand Up @@ -297,15 +295,9 @@ pub(crate) async fn pip_compile(
// Find an interpreter to use for building distributions
let environment_preference = EnvironmentPreference::from_system_flag(system, false);
let python_preference = python_preference.with_system_flag(system);
let client = client_builder.clone().retries(0).build();
let download_list = ManagedPythonDownloadList::new(
&client,
install_mirrors.python_downloads_json_url.as_deref(),
)
.await?;
let reporter = PythonDownloadReporter::single(printer);
let interpreter = if let Some(python) = python.as_ref() {
let request = PythonRequest::parse(python);
let reporter = PythonDownloadReporter::single(printer);
PythonInstallation::find_or_download(
Some(&request),
environment_preference,
Expand Down Expand Up @@ -333,10 +325,16 @@ pub(crate) async fn pip_compile(
&request,
environment_preference,
python_preference,
&download_list,
python_downloads,
&client_builder,
&cache,
Some(&reporter),
install_mirrors.python_install_mirror.as_deref(),
install_mirrors.pypy_install_mirror.as_deref(),
install_mirrors.python_downloads_json_url.as_deref(),
preview,
)
.await
}?
.into_interpreter();

Expand Down
33 changes: 33 additions & 0 deletions crates/uv/tests/it/pip_compile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18110,3 +18110,36 @@ fn compile_missing_python() -> Result<()> {

Ok(())
}

#[cfg(feature = "python-managed")]
#[test]
fn compile_missing_python_version() -> Result<()> {
let context = TestContext::new("3.12")
.with_python_download_cache()
.with_managed_python_dirs();

let requirements_in = context.temp_dir.child("requirements.in");
requirements_in.write_str("anyio==3.7.0")?;

uv_snapshot!(context
.pip_compile()
.arg("--python-version").arg("3.13")
.arg("requirements.in"), @r"
success: true
exit_code: 0
----- stdout -----
# This file was autogenerated by uv via the following command:
# uv pip compile --cache-dir [CACHE_DIR] --python-version 3.13 requirements.in
anyio==3.7.0
# via -r requirements.in
idna==3.6
# via anyio
sniffio==1.3.1
# via anyio

----- stderr -----
Resolved 3 packages in [TIME]
");

Ok(())
}
Loading