Skip to content

Commit 42e3546

Browse files
committed
Infer output type
1 parent d8cea2f commit 42e3546

File tree

6 files changed

+192
-7
lines changed

6 files changed

+192
-7
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-requirements/src/sources.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,3 +228,9 @@ impl std::fmt::Display for RequirementsSource {
228228
}
229229
}
230230
}
231+
232+
/// Returns `true` if a file name matches the `pylock.toml` pattern defined in PEP 751.
233+
#[allow(clippy::case_sensitive_file_extension_comparisons)]
234+
pub fn is_pylock_toml(file_name: &str) -> bool {
235+
file_name.starts_with("pylock.") && file_name.ends_with(".toml")
236+
}

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

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
use std::env;
2+
use std::ffi::OsStr;
3+
use std::path::{Path, PathBuf};
24

35
use anyhow::{Context, Result};
46
use itertools::Itertools;
57
use owo_colors::OwoColorize;
6-
use std::path::{Path, PathBuf};
7-
use uv_settings::PythonInstallMirrors;
88

99
use uv_cache::Cache;
1010
use uv_configuration::{
@@ -13,8 +13,10 @@ use uv_configuration::{
1313
};
1414
use uv_normalize::{DefaultGroups, PackageName};
1515
use uv_python::{PythonDownloads, PythonPreference, PythonRequest};
16+
use uv_requirements::is_pylock_toml;
1617
use uv_resolver::{PylockToml, RequirementsTxtExport};
1718
use uv_scripts::{Pep723ItemRef, Pep723Script};
19+
use uv_settings::PythonInstallMirrors;
1820
use uv_workspace::{DiscoveryOptions, MemberDiscovery, VirtualProject, Workspace, WorkspaceCache};
1921

2022
use crate::commands::pip::loggers::DefaultResolveLogger;
@@ -51,7 +53,7 @@ impl<'lock> From<&'lock ExportTarget> for LockTarget<'lock> {
5153
#[allow(clippy::fn_params_excessive_bools)]
5254
pub(crate) async fn export(
5355
project_dir: &Path,
54-
format: ExportFormat,
56+
format: Option<ExportFormat>,
5557
all_packages: bool,
5658
package: Option<PackageName>,
5759
prune: Vec<PackageName>,
@@ -252,6 +254,26 @@ pub(crate) async fn export(
252254
// Write the resolved dependencies to the output channel.
253255
let mut writer = OutputWriter::new(!quiet || output_file.is_none(), output_file.as_deref());
254256

257+
// Determine the output format.
258+
let format = format.unwrap_or_else(|| {
259+
if output_file
260+
.as_deref()
261+
.and_then(Path::extension)
262+
.is_some_and(|ext| ext.eq_ignore_ascii_case("txt"))
263+
{
264+
ExportFormat::RequirementsTxt
265+
} else if output_file
266+
.as_deref()
267+
.and_then(Path::file_name)
268+
.and_then(OsStr::to_str)
269+
.is_some_and(is_pylock_toml)
270+
{
271+
ExportFormat::PylockToml
272+
} else {
273+
ExportFormat::RequirementsTxt
274+
}
275+
});
276+
255277
// Generate the export.
256278
match format {
257279
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: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3923,3 +3923,156 @@ fn pep_751_sdist_url_subdirectory() -> Result<()> {
39233923

39243924
Ok(())
39253925
}
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+
uv_snapshot!(context.filters(), context.export().arg("-o").arg("pylock.toml"), @r#"
3973+
success: true
3974+
exit_code: 0
3975+
----- stdout -----
3976+
# This file was autogenerated by uv via the following command:
3977+
# uv export --cache-dir [CACHE_DIR] -o pylock.toml
3978+
lock-version = "1.0"
3979+
created-by = "uv"
3980+
requires-python = ">=3.12"
3981+
3982+
[[packages]]
3983+
name = "anyio"
3984+
version = "3.7.0"
3985+
index = "https://pypi.org/simple"
3986+
sdist = { url = "https://files.pythonhosted.org/packages/c6/b3/fefbf7e78ab3b805dec67d698dc18dd505af7a18a8dd08868c9b4fa736b5/anyio-3.7.0.tar.gz", upload-time = 2023-05-27T11:12:46Z, size = 142737, hashes = { sha256 = "275d9973793619a5374e1c89a4f4ad3f4b0a5510a2b5b939444bee8f4c4d37ce" } }
3987+
wheels = [{ url = "https://files.pythonhosted.org/packages/68/fe/7ce1926952c8a403b35029e194555558514b365ad77d75125f521a2bec62/anyio-3.7.0-py3-none-any.whl", upload-time = 2023-05-27T11:12:44Z, size = 80873, hashes = { sha256 = "eddca883c4175f14df8aedce21054bfca3adb70ffe76a9f607aef9d7fa2ea7f0" } }]
3988+
3989+
[[packages]]
3990+
name = "idna"
3991+
version = "3.6"
3992+
index = "https://pypi.org/simple"
3993+
sdist = { url = "https://files.pythonhosted.org/packages/bf/3f/ea4b9117521a1e9c50344b909be7886dd00a519552724809bb1f486986c2/idna-3.6.tar.gz", upload-time = 2023-11-25T15:40:54Z, size = 175426, hashes = { sha256 = "9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca" } }
3994+
wheels = [{ url = "https://files.pythonhosted.org/packages/c2/e7/a82b05cf63a603df6e68d59ae6a68bf5064484a0718ea5033660af4b54a9/idna-3.6-py3-none-any.whl", upload-time = 2023-11-25T15:40:52Z, size = 61567, hashes = { sha256 = "c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f" } }]
3995+
3996+
[[packages]]
3997+
name = "project"
3998+
version = "0.1.0"
3999+
directory = { path = ".", editable = true }
4000+
4001+
[[packages]]
4002+
name = "sniffio"
4003+
version = "1.3.1"
4004+
index = "https://pypi.org/simple"
4005+
sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", upload-time = 2024-02-25T23:20:04Z, size = 20372, hashes = { sha256 = "f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc" } }
4006+
wheels = [{ url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", upload-time = 2024-02-25T23:20:01Z, size = 10235, hashes = { sha256 = "2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2" } }]
4007+
4008+
----- stderr -----
4009+
Resolved 4 packages in [TIME]
4010+
"#);
4011+
4012+
uv_snapshot!(context.filters(), context.export().arg("-o").arg("pylock.dev.toml"), @r#"
4013+
success: true
4014+
exit_code: 0
4015+
----- stdout -----
4016+
# This file was autogenerated by uv via the following command:
4017+
# uv export --cache-dir [CACHE_DIR] -o pylock.dev.toml
4018+
lock-version = "1.0"
4019+
created-by = "uv"
4020+
requires-python = ">=3.12"
4021+
4022+
[[packages]]
4023+
name = "anyio"
4024+
version = "3.7.0"
4025+
index = "https://pypi.org/simple"
4026+
sdist = { url = "https://files.pythonhosted.org/packages/c6/b3/fefbf7e78ab3b805dec67d698dc18dd505af7a18a8dd08868c9b4fa736b5/anyio-3.7.0.tar.gz", upload-time = 2023-05-27T11:12:46Z, size = 142737, hashes = { sha256 = "275d9973793619a5374e1c89a4f4ad3f4b0a5510a2b5b939444bee8f4c4d37ce" } }
4027+
wheels = [{ url = "https://files.pythonhosted.org/packages/68/fe/7ce1926952c8a403b35029e194555558514b365ad77d75125f521a2bec62/anyio-3.7.0-py3-none-any.whl", upload-time = 2023-05-27T11:12:44Z, size = 80873, hashes = { sha256 = "eddca883c4175f14df8aedce21054bfca3adb70ffe76a9f607aef9d7fa2ea7f0" } }]
4028+
4029+
[[packages]]
4030+
name = "idna"
4031+
version = "3.6"
4032+
index = "https://pypi.org/simple"
4033+
sdist = { url = "https://files.pythonhosted.org/packages/bf/3f/ea4b9117521a1e9c50344b909be7886dd00a519552724809bb1f486986c2/idna-3.6.tar.gz", upload-time = 2023-11-25T15:40:54Z, size = 175426, hashes = { sha256 = "9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca" } }
4034+
wheels = [{ url = "https://files.pythonhosted.org/packages/c2/e7/a82b05cf63a603df6e68d59ae6a68bf5064484a0718ea5033660af4b54a9/idna-3.6-py3-none-any.whl", upload-time = 2023-11-25T15:40:52Z, size = 61567, hashes = { sha256 = "c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f" } }]
4035+
4036+
[[packages]]
4037+
name = "project"
4038+
version = "0.1.0"
4039+
directory = { path = ".", editable = true }
4040+
4041+
[[packages]]
4042+
name = "sniffio"
4043+
version = "1.3.1"
4044+
index = "https://pypi.org/simple"
4045+
sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", upload-time = 2024-02-25T23:20:04Z, size = 20372, hashes = { sha256 = "f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc" } }
4046+
wheels = [{ url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", upload-time = 2024-02-25T23:20:01Z, size = 10235, hashes = { sha256 = "2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2" } }]
4047+
4048+
----- stderr -----
4049+
Resolved 4 packages in [TIME]
4050+
"#);
4051+
4052+
// TODO(charlie): Error on `pyproject.toml`. Right now, it's treated as `requirements.txt`.
4053+
uv_snapshot!(context.filters(), context.export().arg("-o").arg("pyproject.toml"), @r"
4054+
success: true
4055+
exit_code: 0
4056+
----- stdout -----
4057+
# This file was autogenerated by uv via the following command:
4058+
# uv export --cache-dir [CACHE_DIR] -o pyproject.toml
4059+
-e .
4060+
anyio==3.7.0 \
4061+
--hash=sha256:275d9973793619a5374e1c89a4f4ad3f4b0a5510a2b5b939444bee8f4c4d37ce \
4062+
--hash=sha256:eddca883c4175f14df8aedce21054bfca3adb70ffe76a9f607aef9d7fa2ea7f0
4063+
# via project
4064+
idna==3.6 \
4065+
--hash=sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca \
4066+
--hash=sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f
4067+
# via anyio
4068+
sniffio==1.3.1 \
4069+
--hash=sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2 \
4070+
--hash=sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc
4071+
# via anyio
4072+
4073+
----- stderr -----
4074+
Resolved 4 packages in [TIME]
4075+
");
4076+
4077+
Ok(())
4078+
}

docs/reference/cli.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -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)