diff --git a/crates/uv/src/commands/project/export.rs b/crates/uv/src/commands/project/export.rs index b3a585732..dbf60af82 100644 --- a/crates/uv/src/commands/project/export.rs +++ b/crates/uv/src/commands/project/export.rs @@ -1,4 +1,7 @@ +use std::env; + use anyhow::{Context, Result}; +use itertools::Itertools; use owo_colors::OwoColorize; use std::path::PathBuf; @@ -131,8 +134,9 @@ pub(crate) async fn export( writeln!( writer, "{}", - "# This file was autogenerated via `uv export`.".green() + "# This file was autogenerated by uv via the following command:".green() )?; + writeln!(writer, "{}", format!("# {}", cmd()).green())?; write!(writer, "{export}")?; } } @@ -141,3 +145,54 @@ pub(crate) async fn export( Ok(ExitStatus::Success) } + +/// Format the uv command used to generate the output file. +fn cmd() -> String { + let args = env::args_os() + .skip(1) + .map(|arg| arg.to_string_lossy().to_string()) + .scan(None, move |skip_next, arg| { + if matches!(skip_next, Some(true)) { + // Reset state; skip this iteration. + *skip_next = None; + return Some(None); + } + + // Always skip the `--upgrade` flag. + if arg == "--upgrade" || arg == "-U" { + *skip_next = None; + return Some(None); + } + + // Always skip the `--upgrade-package` and mark the next item to be skipped + if arg == "--upgrade-package" || arg == "-P" { + *skip_next = Some(true); + return Some(None); + } + + // Skip only this argument if option and value are together + if arg.starts_with("--upgrade-package=") || arg.starts_with("-P") { + // Reset state; skip this iteration. + *skip_next = None; + return Some(None); + } + + // Always skip the `--quiet` flag. + if arg == "--quiet" || arg == "-q" { + *skip_next = None; + return Some(None); + } + + // Always skip the `--verbose` flag. + if arg == "--verbose" || arg == "-v" { + *skip_next = None; + return Some(None); + } + + // Return the argument. + Some(Some(arg)) + }) + .flatten() + .join(" "); + format!("uv {args}") +} diff --git a/crates/uv/tests/common/mod.rs b/crates/uv/tests/common/mod.rs index 6a5713aff..949bdcef2 100644 --- a/crates/uv/tests/common/mod.rs +++ b/crates/uv/tests/common/mod.rs @@ -1030,6 +1030,18 @@ pub enum WindowsFilters { Universal, } +/// Helper method to apply filters to a string. Useful when `!uv_snapshot` cannot be used. +pub fn apply_filters>(mut snapshot: String, filters: impl AsRef<[(T, T)]>) -> String { + for (matcher, replacement) in filters.as_ref() { + // TODO(konstin): Cache regex compilation + let re = Regex::new(matcher.as_ref()).expect("Do you need to regex::escape your filter?"); + if re.is_match(&snapshot) { + snapshot = re.replace_all(&snapshot, replacement.as_ref()).to_string(); + } + } + snapshot +} + /// Execute the command and format its output status, stdout and stderr into a snapshot string. /// /// This function is derived from `insta_cmd`s `spawn_with_info`. @@ -1076,22 +1088,17 @@ pub fn run_and_format_with_status>( .output() .unwrap_or_else(|err| panic!("Failed to spawn {program}: {err}")); - let mut snapshot = format!( - "success: {:?}\nexit_code: {}\n----- stdout -----\n{}\n----- stderr -----\n{}", - output.status.success(), - output.status.code().unwrap_or(!0), - String::from_utf8_lossy(&output.stdout), - String::from_utf8_lossy(&output.stderr) + let mut snapshot = apply_filters( + format!( + "success: {:?}\nexit_code: {}\n----- stdout -----\n{}\n----- stderr -----\n{}", + output.status.success(), + output.status.code().unwrap_or(!0), + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ), + filters, ); - for (matcher, replacement) in filters.as_ref() { - // TODO(konstin): Cache regex compilation - let re = Regex::new(matcher.as_ref()).expect("Do you need to regex::escape your filter?"); - if re.is_match(&snapshot) { - snapshot = re.replace_all(&snapshot, replacement.as_ref()).to_string(); - } - } - // This is a heuristic filter meant to try and make *most* of our tests // pass whether it's on Windows or Unix. In particular, there are some very // common Windows-only dependencies that, when removed from a resolution, diff --git a/crates/uv/tests/export.rs b/crates/uv/tests/export.rs index d0109fd61..6839b36fe 100644 --- a/crates/uv/tests/export.rs +++ b/crates/uv/tests/export.rs @@ -4,7 +4,7 @@ use anyhow::Result; use assert_cmd::assert::OutputAssertExt; use assert_fs::prelude::*; -use common::{uv_snapshot, TestContext}; +use common::{apply_filters, uv_snapshot, TestContext}; use std::process::Stdio; mod common; @@ -34,7 +34,8 @@ fn dependency() -> Result<()> { success: true exit_code: 0 ----- stdout ----- - # This file was autogenerated via `uv export`. + # This file was autogenerated by uv via the following command: + # uv export --cache-dir [CACHE_DIR] -e . anyio==3.7.0 \ --hash=sha256:275d9973793619a5374e1c89a4f4ad3f4b0a5510a2b5b939444bee8f4c4d37ce \ @@ -78,7 +79,8 @@ fn dependency_extra() -> Result<()> { success: true exit_code: 0 ----- stdout ----- - # This file was autogenerated via `uv export`. + # This file was autogenerated by uv via the following command: + # uv export --cache-dir [CACHE_DIR] -e . blinker==1.7.0 \ --hash=sha256:e6820ff6fa4e4d1d8e2747c2283749c3f547e4fee112b98555cdcdae32996182 \ @@ -153,7 +155,8 @@ fn project_extra() -> Result<()> { success: true exit_code: 0 ----- stdout ----- - # This file was autogenerated via `uv export`. + # This file was autogenerated by uv via the following command: + # uv export --cache-dir [CACHE_DIR] -e . typing-extensions==4.10.0 \ --hash=sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb \ @@ -167,7 +170,8 @@ fn project_extra() -> Result<()> { success: true exit_code: 0 ----- stdout ----- - # This file was autogenerated via `uv export`. + # This file was autogenerated by uv via the following command: + # uv export --cache-dir [CACHE_DIR] --extra pytest -e . iniconfig==2.0.0 \ --hash=sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3 \ @@ -184,7 +188,8 @@ fn project_extra() -> Result<()> { success: true exit_code: 0 ----- stdout ----- - # This file was autogenerated via `uv export`. + # This file was autogenerated by uv via the following command: + # uv export --cache-dir [CACHE_DIR] --all-extras -e . anyio==3.7.0 \ --hash=sha256:275d9973793619a5374e1c89a4f4ad3f4b0a5510a2b5b939444bee8f4c4d37ce \ @@ -234,7 +239,8 @@ fn dependency_marker() -> Result<()> { success: true exit_code: 0 ----- stdout ----- - # This file was autogenerated via `uv export`. + # This file was autogenerated by uv via the following command: + # uv export --cache-dir [CACHE_DIR] -e . anyio==4.3.0 ; sys_platform == 'darwin' \ --hash=sha256:f75253795a87df48568485fd18cdd2a3fa5c4f7c5be8e5e36637733fce06fed6 \ @@ -285,7 +291,8 @@ fn dependency_multiple_markers() -> Result<()> { success: true exit_code: 0 ----- stdout ----- - # This file was autogenerated via `uv export`. + # This file was autogenerated by uv via the following command: + # uv export --cache-dir [CACHE_DIR] -e . attrs==23.2.0 ; sys_platform == 'win32' or python_full_version >= '3.12' \ --hash=sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30 \ @@ -355,7 +362,8 @@ fn dependency_conflicting_markers() -> Result<()> { success: true exit_code: 0 ----- stdout ----- - # This file was autogenerated via `uv export`. + # This file was autogenerated by uv via the following command: + # uv export --cache-dir [CACHE_DIR] -e . async-generator==1.10 ; sys_platform == 'win32' \ --hash=sha256:6ebb3d106c12920aaae42ccb6f787ef5eefdcdd166ea3d628fa8476abe712144 \ @@ -441,7 +449,8 @@ fn non_root() -> Result<()> { success: true exit_code: 0 ----- stdout ----- - # This file was autogenerated via `uv export`. + # This file was autogenerated by uv via the following command: + # uv export --cache-dir [CACHE_DIR] --package child -e child iniconfig==2.0.0 \ --hash=sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3 \ @@ -507,9 +516,13 @@ fn relative_path() -> Result<()> { "###); // Read the file contents. - let contents = fs_err::read_to_string(project.child("requirements.txt")).unwrap(); + let contents = apply_filters( + fs_err::read_to_string(project.child("requirements.txt")).unwrap(), + context.filters(), + ); insta::assert_snapshot!(contents, @r###" - # This file was autogenerated via `uv export`. + # This file was autogenerated by uv via the following command: + # uv export --cache-dir [CACHE_DIR] -e . ../dependency iniconfig==2.0.0 \ @@ -563,7 +576,8 @@ fn dev() -> Result<()> { success: true exit_code: 0 ----- stdout ----- - # This file was autogenerated via `uv export`. + # This file was autogenerated by uv via the following command: + # uv export --cache-dir [CACHE_DIR] -e . anyio==4.3.0 \ --hash=sha256:f75253795a87df48568485fd18cdd2a3fa5c4f7c5be8e5e36637733fce06fed6 \ @@ -586,7 +600,8 @@ fn dev() -> Result<()> { success: true exit_code: 0 ----- stdout ----- - # This file was autogenerated via `uv export`. + # This file was autogenerated by uv via the following command: + # uv export --cache-dir [CACHE_DIR] --no-dev -e . typing-extensions==4.10.0 \ --hash=sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb \ @@ -624,7 +639,8 @@ fn no_hashes() -> Result<()> { success: true exit_code: 0 ----- stdout ----- - # This file was autogenerated via `uv export`. + # This file was autogenerated by uv via the following command: + # uv export --cache-dir [CACHE_DIR] --no-hashes -e . anyio==3.7.0 idna==3.6 @@ -662,7 +678,8 @@ fn output_file() -> Result<()> { success: true exit_code: 0 ----- stdout ----- - # This file was autogenerated via `uv export`. + # This file was autogenerated by uv via the following command: + # uv export --cache-dir [CACHE_DIR] --output-file requirements.txt -e . anyio==3.7.0 \ --hash=sha256:275d9973793619a5374e1c89a4f4ad3f4b0a5510a2b5b939444bee8f4c4d37ce \ @@ -678,9 +695,13 @@ fn output_file() -> Result<()> { Resolved 4 packages in [TIME] "###); - let contents = fs_err::read_to_string(context.temp_dir.child("requirements.txt"))?; + let contents = apply_filters( + fs_err::read_to_string(context.temp_dir.child("requirements.txt")).unwrap(), + context.filters(), + ); insta::assert_snapshot!(contents, @r###" - # This file was autogenerated via `uv export`. + # This file was autogenerated by uv via the following command: + # uv export --cache-dir [CACHE_DIR] --output-file requirements.txt -e . anyio==3.7.0 \ --hash=sha256:275d9973793619a5374e1c89a4f4ad3f4b0a5510a2b5b939444bee8f4c4d37ce \ @@ -743,7 +764,8 @@ fn no_emit() -> Result<()> { success: true exit_code: 0 ----- stdout ----- - # This file was autogenerated via `uv export`. + # This file was autogenerated by uv via the following command: + # uv export --cache-dir [CACHE_DIR] --no-emit-package anyio -e . -e child idna==3.6 \ @@ -765,7 +787,8 @@ fn no_emit() -> Result<()> { success: true exit_code: 0 ----- stdout ----- - # This file was autogenerated via `uv export`. + # This file was autogenerated by uv via the following command: + # uv export --cache-dir [CACHE_DIR] --no-emit-project -e child anyio==3.7.0 \ --hash=sha256:275d9973793619a5374e1c89a4f4ad3f4b0a5510a2b5b939444bee8f4c4d37ce \ @@ -789,7 +812,8 @@ fn no_emit() -> Result<()> { success: true exit_code: 0 ----- stdout ----- - # This file was autogenerated via `uv export`. + # This file was autogenerated by uv via the following command: + # uv export --cache-dir [CACHE_DIR] --no-emit-project --package child iniconfig==2.0.0 \ --hash=sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3 \ --hash=sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374 @@ -803,7 +827,8 @@ fn no_emit() -> Result<()> { success: true exit_code: 0 ----- stdout ----- - # This file was autogenerated via `uv export`. + # This file was autogenerated by uv via the following command: + # uv export --cache-dir [CACHE_DIR] --no-emit-workspace anyio==3.7.0 \ --hash=sha256:275d9973793619a5374e1c89a4f4ad3f4b0a5510a2b5b939444bee8f4c4d37ce \ --hash=sha256:eddca883c4175f14df8aedce21054bfca3adb70ffe76a9f607aef9d7fa2ea7f0 @@ -842,7 +867,8 @@ fn no_emit() -> Result<()> { success: true exit_code: 0 ----- stdout ----- - # This file was autogenerated via `uv export`. + # This file was autogenerated by uv via the following command: + # uv export --cache-dir [CACHE_DIR] --no-emit-workspace anyio==3.7.0 \ --hash=sha256:275d9973793619a5374e1c89a4f4ad3f4b0a5510a2b5b939444bee8f4c4d37ce \ --hash=sha256:eddca883c4175f14df8aedce21054bfca3adb70ffe76a9f607aef9d7fa2ea7f0