Skip to content

Commit

Permalink
Include uv export command in output (#7374)
Browse files Browse the repository at this point in the history
## Summary

Updates the output of `uv export` to include the command that produced
it, similar to how `uv pip compile` does. This addresses #7159 - I had
this same itch today, figured it was a good time to dive in!

## Test Plan

All the export unit tests were updated to test the new output format.
  • Loading branch information
dcwatson authored Sep 13, 2024
1 parent 6907164 commit c188836
Show file tree
Hide file tree
Showing 3 changed files with 126 additions and 38 deletions.
57 changes: 56 additions & 1 deletion crates/uv/src/commands/project/export.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
use std::env;

use anyhow::{Context, Result};
use itertools::Itertools;
use owo_colors::OwoColorize;
use std::path::PathBuf;

Expand Down Expand Up @@ -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}")?;
}
}
Expand All @@ -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}")
}
35 changes: 21 additions & 14 deletions crates/uv/tests/common/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<T: AsRef<str>>(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`.
Expand Down Expand Up @@ -1076,22 +1088,17 @@ pub fn run_and_format_with_status<T: AsRef<str>>(
.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,
Expand Down
72 changes: 49 additions & 23 deletions crates/uv/tests/export.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 \
Expand Down Expand Up @@ -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 \
Expand Down Expand Up @@ -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 \
Expand All @@ -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 \
Expand All @@ -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 \
Expand Down Expand Up @@ -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 \
Expand Down Expand Up @@ -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 \
Expand Down Expand Up @@ -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 \
Expand Down Expand Up @@ -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 \
Expand Down Expand Up @@ -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 \
Expand Down Expand Up @@ -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 \
Expand All @@ -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 \
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 \
Expand All @@ -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 \
Expand Down Expand Up @@ -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 \
Expand All @@ -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 \
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit c188836

Please sign in to comment.