From 896ab1c54f4f4052555fa910d523f6899685eed2 Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Thu, 15 Feb 2024 19:25:28 -0600 Subject: [PATCH] Add `--upgrade` support to `pip install` (#1379) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds support for `--upgrade` — similar to `--reinstall`. Closes https://github.com/astral-sh/uv/issues/1391 --- crates/uv-installer/src/plan.rs | 5 ++ crates/uv/src/commands/pip_compile.rs | 5 ++ crates/uv/src/commands/pip_install.rs | 37 ++++++-- crates/uv/src/main.rs | 10 +++ crates/uv/tests/pip_install.rs | 120 +++++++++++++++++++++++++- 5 files changed, 168 insertions(+), 9 deletions(-) diff --git a/crates/uv-installer/src/plan.rs b/crates/uv-installer/src/plan.rs index ccb1e38d0..464f61d58 100644 --- a/crates/uv-installer/src/plan.rs +++ b/crates/uv-installer/src/plan.rs @@ -467,4 +467,9 @@ impl Reinstall { pub fn is_none(&self) -> bool { matches!(self, Self::None) } + + /// Returns `true` if all packages should be reinstalled. + pub fn is_all(&self) -> bool { + matches!(self, Self::All) + } } diff --git a/crates/uv/src/commands/pip_compile.rs b/crates/uv/src/commands/pip_compile.rs index 28f9a76fe..99e5e1464 100644 --- a/crates/uv/src/commands/pip_compile.rs +++ b/crates/uv/src/commands/pip_compile.rs @@ -425,6 +425,11 @@ impl Upgrade { } } + /// Returns `true` if no packages should be upgraded. + pub(crate) fn is_none(&self) -> bool { + matches!(self, Self::None) + } + /// Returns `true` if all packages should be upgraded. pub(crate) fn is_all(&self) -> bool { matches!(self, Self::All) diff --git a/crates/uv/src/commands/pip_install.rs b/crates/uv/src/commands/pip_install.rs index aa2a06ad0..54c68e5d4 100644 --- a/crates/uv/src/commands/pip_install.rs +++ b/crates/uv/src/commands/pip_install.rs @@ -1,4 +1,6 @@ +use std::collections::HashSet; use std::fmt::Write; + use std::path::Path; use anstream::eprint; @@ -38,6 +40,8 @@ use crate::commands::{elapsed, ChangeEvent, ChangeEventKind, ExitStatus}; use crate::printer::Printer; use crate::requirements::{ExtrasSpecification, RequirementsSource, RequirementsSpecification}; +use super::Upgrade; + /// Install packages into the current environment. #[allow(clippy::too_many_arguments)] pub(crate) async fn pip_install( @@ -48,6 +52,7 @@ pub(crate) async fn pip_install( resolution_mode: ResolutionMode, prerelease_mode: PreReleaseMode, dependency_mode: DependencyMode, + upgrade: Upgrade, index_locations: IndexLocations, reinstall: &Reinstall, link_mode: LinkMode, @@ -115,7 +120,10 @@ pub(crate) async fn pip_install( // If the requirements are already satisfied, we're done. Ideally, the resolver would be fast // enough to let us remove this check. But right now, for large environments, it's an order of // magnitude faster to validate the environment than to resolve the requirements. - if reinstall.is_none() && site_packages.satisfies(&requirements, &editables, &constraints)? { + if reinstall.is_none() + && upgrade.is_none() + && site_packages.satisfies(&requirements, &editables, &constraints)? + { let num_requirements = requirements.len() + editables.len(); let s = if num_requirements == 1 { "" } else { "s" }; writeln!( @@ -206,6 +214,7 @@ pub(crate) async fn pip_install( &editables, &site_packages, reinstall, + &upgrade, &interpreter, tags, markers, @@ -378,6 +387,7 @@ async fn resolve( editables: &[BuiltEditable], site_packages: &SitePackages<'_>, reinstall: &Reinstall, + upgrade: &Upgrade, interpreter: &Interpreter, tags: &Tags, markers: &MarkerEnvironment, @@ -390,14 +400,25 @@ async fn resolve( ) -> Result { let start = std::time::Instant::now(); - // Respect preferences from the existing environments. - let preferences: Vec = match reinstall { - Reinstall::All => vec![], - Reinstall::None => site_packages.requirements().collect(), - Reinstall::Packages(packages) => site_packages + let preferences = if upgrade.is_all() || reinstall.is_all() { + vec![] + } else { + // Combine upgrade and reinstall lists + let mut exclusions: HashSet<&PackageName> = if let Reinstall::Packages(packages) = reinstall + { + HashSet::from_iter(packages) + } else { + HashSet::default() + }; + if let Upgrade::Packages(packages) = upgrade { + exclusions.extend(packages); + }; + + // Prefer current site packages, unless in the upgrade or reinstall lists + site_packages .requirements() - .filter(|requirement| !packages.contains(&requirement.name)) - .collect(), + .filter(|requirement| !exclusions.contains(&requirement.name)) + .collect() }; // Map the editables to their metadata. diff --git a/crates/uv/src/main.rs b/crates/uv/src/main.rs index 48b6e2785..0026c9fe1 100644 --- a/crates/uv/src/main.rs +++ b/crates/uv/src/main.rs @@ -470,6 +470,14 @@ struct PipInstallArgs { #[clap(long, conflicts_with = "extra")] all_extras: bool, + /// Allow package upgrades. + #[clap(long)] + upgrade: bool, + + /// Allow upgrade of a specific package. + #[clap(long)] + upgrade_package: Vec, + /// Reinstall all packages, regardless of whether they're already installed. #[clap(long, alias = "force-reinstall")] reinstall: bool, @@ -935,6 +943,7 @@ async fn run() -> Result { ExtrasSpecification::Some(&args.extra) }; let reinstall = Reinstall::from_args(args.reinstall, args.reinstall_package); + let upgrade = Upgrade::from_args(args.upgrade, args.upgrade_package); let no_binary = NoBinary::from_args(args.no_binary); let no_build = NoBuild::from_args(args.only_binary, args.no_build); let dependency_mode = if args.no_deps { @@ -950,6 +959,7 @@ async fn run() -> Result { args.resolution, args.prerelease, dependency_mode, + upgrade, index_urls, &reinstall, args.link_mode, diff --git a/crates/uv/tests/pip_install.rs b/crates/uv/tests/pip_install.rs index d9cabf262..27e20f77f 100644 --- a/crates/uv/tests/pip_install.rs +++ b/crates/uv/tests/pip_install.rs @@ -160,7 +160,7 @@ fn install_requirements_txt() -> Result<()> { /// Respect installed versions when resolving. #[test] -fn respect_installed() -> Result<()> { +fn respect_installed_and_reinstall() -> Result<()> { let context = TestContext::new("3.12"); // Install Flask. @@ -268,6 +268,29 @@ fn respect_installed() -> Result<()> { "### ); + // Re-install Flask. We should install even though the version is current + let requirements_txt = context.temp_dir.child("requirements.txt"); + requirements_txt.touch()?; + requirements_txt.write_str("Flask")?; + + uv_snapshot!(filters, command(&context) + .arg("-r") + .arg("requirements.txt") + .arg("--reinstall-package") + .arg("Flask") + .arg("--strict"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 7 packages in [TIME] + Installed 1 package in [TIME] + - flask==3.0.0 + + flask==3.0.0 + "### + ); + Ok(()) } @@ -894,3 +917,98 @@ fn no_deps() { context.assert_command("import flask").failure(); } + +/// Upgrade a package. +#[test] +fn install_upgrade() { + let context = TestContext::new("3.12"); + + // Install an old version of anyio and httpcore. + uv_snapshot!(command(&context) + .arg("anyio==3.6.2") + .arg("httpcore==0.16.3") + .arg("--strict"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 6 packages in [TIME] + Downloaded 6 packages in [TIME] + Installed 6 packages in [TIME] + + anyio==3.6.2 + + certifi==2023.11.17 + + h11==0.14.0 + + httpcore==0.16.3 + + idna==3.4 + + sniffio==1.3.0 + "### + ); + + context.assert_command("import anyio").success(); + + // Upgrade anyio. + uv_snapshot!(command(&context) + .arg("anyio") + .arg("--upgrade-package") + .arg("anyio"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 3 packages in [TIME] + Downloaded 1 package in [TIME] + Installed 1 package in [TIME] + - anyio==3.6.2 + + anyio==4.0.0 + "### + ); + + // Upgrade anyio again, should not reinstall. + uv_snapshot!(command(&context) + .arg("anyio") + .arg("--upgrade-package") + .arg("anyio"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 3 packages in [TIME] + Audited 3 packages in [TIME] + "### + ); + + // Install httpcore, request anyio upgrade should not reinstall + uv_snapshot!(command(&context) + .arg("httpcore") + .arg("--upgrade-package") + .arg("anyio"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 6 packages in [TIME] + Audited 6 packages in [TIME] + "### + ); + + // Upgrade httpcore with global flag + uv_snapshot!(command(&context) + .arg("httpcore") + .arg("--upgrade"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 3 packages in [TIME] + Downloaded 1 package in [TIME] + Installed 1 package in [TIME] + - httpcore==0.16.3 + + httpcore==1.0.2 + "### + ); +}