diff --git a/Cargo.lock b/Cargo.lock index f7ac2df0d..5d97b0d61 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4360,6 +4360,7 @@ dependencies = [ "hyper 0.14.28", "insta", "install-wheel-rs", + "itertools 0.12.1", "pep440_rs", "pep508_rs", "platform-tags", diff --git a/crates/requirements-txt/src/lib.rs b/crates/requirements-txt/src/lib.rs index 422bdc20e..ad103f487 100644 --- a/crates/requirements-txt/src/lib.rs +++ b/crates/requirements-txt/src/lib.rs @@ -51,7 +51,7 @@ use pep508_rs::{ }; #[cfg(feature = "http")] use uv_client::BaseClient; -use uv_client::BaseClientBuilder; +use uv_client::{BaseClientBuilder, LazyBaseClientBuilder}; use uv_fs::{normalize_url_path, Simplified}; use uv_normalize::ExtraName; use uv_warnings::warn_user; @@ -334,7 +334,7 @@ impl RequirementsTxt { pub async fn parse( requirements_txt: impl AsRef, working_dir: impl AsRef, - client_builder: BaseClientBuilder, + client_builder: &mut LazyBaseClientBuilder, ) -> Result { let requirements_txt = requirements_txt.as_ref(); let working_dir = working_dir.as_ref(); @@ -355,17 +355,17 @@ impl RequirementsTxt { #[cfg(feature = "http")] { // Avoid constructing a client if network is disabled already - if client_builder.is_offline() { - return Err(RequirementsTxtFileError { - file: requirements_txt.to_path_buf(), - error: RequirementsTxtParserError::IO(io::Error::new( - io::ErrorKind::InvalidInput, - format!("Network connectivity is disabled, but a remote requirements file was requested: {}", requirements_txt.display()), - )), - }); - } + // if client_builder.is_offline() { + // return Err(RequirementsTxtFileError { + // file: requirements_txt.to_path_buf(), + // error: RequirementsTxtParserError::IO(io::Error::new( + // io::ErrorKind::InvalidInput, + // format!("Network connectivity is disabled, but a remote requirements file was requested: {}", requirements_txt.display()), + // )), + // }); + // } - let client = client_builder.clone().build(); + let client = client_builder.build(); read_url_to_string(&requirements_txt, client).await } } else { @@ -406,7 +406,7 @@ impl RequirementsTxt { content: &str, working_dir: &Path, requirements_dir: &Path, - client_builder: BaseClientBuilder, + client_builder: &mut LazyBaseClientBuilder, ) -> Result { let mut s = Scanner::new(content); @@ -425,14 +425,13 @@ impl RequirementsTxt { } else { requirements_dir.join(filename.as_ref()) }; - let sub_requirements = - Self::parse(&sub_file, working_dir, client_builder.clone()) - .await - .map_err(|err| RequirementsTxtParserError::Subfile { - source: Box::new(err), - start, - end, - })?; + let sub_requirements = Self::parse(&sub_file, working_dir, client_builder) + .await + .map_err(|err| RequirementsTxtParserError::Subfile { + source: Box::new(err), + start, + end, + })?; // Disallow conflicting `--index-url` in nested `requirements` files. if sub_requirements.index_url.is_some() @@ -464,14 +463,13 @@ impl RequirementsTxt { } else { requirements_dir.join(filename.as_ref()) }; - let sub_constraints = - Self::parse(&sub_file, working_dir, client_builder.clone()) - .await - .map_err(|err| RequirementsTxtParserError::Subfile { - source: Box::new(err), - start, - end, - })?; + let sub_constraints = Self::parse(&sub_file, working_dir, client_builder) + .await + .map_err(|err| RequirementsTxtParserError::Subfile { + source: Box::new(err), + start, + end, + })?; // Treat any nested requirements or constraints as constraints. This differs // from `pip`, which seems to treat `-r` requirements in constraints files as // _requirements_, but we don't want to support that. @@ -835,7 +833,7 @@ fn parse_value<'a, T>( #[cfg(feature = "http")] async fn read_url_to_string( path: impl AsRef, - client: BaseClient, + client: &BaseClient, ) -> Result { // pip would URL-encode the non-UTF-8 bytes of the string; we just don't support them. let path_utf8 = diff --git a/crates/uv-client/Cargo.toml b/crates/uv-client/Cargo.toml index c4ed0ca07..5c7a85b11 100644 --- a/crates/uv-client/Cargo.toml +++ b/crates/uv-client/Cargo.toml @@ -28,6 +28,7 @@ fs-err = { workspace = true, features = ["tokio"] } futures = { workspace = true } html-escape = { workspace = true } http = { workspace = true } +itertools = { workspace = true } reqwest = { workspace = true } reqwest-middleware = { workspace = true } reqwest-retry = { workspace = true } diff --git a/crates/uv-client/src/base_client.rs b/crates/uv-client/src/base_client.rs index 3638cbcc9..234c48f3b 100644 --- a/crates/uv-client/src/base_client.rs +++ b/crates/uv-client/src/base_client.rs @@ -1,10 +1,13 @@ +use itertools::Either; use reqwest::{Client, ClientBuilder}; use reqwest_middleware::ClientWithMiddleware; use reqwest_retry::policies::ExponentialBackoff; use reqwest_retry::RetryTransientMiddleware; use std::env; use std::fmt::Debug; +use std::ops::Deref; use std::path::Path; +use std::sync::Mutex; use tracing::debug; use uv_auth::{AuthMiddleware, KeyringProvider}; use uv_fs::Simplified; @@ -78,6 +81,10 @@ impl BaseClientBuilder { matches!(self.connectivity, Connectivity::Offline) } + pub fn to_lazy_builder(self) -> LazyBaseClientBuilder { + LazyBaseClientBuilder::new(self) + } + pub fn build(self) -> BaseClient { // Create user agent. let user_agent_string = format!("uv/{}", version()); @@ -185,3 +192,27 @@ impl BaseClient { self.connectivity } } + +pub struct LazyBaseClientBuilder { + builder: Option, + client: Option, +} + +impl LazyBaseClientBuilder { + fn new(builder: BaseClientBuilder) -> Self { + Self { + builder: Some(builder), + client: None, + } + } + + pub fn build(&mut self) -> &BaseClient { + if let Some(ref client) = self.client { + client + } else { + let builder = self.builder.take().unwrap(); + let client = self.client.insert(builder.build()); + client + } + } +} diff --git a/crates/uv-client/src/lib.rs b/crates/uv-client/src/lib.rs index c777e8136..ea1cf07e8 100644 --- a/crates/uv-client/src/lib.rs +++ b/crates/uv-client/src/lib.rs @@ -1,4 +1,4 @@ -pub use base_client::{BaseClient, BaseClientBuilder}; +pub use base_client::{BaseClient, BaseClientBuilder, LazyBaseClientBuilder}; pub use cached_client::{CacheControl, CachedClient, CachedClientError, DataWithCachePolicy}; pub use error::{BetterReqwestError, Error, ErrorKind}; pub use flat_index::{FlatDistributions, FlatIndex, FlatIndexClient, FlatIndexError}; diff --git a/crates/uv/src/requirements.rs b/crates/uv/src/requirements.rs index 895b796f6..94ddce9d3 100644 --- a/crates/uv/src/requirements.rs +++ b/crates/uv/src/requirements.rs @@ -12,7 +12,7 @@ use tracing::{instrument, Level}; use distribution_types::{FlatIndexLocation, IndexUrl}; use pep508_rs::Requirement; use requirements_txt::{EditableRequirement, FindLink, RequirementsTxt}; -use uv_client::{BaseClientBuilder, Connectivity}; +use uv_client::{BaseClient, BaseClientBuilder, Connectivity, LazyBaseClientBuilder}; use uv_fs::Simplified; use uv_normalize::{ExtraName, PackageName}; use uv_warnings::warn_user; @@ -142,7 +142,7 @@ impl RequirementsSpecification { pub(crate) async fn from_source( source: &RequirementsSource, extras: &ExtrasSpecification<'_>, - client_builder: BaseClientBuilder, + client_builder: &mut LazyBaseClientBuilder, ) -> Result { Ok(match source { RequirementsSource::Package(name) => { @@ -284,12 +284,13 @@ impl RequirementsSpecification { client_builder: BaseClientBuilder, ) -> Result { let mut spec = Self::default(); + let mut client_builder = client_builder.to_lazy_builder(); // Read all requirements, and keep track of all requirements _and_ constraints. // A `requirements.txt` can contain a `-c constraints.txt` directive within it, so reading // a requirements file can also add constraints. for source in requirements { - let source = Self::from_source(source, extras, client_builder.clone()).await?; + let source = Self::from_source(source, extras, &mut client_builder).await?; spec.requirements.extend(source.requirements); spec.constraints.extend(source.constraints); spec.overrides.extend(source.overrides); @@ -316,7 +317,7 @@ impl RequirementsSpecification { // Read all constraints, treating _everything_ as a constraint. for source in constraints { - let source = Self::from_source(source, extras, client_builder.clone()).await?; + let source = Self::from_source(source, extras, &mut client_builder).await?; spec.constraints.extend(source.requirements); spec.constraints.extend(source.constraints); spec.constraints.extend(source.overrides); @@ -336,7 +337,7 @@ impl RequirementsSpecification { // Read all overrides, treating both requirements _and_ constraints as overrides. for source in overrides { - let source = Self::from_source(source, extras, client_builder.clone()).await?; + let source = Self::from_source(source, extras, &mut client_builder).await?; spec.overrides.extend(source.requirements); spec.overrides.extend(source.constraints); spec.overrides.extend(source.overrides); @@ -458,7 +459,9 @@ pub(crate) async fn read_lockfile( let requirements_txt = RequirementsTxt::parse( output_file, std::env::current_dir()?, - BaseClientBuilder::new().connectivity(Connectivity::Offline), + &mut BaseClientBuilder::new() + .connectivity(Connectivity::Offline) + .to_lazy_builder(), ) .await?; let requirements = requirements_txt