From b92321bd2d54867e6bcd6f1a591f631ff7739da3 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 22 May 2024 12:30:54 -0400 Subject: [PATCH] Allow `--constraint` files in `pip sync` (#3741) ## Summary Trivial now that this follows the same strategy and internals as `pip install`. Closes https://github.com/astral-sh/uv/issues/3438. --- crates/uv/src/cli.rs | 10 ++++ crates/uv/src/commands/pip/sync.rs | 6 +-- crates/uv/src/main.rs | 12 ++++- crates/uv/src/settings.rs | 6 +++ crates/uv/tests/pip_sync.rs | 85 ++++++++++++++++++++++++++++++ 5 files changed, 114 insertions(+), 5 deletions(-) diff --git a/crates/uv/src/cli.rs b/crates/uv/src/cli.rs index e837d2822..aec1053b2 100644 --- a/crates/uv/src/cli.rs +++ b/crates/uv/src/cli.rs @@ -639,6 +639,16 @@ pub(crate) struct PipSyncArgs { #[arg(required(true))] pub(crate) src_file: Vec, + /// Constrain versions using the given requirements files. + /// + /// Constraints files are `requirements.txt`-like files that only control the _version_ of a + /// requirement that's installed. However, including a package in a constraints file will _not_ + /// trigger the installation of that package. + /// + /// This is equivalent to pip's `--constraint` option. + #[arg(long, short, env = "UV_CONSTRAINT", value_delimiter = ' ', value_parser = parse_file_path)] + pub(crate) constraint: Vec>, + /// Reinstall all packages, regardless of whether they're already installed. #[arg(long, alias = "force-reinstall", overrides_with("no_reinstall"))] pub(crate) reinstall: bool, diff --git a/crates/uv/src/commands/pip/sync.rs b/crates/uv/src/commands/pip/sync.rs index fb2104680..dbdaa8296 100644 --- a/crates/uv/src/commands/pip/sync.rs +++ b/crates/uv/src/commands/pip/sync.rs @@ -42,7 +42,8 @@ use crate::printer::Printer; /// Install a set of locked requirements into the current Python environment. #[allow(clippy::too_many_arguments, clippy::fn_params_excessive_bools)] pub(crate) async fn pip_sync( - sources: &[RequirementsSource], + requirements: &[RequirementsSource], + constraints: &[RequirementsSource], reinstall: &Reinstall, link_mode: LinkMode, compile: bool, @@ -77,7 +78,6 @@ pub(crate) async fn pip_sync( .keyring(keyring_provider); // Initialize a few defaults. - let constraints = &[]; let overrides = &[]; let extras = ExtrasSpecification::default(); let upgrade = Upgrade::default(); @@ -101,7 +101,7 @@ pub(crate) async fn pip_sync( no_build: specified_no_build, extras: _, } = operations::read_requirements( - sources, + requirements, constraints, overrides, &ExtrasSpecification::default(), diff --git a/crates/uv/src/main.rs b/crates/uv/src/main.rs index 06f071967..3507715e3 100644 --- a/crates/uv/src/main.rs +++ b/crates/uv/src/main.rs @@ -185,6 +185,7 @@ async fn run() -> Result { // Initialize the cache. let cache = cache.init()?.with_refresh(args.refresh); + let requirements = args .src_file .into_iter() @@ -260,14 +261,21 @@ async fn run() -> Result { // Initialize the cache. let cache = cache.init()?.with_refresh(args.refresh); - let sources = args + + let requirements = args .src_file .into_iter() .map(RequirementsSource::from_requirements_file) .collect::>(); + let constraints = args + .constraint + .into_iter() + .map(RequirementsSource::from_constraints_txt) + .collect::>(); commands::pip_sync( - &sources, + &requirements, + &constraints, &args.reinstall, args.shared.link_mode, args.shared.compile_bytecode, diff --git a/crates/uv/src/settings.rs b/crates/uv/src/settings.rs index 953540e26..1b843cfe2 100644 --- a/crates/uv/src/settings.rs +++ b/crates/uv/src/settings.rs @@ -330,6 +330,7 @@ impl PipCompileSettings { pub(crate) struct PipSyncSettings { // CLI-only settings. pub(crate) src_file: Vec, + pub(crate) constraint: Vec, pub(crate) reinstall: Reinstall, pub(crate) refresh: Refresh, pub(crate) dry_run: bool, @@ -343,6 +344,7 @@ impl PipSyncSettings { pub(crate) fn resolve(args: PipSyncArgs, workspace: Option) -> Self { let PipSyncArgs { src_file, + constraint, reinstall, no_reinstall, reinstall_package, @@ -387,6 +389,10 @@ impl PipSyncSettings { Self { // CLI-only settings. src_file, + constraint: constraint + .into_iter() + .filter_map(Maybe::into_option) + .collect(), reinstall: Reinstall::from_args(flag(reinstall, no_reinstall), reinstall_package), refresh: Refresh::from_args(flag(refresh, no_refresh), refresh_package), dry_run, diff --git a/crates/uv/tests/pip_sync.rs b/crates/uv/tests/pip_sync.rs index 737aed14a..5af71ad65 100644 --- a/crates/uv/tests/pip_sync.rs +++ b/crates/uv/tests/pip_sync.rs @@ -2814,6 +2814,91 @@ fn offline() -> Result<()> { Ok(()) } +/// Include a `constraints.txt` file with a compatible constraint. +#[test] +fn compatible_constraint() -> Result<()> { + let context = TestContext::new("3.12"); + let requirements_txt = context.temp_dir.child("requirements.txt"); + requirements_txt.write_str("anyio==3.7.0")?; + + let constraints_txt = context.temp_dir.child("constraints.txt"); + constraints_txt.write_str("anyio==3.7.0")?; + + uv_snapshot!(command(&context) + .arg("requirements.txt") + .arg("--constraint") + .arg("constraints.txt"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 1 package in [TIME] + Downloaded 1 package in [TIME] + Installed 1 package in [TIME] + + anyio==3.7.0 + "### + ); + + Ok(()) +} + +/// Include a `constraints.txt` file with an incompatible constraint. +#[test] +fn incompatible_constraint() -> Result<()> { + let context = TestContext::new("3.12"); + let requirements_txt = context.temp_dir.child("requirements.txt"); + requirements_txt.write_str("anyio==3.7.0")?; + + let constraints_txt = context.temp_dir.child("constraints.txt"); + constraints_txt.write_str("anyio==3.6.0")?; + + uv_snapshot!(command(&context) + .arg("requirements.txt") + .arg("--constraint") + .arg("constraints.txt"), @r###" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + × No solution found when resolving dependencies: + ╰─▶ Because you require anyio==3.7.0 and anyio==3.6.0, we can conclude that the requirements are unsatisfiable. + "### + ); + + Ok(()) +} + +/// Include a `constraints.txt` file with an irrelevant constraint. +#[test] +fn irrelevant_constraint() -> Result<()> { + let context = TestContext::new("3.12"); + let requirements_txt = context.temp_dir.child("requirements.txt"); + requirements_txt.write_str("anyio==3.7.0")?; + + let constraints_txt = context.temp_dir.child("constraints.txt"); + constraints_txt.write_str("black==23.10.1")?; + + uv_snapshot!(command(&context) + .arg("requirements.txt") + .arg("--constraint") + .arg("constraints.txt"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 1 package in [TIME] + Downloaded 1 package in [TIME] + Installed 1 package in [TIME] + + anyio==3.7.0 + "### + ); + + Ok(()) +} + /// Sync with a repeated `anyio` requirement. #[test] fn repeat_requirement_identical() -> Result<()> {