mirror of https://github.com/astral-sh/uv
Add `--output-file` to `uv export` (#7109)
## Summary Closes https://github.com/astral-sh/uv/issues/7058.
This commit is contained in:
parent
3d57b6e795
commit
d0f9016eda
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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(())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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.
|
||||
|
|
@ -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(())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
use anyhow::{Context, Result};
|
||||
use owo_colors::OwoColorize;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use uv_cache::Cache;
|
||||
use uv_client::Connectivity;
|
||||
|
|
@ -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;
|
||||
|
||||
|
|
@ -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,
|
||||
|
|
@ -34,6 +36,7 @@ pub(crate) async fn export(
|
|||
connectivity: Connectivity,
|
||||
concurrency: Concurrency,
|
||||
native_tls: bool,
|
||||
quiet: bool,
|
||||
cache: &Cache,
|
||||
printer: Printer,
|
||||
) -> Result<ExitStatus> {
|
||||
|
|
@ -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 => {
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1323,6 +1323,7 @@ async fn run_project(
|
|||
args.format,
|
||||
args.package,
|
||||
args.hashes,
|
||||
args.output_file,
|
||||
args.extras,
|
||||
args.dev,
|
||||
args.locked,
|
||||
|
|
@ -1334,6 +1335,7 @@ async fn run_project(
|
|||
globals.connectivity,
|
||||
globals.concurrency,
|
||||
globals.native_tls,
|
||||
globals.quiet,
|
||||
&cache,
|
||||
printer,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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>,
|
||||
|
|
@ -978,6 +979,7 @@ impl ExportSettings {
|
|||
no_dev,
|
||||
hashes,
|
||||
no_hashes,
|
||||
output_file,
|
||||
locked,
|
||||
frozen,
|
||||
resolver,
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in New Issue