diff --git a/crates/uv/src/commands/pip/compile.rs b/crates/uv/src/commands/pip/compile.rs index 06106ba5d..c6fafe409 100644 --- a/crates/uv/src/commands/pip/compile.rs +++ b/crates/uv/src/commands/pip/compile.rs @@ -1,5 +1,6 @@ use std::collections::{BTreeMap, BTreeSet}; use std::env; +use std::ffi::OsStr; use std::path::{Path, PathBuf}; use std::str::FromStr; use std::sync::Arc; @@ -33,7 +34,7 @@ use uv_python::{ }; use uv_requirements::upgrade::{read_pylock_toml_requirements, LockedRequirements}; use uv_requirements::{ - upgrade::read_requirements_txt, RequirementsSource, RequirementsSpecification, + is_pylock_toml, upgrade::read_requirements_txt, RequirementsSource, RequirementsSpecification, }; use uv_resolver::{ AnnotationStyle, DependencyMode, DisplayResolutionGraph, ExcludeNewer, FlatIndex, ForkStrategy, @@ -133,6 +134,20 @@ pub(crate) async fn pip_compile( } }); + // If the user is exporting to PEP 751, ensure the filename matches the specification. + if matches!(format, ExportFormat::PylockToml) { + if let Some(file_name) = output_file + .and_then(Path::file_name) + .and_then(OsStr::to_str) + { + if !is_pylock_toml(file_name) { + return Err(anyhow!( + "Expected the output filename to start with `pylock.` and end with `.toml` (e.g., `pylock.toml`, `pylock.dev.toml`); `{file_name}` won't be recognized as a `pylock.toml` file in subsequent commands", + )); + } + } + } + // Respect `UV_PYTHON` if python.is_none() && python_version.is_none() { if let Ok(request) = std::env::var("UV_PYTHON") { diff --git a/crates/uv/src/commands/project/export.rs b/crates/uv/src/commands/project/export.rs index 577f19d7a..3363354cc 100644 --- a/crates/uv/src/commands/project/export.rs +++ b/crates/uv/src/commands/project/export.rs @@ -2,7 +2,7 @@ use std::env; use std::ffi::OsStr; use std::path::{Path, PathBuf}; -use anyhow::{Context, Result}; +use anyhow::{anyhow, Context, Result}; use itertools::Itertools; use owo_colors::OwoColorize; @@ -282,6 +282,21 @@ pub(crate) async fn export( } }); + // If the user is exporting to PEP 751, ensure the filename matches the specification. + if matches!(format, ExportFormat::PylockToml) { + if let Some(file_name) = output_file + .as_deref() + .and_then(Path::file_name) + .and_then(OsStr::to_str) + { + if !is_pylock_toml(file_name) { + return Err(anyhow!( + "Expected the output filename to start with `pylock.` and end with `.toml` (e.g., `pylock.toml`, `pylock.dev.toml`); `{file_name}` won't be recognized as a `pylock.toml` file in subsequent commands", + )); + } + } + } + // Generate the export. match format { ExportFormat::RequirementsTxt => { diff --git a/crates/uv/tests/it/export.rs b/crates/uv/tests/it/export.rs index 3836711e2..359fd5af7 100644 --- a/crates/uv/tests/it/export.rs +++ b/crates/uv/tests/it/export.rs @@ -4066,3 +4066,37 @@ fn pep_751_infer_output_format() -> Result<()> { Ok(()) } + +#[test] +fn pep_751_filename() -> 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("--format").arg("pylock.toml").arg("-o").arg("test.toml"), @r" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + Resolved 4 packages in [TIME] + error: Expected the output filename to start with `pylock.` and end with `.toml` (e.g., `pylock.toml`, `pylock.dev.toml`); `test.toml` won't be recognized as a `pylock.toml` file in subsequent commands + "); + + Ok(()) +} diff --git a/crates/uv/tests/it/pip_compile.rs b/crates/uv/tests/it/pip_compile.rs index 2dd6cbc7c..f3655b58f 100644 --- a/crates/uv/tests/it/pip_compile.rs +++ b/crates/uv/tests/it/pip_compile.rs @@ -16262,6 +16262,32 @@ fn compile_invalid_output_file() -> Result<()> { Ok(()) } +#[test] +fn pep_751_filename() -> Result<()> { + let context = TestContext::new("3.12"); + + let requirements_txt = context.temp_dir.child("requirements.txt"); + requirements_txt.write_str("iniconfig")?; + + uv_snapshot!(context.filters(), context + .pip_compile() + .arg("requirements.txt") + .arg("--universal") + .arg("--format") + .arg("pylock.toml") + .arg("-o") + .arg("test.toml"), @r" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: Expected the output filename to start with `pylock.` and end with `.toml` (e.g., `pylock.toml`, `pylock.dev.toml`); `test.toml` won't be recognized as a `pylock.toml` file in subsequent commands + "); + + Ok(()) +} + #[test] fn pep_751_compile_registry_wheel() -> Result<()> { let context = TestContext::new("3.12");