diff --git a/crates/uv-resolver/src/lock/export/pylock_toml.rs b/crates/uv-resolver/src/lock/export/pylock_toml.rs index f5e5f6dd4d611..cf6be595081d3 100644 --- a/crates/uv-resolver/src/lock/export/pylock_toml.rs +++ b/crates/uv-resolver/src/lock/export/pylock_toml.rs @@ -1021,7 +1021,10 @@ impl<'lock> PylockToml { }; doc.insert("attestation-identities", value(attestation_identities)); } - if !self.packages.is_empty() { + if self.packages.is_empty() { + // `packages` is a required key in PEP 751, even when empty. + doc.insert("packages", value(Array::new())); + } else { let mut packages = ArrayOfTables::new(); for dist in &self.packages { packages.push(dist.to_toml()?); diff --git a/crates/uv/tests/project/export.rs b/crates/uv/tests/project/export.rs index 00dedf4a2e6be..caac842723eef 100644 --- a/crates/uv/tests/project/export.rs +++ b/crates/uv/tests/project/export.rs @@ -4074,6 +4074,41 @@ fn pep_751_dependency() -> Result<()> { Ok(()) } +/// `packages` is a required key in PEP 751, even when there are no packages to export. +#[test] +fn pep_751_no_packages() -> Result<()> { + let context = uv_test::test_context!("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" + "#, + )?; + + context.lock().assert().success(); + + uv_snapshot!(context.filters(), context.export().arg("--format").arg("pylock.toml").arg("--no-emit-project"), @r#" + success: true + exit_code: 0 + ----- stdout ----- + # This file was autogenerated by uv via the following command: + # uv export --cache-dir [CACHE_DIR] --format pylock.toml --no-emit-project + lock-version = "1.0" + created-by = "uv" + requires-python = ">=3.12" + packages = [] + + ----- stderr ----- + Resolved 1 package in [TIME] + "#); + + Ok(()) +} + #[test] fn pep_751_export_no_header() -> Result<()> { let context = uv_test::test_context!("3.12");