Skip to content

Commit c3b34f5

Browse files
committed
Infer output type
1 parent 2d56988 commit c3b34f5

File tree

5 files changed

+112
-6
lines changed

5 files changed

+112
-6
lines changed

crates/uv-cli/src/lib.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3764,8 +3764,11 @@ pub struct ExportArgs {
37643764
/// The format to which `uv.lock` should be exported.
37653765
///
37663766
/// Supports both `requirements.txt` and `pylock.toml` (PEP 751) output formats.
3767-
#[arg(long, value_enum, default_value_t = ExportFormat::default())]
3768-
pub format: ExportFormat,
3767+
///
3768+
/// uv will infer the output format from the file extension of the output file, if
3769+
/// provided. Otherwise, defaults to `requirements.txt`.
3770+
#[arg(long, value_enum)]
3771+
pub format: Option<ExportFormat>,
37693772

37703773
/// Export the entire workspace.
37713774
///

crates/uv/src/commands/project/export.rs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ impl<'lock> From<&'lock ExportTarget> for LockTarget<'lock> {
5151
#[allow(clippy::fn_params_excessive_bools)]
5252
pub(crate) async fn export(
5353
project_dir: &Path,
54-
format: ExportFormat,
54+
format: Option<ExportFormat>,
5555
all_packages: bool,
5656
package: Option<PackageName>,
5757
prune: Vec<PackageName>,
@@ -252,6 +252,18 @@ pub(crate) async fn export(
252252
// Write the resolved dependencies to the output channel.
253253
let mut writer = OutputWriter::new(!quiet || output_file.is_none(), output_file.as_deref());
254254

255+
// Determine the output format.
256+
let format = format.unwrap_or_else(|| {
257+
let extension = output_file.as_deref().and_then(Path::extension);
258+
if extension.is_some_and(|ext| ext.eq_ignore_ascii_case("txt")) {
259+
ExportFormat::RequirementsTxt
260+
} else if extension.is_some_and(|ext| ext.eq_ignore_ascii_case("toml")) {
261+
ExportFormat::PylockToml
262+
} else {
263+
ExportFormat::RequirementsTxt
264+
}
265+
});
266+
255267
// Generate the export.
256268
match format {
257269
ExportFormat::RequirementsTxt => {

crates/uv/src/settings.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1527,7 +1527,7 @@ impl TreeSettings {
15271527
#[allow(clippy::struct_excessive_bools, dead_code)]
15281528
#[derive(Debug, Clone)]
15291529
pub(crate) struct ExportSettings {
1530-
pub(crate) format: ExportFormat,
1530+
pub(crate) format: Option<ExportFormat>,
15311531
pub(crate) all_packages: bool,
15321532
pub(crate) package: Option<PackageName>,
15331533
pub(crate) prune: Vec<PackageName>,

crates/uv/tests/it/export.rs

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3921,5 +3921,95 @@ fn pep_751_sdist_url_subdirectory() -> Result<()> {
39213921
Resolved 5 packages in [TIME]
39223922
"#);
39233923

3924+
Ok(())
3925+
}
3926+
3927+
#[test]
3928+
fn pep_751_infer_output_format() -> Result<()> {
3929+
let context = TestContext::new("3.12");
3930+
3931+
let pyproject_toml = context.temp_dir.child("pyproject.toml");
3932+
pyproject_toml.write_str(
3933+
r#"
3934+
[project]
3935+
name = "project"
3936+
version = "0.1.0"
3937+
requires-python = ">=3.12"
3938+
dependencies = ["anyio==3.7.0"]
3939+
3940+
[build-system]
3941+
requires = ["setuptools>=42"]
3942+
build-backend = "setuptools.build_meta"
3943+
"#,
3944+
)?;
3945+
3946+
context.lock().assert().success();
3947+
3948+
uv_snapshot!(context.filters(), context.export().arg("-o").arg("requirements.txt"), @r"
3949+
success: true
3950+
exit_code: 0
3951+
----- stdout -----
3952+
# This file was autogenerated by uv via the following command:
3953+
# uv export --cache-dir [CACHE_DIR] -o requirements.txt
3954+
-e .
3955+
anyio==3.7.0 \
3956+
--hash=sha256:275d9973793619a5374e1c89a4f4ad3f4b0a5510a2b5b939444bee8f4c4d37ce \
3957+
--hash=sha256:eddca883c4175f14df8aedce21054bfca3adb70ffe76a9f607aef9d7fa2ea7f0
3958+
# via project
3959+
idna==3.6 \
3960+
--hash=sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca \
3961+
--hash=sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f
3962+
# via anyio
3963+
sniffio==1.3.1 \
3964+
--hash=sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2 \
3965+
--hash=sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc
3966+
# via anyio
3967+
3968+
----- stderr -----
3969+
Resolved 4 packages in [TIME]
3970+
");
3971+
3972+
3973+
uv_snapshot!(context.filters(), context.export().arg("-o").arg("pylock.toml"), @r#"
3974+
success: true
3975+
exit_code: 0
3976+
----- stdout -----
3977+
# This file was autogenerated by uv via the following command:
3978+
# uv export --cache-dir [CACHE_DIR] -o pylock.toml
3979+
lock-version = "1.0"
3980+
created-by = "uv"
3981+
requires-python = ">=3.12"
3982+
3983+
[[package]]
3984+
name = "anyio"
3985+
version = "3.7.0"
3986+
index = "https://pypi.org/simple"
3987+
sdist = { url = "https://files.pythonhosted.org/packages/c6/b3/fefbf7e78ab3b805dec67d698dc18dd505af7a18a8dd08868c9b4fa736b5/anyio-3.7.0.tar.gz", size = 142737, hashes = { sha256 = "275d9973793619a5374e1c89a4f4ad3f4b0a5510a2b5b939444bee8f4c4d37ce" } }
3988+
wheels = [{ url = "https://files.pythonhosted.org/packages/68/fe/7ce1926952c8a403b35029e194555558514b365ad77d75125f521a2bec62/anyio-3.7.0-py3-none-any.whl", size = 80873, hashes = { sha256 = "eddca883c4175f14df8aedce21054bfca3adb70ffe76a9f607aef9d7fa2ea7f0" } }]
3989+
3990+
[[package]]
3991+
name = "idna"
3992+
version = "3.6"
3993+
index = "https://pypi.org/simple"
3994+
sdist = { url = "https://files.pythonhosted.org/packages/bf/3f/ea4b9117521a1e9c50344b909be7886dd00a519552724809bb1f486986c2/idna-3.6.tar.gz", size = 175426, hashes = { sha256 = "9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca" } }
3995+
wheels = [{ url = "https://files.pythonhosted.org/packages/c2/e7/a82b05cf63a603df6e68d59ae6a68bf5064484a0718ea5033660af4b54a9/idna-3.6-py3-none-any.whl", size = 61567, hashes = { sha256 = "c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f" } }]
3996+
3997+
[[package]]
3998+
name = "project"
3999+
version = "0.1.0"
4000+
directory = { path = ".", editable = true }
4001+
4002+
[[package]]
4003+
name = "sniffio"
4004+
version = "1.3.1"
4005+
index = "https://pypi.org/simple"
4006+
sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", size = 20372, hashes = { sha256 = "f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc" } }
4007+
wheels = [{ url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", size = 10235, hashes = { sha256 = "2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2" } }]
4008+
4009+
----- stderr -----
4010+
Resolved 4 packages in [TIME]
4011+
"#);
4012+
4013+
39244014
Ok(())
39254015
}

docs/reference/cli.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2225,7 +2225,7 @@ If operating in a workspace, the root will be exported by default; however, a sp
22252225
<h3 class="cli-reference">Usage</h3>
22262226

22272227
```
2228-
uv export [OPTIONS]
2228+
uv export [OPTIONS] --format <FORMAT>
22292229
```
22302230

22312231
<h3 class="cli-reference">Options</h3>
@@ -2332,7 +2332,8 @@ uv export [OPTIONS]
23322332

23332333
<p>Supports both <code>requirements.txt</code> and <code>pylock.toml</code> (PEP 751) output formats.</p>
23342334

2335-
<p>[default: requirements.txt]</p>
2335+
<p>uv will infer the output format from the file extension of the output file, if provided. Otherwise, defaults to <code>requirements.txt</code>.</p>
2336+
23362337
<p>Possible values:</p>
23372338

23382339
<ul>

0 commit comments

Comments
 (0)