From ea54bee3101cbaaf13822cc33ee92d54d47d79f4 Mon Sep 17 00:00:00 2001 From: Jacob Coffee Date: Thu, 15 Feb 2024 23:39:20 -0600 Subject: [PATCH 01/18] feat(pip-install): add `--dry-run` flag --- crates/uv/src/commands/pip_install.rs | 9 +++++++++ crates/uv/src/main.rs | 6 ++++++ 2 files changed, 15 insertions(+) diff --git a/crates/uv/src/commands/pip_install.rs b/crates/uv/src/commands/pip_install.rs index 6fd4e3e96d5c..03f4bca4f481 100644 --- a/crates/uv/src/commands/pip_install.rs +++ b/crates/uv/src/commands/pip_install.rs @@ -65,6 +65,7 @@ pub(crate) async fn pip_install( exclude_newer: Option>, cache: Cache, mut printer: Printer, + dry_run: bool, ) -> Result { let start = std::time::Instant::now(); @@ -239,6 +240,14 @@ pub(crate) async fn pip_install( Err(err) => return Err(err.into()), }; + if dry_run { + println!("Would have installed:"); + for package in resolution.packages() { + println!(" {}", package); + } + return Ok(ExitStatus::Success); + } + // Re-initialize the in-flight map. let in_flight = InFlight::default(); diff --git a/crates/uv/src/main.rs b/crates/uv/src/main.rs index ab8384f03e2f..313781022e8e 100644 --- a/crates/uv/src/main.rs +++ b/crates/uv/src/main.rs @@ -657,6 +657,11 @@ struct PipInstallArgs { /// format (e.g., `2006-12-02`). #[arg(long, value_parser = date_or_datetime, hide = true)] exclude_newer: Option>, + + /// Perform a dry run, i.e., don't actually install anything but resolve the dependencies and + /// print the resulting plan. + #[clap(long)] + dry_run: bool, } #[derive(Args)] @@ -1064,6 +1069,7 @@ async fn run() -> Result { args.exclude_newer, cache, printer, + args.dry_run, ) .await } From 9588802093b7e78f678a44728b9e499f4b45d1ab Mon Sep 17 00:00:00 2001 From: Jacob Coffee Date: Fri, 16 Feb 2024 01:23:00 -0600 Subject: [PATCH 02/18] feat(pip-install): properly print versions --- crates/uv/src/commands/pip_install.rs | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/crates/uv/src/commands/pip_install.rs b/crates/uv/src/commands/pip_install.rs index 03f4bca4f481..fca8d1076fb6 100644 --- a/crates/uv/src/commands/pip_install.rs +++ b/crates/uv/src/commands/pip_install.rs @@ -241,9 +241,30 @@ pub(crate) async fn pip_install( }; if dry_run { - println!("Would have installed:"); - for package in resolution.packages() { - println!(" {}", package); + writeln!( + printer, + "{}", + format!( + "Would install {}", + format!("{} packages", resolution.len()).bold(), + ) + .dimmed() + )?; + + for package_name in resolution.packages() { + if let Some(dist) = resolution.get(package_name) { + let version = dist + .version() + .map_or_else(String::new, |version| format!("=={version}")); + + writeln!( + printer, + " {} {}{}", + "-".blue(), + package_name.as_ref().white().bold(), + version.dimmed() + )?; + } } return Ok(ExitStatus::Success); } From ff022550f89613ac17c3cdfce10b30ad642d25e0 Mon Sep 17 00:00:00 2001 From: Jacob Coffee Date: Fri, 16 Feb 2024 01:52:54 -0600 Subject: [PATCH 03/18] test(pip-install): attempt to add (probably shitty) test --- crates/uv/src/commands/pip_install.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/uv/src/commands/pip_install.rs b/crates/uv/src/commands/pip_install.rs index fca8d1076fb6..c883d48ba7e3 100644 --- a/crates/uv/src/commands/pip_install.rs +++ b/crates/uv/src/commands/pip_install.rs @@ -260,7 +260,7 @@ pub(crate) async fn pip_install( writeln!( printer, " {} {}{}", - "-".blue(), + "~".blue(), package_name.as_ref().white().bold(), version.dimmed() )?; From 5cd1473c781d614956713f92873cb83b94b388f6 Mon Sep 17 00:00:00 2001 From: Jacob Coffee Date: Tue, 20 Feb 2024 17:25:07 -0600 Subject: [PATCH 04/18] feat(pip-install): move dry run into `install` function --- crates/uv/src/commands/pip_install.rs | 97 +++++++++++++++++++-------- 1 file changed, 68 insertions(+), 29 deletions(-) diff --git a/crates/uv/src/commands/pip_install.rs b/crates/uv/src/commands/pip_install.rs index c883d48ba7e3..2d7d55f79ebc 100644 --- a/crates/uv/src/commands/pip_install.rs +++ b/crates/uv/src/commands/pip_install.rs @@ -240,35 +240,6 @@ pub(crate) async fn pip_install( Err(err) => return Err(err.into()), }; - if dry_run { - writeln!( - printer, - "{}", - format!( - "Would install {}", - format!("{} packages", resolution.len()).bold(), - ) - .dimmed() - )?; - - for package_name in resolution.packages() { - if let Some(dist) = resolution.get(package_name) { - let version = dist - .version() - .map_or_else(String::new, |version| format!("=={version}")); - - writeln!( - printer, - " {} {}{}", - "~".blue(), - package_name.as_ref().white().bold(), - version.dimmed() - )?; - } - } - return Ok(ExitStatus::Success); - } - // Re-initialize the in-flight map. let in_flight = InFlight::default(); @@ -310,6 +281,7 @@ pub(crate) async fn pip_install( &cache, &venv, printer, + dry_run, ) .await?; @@ -523,6 +495,7 @@ async fn install( cache: &Cache, venv: &Virtualenv, mut printer: Printer, + dry_run: bool, ) -> Result<(), Error> { let start = std::time::Instant::now(); @@ -554,6 +527,72 @@ async fn install( ) .context("Failed to determine installation plan")?; + if dry_run { + if !remote.is_empty() { + writeln!( + printer, + "{} The following packages would be downloaded:", + "DRY RUN".white().bold() + )?; + for dist in &remote { + let version = resolution + .get(&dist.name) + .map(|r| r.version().map_or(String::new(), |v| format!("=={v}"))) + .unwrap_or_default(); + writeln!( + printer, + " {} {}{}", + "~".blue(), + dist.name.as_ref().white().bold(), + version.dimmed() + )?; + } + } + + if !reinstalls.is_empty() { + writeln!( + printer, + "{} The following packages would be reinstalled:", + "DRY RUN".white().bold() + )?; + for dist_info in &reinstalls { + let version = resolution + .get(dist_info.name()) + .map(|r| r.version().map_or(String::new(), |v| format!("=={v}"))) + .unwrap_or_default(); + writeln!( + printer, + " {} {}{}", + "~".blue(), + dist_info.name().white().bold(), + version.dimmed() + )?; + } + } + + if !local.is_empty() { + writeln!( + printer, + "{} The following packages would be installed from local cache:", + "DRY RUN".white().bold() + )?; + for local_dist in &local { + let version = resolution + .get(local_dist.name()) + .map(|r| r.version().map_or(String::new(), |v| format!("=={v}"))) + .unwrap_or_default(); + writeln!( + printer, + " {} {}{}", + "~".blue(), + local_dist.name().white().bold(), + version.dimmed() + )?; + } + } + return Ok(()); + } + // Nothing to do. if remote.is_empty() && local.is_empty() && reinstalls.is_empty() { let s = if resolution.len() == 1 { "" } else { "s" }; From 6892b17c8b187667cd9ee312cc4eb78b8ad3e10d Mon Sep 17 00:00:00 2001 From: Jacob Coffee Date: Sun, 25 Feb 2024 15:27:13 -0600 Subject: [PATCH 05/18] feat(pip-install): add tests, match prod formatting, add DryRunEvent --- crates/uv/src/commands/mod.rs | 10 +- crates/uv/src/commands/pip_install.rs | 217 +++++++++++++++++--------- crates/uv/tests/pip_install.rs | 190 ++++++++++++++++++++++ 3 files changed, 343 insertions(+), 74 deletions(-) diff --git a/crates/uv/src/commands/mod.rs b/crates/uv/src/commands/mod.rs index acaf252fbfa5..33ad24792d1b 100644 --- a/crates/uv/src/commands/mod.rs +++ b/crates/uv/src/commands/mod.rs @@ -1,5 +1,5 @@ -use std::process::ExitCode; use std::time::Duration; +use std::{fmt::Display, process::ExitCode}; pub(crate) use cache_clean::cache_clean; pub(crate) use cache_dir::cache_dir; @@ -9,6 +9,7 @@ pub(crate) use pip_freeze::pip_freeze; pub(crate) use pip_install::pip_install; pub(crate) use pip_sync::pip_sync; pub(crate) use pip_uninstall::pip_uninstall; +use uv_normalize::PackageName; pub(crate) use venv::venv; pub(crate) use version::version; @@ -75,6 +76,13 @@ pub(super) struct ChangeEvent { kind: ChangeEventKind, } +#[derive(Debug)] +pub(super) struct DryRunEvent { + name: PackageName, + version: T, + kind: ChangeEventKind, +} + #[derive(Debug, Clone, Copy, clap::ValueEnum)] pub(crate) enum VersionFormat { Text, diff --git a/crates/uv/src/commands/pip_install.rs b/crates/uv/src/commands/pip_install.rs index 2d7d55f79ebc..ee0a8ba607d3 100644 --- a/crates/uv/src/commands/pip_install.rs +++ b/crates/uv/src/commands/pip_install.rs @@ -2,6 +2,7 @@ use std::collections::HashSet; use std::fmt::Write; use std::path::Path; +use std::time::Instant; use anstream::eprint; use anyhow::{anyhow, Context, Result}; @@ -40,7 +41,7 @@ use crate::commands::{elapsed, ChangeEvent, ChangeEventKind, ExitStatus}; use crate::printer::Printer; use crate::requirements::{ExtrasSpecification, RequirementsSource, RequirementsSpecification}; -use super::Upgrade; +use super::{DryRunEvent, Upgrade}; /// Install packages into the current environment. #[allow(clippy::too_many_arguments)] @@ -344,7 +345,7 @@ async fn build_editables( build_dispatch: &BuildDispatch<'_>, mut printer: Printer, ) -> Result, Error> { - let start = std::time::Instant::now(); + let start = Instant::now(); let downloader = Downloader::new(cache, tags, client, build_dispatch) .with_reporter(DownloadReporter::from(printer).with_length(editables.len() as u64)); @@ -509,12 +510,7 @@ async fn install( // Partition into those that should be linked from the cache (`local`), those that need to be // downloaded (`remote`), and those that should be removed (`extraneous`). - let Plan { - local, - remote, - reinstalls, - extraneous: _, - } = Planner::with_requirements(&requirements) + let plan = Planner::with_requirements(&requirements) .with_editable_requirements(&editables) .build( site_packages, @@ -528,71 +524,16 @@ async fn install( .context("Failed to determine installation plan")?; if dry_run { - if !remote.is_empty() { - writeln!( - printer, - "{} The following packages would be downloaded:", - "DRY RUN".white().bold() - )?; - for dist in &remote { - let version = resolution - .get(&dist.name) - .map(|r| r.version().map_or(String::new(), |v| format!("=={v}"))) - .unwrap_or_default(); - writeln!( - printer, - " {} {}{}", - "~".blue(), - dist.name.as_ref().white().bold(), - version.dimmed() - )?; - } - } - - if !reinstalls.is_empty() { - writeln!( - printer, - "{} The following packages would be reinstalled:", - "DRY RUN".white().bold() - )?; - for dist_info in &reinstalls { - let version = resolution - .get(dist_info.name()) - .map(|r| r.version().map_or(String::new(), |v| format!("=={v}"))) - .unwrap_or_default(); - writeln!( - printer, - " {} {}{}", - "~".blue(), - dist_info.name().white().bold(), - version.dimmed() - )?; - } - } - - if !local.is_empty() { - writeln!( - printer, - "{} The following packages would be installed from local cache:", - "DRY RUN".white().bold() - )?; - for local_dist in &local { - let version = resolution - .get(local_dist.name()) - .map(|r| r.version().map_or(String::new(), |v| format!("=={v}"))) - .unwrap_or_default(); - writeln!( - printer, - " {} {}{}", - "~".blue(), - local_dist.name().white().bold(), - version.dimmed() - )?; - } - } - return Ok(()); + return report_dry_run(resolution, plan, start, printer); } + let Plan { + local, + remote, + reinstalls, + extraneous: _, + } = plan; + // Nothing to do. if remote.is_empty() && local.is_empty() && reinstalls.is_empty() { let s = if resolution.len() == 1 { "" } else { "s" }; @@ -606,7 +547,6 @@ async fn install( ) .dimmed() )?; - return Ok(()); } @@ -625,7 +565,7 @@ async fn install( let wheels = if remote.is_empty() { vec![] } else { - let start = std::time::Instant::now(); + let start = Instant::now(); let downloader = Downloader::new(cache, tags, client, build_dispatch) .with_reporter(DownloadReporter::from(printer).with_length(remote.len() as u64)); @@ -727,6 +667,137 @@ async fn install( } } + #[allow(clippy::items_after_statements)] + fn report_dry_run( + resolution: &Resolution, + plan: Plan, + start: Instant, + mut printer: Printer, + ) -> Result<(), Error> { + let Plan { + local, + remote, + reinstalls, + extraneous: _, + } = plan; + + // Nothing to do. + if remote.is_empty() && local.is_empty() && reinstalls.is_empty() { + let s = if resolution.len() == 1 { "" } else { "s" }; + writeln!( + printer, + "{}", + format!( + "Audited {} in {}", + format!("{} package{}", resolution.len(), s).bold(), + elapsed(start.elapsed()) + ) + .dimmed() + )?; + writeln!(printer, "Would make no changes")?; + return Ok(()); + } + + // Map any registry-based requirements back to those returned by the resolver. + let remote = remote + .iter() + .map(|dist| { + resolution + .get(&dist.name) + .cloned() + .expect("Resolution should contain all packages") + }) + .collect::>(); + + // Download, build, and unzip any missing distributions. + let wheels = if remote.is_empty() { + vec![] + } else { + let s = if remote.len() == 1 { "" } else { "s" }; + writeln!( + printer, + "{}", + format!( + "Would download {}", + format!("{} package{}", remote.len(), s).bold(), + ) + .dimmed() + )?; + remote + }; + + // Remove any existing installations. + if !reinstalls.is_empty() { + let s = if reinstalls.len() == 1 { "" } else { "s" }; + writeln!( + printer, + "{}", + format!( + "Would uninstall {}", + format!("{} package{}", reinstalls.len(), s).bold(), + ) + .dimmed() + )?; + } + + // Install the resolved distributions. + let installs = wheels.len() + local.len(); + + if installs > 0 { + let s = if installs == 1 { "" } else { "s" }; + writeln!( + printer, + "{}", + format!("Would install {}", format!("{installs} package{s}").bold(),).dimmed() + )?; + } + + for event in reinstalls + .into_iter() + .map(|distribution| DryRunEvent { + name: distribution.name().clone(), + version: distribution.version().to_string(), + kind: ChangeEventKind::Removed, + }) + .chain(wheels.into_iter().map(|distribution| DryRunEvent { + name: distribution.name().clone(), + version: distribution.version().unwrap().to_string(), + kind: ChangeEventKind::Added, + })) + .chain(local.into_iter().map(|distribution| DryRunEvent { + name: distribution.name().clone(), + version: distribution.installed_version().to_string(), + kind: ChangeEventKind::Added, + })) + .sorted_unstable_by(|a, b| a.name.cmp(&b.name).then_with(|| a.kind.cmp(&b.kind))) + { + match event.kind { + ChangeEventKind::Added => { + writeln!( + printer, + " {} {}{}{}", + "+".green(), + event.name.as_ref().bold(), + "==".dimmed(), + event.version.dimmed() + )?; + } + ChangeEventKind::Removed => { + writeln!( + printer, + " {} {}{}{}", + "-".red(), + event.name.as_ref().bold(), + "==".dimmed(), + event.version.dimmed() + )?; + } + } + } + + Ok(()) + } + // TODO(konstin): Also check the cache whether any cached or installed dist is already known to // have been yanked, we currently don't show this message on the second run anymore for dist in &remote { diff --git a/crates/uv/tests/pip_install.rs b/crates/uv/tests/pip_install.rs index bf1447e6a7c4..8b8429b7652e 100644 --- a/crates/uv/tests/pip_install.rs +++ b/crates/uv/tests/pip_install.rs @@ -1908,3 +1908,193 @@ requires-python = ">=3.8" Ok(()) } + +#[test] +fn dry_run_install() -> std::result::Result<(), Box> { + let context = TestContext::new("3.12"); + + // Set up a requirements.txt with some packages + let requirements_txt = context.temp_dir.child("requirements.txt"); + requirements_txt.touch()?; + requirements_txt.write_str("litestar==2.0.0")?; + + // Run the installation command with our dry-run and strict flags set + uv_snapshot!(command(&context) + .arg("-r") + .arg("requirements.txt") + .arg("--dry-run") + .arg("--strict"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 17 packages in [TIME] + Would download 17 packages + Would install 17 packages + + anyio==4.0.0 + + certifi==2023.11.17 + + faker==20.0.3 + + fast-query-parsers==1.0.3 + + h11==0.14.0 + + httpcore==1.0.2 + + httpx==0.25.1 + + idna==3.4 + + litestar==2.0.0 + + msgspec==0.18.4 + + multidict==6.0.4 + + polyfactory==2.12.0 + + python-dateutil==2.8.2 + + pyyaml==6.0.1 + + six==1.16.0 + + sniffio==1.3.0 + + typing-extensions==4.8.0 + "### + ); + + Ok(()) +} + +#[test] +fn dry_run_install_already_installed() -> std::result::Result<(), Box> { + // Test that --dry-run properly audits when the package is already installed + let context = TestContext::new("3.12"); + + // Set up a requirements.txt with some packages + let requirements_txt = context.temp_dir.child("requirements.txt"); + requirements_txt.touch()?; + requirements_txt.write_str("litestar==2.0.0")?; + + // Actually install the package + uv_snapshot!(command(&context) + .arg("-r") + .arg("requirements.txt") + .arg("--strict"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 17 packages in [TIME] + Downloaded 17 packages in [TIME] + Installed 17 packages in [TIME] + + anyio==4.0.0 + + certifi==2023.11.17 + + faker==20.0.3 + + fast-query-parsers==1.0.3 + + h11==0.14.0 + + httpcore==1.0.2 + + httpx==0.25.1 + + idna==3.4 + + litestar==2.0.0 + + msgspec==0.18.4 + + multidict==6.0.4 + + polyfactory==2.12.0 + + python-dateutil==2.8.2 + + pyyaml==6.0.1 + + six==1.16.0 + + sniffio==1.3.0 + + typing-extensions==4.8.0 + "### + ); + + // Install it again, but with --dry-run. Shouldn't actually install anything + uv_snapshot!(command(&context) + .arg("-r") + .arg("requirements.txt") + .arg("--dry-run") + .arg("--strict"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Audited 1 package in [TIME] + "### + ); + + Ok(()) +} + +#[test] +fn dry_run_install_then_upgrade() -> std::result::Result<(), Box> { + // Test that --dry-run + --upgrade properly displays the "would be" upgrade of an installed package + let context = TestContext::new("3.12"); + + // Set up a requirements.txt with some packages + let requirements_txt = context.temp_dir.child("requirements.txt"); + requirements_txt.touch()?; + requirements_txt.write_str("litestar==2.0.0")?; + + // Actually install the package + uv_snapshot!(command(&context) + .arg("-r") + .arg("requirements.txt") + .arg("--strict"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 17 packages in [TIME] + Downloaded 17 packages in [TIME] + Installed 17 packages in [TIME] + + anyio==4.0.0 + + certifi==2023.11.17 + + faker==20.0.3 + + fast-query-parsers==1.0.3 + + h11==0.14.0 + + httpcore==1.0.2 + + httpx==0.25.1 + + idna==3.4 + + litestar==2.0.0 + + msgspec==0.18.4 + + multidict==6.0.4 + + polyfactory==2.12.0 + + python-dateutil==2.8.2 + + pyyaml==6.0.1 + + six==1.16.0 + + sniffio==1.3.0 + + typing-extensions==4.8.0 + "### + ); + + // Upgrade the package, but with no version changes and --dry-run to see what would happen + uv_snapshot!(command(&context) + .arg("-r") + .arg("requirements.txt") + .arg("--upgrade") + .arg("--strict"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 17 packages in [TIME] + Audited 17 packages in [TIME] + "### + ); + + // Bump the version, and upgrade the package with --dry-run to see what would happen + requirements_txt.write_str("litestar==2.0.1")?; + uv_snapshot!(command(&context) + .arg("-r") + .arg("requirements.txt") + .arg("--upgrade") + .arg("--dry-run"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 17 packages in [TIME] + Would download 1 package + Would uninstall 1 package + Would install 1 package + - litestar==2.0.0 + + litestar==2.0.1 + "### + ); + + Ok(()) +} From fee46b34aafd1fe400f38fa7ed0517885e2a332b Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Mon, 4 Mar 2024 17:10:03 -0600 Subject: [PATCH 06/18] Fix duplicate `==` signs in display --- crates/uv/src/commands/pip_install.rs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/crates/uv/src/commands/pip_install.rs b/crates/uv/src/commands/pip_install.rs index ee0a8ba607d3..e5bd2c380c30 100644 --- a/crates/uv/src/commands/pip_install.rs +++ b/crates/uv/src/commands/pip_install.rs @@ -756,12 +756,12 @@ async fn install( .into_iter() .map(|distribution| DryRunEvent { name: distribution.name().clone(), - version: distribution.version().to_string(), + version: format!("=={}", distribution.version().to_string()), kind: ChangeEventKind::Removed, }) .chain(wheels.into_iter().map(|distribution| DryRunEvent { name: distribution.name().clone(), - version: distribution.version().unwrap().to_string(), + version: format!("=={}", distribution.version().unwrap().to_string()), kind: ChangeEventKind::Added, })) .chain(local.into_iter().map(|distribution| DryRunEvent { @@ -775,20 +775,18 @@ async fn install( ChangeEventKind::Added => { writeln!( printer, - " {} {}{}{}", + " {} {}{}", "+".green(), event.name.as_ref().bold(), - "==".dimmed(), event.version.dimmed() )?; } ChangeEventKind::Removed => { writeln!( printer, - " {} {}{}{}", + " {} {}{}", "-".red(), event.name.as_ref().bold(), - "==".dimmed(), event.version.dimmed() )?; } From 7eb0f93aae26448e9c26acb76f446827a48eb23a Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Mon, 4 Mar 2024 17:18:08 -0600 Subject: [PATCH 07/18] Append tests to `main` --- crates/uv/tests/pip_install.rs | 379 +++++++++++++++++++++++++++++++-- 1 file changed, 359 insertions(+), 20 deletions(-) diff --git a/crates/uv/tests/pip_install.rs b/crates/uv/tests/pip_install.rs index 8b8429b7652e..2fe291b58b3b 100644 --- a/crates/uv/tests/pip_install.rs +++ b/crates/uv/tests/pip_install.rs @@ -36,16 +36,33 @@ fn decode_token(content: &[&str]) -> String { /// Create a `pip install` command with options shared across scenarios. fn command(context: &TestContext) -> Command { + let mut command = command_without_exclude_newer(context); + command.arg("--exclude-newer").arg(EXCLUDE_NEWER); + command +} + +/// Create a `pip install` command with no `--exclude-newer` option. +/// +/// One should avoid using this in tests to the extent possible because +/// it can result in tests failing when the index state changes. Therefore, +/// if you use this, there should be some other kind of mitigation in place. +/// For example, pinning package versions. +fn command_without_exclude_newer(context: &TestContext) -> Command { let mut command = Command::new(get_bin()); command .arg("pip") .arg("install") .arg("--cache-dir") .arg(context.cache_dir.path()) - .arg("--exclude-newer") - .arg(EXCLUDE_NEWER) .env("VIRTUAL_ENV", context.venv.as_os_str()) .current_dir(&context.temp_dir); + + if cfg!(all(windows, debug_assertions)) { + // TODO(konstin): Reduce stack usage in debug mode enough that the tests pass with the + // default windows stack of 1MB + command.env("UV_STACK_SIZE", (2 * 1024 * 1024).to_string()); + } + command } @@ -59,6 +76,13 @@ fn uninstall_command(context: &TestContext) -> Command { .arg(context.cache_dir.path()) .env("VIRTUAL_ENV", context.venv.as_os_str()) .current_dir(&context.temp_dir); + + if cfg!(all(windows, debug_assertions)) { + // TODO(konstin): Reduce stack usage in debug mode enough that the tests pass with the + // default windows stack of 1MB + command.env("UV_STACK_SIZE", (2 * 1024 * 1024).to_string()); + } + command } @@ -802,14 +826,71 @@ fn install_no_index_version() { context.assert_command("import flask").failure(); } +/// Install a package via --extra-index-url. +/// +/// This is a regression test where previously `uv` would consult test.pypi.org +/// first, and if the package was found there, `uv` would not look at any other +/// indexes. We fixed this by flipping the priority order of indexes so that +/// test.pypi.org becomes the fallback (in this example) and the extra indexes +/// (regular PyPI) are checked first. +/// +/// (Neither approach matches `pip`'s behavior, which considers versions of +/// each package from all indexes. `uv` stops at the first index it finds a +/// package in.) +/// +/// Ref: +#[test] +fn install_extra_index_url_has_priority() { + let context = TestContext::new("3.12"); + + uv_snapshot!(command_without_exclude_newer(&context) + .arg("--index-url") + .arg("https://test.pypi.org/simple") + .arg("--extra-index-url") + .arg("https://pypi.org/simple") + // This tests what we want because BOTH of the following + // are true: `black` is on pypi.org and test.pypi.org, AND + // `black==24.2.0` is on pypi.org and NOT test.pypi.org. So + // this would previously check for `black` on test.pypi.org, + // find it, but then not find a compatible version. After + // the fix, `uv` will check pypi.org first since it is given + // priority via --extra-index-url. + .arg("black==24.2.0"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 6 packages in [TIME] + Downloaded 6 packages in [TIME] + Installed 6 packages in [TIME] + + black==24.2.0 + + click==8.1.7 + + mypy-extensions==1.0.0 + + packaging==23.2 + + pathspec==0.12.1 + + platformdirs==4.2.0 + "### + ); + + context.assert_command("import flask").failure(); +} + /// Install a package from a public GitHub repository #[test] #[cfg(feature = "git")] fn install_git_public_https() { let context = TestContext::new("3.8"); - uv_snapshot!(command(&context) - .arg("uv-public-pypackage @ git+https://github.com/astral-test/uv-public-pypackage") + let mut command = command(&context); + command.arg("uv-public-pypackage @ git+https://github.com/astral-test/uv-public-pypackage"); + if cfg!(all(windows, debug_assertions)) { + // TODO(konstin): Reduce stack usage in debug mode enough that the tests pass with the + // default windows stack of 1MB + command.env("UV_STACK_SIZE", (2 * 1024 * 1024).to_string()); + } + + uv_snapshot!(command , @r###" success: true exit_code: 0 @@ -838,8 +919,7 @@ fn install_git_public_https_missing_branch_or_tag() { uv_snapshot!(filters, command(&context) // 2.0.0 does not exist - .arg("uv-public-pypackage @ git+https://github.com/astral-test/uv-public-pypackage@2.0.0") - , @r###" + .arg("uv-public-pypackage @ git+https://github.com/astral-test/uv-public-pypackage@2.0.0"), @r###" success: false exit_code: 2 ----- stdout ----- @@ -897,8 +977,17 @@ fn install_git_private_https_pat() { let mut filters = INSTA_FILTERS.to_vec(); filters.insert(0, (&token, "***")); - uv_snapshot!(filters, command(&context) - .arg(format!("uv-private-pypackage @ git+https://{token}@github.com/astral-test/uv-private-pypackage")) + let mut command = command(&context); + command.arg(format!( + "uv-private-pypackage @ git+https://{token}@github.com/astral-test/uv-private-pypackage" + )); + if cfg!(all(windows, debug_assertions)) { + // TODO(konstin): Reduce stack usage in debug mode enough that the tests pass with the + // default windows stack of 1MB + command.env("UV_STACK_SIZE", (2 * 1024 * 1024).to_string()); + } + + uv_snapshot!(filters, command , @r###" success: true exit_code: 0 @@ -933,9 +1022,15 @@ fn install_git_private_https_pat_at_ref() { "" }; - uv_snapshot!(filters, command(&context) - .arg(format!("uv-private-pypackage @ git+https://{user}{token}@github.com/astral-test/uv-private-pypackage@6c09ce9ae81f50670a60abd7d95f30dd416d00ac")) - , @r###" + let mut command = command(&context); + command.arg(format!("uv-private-pypackage @ git+https://{user}{token}@github.com/astral-test/uv-private-pypackage@6c09ce9ae81f50670a60abd7d95f30dd416d00ac")); + if cfg!(all(windows, debug_assertions)) { + // TODO(konstin): Reduce stack usage in debug mode enough that the tests pass with the + // default windows stack of 1MB + command.env("UV_STACK_SIZE", (2 * 1024 * 1024).to_string()); + } + + uv_snapshot!(filters, command, @r###" success: true exit_code: 0 ----- stdout ----- @@ -952,8 +1047,12 @@ fn install_git_private_https_pat_at_ref() { /// Install a package from a private GitHub repository using a PAT and username /// An arbitrary username is supported when using a PAT. +/// +/// TODO(charlie): This test modifies the user's keyring. +/// See: . #[test] #[cfg(feature = "git")] +#[ignore] fn install_git_private_https_pat_and_username() { let context = TestContext::new("3.8"); let token = decode_token(READ_ONLY_GITHUB_TOKEN); @@ -962,8 +1061,15 @@ fn install_git_private_https_pat_and_username() { let mut filters = INSTA_FILTERS.to_vec(); filters.insert(0, (&token, "***")); - uv_snapshot!(filters, command(&context) - .arg(format!("uv-private-pypackage @ git+https://{user}:{token}@github.com/astral-test/uv-private-pypackage")) + let mut command = command(&context); + command.arg(format!("uv-private-pypackage @ git+https://{user}:{token}@github.com/astral-test/uv-private-pypackage")); + if cfg!(all(windows, debug_assertions)) { + // TODO(konstin): Reduce stack usage in debug mode enough that the tests pass with the + // default windows stack of 1MB + command.env("UV_STACK_SIZE", (2 * 1024 * 1024).to_string()); + } + + uv_snapshot!(filters, command , @r###" success: true exit_code: 0 @@ -989,7 +1095,7 @@ fn install_git_private_https_pat_not_authorized() { let token = "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"; let mut filters = context.filters(); - filters.insert(0, (&token, "***")); + filters.insert(0, (token, "***")); // We provide a username otherwise (since the token is invalid), the git cli will prompt for a password // and hang the test @@ -1007,7 +1113,7 @@ fn install_git_private_https_pat_not_authorized() { Caused by: process didn't exit successfully: `git fetch --force --update-head-ok 'https://git:***@github.com/astral-test/uv-private-pypackage' '+HEAD:refs/remotes/origin/HEAD'` (exit status: 128) --- stderr remote: Support for password authentication was removed on August 13, 2021. - remote: Please see https://docs.github.com/en/get-started/getting-started-with-git/about-remote-repositories#cloning-with-https-urls for information on currently recommended modes of authentication. + remote: Please see https://docs.github.com/get-started/getting-started-with-git/about-remote-repositories#cloning-with-https-urls for information on currently recommended modes of authentication. fatal: Authentication failed for 'https://github.com/astral-test/uv-private-pypackage/' "###); @@ -1476,7 +1582,14 @@ fn direct_url_zip_file_bunk_permissions() -> Result<()> { "opensafely-pipeline @ https://github.com/opensafely-core/pipeline/archive/refs/tags/v2023.11.06.145820.zip", )?; - uv_snapshot!(command(&context) + let mut command = command(&context); + if cfg!(all(windows, debug_assertions)) { + // TODO(konstin): Reduce stack usage in debug mode enough that the tests pass with the + // default windows stack of 1MB + command.env("UV_STACK_SIZE", (2 * 1024 * 1024).to_string()); + } + + uv_snapshot!(command .arg("-r") .arg("requirements.txt") .arg("--strict"), @r###" @@ -1581,11 +1694,12 @@ fn launcher_with_symlink() -> Result<()> { context.venv.join("Scripts\\simple_launcher.exe"), context.temp_dir.join("simple_launcher.exe"), ) { - if error.kind() == std::io::ErrorKind::PermissionDenied { - // Not running as an administrator or developer mode isn't enabled. - // Ignore the test + // Os { code: 1314, kind: Uncategorized, message: "A required privilege is not held by the client." } + // where `Uncategorized` is unstable. + if error.raw_os_error() == Some(1314) { return Ok(()); } + return Err(error.into()); } #[cfg(unix)] @@ -1820,7 +1934,7 @@ fn install_symlink() { } #[test] -fn invalidate_on_change() -> Result<()> { +fn invalidate_editable_on_change() -> Result<()> { let context = TestContext::new("3.12"); // Create an editable package. @@ -1909,6 +2023,231 @@ requires-python = ">=3.8" Ok(()) } +#[test] +fn invalidate_editable_dynamic() -> Result<()> { + let context = TestContext::new("3.12"); + + // Create an editable package with dynamic metadata + let editable_dir = assert_fs::TempDir::new()?; + let pyproject_toml = editable_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" +[project] +name = "example" +version = "0.1.0" +dynamic = ["dependencies"] +requires-python = ">=3.11,<3.13" + +[tool.setuptools.dynamic] +dependencies = {file = ["requirements.txt"]} +"#, + )?; + + let requirements_txt = editable_dir.child("requirements.txt"); + requirements_txt.write_str("anyio==4.0.0")?; + + let filters = [(r"\(from file://.*\)", "(from [WORKSPACE_DIR])")] + .into_iter() + .chain(INSTA_FILTERS.to_vec()) + .collect::>(); + + uv_snapshot!(filters, command(&context) + .arg("--editable") + .arg(editable_dir.path()), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Built 1 editable in [TIME] + Resolved 4 packages in [TIME] + Downloaded 3 packages in [TIME] + Installed 4 packages in [TIME] + + anyio==4.0.0 + + example==0.1.0 (from [WORKSPACE_DIR]) + + idna==3.4 + + sniffio==1.3.0 + "### + ); + + // Re-installing should re-install. + uv_snapshot!(filters, command(&context) + .arg("--editable") + .arg(editable_dir.path()), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Built 1 editable in [TIME] + Resolved 4 packages in [TIME] + Installed 1 package in [TIME] + - example==0.1.0 (from [WORKSPACE_DIR]) + + example==0.1.0 (from [WORKSPACE_DIR]) + "### + ); + + // Modify the requirements. + requirements_txt.write_str("anyio==3.7.1")?; + + // Re-installing should update the package. + uv_snapshot!(filters, command(&context) + .arg("--editable") + .arg(editable_dir.path()), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Built 1 editable in [TIME] + Resolved 4 packages in [TIME] + Downloaded 1 package in [TIME] + Installed 2 packages in [TIME] + - anyio==4.0.0 + + anyio==3.7.1 + - example==0.1.0 (from [WORKSPACE_DIR]) + + example==0.1.0 (from [WORKSPACE_DIR]) + "### + ); + + Ok(()) +} + +#[test] +fn invalidate_path_on_change() -> Result<()> { + let context = TestContext::new("3.12"); + + // Create a local package. + let editable_dir = assert_fs::TempDir::new()?; + let pyproject_toml = editable_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#"[project] +name = "example" +version = "0.0.0" +dependencies = [ + "anyio==4.0.0" +] +requires-python = ">=3.8" +"#, + )?; + + let filters = [(r"\(from file://.*\)", "(from [WORKSPACE_DIR])")] + .into_iter() + .chain(INSTA_FILTERS.to_vec()) + .collect::>(); + + uv_snapshot!(filters, command(&context) + .arg("example @ .") + .current_dir(editable_dir.path()), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 4 packages in [TIME] + Downloaded 4 packages in [TIME] + Installed 4 packages in [TIME] + + anyio==4.0.0 + + example==0.0.0 (from [WORKSPACE_DIR]) + + idna==3.4 + + sniffio==1.3.0 + "### + ); + + // Re-installing should be a no-op. + uv_snapshot!(filters, command(&context) + .arg("example @ .") + .current_dir(editable_dir.path()), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Audited 1 package in [TIME] + "### + ); + + // Modify the editable package. + pyproject_toml.write_str( + r#"[project] +name = "example" +version = "0.0.0" +dependencies = [ + "anyio==3.7.1" +] +requires-python = ">=3.8" +"#, + )?; + + // Re-installing should update the package. + uv_snapshot!(filters, command(&context) + .arg("example @ .") + .current_dir(editable_dir.path()), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 4 packages in [TIME] + Downloaded 2 packages in [TIME] + Installed 2 packages in [TIME] + - anyio==4.0.0 + + anyio==3.7.1 + - example==0.0.0 (from [WORKSPACE_DIR]) + + example==0.0.0 (from [WORKSPACE_DIR]) + "### + ); + + Ok(()) +} + +/// Ignore a URL dependency with a non-matching marker. +#[test] +fn editable_url_with_marker() -> Result<()> { + let context = TestContext::new("3.12"); + + let editable_dir = assert_fs::TempDir::new()?; + let pyproject_toml = editable_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" +[project] +name = "example" +version = "0.1.0" +dependencies = [ + "anyio==4.0.0; python_version >= '3.11'", + "anyio @ https://files.pythonhosted.org/packages/2d/b8/7333d87d5f03247215d86a86362fd3e324111788c6cdd8d2e6196a6ba833/anyio-4.2.0.tar.gz ; python_version < '3.11'" +] +requires-python = ">=3.11,<3.13" +"#, + )?; + + let filters = [(r"\(from file://.*\)", "(from [WORKSPACE_DIR])")] + .into_iter() + .chain(INSTA_FILTERS.to_vec()) + .collect::>(); + + uv_snapshot!(filters, command(&context) + .arg("--editable") + .arg(editable_dir.path()), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Built 1 editable in [TIME] + Resolved 4 packages in [TIME] + Downloaded 3 packages in [TIME] + Installed 4 packages in [TIME] + + anyio==4.0.0 + + example==0.1.0 (from [WORKSPACE_DIR]) + + idna==3.4 + + sniffio==1.3.0 + "### + ); + + Ok(()) +} + #[test] fn dry_run_install() -> std::result::Result<(), Box> { let context = TestContext::new("3.12"); From 0f5a423dbddadefa355bf939e42d346fec228353 Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Mon, 4 Mar 2024 17:23:02 -0600 Subject: [PATCH 08/18] Remove unncessary `to_string()` --- crates/uv/src/commands/pip_install.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/uv/src/commands/pip_install.rs b/crates/uv/src/commands/pip_install.rs index cef7b699dafb..e856122700da 100644 --- a/crates/uv/src/commands/pip_install.rs +++ b/crates/uv/src/commands/pip_install.rs @@ -777,12 +777,12 @@ async fn install( .into_iter() .map(|distribution| DryRunEvent { name: distribution.name().clone(), - version: format!("=={}", distribution.version().to_string()), + version: format!("=={}", distribution.version()), kind: ChangeEventKind::Removed, }) .chain(wheels.into_iter().map(|distribution| DryRunEvent { name: distribution.name().clone(), - version: format!("=={}", distribution.version().unwrap().to_string()), + version: format!("=={}", distribution.version().unwrap()), kind: ChangeEventKind::Added, })) .chain(local.into_iter().map(|distribution| DryRunEvent { From d5ef665b12ab1f60fe38aafa483ef4eb11005b46 Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Mon, 4 Mar 2024 17:43:57 -0600 Subject: [PATCH 09/18] Clean up test cases --- crates/uv/tests/pip_install.rs | 156 +++++++++++++++++++-------------- 1 file changed, 91 insertions(+), 65 deletions(-) diff --git a/crates/uv/tests/pip_install.rs b/crates/uv/tests/pip_install.rs index 2fe291b58b3b..23278aefdc96 100644 --- a/crates/uv/tests/pip_install.rs +++ b/crates/uv/tests/pip_install.rs @@ -2251,13 +2251,10 @@ requires-python = ">=3.11,<3.13" #[test] fn dry_run_install() -> std::result::Result<(), Box> { let context = TestContext::new("3.12"); - - // Set up a requirements.txt with some packages let requirements_txt = context.temp_dir.child("requirements.txt"); requirements_txt.touch()?; - requirements_txt.write_str("litestar==2.0.0")?; + requirements_txt.write_str("httpx==0.25.1")?; - // Run the installation command with our dry-run and strict flags set uv_snapshot!(command(&context) .arg("-r") .arg("requirements.txt") @@ -2268,26 +2265,16 @@ fn dry_run_install() -> std::result::Result<(), Box> { ----- stdout ----- ----- stderr ----- - Resolved 17 packages in [TIME] - Would download 17 packages - Would install 17 packages + Resolved 7 packages in [TIME] + Would download 7 packages + Would install 7 packages + anyio==4.0.0 + certifi==2023.11.17 - + faker==20.0.3 - + fast-query-parsers==1.0.3 + h11==0.14.0 + httpcore==1.0.2 + httpx==0.25.1 + idna==3.4 - + litestar==2.0.0 - + msgspec==0.18.4 - + multidict==6.0.4 - + polyfactory==2.12.0 - + python-dateutil==2.8.2 - + pyyaml==6.0.1 - + six==1.16.0 + sniffio==1.3.0 - + typing-extensions==4.8.0 "### ); @@ -2295,16 +2282,42 @@ fn dry_run_install() -> std::result::Result<(), Box> { } #[test] -fn dry_run_install_already_installed() -> std::result::Result<(), Box> { - // Test that --dry-run properly audits when the package is already installed +fn dry_run_install_url_dependency() -> std::result::Result<(), Box> { let context = TestContext::new("3.12"); + let requirements_txt = context.temp_dir.child("requirements.txt"); + requirements_txt.touch()?; + requirements_txt.write_str("anyio @ https://files.pythonhosted.org/packages/2d/b8/7333d87d5f03247215d86a86362fd3e324111788c6cdd8d2e6196a6ba833/anyio-4.2.0.tar.gz")?; + + uv_snapshot!(command(&context) + .arg("-r") + .arg("requirements.txt") + .arg("--dry-run") + .arg("--strict"), @r###" + success: false + exit_code: 101 + ----- stdout ----- - // Set up a requirements.txt with some packages + ----- stderr ----- + Resolved 3 packages in [TIME] + Would download 3 packages + Would install 3 packages + thread 'main' panicked at crates/uv/src/commands/pip_install.rs:785:65: + called `Option::unwrap()` on a `None` value + note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + "### + ); + + Ok(()) +} + +#[test] +fn dry_run_install_already_installed() -> std::result::Result<(), Box> { + let context = TestContext::new("3.12"); let requirements_txt = context.temp_dir.child("requirements.txt"); requirements_txt.touch()?; - requirements_txt.write_str("litestar==2.0.0")?; + requirements_txt.write_str("httpx==0.25.1")?; - // Actually install the package + // Install the package uv_snapshot!(command(&context) .arg("-r") .arg("requirements.txt") @@ -2314,30 +2327,20 @@ fn dry_run_install_already_installed() -> std::result::Result<(), Box std::result::Result<(), Box std::result::Result<(), Box> { - // Test that --dry-run + --upgrade properly displays the "would be" upgrade of an installed package +fn dry_run_install_transitive_dependency_already_installed( +) -> std::result::Result<(), Box> { let context = TestContext::new("3.12"); - // Set up a requirements.txt with some packages let requirements_txt = context.temp_dir.child("requirements.txt"); requirements_txt.touch()?; - requirements_txt.write_str("litestar==2.0.0")?; + requirements_txt.write_str("httpcore==1.0.2")?; - // Actually install the package + // Install a dependency of httpx uv_snapshot!(command(&context) .arg("-r") .arg("requirements.txt") @@ -2375,63 +2377,87 @@ fn dry_run_install_then_upgrade() -> std::result::Result<(), Box std::result::Result<(), Box> { + let context = TestContext::new("3.12"); + let requirements_txt = context.temp_dir.child("requirements.txt"); + requirements_txt.touch()?; + requirements_txt.write_str("httpx==0.25.0")?; + + // Install the package uv_snapshot!(command(&context) .arg("-r") .arg("requirements.txt") - .arg("--upgrade") .arg("--strict"), @r###" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- - Resolved 17 packages in [TIME] - Audited 17 packages in [TIME] + Resolved 7 packages in [TIME] + Downloaded 7 packages in [TIME] + Installed 7 packages in [TIME] + + anyio==4.0.0 + + certifi==2023.11.17 + + h11==0.14.0 + + httpcore==0.18.0 + + httpx==0.25.0 + + idna==3.4 + + sniffio==1.3.0 "### ); - // Bump the version, and upgrade the package with --dry-run to see what would happen - requirements_txt.write_str("litestar==2.0.1")?; + // Bump the version and install with dry run enabled + requirements_txt.write_str("httpx==0.25.1")?; uv_snapshot!(command(&context) .arg("-r") .arg("requirements.txt") - .arg("--upgrade") .arg("--dry-run"), @r###" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- - Resolved 17 packages in [TIME] + Resolved 7 packages in [TIME] Would download 1 package Would uninstall 1 package Would install 1 package - - litestar==2.0.0 - + litestar==2.0.1 + - httpx==0.25.0 + + httpx==0.25.1 "### ); From 1b4ca0b3a600745cb369a0a67b23971e93efaadc Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Tue, 5 Mar 2024 20:05:54 -0600 Subject: [PATCH 10/18] Fix display of added URLs --- crates/uv/src/commands/pip_install.rs | 5 +++-- crates/uv/tests/pip_install.rs | 10 +++++----- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/crates/uv/src/commands/pip_install.rs b/crates/uv/src/commands/pip_install.rs index e856122700da..4aa998b17e86 100644 --- a/crates/uv/src/commands/pip_install.rs +++ b/crates/uv/src/commands/pip_install.rs @@ -12,7 +12,8 @@ use tempfile::tempdir_in; use tracing::debug; use distribution_types::{ - IndexLocations, InstalledMetadata, LocalDist, LocalEditable, Name, Resolution, + DistributionMetadata, IndexLocations, InstalledMetadata, LocalDist, LocalEditable, Name, + Resolution, }; use install_wheel_rs::linker::LinkMode; use pep508_rs::{MarkerEnvironment, Requirement}; @@ -782,7 +783,7 @@ async fn install( }) .chain(wheels.into_iter().map(|distribution| DryRunEvent { name: distribution.name().clone(), - version: format!("=={}", distribution.version().unwrap()), + version: format!("{}", distribution.version_or_url()), kind: ChangeEventKind::Added, })) .chain(local.into_iter().map(|distribution| DryRunEvent { diff --git a/crates/uv/tests/pip_install.rs b/crates/uv/tests/pip_install.rs index 23278aefdc96..0593b52bf0af 100644 --- a/crates/uv/tests/pip_install.rs +++ b/crates/uv/tests/pip_install.rs @@ -2293,17 +2293,17 @@ fn dry_run_install_url_dependency() -> std::result::Result<(), Box Date: Tue, 5 Mar 2024 20:10:41 -0600 Subject: [PATCH 11/18] Add uninstall URL dependency test case --- crates/uv/tests/pip_install.rs | 52 ++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/crates/uv/tests/pip_install.rs b/crates/uv/tests/pip_install.rs index 0593b52bf0af..8059d85ea698 100644 --- a/crates/uv/tests/pip_install.rs +++ b/crates/uv/tests/pip_install.rs @@ -2310,6 +2310,58 @@ fn dry_run_install_url_dependency() -> std::result::Result<(), Box std::result::Result<(), Box> { + let context = TestContext::new("3.12"); + let requirements_txt = context.temp_dir.child("requirements.txt"); + requirements_txt.touch()?; + requirements_txt.write_str("anyio @ https://files.pythonhosted.org/packages/2d/b8/7333d87d5f03247215d86a86362fd3e324111788c6cdd8d2e6196a6ba833/anyio-4.2.0.tar.gz")?; + + // Install the URL dependency + uv_snapshot!(command(&context) + .arg("-r") + .arg("requirements.txt") + .arg("--strict"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 3 packages in [TIME] + Downloaded 3 packages in [TIME] + Installed 3 packages in [TIME] + + anyio==4.2.0 (from https://files.pythonhosted.org/packages/2d/b8/7333d87d5f03247215d86a86362fd3e324111788c6cdd8d2e6196a6ba833/anyio-4.2.0.tar.gz) + + idna==3.4 + + sniffio==1.3.0 + "### + ); + + // Then switch to a registry dependency + requirements_txt.write_str("anyio")?; + uv_snapshot!(command(&context) + .arg("-r") + .arg("requirements.txt") + .arg("--upgrade-package") + .arg("anyio") + .arg("--dry-run") + .arg("--strict"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 3 packages in [TIME] + Would download 1 package + Would uninstall 1 package + Would install 1 package + - anyio==4.2.0 + + anyio==4.0.0 + "### + ); + + Ok(()) +} + #[test] fn dry_run_install_already_installed() -> std::result::Result<(), Box> { let context = TestContext::new("3.12"); From 91ad98b1c7b80a01147ba6c08c56fbf215b45d72 Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Tue, 5 Mar 2024 20:11:46 -0600 Subject: [PATCH 12/18] Use `installed_version` display for uninstalls --- crates/uv/src/commands/pip_install.rs | 4 ++-- crates/uv/tests/pip_install.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/uv/src/commands/pip_install.rs b/crates/uv/src/commands/pip_install.rs index 4aa998b17e86..2b5ee904efde 100644 --- a/crates/uv/src/commands/pip_install.rs +++ b/crates/uv/src/commands/pip_install.rs @@ -778,12 +778,12 @@ async fn install( .into_iter() .map(|distribution| DryRunEvent { name: distribution.name().clone(), - version: format!("=={}", distribution.version()), + version: distribution.installed_version().to_string(), kind: ChangeEventKind::Removed, }) .chain(wheels.into_iter().map(|distribution| DryRunEvent { name: distribution.name().clone(), - version: format!("{}", distribution.version_or_url()), + version: distribution.version_or_url().to_string(), kind: ChangeEventKind::Added, })) .chain(local.into_iter().map(|distribution| DryRunEvent { diff --git a/crates/uv/tests/pip_install.rs b/crates/uv/tests/pip_install.rs index 8059d85ea698..74d21fd87398 100644 --- a/crates/uv/tests/pip_install.rs +++ b/crates/uv/tests/pip_install.rs @@ -2354,7 +2354,7 @@ fn dry_run_uninstall_url_dependency() -> std::result::Result<(), Box Date: Tue, 5 Mar 2024 20:14:42 -0600 Subject: [PATCH 13/18] Display message when no changes would be mad --- crates/uv/src/commands/pip_install.rs | 3 +++ crates/uv/tests/pip_install.rs | 1 + 2 files changed, 4 insertions(+) diff --git a/crates/uv/src/commands/pip_install.rs b/crates/uv/src/commands/pip_install.rs index 2b5ee904efde..f8d1e56837a9 100644 --- a/crates/uv/src/commands/pip_install.rs +++ b/crates/uv/src/commands/pip_install.rs @@ -163,6 +163,9 @@ pub(crate) async fn pip_install( ) .dimmed() )?; + if dry_run { + writeln!(printer, "Would make no changes")?; + } return Ok(ExitStatus::Success); } diff --git a/crates/uv/tests/pip_install.rs b/crates/uv/tests/pip_install.rs index 74d21fd87398..eef2ec62a58a 100644 --- a/crates/uv/tests/pip_install.rs +++ b/crates/uv/tests/pip_install.rs @@ -2404,6 +2404,7 @@ fn dry_run_install_already_installed() -> std::result::Result<(), Box Date: Tue, 5 Mar 2024 20:17:59 -0600 Subject: [PATCH 14/18] Include tests from `main` --- crates/uv/tests/pip_install.rs | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/crates/uv/tests/pip_install.rs b/crates/uv/tests/pip_install.rs index eef2ec62a58a..9fbcc51167a5 100644 --- a/crates/uv/tests/pip_install.rs +++ b/crates/uv/tests/pip_install.rs @@ -2248,6 +2248,40 @@ requires-python = ">=3.11,<3.13" Ok(()) } +/// Raise an error when an editable's `Requires-Python` constraint is not met. +#[test] +fn requires_python_editable() -> Result<()> { + let context = TestContext::new("3.12"); + + // Create an editable package with a `Requires-Python` constraint that is not met. + let editable_dir = assert_fs::TempDir::new()?; + let pyproject_toml = editable_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#"[project] +name = "example" +version = "0.0.0" +dependencies = [ + "anyio==4.0.0" +] +requires-python = "<=3.8" +"#, + )?; + + uv_snapshot!(command(&context) + .arg("--editable") + .arg(editable_dir.path()), @r###" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: Editable `example` requires Python <=3.8, but 3.12.1 is installed + "### + ); + + Ok(()) +} + #[test] fn dry_run_install() -> std::result::Result<(), Box> { let context = TestContext::new("3.12"); From 415a4f49523ef1e950eb6425490f73661df84b01 Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Tue, 5 Mar 2024 21:57:35 -0600 Subject: [PATCH 15/18] Ignore lint --- crates/uv/src/commands/pip_install.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/uv/src/commands/pip_install.rs b/crates/uv/src/commands/pip_install.rs index d4f9b2883149..afb9f6551902 100644 --- a/crates/uv/src/commands/pip_install.rs +++ b/crates/uv/src/commands/pip_install.rs @@ -44,7 +44,7 @@ use crate::requirements::{ExtrasSpecification, RequirementsSource, RequirementsS use super::{DryRunEvent, Upgrade}; /// Install packages into the current environment. -#[allow(clippy::too_many_arguments)] +#[allow(clippy::too_many_arguments, clippy::fn_params_excessive_bools)] pub(crate) async fn pip_install( requirements: &[RequirementsSource], constraints: &[RequirementsSource], From 73c0984ef15080d217390c0e15ec9ea58e1ade36 Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Tue, 5 Mar 2024 22:06:41 -0600 Subject: [PATCH 16/18] Fix extraneous comma --- crates/uv/src/commands/pip_install.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/uv/src/commands/pip_install.rs b/crates/uv/src/commands/pip_install.rs index afb9f6551902..9c593d6d7562 100644 --- a/crates/uv/src/commands/pip_install.rs +++ b/crates/uv/src/commands/pip_install.rs @@ -798,7 +798,7 @@ async fn install( writeln!( printer.stderr(), "{}", - format!("Would install {}", format!("{installs} package{s}").bold(),).dimmed() + format!("Would install {}", format!("{installs} package{s}").bold()).dimmed() )?; } From 85b06e944267727903d05de92529de17f1da182d Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Thu, 7 Mar 2024 11:46:17 -0600 Subject: [PATCH 17/18] Copy tests from `main` --- crates/uv/tests/pip_install.rs | 140 ++++++++++++++++++++++++++++++++- 1 file changed, 139 insertions(+), 1 deletion(-) diff --git a/crates/uv/tests/pip_install.rs b/crates/uv/tests/pip_install.rs index 9fbcc51167a5..28e65bec438a 100644 --- a/crates/uv/tests/pip_install.rs +++ b/crates/uv/tests/pip_install.rs @@ -100,7 +100,7 @@ fn missing_requirements_txt() { ----- stdout ----- ----- stderr ----- - error: failed to open file `requirements.txt` + error: failed to read from file `requirements.txt` Caused by: No such file or directory (os error 2) "### ); @@ -1509,6 +1509,73 @@ fn install_constraints_inline() -> Result<()> { Ok(()) } +/// Install a package from a `constraints.txt` file on a remote http server. +#[test] +fn install_constraints_remote() { + let context = TestContext::new("3.12"); + + uv_snapshot!(command(&context) + .arg("-c") + .arg("https://raw.githubusercontent.com/apache/airflow/constraints-2-6/constraints-3.11.txt") + .arg("typing_extensions>=4.0"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 1 package in [TIME] + Downloaded 1 package in [TIME] + Installed 1 package in [TIME] + + typing-extensions==4.7.1 + "### + ); // would yield typing-extensions==4.8.2 without constraint file +} + +/// Install a package from a `requirements.txt` file, with an inline constraint, which points +/// to a remote http server. +#[test] +fn install_constraints_inline_remote() -> Result<()> { + let context = TestContext::new("3.12"); + let requirementstxt = context.temp_dir.child("requirements.txt"); + requirementstxt.write_str("typing-extensions>=4.0\n-c https://raw.githubusercontent.com/apache/airflow/constraints-2-6/constraints-3.11.txt")?; + + uv_snapshot!(command(&context) + .arg("-r") + .arg("requirements.txt"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 1 package in [TIME] + Downloaded 1 package in [TIME] + Installed 1 package in [TIME] + + typing-extensions==4.7.1 + "### // would yield typing-extensions==4.8.2 without constraint file + ); + + Ok(()) +} + +#[test] +fn install_constraints_respects_offline_mode() { + let context = TestContext::new("3.12"); + + uv_snapshot!(command(&context) + .arg("--offline") + .arg("-r") + .arg("http://example.com/requirements.txt"), @r###" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: Error while accessing remote requirements file http://example.com/requirements.txt: Middleware error: Network connectivity is disabled, but the requested data wasn't found in the cache for: `http://example.com/requirements.txt` + Caused by: Network connectivity is disabled, but the requested data wasn't found in the cache for: `http://example.com/requirements.txt` + "### + ); +} + /// Tests that we can install `polars==0.14.0`, which has this odd dependency /// requirement in its wheel metadata: `pyarrow>=4.0.*; extra == 'pyarrow'`. /// @@ -2282,6 +2349,77 @@ requires-python = "<=3.8" Ok(()) } +/// Install with `--no-build-isolation`, to disable isolation during PEP 517 builds. +#[test] +fn no_build_isolation() -> Result<()> { + let context = TestContext::new("3.12"); + let requirements_in = context.temp_dir.child("requirements.in"); + requirements_in.write_str("anyio @ https://files.pythonhosted.org/packages/db/4d/3970183622f0330d3c23d9b8a5f52e365e50381fd484d08e3285104333d3/anyio-4.3.0.tar.gz")?; + + // We expect the build to fail, because `setuptools` is not installed. + let filters = std::iter::once((r"exit code: 1", "exit status: 1")) + .chain(INSTA_FILTERS.to_vec()) + .collect::>(); + uv_snapshot!(filters, command(&context) + .arg("-r") + .arg("requirements.in") + .arg("--no-build-isolation"), @r###" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: Failed to download and build: anyio @ https://files.pythonhosted.org/packages/db/4d/3970183622f0330d3c23d9b8a5f52e365e50381fd484d08e3285104333d3/anyio-4.3.0.tar.gz + Caused by: Failed to build: anyio @ https://files.pythonhosted.org/packages/db/4d/3970183622f0330d3c23d9b8a5f52e365e50381fd484d08e3285104333d3/anyio-4.3.0.tar.gz + Caused by: Build backend failed to determine metadata through `prepare_metadata_for_build_wheel` with exit status: 1 + --- stdout: + + --- stderr: + Traceback (most recent call last): + File "", line 8, in + ModuleNotFoundError: No module named 'setuptools' + --- + "### + ); + + // Install `setuptools` and `wheel`. + uv_snapshot!(command(&context) + .arg("setuptools") + .arg("wheel"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 2 packages in [TIME] + Downloaded 2 packages in [TIME] + Installed 2 packages in [TIME] + + setuptools==68.2.2 + + wheel==0.41.3 + "###); + + // We expect the build to succeed, since `setuptools` is now installed. + uv_snapshot!(command(&context) + .arg("-r") + .arg("requirements.in") + .arg("--no-build-isolation"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 3 packages in [TIME] + Downloaded 3 packages in [TIME] + Installed 3 packages in [TIME] + + anyio==0.0.0 (from https://files.pythonhosted.org/packages/db/4d/3970183622f0330d3c23d9b8a5f52e365e50381fd484d08e3285104333d3/anyio-4.3.0.tar.gz) + + idna==3.4 + + sniffio==1.3.0 + "### + ); + + Ok(()) +} + #[test] fn dry_run_install() -> std::result::Result<(), Box> { let context = TestContext::new("3.12"); From 4dd46dff9951e639e74c1c4396bf217cd2608f47 Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Tue, 12 Mar 2024 00:29:37 -0500 Subject: [PATCH 18/18] Copy tests from `main` --- crates/uv/tests/pip_install.rs | 191 ++++++++++++++++----------------- 1 file changed, 93 insertions(+), 98 deletions(-) diff --git a/crates/uv/tests/pip_install.rs b/crates/uv/tests/pip_install.rs index 28e65bec438a..fa1e2ca25c06 100644 --- a/crates/uv/tests/pip_install.rs +++ b/crates/uv/tests/pip_install.rs @@ -516,17 +516,10 @@ fn install_editable() -> Result<()> { .collect::>(); // Install the editable package. - uv_snapshot!(filters, Command::new(get_bin()) - .arg("pip") - .arg("install") + uv_snapshot!(filters, command(&context) .arg("-e") .arg("../../scripts/editable-installs/poetry_editable") - .arg("--strict") - .arg("--cache-dir") - .arg(context.cache_dir.path()) - .arg("--exclude-newer") - .arg(EXCLUDE_NEWER) - .env("VIRTUAL_ENV", context.venv.as_os_str()) + .current_dir(¤t_dir) .env("CARGO_TARGET_DIR", "../../../target/target_install_editable"), @r###" success: true exit_code: 0 @@ -618,16 +611,9 @@ fn install_editable_and_registry() -> Result<()> { .collect(); // Install the registry-based version of Black. - uv_snapshot!(filters, Command::new(get_bin()) - .arg("pip") - .arg("install") + uv_snapshot!(filters, command(&context) .arg("black") - .arg("--strict") - .arg("--cache-dir") - .arg(context.cache_dir.path()) - .arg("--exclude-newer") - .arg(EXCLUDE_NEWER) - .env("VIRTUAL_ENV", context.venv.as_os_str()) + .current_dir(¤t_dir) .env("CARGO_TARGET_DIR", "../../../target/target_install_editable"), @r###" success: true exit_code: 0 @@ -647,17 +633,10 @@ fn install_editable_and_registry() -> Result<()> { ); // Install the editable version of Black. This should remove the registry-based version. - uv_snapshot!(filters, Command::new(get_bin()) - .arg("pip") - .arg("install") + uv_snapshot!(filters, command(&context) .arg("-e") .arg("../../scripts/editable-installs/black_editable") - .arg("--strict") - .arg("--cache-dir") - .arg(context.cache_dir.path()) - .arg("--exclude-newer") - .arg(EXCLUDE_NEWER) - .env("VIRTUAL_ENV", context.venv.as_os_str()) + .current_dir(¤t_dir) .env("CARGO_TARGET_DIR", "../../../target/target_install_editable"), @r###" success: true exit_code: 0 @@ -674,16 +653,10 @@ fn install_editable_and_registry() -> Result<()> { // Re-install the registry-based version of Black. This should be a no-op, since we have a // version of Black installed (the editable version) that satisfies the requirements. - uv_snapshot!(filters, Command::new(get_bin()) - .arg("pip") - .arg("install") + uv_snapshot!(filters, command(&context) .arg("black") .arg("--strict") - .arg("--cache-dir") - .arg(context.cache_dir.path()) - .arg("--exclude-newer") - .arg(EXCLUDE_NEWER) - .env("VIRTUAL_ENV", context.venv.as_os_str()) + .current_dir(¤t_dir) .env("CARGO_TARGET_DIR", "../../../target/target_install_editable"), @r###" success: true exit_code: 0 @@ -703,16 +676,9 @@ fn install_editable_and_registry() -> Result<()> { .collect(); // Re-install Black at a specific version. This should replace the editable version. - uv_snapshot!(filters2, Command::new(get_bin()) - .arg("pip") - .arg("install") + uv_snapshot!(filters2, command(&context) .arg("black==23.10.0") - .arg("--strict") - .arg("--cache-dir") - .arg(context.cache_dir.path()) - .arg("--exclude-newer") - .arg(EXCLUDE_NEWER) - .env("VIRTUAL_ENV", context.venv.as_os_str()) + .current_dir(¤t_dir) .env("CARGO_TARGET_DIR", "../../../target/target_install_editable"), @r###" success: true exit_code: 0 @@ -855,21 +821,19 @@ fn install_extra_index_url_has_priority() { // find it, but then not find a compatible version. After // the fix, `uv` will check pypi.org first since it is given // priority via --extra-index-url. - .arg("black==24.2.0"), @r###" + .arg("black==24.2.0") + .arg("--no-deps") + .arg("--exclude-newer") + .arg("2024-03-09"), @r###" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- - Resolved 6 packages in [TIME] - Downloaded 6 packages in [TIME] - Installed 6 packages in [TIME] + Resolved 1 package in [TIME] + Downloaded 1 package in [TIME] + Installed 1 package in [TIME] + black==24.2.0 - + click==8.1.7 - + mypy-extensions==1.0.0 - + packaging==23.2 - + pathspec==0.12.1 - + platformdirs==4.2.0 "### ); @@ -884,11 +848,6 @@ fn install_git_public_https() { let mut command = command(&context); command.arg("uv-public-pypackage @ git+https://github.com/astral-test/uv-public-pypackage"); - if cfg!(all(windows, debug_assertions)) { - // TODO(konstin): Reduce stack usage in debug mode enough that the tests pass with the - // default windows stack of 1MB - command.env("UV_STACK_SIZE", (2 * 1024 * 1024).to_string()); - } uv_snapshot!(command , @r###" @@ -981,11 +940,6 @@ fn install_git_private_https_pat() { command.arg(format!( "uv-private-pypackage @ git+https://{token}@github.com/astral-test/uv-private-pypackage" )); - if cfg!(all(windows, debug_assertions)) { - // TODO(konstin): Reduce stack usage in debug mode enough that the tests pass with the - // default windows stack of 1MB - command.env("UV_STACK_SIZE", (2 * 1024 * 1024).to_string()); - } uv_snapshot!(filters, command , @r###" @@ -1024,11 +978,6 @@ fn install_git_private_https_pat_at_ref() { let mut command = command(&context); command.arg(format!("uv-private-pypackage @ git+https://{user}{token}@github.com/astral-test/uv-private-pypackage@6c09ce9ae81f50670a60abd7d95f30dd416d00ac")); - if cfg!(all(windows, debug_assertions)) { - // TODO(konstin): Reduce stack usage in debug mode enough that the tests pass with the - // default windows stack of 1MB - command.env("UV_STACK_SIZE", (2 * 1024 * 1024).to_string()); - } uv_snapshot!(filters, command, @r###" success: true @@ -1063,11 +1012,6 @@ fn install_git_private_https_pat_and_username() { let mut command = command(&context); command.arg(format!("uv-private-pypackage @ git+https://{user}:{token}@github.com/astral-test/uv-private-pypackage")); - if cfg!(all(windows, debug_assertions)) { - // TODO(konstin): Reduce stack usage in debug mode enough that the tests pass with the - // default windows stack of 1MB - command.env("UV_STACK_SIZE", (2 * 1024 * 1024).to_string()); - } uv_snapshot!(filters, command , @r###" @@ -1127,11 +1071,6 @@ fn reinstall_no_binary() { // The first installation should use a pre-built wheel let mut command = command(&context); command.arg("anyio").arg("--strict"); - if cfg!(all(windows, debug_assertions)) { - // TODO(konstin): Reduce stack usage in debug mode enough that the tests pass with the - // default windows stack of 1MB - command.env("UV_STACK_SIZE", (2 * 1024 * 1024).to_string()); - } uv_snapshot!(command, @r###" success: true exit_code: 0 @@ -1157,11 +1096,6 @@ fn reinstall_no_binary() { .arg("--no-binary") .arg(":all:") .arg("--strict"); - if cfg!(all(windows, debug_assertions)) { - // TODO(konstin): Reduce stack usage in debug mode enough that the tests pass with the - // default windows stack of 1MB - command.env("UV_STACK_SIZE", (2 * 1024 * 1024).to_string()); - } uv_snapshot!(command, @r###" success: true exit_code: 0 @@ -1193,11 +1127,6 @@ fn reinstall_no_binary() { .arg("--reinstall-package") .arg("anyio") .arg("--strict"); - if cfg!(all(windows, debug_assertions)) { - // TODO(konstin): Reduce stack usage in debug mode enough that the tests pass with the - // default windows stack of 1MB - command.env("UV_STACK_SIZE", (2 * 1024 * 1024).to_string()); - } uv_snapshot!(filters, command, @r###" success: true exit_code: 0 @@ -1570,8 +1499,7 @@ fn install_constraints_respects_offline_mode() { ----- stdout ----- ----- stderr ----- - error: Error while accessing remote requirements file http://example.com/requirements.txt: Middleware error: Network connectivity is disabled, but the requested data wasn't found in the cache for: `http://example.com/requirements.txt` - Caused by: Network connectivity is disabled, but the requested data wasn't found in the cache for: `http://example.com/requirements.txt` + error: Network connectivity is disabled, but a remote requirements file was requested: http://example.com/requirements.txt "### ); } @@ -1649,14 +1577,7 @@ fn direct_url_zip_file_bunk_permissions() -> Result<()> { "opensafely-pipeline @ https://github.com/opensafely-core/pipeline/archive/refs/tags/v2023.11.06.145820.zip", )?; - let mut command = command(&context); - if cfg!(all(windows, debug_assertions)) { - // TODO(konstin): Reduce stack usage in debug mode enough that the tests pass with the - // default windows stack of 1MB - command.env("UV_STACK_SIZE", (2 * 1024 * 1024).to_string()); - } - - uv_snapshot!(command + uv_snapshot!(command(&context) .arg("-r") .arg("requirements.txt") .arg("--strict"), @r###" @@ -2420,6 +2341,80 @@ fn no_build_isolation() -> Result<()> { Ok(()) } +/// This tests that `uv` can read UTF-16LE encoded requirements.txt files. +/// +/// Ref: +#[test] +fn install_utf16le_requirements() -> Result<()> { + let context = TestContext::new("3.12"); + let requirements_txt = context.temp_dir.child("requirements.txt"); + requirements_txt.touch()?; + requirements_txt.write_binary(&utf8_to_utf16_with_bom_le("tomli"))?; + + uv_snapshot!(command_without_exclude_newer(&context) + .arg("-r") + .arg("requirements.txt"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 1 package in [TIME] + Downloaded 1 package in [TIME] + Installed 1 package in [TIME] + + tomli==2.0.1 + "### + ); + Ok(()) +} + +/// This tests that `uv` can read UTF-16BE encoded requirements.txt files. +/// +/// Ref: +#[test] +fn install_utf16be_requirements() -> Result<()> { + let context = TestContext::new("3.12"); + let requirements_txt = context.temp_dir.child("requirements.txt"); + requirements_txt.touch()?; + requirements_txt.write_binary(&utf8_to_utf16_with_bom_be("tomli"))?; + + uv_snapshot!(command_without_exclude_newer(&context) + .arg("-r") + .arg("requirements.txt"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 1 package in [TIME] + Downloaded 1 package in [TIME] + Installed 1 package in [TIME] + + tomli==2.0.1 + "### + ); + Ok(()) +} + +fn utf8_to_utf16_with_bom_le(s: &str) -> Vec { + use byteorder::ByteOrder; + + let mut u16s = vec![0xFEFF]; + u16s.extend(s.encode_utf16()); + let mut u8s = vec![0; u16s.len() * 2]; + byteorder::LittleEndian::write_u16_into(&u16s, &mut u8s); + u8s +} + +fn utf8_to_utf16_with_bom_be(s: &str) -> Vec { + use byteorder::ByteOrder; + + let mut u16s = vec![0xFEFF]; + u16s.extend(s.encode_utf16()); + let mut u8s = vec![0; u16s.len() * 2]; + byteorder::BigEndian::write_u16_into(&u16s, &mut u8s); + u8s +} + #[test] fn dry_run_install() -> std::result::Result<(), Box> { let context = TestContext::new("3.12");