diff --git a/Cargo.lock b/Cargo.lock index a24c1be35..90ad731ff 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4838,6 +4838,7 @@ dependencies = [ name = "uv-requirements" version = "0.1.0" dependencies = [ + "anstream", "anyhow", "cache-key", "configparser", diff --git a/crates/uv-requirements/Cargo.toml b/crates/uv-requirements/Cargo.toml index 623f578e4..593a3c9f3 100644 --- a/crates/uv-requirements/Cargo.toml +++ b/crates/uv-requirements/Cargo.toml @@ -27,6 +27,7 @@ uv-resolver = { workspace = true, features = ["clap"] } uv-types = { workspace = true } uv-warnings = { workspace = true } +anstream = { workspace = true } anyhow = { workspace = true } configparser = { workspace = true } console = { workspace = true } diff --git a/crates/uv-requirements/src/upgrade.rs b/crates/uv-requirements/src/upgrade.rs index eb8c7b005..6ed477ff3 100644 --- a/crates/uv-requirements/src/upgrade.rs +++ b/crates/uv-requirements/src/upgrade.rs @@ -2,21 +2,26 @@ use std::path::Path; use anyhow::Result; +use anstream::eprint; use requirements_txt::RequirementsTxt; use uv_client::{BaseClientBuilder, Connectivity}; use uv_configuration::Upgrade; -use uv_resolver::{Preference, PreferenceError}; +use uv_resolver::{Lock, Preference, PreferenceError}; -/// Load the preferred requirements from an existing lockfile, applying the upgrade strategy. -pub async fn read_lockfile( +use crate::ProjectWorkspace; + +/// Load the preferred requirements from an existing `requirements.txt`, applying the upgrade strategy. +pub async fn read_requirements_txt( output_file: Option<&Path>, - upgrade: Upgrade, + upgrade: &Upgrade, ) -> Result> { // As an optimization, skip reading the lockfile is we're upgrading all packages anyway. - let Some(output_file) = output_file - .filter(|_| !upgrade.is_all()) - .filter(|output_file| output_file.exists()) - else { + if upgrade.is_all() { + return Ok(Vec::new()); + } + + // If the lockfile doesn't exist, don't respect any pinned versions. + let Some(output_file) = output_file.filter(|path| path.exists()) else { return Ok(Vec::new()); }; @@ -27,6 +32,8 @@ pub async fn read_lockfile( &BaseClientBuilder::new().connectivity(Connectivity::Offline), ) .await?; + + // Map each entry in the lockfile to a preference. let preferences = requirements_txt .requirements .into_iter() @@ -47,3 +54,50 @@ pub async fn read_lockfile( .collect(), }) } + +/// Load the preferred requirements from an existing lockfile, applying the upgrade strategy. +pub async fn read_lockfile( + project: &ProjectWorkspace, + upgrade: &Upgrade, +) -> Result> { + // As an optimization, skip reading the lockfile is we're upgrading all packages anyway. + if upgrade.is_all() { + return Ok(Vec::new()); + } + + // If an existing lockfile exists, build up a set of preferences. + let lockfile = project.workspace().root().join("uv.lock"); + let lock = match fs_err::tokio::read_to_string(&lockfile).await { + Ok(encoded) => match toml::from_str::(&encoded) { + Ok(lock) => lock, + Err(err) => { + eprint!("Failed to parse lockfile; ignoring locked requirements: {err}"); + return Ok(Vec::new()); + } + }, + Err(err) if err.kind() == std::io::ErrorKind::NotFound => { + return Ok(Vec::new()); + } + Err(err) => return Err(err.into()), + }; + + // Map each entry in the lockfile to a preference. + let preferences: Vec = lock + .distributions() + .iter() + .map(Preference::from_lock) + .collect(); + + // Apply the upgrade strategy to the requirements. + Ok(match upgrade { + // Respect all pinned versions from the existing lockfile. + Upgrade::None => preferences, + // Ignore all pinned versions from the existing lockfile. + Upgrade::All => vec![], + // Ignore pinned versions for the specified packages. + Upgrade::Packages(packages) => preferences + .into_iter() + .filter(|preference| !packages.contains(preference.name())) + .collect(), + }) +} diff --git a/crates/uv/src/commands/pip/compile.rs b/crates/uv/src/commands/pip/compile.rs index e2a1da974..48da8a1b8 100644 --- a/crates/uv/src/commands/pip/compile.rs +++ b/crates/uv/src/commands/pip/compile.rs @@ -34,8 +34,8 @@ use uv_interpreter::{ use uv_interpreter::{PythonVersion, SourceSelector}; use uv_normalize::{ExtraName, PackageName}; use uv_requirements::{ - upgrade::read_lockfile, LookaheadResolver, NamedRequirementsResolver, RequirementsSource, - RequirementsSpecification, SourceTreeResolver, + upgrade::read_requirements_txt, LookaheadResolver, NamedRequirementsResolver, + RequirementsSource, RequirementsSpecification, SourceTreeResolver, }; use uv_resolver::{ AnnotationStyle, DependencyMode, DisplayResolutionGraph, ExcludeNewer, Exclusions, FlatIndex, @@ -288,7 +288,7 @@ pub(crate) async fn pip_compile( .build(); // Read the lockfile, if present. - let preferences = read_lockfile(output_file, upgrade).await?; + let preferences = read_requirements_txt(output_file, &upgrade).await?; // Resolve the flat indexes from `--find-links`. let flat_index = { diff --git a/crates/uv/src/commands/pip/operations.rs b/crates/uv/src/commands/pip/operations.rs index 743d46b8a..8a3ac0b3e 100644 --- a/crates/uv/src/commands/pip/operations.rs +++ b/crates/uv/src/commands/pip/operations.rs @@ -197,12 +197,6 @@ pub(crate) async fn resolve( // TODO(zanieb): Consider consuming these instead of cloning let exclusions = Exclusions::new(reinstall.clone(), upgrade.clone()); - // Filter out any excluded distributions from the preferences. - let preferences = preferences - .into_iter() - .filter(|dist| !exclusions.contains(dist.name())) - .collect::>(); - // Create a manifest of the requirements. let manifest = Manifest::new( requirements, diff --git a/crates/uv/src/commands/project/lock.rs b/crates/uv/src/commands/project/lock.rs index cf472114a..0e3430162 100644 --- a/crates/uv/src/commands/project/lock.rs +++ b/crates/uv/src/commands/project/lock.rs @@ -11,8 +11,9 @@ use uv_configuration::{ }; use uv_dispatch::BuildDispatch; use uv_interpreter::PythonEnvironment; +use uv_requirements::upgrade::read_lockfile; use uv_requirements::ProjectWorkspace; -use uv_resolver::{ExcludeNewer, FlatIndex, InMemoryIndex, Lock, OptionsBuilder, Preference}; +use uv_resolver::{ExcludeNewer, FlatIndex, InMemoryIndex, Lock, OptionsBuilder}; use uv_types::{BuildIsolation, EmptyInstalledPackages, HashStrategy, InFlight}; use uv_warnings::warn_user; @@ -105,26 +106,7 @@ pub(super) async fn do_lock( let options = OptionsBuilder::new().exclude_newer(exclude_newer).build(); // If an existing lockfile exists, build up a set of preferences. - let lockfile = project.workspace().root().join("uv.lock"); - let lock = match fs_err::tokio::read_to_string(&lockfile).await { - Ok(encoded) => match toml::from_str::(&encoded) { - Ok(lock) => Some(lock), - Err(err) => { - eprint!("Failed to parse lockfile; ignoring locked requirements: {err}"); - None - } - }, - Err(err) if err.kind() == std::io::ErrorKind::NotFound => None, - Err(err) => return Err(err.into()), - }; - let preferences: Vec = lock - .map(|lock| { - lock.distributions() - .iter() - .map(Preference::from_lock) - .collect() - }) - .unwrap_or_default(); + let preferences = read_lockfile(project, &upgrade).await?; // Create a build dispatch. let build_dispatch = BuildDispatch::new(