From b6490570cd028cf47e35daa1d086a345132c50d3 Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Mon, 22 Jun 2026 20:18:31 -0500 Subject: [PATCH 1/2] Fix upgrade precedence with configured packages --- .../uv-configuration/src/package_options.rs | 11 +- crates/uv/tests/sync/show_settings.rs | 126 ++++++++++-------- 2 files changed, 77 insertions(+), 60 deletions(-) diff --git a/crates/uv-configuration/src/package_options.rs b/crates/uv-configuration/src/package_options.rs index 54700ad0dc2d8..7546d820bb69e 100644 --- a/crates/uv-configuration/src/package_options.rs +++ b/crates/uv-configuration/src/package_options.rs @@ -255,11 +255,15 @@ impl Upgrade { /// Combine a set of [`Upgrade`] values. #[must_use] pub fn combine(self, other: Self) -> Self { - // For `strategy`: `other` takes precedence for an explicit `All` or `None`; otherwise, + // For `strategy`: an explicit `All` or `None` in `self` takes precedence; otherwise, // merge. let strategy = match (self.strategy, other.strategy) { - (_, UpgradeStrategy::All) => UpgradeStrategy::All, - (_, UpgradeStrategy::None) => UpgradeStrategy::None, + (UpgradeStrategy::All, _) => UpgradeStrategy::All, + (UpgradeStrategy::None, _) => UpgradeStrategy::None, + (UpgradeStrategy::Some(_, _), UpgradeStrategy::All) => UpgradeStrategy::All, + (UpgradeStrategy::Some(packages, groups), UpgradeStrategy::None) => { + UpgradeStrategy::Some(packages, groups) + } ( UpgradeStrategy::Some(mut self_packages, mut self_groups), UpgradeStrategy::Some(other_packages, other_groups), @@ -268,7 +272,6 @@ impl Upgrade { self_groups.extend(other_groups); UpgradeStrategy::Some(self_packages, self_groups) } - (_, UpgradeStrategy::Some(packages, groups)) => UpgradeStrategy::Some(packages, groups), }; // For `constraints`: always merge the constraints of `self` and `other`. diff --git a/crates/uv/tests/sync/show_settings.rs b/crates/uv/tests/sync/show_settings.rs index 8d05984e48456..c964654355bc9 100644 --- a/crates/uv/tests/sync/show_settings.rs +++ b/crates/uv/tests/sync/show_settings.rs @@ -4055,21 +4055,14 @@ fn upgrade_pip_cli_config_interaction() -> anyhow::Result<()> { .arg("requirements.in"), @r#" ... - Verify, - ), - upgrade: Upgrade { - - strategy: Some( - - { - - PackageName( + strategy: Some( + { + PackageName( - "sniffio", - - ), - - }, - - {}, - - ), - + strategy: None, - constraints: {}, - }, - reinstall: None, + + "idna", + ), + }, + {}, ... "# ); @@ -4121,14 +4114,21 @@ fn upgrade_pip_cli_config_interaction() -> anyhow::Result<()> { .arg("--show-settings") .arg("requirements.in"), @r#" ... - strategy: Some( - { - PackageName( + Verify, + ), + upgrade: Upgrade { + - strategy: Some( + - { + - PackageName( - "sniffio", - + "idna", - ), - }, - {}, + - ), + - }, + - {}, + - ), + + strategy: None, + constraints: {}, + }, + reinstall: None, ... "# ); @@ -4140,14 +4140,21 @@ fn upgrade_pip_cli_config_interaction() -> anyhow::Result<()> { .arg("--show-settings") .arg("requirements.in"), @r#" ... - strategy: Some( - { - PackageName( + Verify, + ), + upgrade: Upgrade { + - strategy: Some( + - { + - PackageName( - "sniffio", - + "idna", - ), - }, - {}, + - ), + - }, + - {}, + - ), + + strategy: All, + constraints: {}, + }, + reinstall: None, ... "# ); @@ -4248,21 +4255,14 @@ fn upgrade_project_cli_config_interaction() -> anyhow::Result<()> { .arg("--show-settings"), @r#" ... - cuda_driver_version: None, - amd_gpu_architecture: None, - upgrade: Upgrade { - - strategy: Some( - - { - - PackageName( + strategy: Some( + { + PackageName( - "sniffio", - - ), - - }, - - {}, - - ), - + strategy: None, - constraints: {}, - }, - }, + + "idna", + ), + }, + {}, ... "# ); @@ -4320,14 +4320,21 @@ fn upgrade_project_cli_config_interaction() -> anyhow::Result<()> { .arg("--no-upgrade") .arg("--show-settings"), @r#" ... - strategy: Some( - { - PackageName( + cuda_driver_version: None, + amd_gpu_architecture: None, + upgrade: Upgrade { + - strategy: Some( + - { + - PackageName( - "sniffio", - + "idna", - ), - }, - {}, + - ), + - }, + - {}, + - ), + + strategy: None, + constraints: {}, + }, + }, ... "# ); @@ -4338,14 +4345,21 @@ fn upgrade_project_cli_config_interaction() -> anyhow::Result<()> { .arg("--upgrade") .arg("--show-settings"), @r#" ... - strategy: Some( - { - PackageName( + cuda_driver_version: None, + amd_gpu_architecture: None, + upgrade: Upgrade { + - strategy: Some( + - { + - PackageName( - "sniffio", - + "idna", - ), - }, - {}, + - ), + - }, + - {}, + - ), + + strategy: All, + constraints: {}, + }, + }, ... "# ); From 796d72381bb32b7055fda638c3a1564c706bdbc9 Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Tue, 7 Jul 2026 09:22:35 -0500 Subject: [PATCH 2/2] Add upgrade precedence regression coverage --- crates/uv/tests/pip_install/pip_install.rs | 54 ++++++++++++++++++++++ crates/uv/tests/sync/show_settings.rs | 50 ++++++++++---------- 2 files changed, 78 insertions(+), 26 deletions(-) diff --git a/crates/uv/tests/pip_install/pip_install.rs b/crates/uv/tests/pip_install/pip_install.rs index e65aa268a5a20..808daad689eb7 100644 --- a/crates/uv/tests/pip_install/pip_install.rs +++ b/crates/uv/tests/pip_install/pip_install.rs @@ -4117,6 +4117,60 @@ fn install_upgrade() { ); } +/// `--upgrade` takes precedence over `upgrade-package` in configuration. +#[test] +fn install_upgrade_overrides_configured_upgrade_package() -> Result<()> { + let context = uv_test::test_context!("3.12"); + + // Install old versions of two packages. + uv_snapshot!(context.filters(), context.pip_install() + .arg("anyio==3.6.2") + .arg("httpcore==0.16.3"), @" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 6 packages in [TIME] + Prepared 6 packages in [TIME] + Installed 6 packages in [TIME] + + anyio==3.6.2 + + certifi==2024.2.2 + + h11==0.14.0 + + httpcore==0.16.3 + + idna==3.6 + + sniffio==1.3.1 + "); + + let uv_toml = context.temp_dir.child("uv.toml"); + uv_toml.write_str(indoc! {r#" + [pip] + upgrade-package = ["anyio"] + "#})?; + + // Upgrade both packages, including `httpcore`, which is not selected in the configuration. + uv_snapshot!(context.filters(), context.pip_install() + .arg("anyio") + .arg("httpcore") + .arg("--upgrade"), @" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 6 packages in [TIME] + Prepared 2 packages in [TIME] + Uninstalled 2 packages in [TIME] + Installed 2 packages in [TIME] + - anyio==3.6.2 + + anyio==4.3.0 + - httpcore==0.16.3 + + httpcore==1.0.4 + "); + + Ok(()) +} + /// Install a package from a `requirements.txt` file, with a `constraints.txt` file. #[test] fn install_constraints_txt() -> Result<()> { diff --git a/crates/uv/tests/sync/show_settings.rs b/crates/uv/tests/sync/show_settings.rs index c964654355bc9..628e2aefe9267 100644 --- a/crates/uv/tests/sync/show_settings.rs +++ b/crates/uv/tests/sync/show_settings.rs @@ -4008,9 +4008,8 @@ fn upgrade_pip_cli_config_interaction() -> anyhow::Result<()> { let requirements_in = context.temp_dir.child("requirements.in"); requirements_in.write_str("anyio>3.0.0")?; - // `--no-upgrade` overrides `--upgrade-package`. - // TODO(charlie): This should mark `sniffio` for upgrade, but it doesn't. - let no_upgrade = diff_uv_snapshot!(context.filters(), &baseline, add_shared_args(context.pip_compile()) + // `--upgrade-package` takes precedence over `--no-upgrade`. + let upgrade_package = diff_uv_snapshot!(context.filters(), &baseline, add_shared_args(context.pip_compile()) .arg("--no-upgrade") .arg("--upgrade-package") .arg("sniffio") @@ -4044,10 +4043,10 @@ fn upgrade_pip_cli_config_interaction() -> anyhow::Result<()> { "})?; // Despite `upgrade = false` in the configuration file, we should mark `idna` for upgrade. - // Compare against output before adding `upgrade = false`, with `--no-upgrade --upgrade-package sniffio`. + // Compare against the CLI `--no-upgrade --upgrade-package sniffio` baseline. diff_uv_snapshot!( context.filters(), - &no_upgrade, + &upgrade_package, add_shared_args(context.pip_compile()) .arg("--upgrade-package") .arg("idna") @@ -4075,8 +4074,8 @@ fn upgrade_pip_cli_config_interaction() -> anyhow::Result<()> { "})?; // Despite `--upgrade-package idna` in the command line, we should upgrade all packages. - // Compare against output before adding `upgrade = true`, with `--no-upgrade --upgrade-package sniffio`. - diff_uv_snapshot!(context.filters(), &no_upgrade, add_shared_args(context.pip_compile()) + // Compare against the CLI `--no-upgrade --upgrade-package sniffio` baseline. + diff_uv_snapshot!(context.filters(), &upgrade_package, add_shared_args(context.pip_compile()) .arg("--upgrade-package") .arg("idna") .arg("--show-settings") @@ -4108,8 +4107,8 @@ fn upgrade_pip_cli_config_interaction() -> anyhow::Result<()> { "#})?; // Despite `upgrade-package = ["idna"]` in the configuration file, we should disable upgrades. - // Compare against output before adding `upgrade-package = ["idna"]`, with `--upgrade-package sniffio`. - diff_uv_snapshot!(context.filters(), &no_upgrade, add_shared_args(context.pip_compile()) + // Compare against the CLI `--no-upgrade --upgrade-package sniffio` baseline. + diff_uv_snapshot!(context.filters(), &upgrade_package, add_shared_args(context.pip_compile()) .arg("--no-upgrade") .arg("--show-settings") .arg("requirements.in"), @r#" @@ -4134,8 +4133,8 @@ fn upgrade_pip_cli_config_interaction() -> anyhow::Result<()> { ); // Despite `upgrade-package = ["idna"]` in the configuration file, we should enable all upgrades. - // Compare against output before adding `upgrade-package = ["idna"]`, with `--no-upgrade --upgrade-package sniffio`. - diff_uv_snapshot!(context.filters(), &no_upgrade, add_shared_args(context.pip_compile()) + // Compare against the CLI `--no-upgrade --upgrade-package sniffio` baseline. + diff_uv_snapshot!(context.filters(), &upgrade_package, add_shared_args(context.pip_compile()) .arg("--upgrade") .arg("--show-settings") .arg("requirements.in"), @r#" @@ -4160,8 +4159,8 @@ fn upgrade_pip_cli_config_interaction() -> anyhow::Result<()> { ); // Mark both `sniffio` and `idna` for upgrade. - // Compare against output before adding `upgrade-package = ["idna"]`, with `--no-upgrade`. - diff_uv_snapshot!(context.filters(), &no_upgrade, add_shared_args(context.pip_compile()) + // Compare against the CLI `--no-upgrade --upgrade-package sniffio` baseline. + diff_uv_snapshot!(context.filters(), &upgrade_package, add_shared_args(context.pip_compile()) .arg("--upgrade-package") .arg("sniffio") .arg("--show-settings") @@ -4206,9 +4205,8 @@ fn upgrade_project_cli_config_interaction() -> anyhow::Result<()> { dependencies = ["anyio>3.0.0"] "#})?; - // `--no-upgrade` overrides `--upgrade-package`. - // TODO(charlie): This should mark `sniffio` for upgrade, but it doesn't. - let no_upgrade = diff_uv_snapshot!(context.filters(), &baseline, add_shared_args(context.lock()) + // `--upgrade-package` takes precedence over `--no-upgrade`. + let upgrade_package = diff_uv_snapshot!(context.filters(), &baseline, add_shared_args(context.lock()) .arg("--no-upgrade") .arg("--upgrade-package") .arg("sniffio") @@ -4245,10 +4243,10 @@ fn upgrade_project_cli_config_interaction() -> anyhow::Result<()> { "#})?; // Despite `upgrade = false` in the configuration file, we should mark `idna` for upgrade. - // Compare against output before adding `upgrade = false`, with `--no-upgrade --upgrade-package sniffio`. + // Compare against the CLI `--no-upgrade --upgrade-package sniffio` baseline. diff_uv_snapshot!( context.filters(), - &no_upgrade, + &upgrade_package, add_shared_args(context.lock()) .arg("--upgrade-package") .arg("idna") @@ -4279,8 +4277,8 @@ fn upgrade_project_cli_config_interaction() -> anyhow::Result<()> { "#})?; // Despite `--upgrade-package idna` on the CLI, we should upgrade all packages. - // Compare against output before adding `upgrade = true`, with `--no-upgrade --upgrade-package sniffio`. - diff_uv_snapshot!(context.filters(), &no_upgrade, add_shared_args(context.lock()) + // Compare against the CLI `--no-upgrade --upgrade-package sniffio` baseline. + diff_uv_snapshot!(context.filters(), &upgrade_package, add_shared_args(context.lock()) .arg("--upgrade-package") .arg("idna") .arg("--show-settings"), @r#" @@ -4315,8 +4313,8 @@ fn upgrade_project_cli_config_interaction() -> anyhow::Result<()> { "#})?; // Despite `upgrade-package = ["idna"]` in the configuration file, we should disable upgrades. - // Compare against output before adding `upgrade-package = ["idna"]`, with `--upgrade-package sniffio`. - diff_uv_snapshot!(context.filters(), &no_upgrade, add_shared_args(context.lock()) + // Compare against the CLI `--no-upgrade --upgrade-package sniffio` baseline. + diff_uv_snapshot!(context.filters(), &upgrade_package, add_shared_args(context.lock()) .arg("--no-upgrade") .arg("--show-settings"), @r#" ... @@ -4340,8 +4338,8 @@ fn upgrade_project_cli_config_interaction() -> anyhow::Result<()> { ); // Despite `upgrade-package = ["idna"]` in the configuration file, we should enable all upgrades. - // Compare against output before adding `upgrade-package = ["idna"]`, with `--no-upgrade --upgrade-package sniffio`. - diff_uv_snapshot!(context.filters(), &no_upgrade, add_shared_args(context.lock()) + // Compare against the CLI `--no-upgrade --upgrade-package sniffio` baseline. + diff_uv_snapshot!(context.filters(), &upgrade_package, add_shared_args(context.lock()) .arg("--upgrade") .arg("--show-settings"), @r#" ... @@ -4365,8 +4363,8 @@ fn upgrade_project_cli_config_interaction() -> anyhow::Result<()> { ); // Mark both `sniffio` and `idna` for upgrade. - // Compare against output before adding `upgrade-package = ["idna"]`, with `--no-upgrade`. - diff_uv_snapshot!(context.filters(), &no_upgrade, add_shared_args(context.lock()) + // Compare against the CLI `--no-upgrade --upgrade-package sniffio` baseline. + diff_uv_snapshot!(context.filters(), &upgrade_package, add_shared_args(context.lock()) .arg("--upgrade-package") .arg("sniffio") .arg("--show-settings"), @r#"