Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add --output-file to uv export #7109

Merged
merged 1 commit into from
Sep 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions crates/uv-cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2966,6 +2966,10 @@ pub struct ExportArgs {
#[arg(long, overrides_with("hashes"))]
pub no_hashes: bool,

/// Write the compiled requirements to the given `requirements.txt` file.
#[arg(long, short)]
pub output_file: Option<PathBuf>,

/// Assert that the `uv.lock` will remain unchanged.
///
/// Requires that the lockfile is up-to-date. If the lockfile is missing or
Expand Down
60 changes: 57 additions & 3 deletions crates/uv/src/commands/mod.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
use std::time::Duration;
use std::{fmt::Display, fmt::Write, process::ExitCode};

use anstream::AutoStream;
use anyhow::Context;
use owo_colors::OwoColorize;
use std::borrow::Cow;
use std::io::stdout;
use std::path::Path;
use std::time::Duration;
use std::{fmt::Display, fmt::Write, process::ExitCode};

pub(crate) use build::build;
pub(crate) use cache_clean::cache_clean;
Expand Down Expand Up @@ -198,3 +201,54 @@ pub(crate) struct SharedState {
/// The downloaded distributions.
pub(crate) in_flight: InFlight,
}

/// A multicasting writer that writes to both the standard output and an output file, if present.
#[allow(clippy::disallowed_types)]
struct OutputWriter<'a> {
stdout: Option<AutoStream<std::io::Stdout>>,
output_file: Option<&'a Path>,
buffer: Vec<u8>,
}

#[allow(clippy::disallowed_types)]
impl<'a> OutputWriter<'a> {
/// Create a new output writer.
fn new(include_stdout: bool, output_file: Option<&'a Path>) -> Self {
let stdout = include_stdout.then(|| AutoStream::<std::io::Stdout>::auto(stdout()));
Self {
stdout,
output_file,
buffer: Vec::new(),
}
}

/// Write the given arguments to both standard output and the output buffer, if present.
fn write_fmt(&mut self, args: std::fmt::Arguments<'_>) -> std::io::Result<()> {
use std::io::Write;

// Write to the buffer.
if self.output_file.is_some() {
self.buffer.write_fmt(args)?;
}

// Write to standard output.
if let Some(stdout) = &mut self.stdout {
write!(stdout, "{args}")?;
}

Ok(())
}

/// Commit the buffer to the output file.
async fn commit(self) -> std::io::Result<()> {
if let Some(output_file) = self.output_file {
// If the output file is an existing symlink, write to the destination instead.
let output_file = fs_err::read_link(output_file)
.map(Cow::Owned)
.unwrap_or(Cow::Borrowed(output_file));
let stream = anstream::adapter::strip_bytes(&self.buffer).into_vec();
uv_fs::write_atomic(output_file, &stream).await?;
}
Ok(())
}
}
57 changes: 2 additions & 55 deletions crates/uv/src/commands/pip/compile.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
use std::borrow::Cow;
use std::env;
use std::io::stdout;
use std::path::Path;

use anstream::{eprint, AutoStream};
use anstream::eprint;
use anyhow::{anyhow, Result};
use itertools::Itertools;
use owo_colors::OwoColorize;
Expand Down Expand Up @@ -43,7 +41,7 @@ use uv_warnings::warn_user;

use crate::commands::pip::loggers::DefaultResolveLogger;
use crate::commands::pip::{operations, resolution_environment};
use crate::commands::ExitStatus;
use crate::commands::{ExitStatus, OutputWriter};
use crate::printer::Printer;

/// Resolve a set of requirements into a set of pinned versions.
Expand Down Expand Up @@ -621,54 +619,3 @@ fn cmd(
.join(" ");
format!("uv {args}")
}

/// A multicasting writer that writes to both the standard output and an output file, if present.
#[allow(clippy::disallowed_types)]
struct OutputWriter<'a> {
stdout: Option<AutoStream<std::io::Stdout>>,
output_file: Option<&'a Path>,
buffer: Vec<u8>,
}

#[allow(clippy::disallowed_types)]
impl<'a> OutputWriter<'a> {
/// Create a new output writer.
fn new(include_stdout: bool, output_file: Option<&'a Path>) -> Self {
let stdout = include_stdout.then(|| AutoStream::<std::io::Stdout>::auto(stdout()));
Self {
stdout,
output_file,
buffer: Vec::new(),
}
}

/// Write the given arguments to both standard output and the output buffer, if present.
fn write_fmt(&mut self, args: std::fmt::Arguments<'_>) -> std::io::Result<()> {
use std::io::Write;

// Write to the buffer.
if self.output_file.is_some() {
self.buffer.write_fmt(args)?;
}

// Write to standard output.
if let Some(stdout) = &mut self.stdout {
write!(stdout, "{args}")?;
}

Ok(())
}

/// Commit the buffer to the output file.
async fn commit(self) -> std::io::Result<()> {
if let Some(output_file) = self.output_file {
// If the output file is an existing symlink, write to the destination instead.
let output_file = fs_err::read_link(output_file)
.map(Cow::Owned)
.unwrap_or(Cow::Borrowed(output_file));
let stream = anstream::adapter::strip_bytes(&self.buffer).into_vec();
uv_fs::write_atomic(output_file, &stream).await?;
}
Ok(())
}
}
17 changes: 13 additions & 4 deletions crates/uv/src/commands/project/export.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use anyhow::{Context, Result};
use owo_colors::OwoColorize;
use std::path::PathBuf;

use uv_cache::Cache;
use uv_client::Connectivity;
Expand All @@ -13,7 +14,7 @@ use uv_workspace::{DiscoveryOptions, MemberDiscovery, VirtualProject, Workspace}
use crate::commands::pip::loggers::DefaultResolveLogger;
use crate::commands::project::lock::do_safe_lock;
use crate::commands::project::{FoundInterpreter, ProjectError};
use crate::commands::{pip, ExitStatus};
use crate::commands::{pip, ExitStatus, OutputWriter};
use crate::printer::Printer;
use crate::settings::ResolverSettings;

Expand All @@ -23,6 +24,7 @@ pub(crate) async fn export(
format: ExportFormat,
package: Option<PackageName>,
hashes: bool,
output_file: Option<PathBuf>,
extras: ExtrasSpecification,
dev: bool,
locked: bool,
Expand All @@ -34,6 +36,7 @@ pub(crate) async fn export(
connectivity: Connectivity,
concurrency: Concurrency,
native_tls: bool,
quiet: bool,
cache: &Cache,
printer: Printer,
) -> Result<ExitStatus> {
Expand Down Expand Up @@ -110,6 +113,9 @@ pub(crate) async fn export(
vec![]
};

// Write the resolved dependencies to the output channel.
let mut writer = OutputWriter::new(!quiet || output_file.is_none(), output_file.as_deref());

// Generate the export.
match format {
ExportFormat::RequirementsTxt => {
Expand All @@ -120,13 +126,16 @@ pub(crate) async fn export(
&dev,
hashes,
)?;
anstream::println!(
writeln!(
writer,
"{}",
"# This file was autogenerated via `uv export`.".green()
);
anstream::print!("{export}");
)?;
write!(writer, "{export}")?;
}
}

writer.commit().await?;

Ok(ExitStatus::Success)
}
2 changes: 2 additions & 0 deletions crates/uv/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1323,6 +1323,7 @@ async fn run_project(
args.format,
args.package,
args.hashes,
args.output_file,
args.extras,
args.dev,
args.locked,
Expand All @@ -1334,6 +1335,7 @@ async fn run_project(
globals.connectivity,
globals.concurrency,
globals.native_tls,
globals.quiet,
&cache,
printer,
)
Expand Down
3 changes: 3 additions & 0 deletions crates/uv/src/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -957,6 +957,7 @@ pub(crate) struct ExportSettings {
pub(crate) extras: ExtrasSpecification,
pub(crate) dev: bool,
pub(crate) hashes: bool,
pub(crate) output_file: Option<PathBuf>,
pub(crate) locked: bool,
pub(crate) frozen: bool,
pub(crate) python: Option<String>,
Expand All @@ -978,6 +979,7 @@ impl ExportSettings {
no_dev,
hashes,
no_hashes,
output_file,
locked,
frozen,
resolver,
Expand All @@ -995,6 +997,7 @@ impl ExportSettings {
),
dev: flag(dev, no_dev).unwrap_or(true),
hashes: flag(hashes, no_hashes).unwrap_or(true),
output_file,
locked,
frozen,
python,
Expand Down
59 changes: 59 additions & 0 deletions crates/uv/tests/export.rs
Original file line number Diff line number Diff line change
Expand Up @@ -636,3 +636,62 @@ fn no_hashes() -> Result<()> {

Ok(())
}

#[test]
fn output_file() -> Result<()> {
let context = TestContext::new("3.12");

let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(
r#"
[project]
name = "project"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["anyio==3.7.0"]

[build-system]
requires = ["setuptools>=42"]
build-backend = "setuptools.build_meta"
"#,
)?;

context.lock().assert().success();

uv_snapshot!(context.filters(), context.export().arg("--output-file").arg("requirements.txt"), @r###"
success: true
exit_code: 0
----- stdout -----
# This file was autogenerated via `uv export`.
-e .
anyio==3.7.0 \
--hash=sha256:275d9973793619a5374e1c89a4f4ad3f4b0a5510a2b5b939444bee8f4c4d37ce \
--hash=sha256:eddca883c4175f14df8aedce21054bfca3adb70ffe76a9f607aef9d7fa2ea7f0
idna==3.6 \
--hash=sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca \
--hash=sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f
sniffio==1.3.1 \
--hash=sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc \
--hash=sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2

----- stderr -----
Resolved 4 packages in [TIME]
"###);

let contents = fs_err::read_to_string(context.temp_dir.child("requirements.txt"))?;
insta::assert_snapshot!(contents, @r###"
# This file was autogenerated via `uv export`.
-e .
anyio==3.7.0 \
--hash=sha256:275d9973793619a5374e1c89a4f4ad3f4b0a5510a2b5b939444bee8f4c4d37ce \
--hash=sha256:eddca883c4175f14df8aedce21054bfca3adb70ffe76a9f607aef9d7fa2ea7f0
idna==3.6 \
--hash=sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca \
--hash=sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f
sniffio==1.3.1 \
--hash=sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc \
--hash=sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2
"###);

Ok(())
}
2 changes: 2 additions & 0 deletions docs/reference/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -1873,6 +1873,8 @@ uv export [OPTIONS]

<p>When disabled, uv will only use locally cached data and locally available files.</p>

</dd><dt><code>--output-file</code>, <code>-o</code> <i>output-file</i></dt><dd><p>Write the compiled requirements to the given <code>requirements.txt</code> file</p>

</dd><dt><code>--package</code> <i>package</i></dt><dd><p>Export the dependencies for a specific package in the workspace.</p>

<p>If the workspace member does not exist, uv will exit with an error.</p>
Expand Down
Loading