diff --git a/crates/uv-client/src/base_client.rs b/crates/uv-client/src/base_client.rs index cf9ccf5cef6a..c3ae039df904 100644 --- a/crates/uv-client/src/base_client.rs +++ b/crates/uv-client/src/base_client.rs @@ -32,7 +32,7 @@ pub struct BaseClientBuilder<'a> { keyring: KeyringProviderType, native_tls: bool, retries: u32, - connectivity: Connectivity, + pub connectivity: Connectivity, client: Option, markers: Option<&'a MarkerEnvironment>, platform: Option<&'a Platform>, diff --git a/crates/uv-client/src/registry_client.rs b/crates/uv-client/src/registry_client.rs index d9bd6794ef7c..b758994e96df 100644 --- a/crates/uv-client/src/registry_client.rs +++ b/crates/uv-client/src/registry_client.rs @@ -929,6 +929,16 @@ pub enum Connectivity { Offline, } +impl Connectivity { + pub fn is_online(&self) -> bool { + matches!(self, Self::Online) + } + + pub fn is_offline(&self) -> bool { + matches!(self, Self::Offline) + } +} + #[cfg(test)] mod tests { use std::str::FromStr; diff --git a/crates/uv-toolchain/src/toolchain.rs b/crates/uv-toolchain/src/toolchain.rs index 652d59869b90..7b7b6190f25a 100644 --- a/crates/uv-toolchain/src/toolchain.rs +++ b/crates/uv-toolchain/src/toolchain.rs @@ -144,7 +144,11 @@ impl Toolchain { // Perform a find first match Self::find(python.clone(), system, preview, cache) { Ok(venv) => Ok(venv), - Err(Error::NotFound(_)) if system.is_allowed() && preview.is_enabled() => { + Err(Error::NotFound(_)) + if system.is_allowed() + && preview.is_enabled() + && client_builder.connectivity.is_online() => + { debug!("Requested Python not found, checking for available download..."); let request = if let Some(request) = python { request diff --git a/crates/uv/src/commands/project/add.rs b/crates/uv/src/commands/project/add.rs index edae1c7dec69..eae2a395b2a6 100644 --- a/crates/uv/src/commands/project/add.rs +++ b/crates/uv/src/commands/project/add.rs @@ -5,6 +5,7 @@ use uv_distribution::pyproject_mut::PyProjectTomlMut; use uv_git::GitResolver; use uv_requirements::{NamedRequirementsResolver, RequirementsSource, RequirementsSpecification}; use uv_resolver::{FlatIndex, InMemoryIndex, OptionsBuilder}; +use uv_toolchain::ToolchainRequest; use uv_types::{BuildIsolation, HashStrategy, InFlight}; use uv_cache::Cache; @@ -41,7 +42,15 @@ pub(crate) async fn add( let project = ProjectWorkspace::discover(&std::env::current_dir()?, None).await?; // Discover or create the virtual environment. - let venv = project::init_environment(project.workspace(), python.as_deref(), cache, printer)?; + let venv = project::init_environment( + project.workspace(), + python.as_deref().map(ToolchainRequest::parse), + connectivity, + native_tls, + cache, + printer, + ) + .await?; let client_builder = BaseClientBuilder::new() .connectivity(connectivity) diff --git a/crates/uv/src/commands/project/lock.rs b/crates/uv/src/commands/project/lock.rs index 5aadc789a802..133814dd084a 100644 --- a/crates/uv/src/commands/project/lock.rs +++ b/crates/uv/src/commands/project/lock.rs @@ -18,7 +18,7 @@ use uv_resolver::{ ExcludeNewer, FlatIndex, InMemoryIndex, Lock, OptionsBuilder, PreReleaseMode, RequiresPython, ResolutionMode, }; -use uv_toolchain::Interpreter; +use uv_toolchain::{Interpreter, ToolchainRequest}; use uv_types::{BuildIsolation, EmptyInstalledPackages, HashStrategy, InFlight}; use uv_warnings::warn_user; @@ -47,7 +47,15 @@ pub(crate) async fn lock( let workspace = Workspace::discover(&std::env::current_dir()?, None).await?; // Find an interpreter for the project - let interpreter = project::find_interpreter(&workspace, python.as_deref(), cache, printer)?; + let interpreter = project::find_interpreter( + &workspace, + python.as_deref().map(ToolchainRequest::parse), + connectivity, + native_tls, + cache, + printer, + ) + .await?; // Perform the lock operation. match do_lock( diff --git a/crates/uv/src/commands/project/mod.rs b/crates/uv/src/commands/project/mod.rs index 52014eed23f5..89c0f2ab3d8d 100644 --- a/crates/uv/src/commands/project/mod.rs +++ b/crates/uv/src/commands/project/mod.rs @@ -18,7 +18,8 @@ use uv_installer::{SatisfiesResult, SitePackages}; use uv_requirements::{RequirementsSource, RequirementsSpecification}; use uv_resolver::{FlatIndex, InMemoryIndex, OptionsBuilder, RequiresPython}; use uv_toolchain::{ - Interpreter, PythonEnvironment, SystemPython, Toolchain, ToolchainRequest, VersionRequest, + request_from_version_file, Interpreter, PythonEnvironment, SystemPython, Toolchain, + ToolchainRequest, VersionRequest, }; use uv_types::{BuildIsolation, HashStrategy, InFlight}; use uv_warnings::warn_user; @@ -99,7 +100,7 @@ pub(crate) fn find_environment( /// Check if the given interpreter satisfies the project's requirements. pub(crate) fn interpreter_meets_requirements( interpreter: &Interpreter, - requested_python: Option<&str>, + requested_python: Option<&ToolchainRequest>, requires_python: Option<&RequiresPython>, cache: &Cache, ) -> bool { @@ -108,8 +109,7 @@ pub(crate) fn interpreter_meets_requirements( // we'll fail at the build or at last the install step when we aren't able to install // the editable wheel for the current project into the venv. // TODO(konsti): Do we want to support a workspace python version requirement? - if let Some(python) = requested_python { - let request = ToolchainRequest::parse(python); + if let Some(request) = requested_python { if request.satisfied(interpreter, cache) { debug!("Interpreter meets the requested python {}", request); return true; @@ -136,20 +136,36 @@ pub(crate) fn interpreter_meets_requirements( } /// Find the interpreter to use in the current project. -pub(crate) fn find_interpreter( +pub(crate) async fn find_interpreter( workspace: &Workspace, - python: Option<&str>, + python_request: Option, + connectivity: Connectivity, + native_tls: bool, cache: &Cache, printer: Printer, ) -> Result { let requires_python = find_requires_python(workspace)?; + // (1) Explicit request from user + let python_request = if let Some(request) = python_request { + Some(request) + // (2) Request from `.python-version` + } else if let Some(request) = request_from_version_file().await? { + Some(request) + // (3) `Requires-Python` in `pyproject.toml` + } else { + requires_python + .as_ref() + .map(RequiresPython::specifiers) + .map(|specifiers| ToolchainRequest::Version(VersionRequest::Range(specifiers.clone()))) + }; + // Read from the virtual environment first match find_environment(workspace, cache) { Ok(venv) => { if interpreter_meets_requirements( venv.interpreter(), - python, + python_request.as_ref(), requires_python.as_ref(), cache, ) { @@ -160,49 +176,47 @@ pub(crate) fn find_interpreter( Err(e) => return Err(e.into()), }; - // Otherwise, find a system interpreter to use - let interpreter = if let Some(request) = python.map(ToolchainRequest::parse).or(requires_python - .as_ref() - .map(RequiresPython::specifiers) - .map(|specifiers| ToolchainRequest::Version(VersionRequest::Range(specifiers.clone())))) - { - Toolchain::find_requested( - &request, - SystemPython::Required, - PreviewMode::Enabled, - cache, - ) - } else { - Toolchain::find(None, SystemPython::Required, PreviewMode::Enabled, cache) - }? + let client_builder = BaseClientBuilder::default() + .connectivity(connectivity) + .native_tls(native_tls); + + // Locate the Python interpreter to use in the environment + let interpreter = Toolchain::find_or_fetch( + python_request, + SystemPython::Required, + PreviewMode::Enabled, + client_builder, + cache, + ) + .await? .into_interpreter(); + writeln!( + printer.stderr(), + "Using Python {} interpreter at: {}", + interpreter.python_version(), + interpreter.sys_executable().user_display().cyan() + )?; + if let Some(requires_python) = requires_python.as_ref() { if !requires_python.contains(interpreter.python_version()) { warn_user!( - "The Python {} you requested with {} is incompatible with the requirement of the \ - project of {}", + "The Python interpreter ({}) is incompatible with the project Python requirement {}", interpreter.python_version(), - python.unwrap_or("(default)"), requires_python ); } } - writeln!( - printer.stderr(), - "Using Python {} interpreter at: {}", - interpreter.python_version(), - interpreter.sys_executable().user_display().cyan() - )?; - Ok(interpreter) } /// Initialize a virtual environment for the current project. -pub(crate) fn init_environment( +pub(crate) async fn init_environment( workspace: &Workspace, - python: Option<&str>, + python: Option, + connectivity: Connectivity, + native_tls: bool, cache: &Cache, printer: Printer, ) -> Result { @@ -213,7 +227,7 @@ pub(crate) fn init_environment( Ok(venv) => { if interpreter_meets_requirements( venv.interpreter(), - python, + python.as_ref(), requires_python.as_ref(), cache, ) { @@ -234,7 +248,8 @@ pub(crate) fn init_environment( }; // Find an interpreter to create the environment with - let interpreter = find_interpreter(workspace, python, cache, printer)?; + let interpreter = + find_interpreter(workspace, python, connectivity, native_tls, cache, printer).await?; let venv = workspace.venv(); writeln!( diff --git a/crates/uv/src/commands/project/remove.rs b/crates/uv/src/commands/project/remove.rs index e7b89dabee03..b7808da2a983 100644 --- a/crates/uv/src/commands/project/remove.rs +++ b/crates/uv/src/commands/project/remove.rs @@ -6,6 +6,7 @@ use uv_client::Connectivity; use uv_configuration::{Concurrency, ExtrasSpecification, PreviewMode}; use uv_distribution::pyproject_mut::PyProjectTomlMut; use uv_distribution::ProjectWorkspace; +use uv_toolchain::ToolchainRequest; use uv_warnings::warn_user; use crate::commands::pip::operations::Modifications; @@ -81,7 +82,15 @@ pub(crate) async fn remove( )?; // Discover or create the virtual environment. - let venv = project::init_environment(project.workspace(), python.as_deref(), cache, printer)?; + let venv = project::init_environment( + project.workspace(), + python.as_deref().map(ToolchainRequest::parse), + connectivity, + native_tls, + cache, + printer, + ) + .await?; // Use the default settings. let settings = ResolverSettings::default(); diff --git a/crates/uv/src/commands/project/run.rs b/crates/uv/src/commands/project/run.rs index 1baa833dae7e..74ad03824bf3 100644 --- a/crates/uv/src/commands/project/run.rs +++ b/crates/uv/src/commands/project/run.rs @@ -60,8 +60,15 @@ pub(crate) async fn run( } else { ProjectWorkspace::discover(&std::env::current_dir()?, None).await? }; - let venv = - project::init_environment(project.workspace(), python.as_deref(), cache, printer)?; + let venv = project::init_environment( + project.workspace(), + python.as_deref().map(ToolchainRequest::parse), + connectivity, + native_tls, + cache, + printer, + ) + .await?; // Lock and sync the environment. let lock = project::lock::do_lock( diff --git a/crates/uv/src/commands/project/sync.rs b/crates/uv/src/commands/project/sync.rs index f1e0174f4ca3..551ba6687539 100644 --- a/crates/uv/src/commands/project/sync.rs +++ b/crates/uv/src/commands/project/sync.rs @@ -16,7 +16,7 @@ use uv_git::GitResolver; use uv_installer::SitePackages; use uv_normalize::PackageName; use uv_resolver::{FlatIndex, InMemoryIndex, Lock}; -use uv_toolchain::PythonEnvironment; +use uv_toolchain::{PythonEnvironment, ToolchainRequest}; use uv_types::{BuildIsolation, HashStrategy, InFlight}; use uv_warnings::warn_user; @@ -49,7 +49,15 @@ pub(crate) async fn sync( let project = ProjectWorkspace::discover(&std::env::current_dir()?, None).await?; // Discover or create the virtual environment. - let venv = project::init_environment(project.workspace(), python.as_deref(), cache, printer)?; + let venv = project::init_environment( + project.workspace(), + python.as_deref().map(ToolchainRequest::parse), + connectivity, + native_tls, + cache, + printer, + ) + .await?; // Read the lockfile. let lock: Lock = { diff --git a/crates/uv/tests/common/mod.rs b/crates/uv/tests/common/mod.rs index c97bcf618793..66cccd28a1dd 100644 --- a/crates/uv/tests/common/mod.rs +++ b/crates/uv/tests/common/mod.rs @@ -302,9 +302,7 @@ impl TestContext { .arg(self.cache_dir.path()) .env("VIRTUAL_ENV", self.venv.as_os_str()) .env("UV_TOOLCHAIN_DIR", "") - .env("UV_TEST_PYTHON_PATH", &self.python_path()) .env("UV_NO_WRAP", "1") - .env("UV_TOOLCHAIN_DIR", "") .env("UV_TEST_PYTHON_PATH", &self.python_path()) .current_dir(&self.temp_dir); diff --git a/crates/uv/tests/lock.rs b/crates/uv/tests/lock.rs index cbddbb72bbeb..02a21ad94731 100644 --- a/crates/uv/tests/lock.rs +++ b/crates/uv/tests/lock.rs @@ -1887,7 +1887,8 @@ fn lock_requires_python() -> Result<()> { .collect(); // Install from the lockfile. - uv_snapshot!(filters, context38.sync(), @r###" + // Note we need `--offline` otherwise we'll just fetch a 3.12 interpreter! + uv_snapshot!(filters, context38.sync().arg("--offline"), @r###" success: false exit_code: 2 ----- stdout -----