Add --upgrade support to pip install (#1379)

Adds support for `--upgrade` — similar to `--reinstall`.

Closes https://github.com/astral-sh/uv/issues/1391
This commit is contained in:
Zanie Blue 2024-02-15 19:25:28 -06:00 committed by GitHub
parent e9d82cf0fa
commit 896ab1c54f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 168 additions and 9 deletions

View file

@ -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)
}
}

View file

@ -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)

View file

@ -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<ResolutionGraph, Error> {
let start = std::time::Instant::now();
// Respect preferences from the existing environments.
let preferences: Vec<Requirement> = 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.

View file

@ -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<PackageName>,
/// 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<ExitStatus> {
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<ExitStatus> {
args.resolution,
args.prerelease,
dependency_mode,
upgrade,
index_urls,
&reinstall,
args.link_mode,

View file

@ -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
"###
);
}