Skip to content

Commit

Permalink
Add --output-file to uv export
Browse files Browse the repository at this point in the history
  • Loading branch information
charliermarsh committed Sep 6, 2024
1 parent 84f25e8 commit 8bdba6c
Show file tree
Hide file tree
Showing 8 changed files with 142 additions and 62 deletions.
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

0 comments on commit 8bdba6c

Please sign in to comment.