Validate that PEP 751 entries don't include multiple sources (#12993)

## Summary

The spec defines these as mutually exclusive, so we now error when
trying to install such a package.
This commit is contained in:
Charlie Marsh 2025-04-21 18:22:03 -04:00 committed by GitHub
parent cda72b297f
commit ffcd5eb14f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 99 additions and 1 deletions

View file

@ -37,7 +37,27 @@ use crate::{Installable, LockError, RequiresPython};
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
pub enum PylockTomlError { pub enum PylockTomlError {
#[error("`packages` entry for `{0}` must contain one of: `wheels`, `directory`, `archive`, `sdist`, or `vcs`")] #[error("Package `{0}` includes both a registry (`packages.wheels`) and a directory source (`packages.directory`)")]
WheelWithDirectory(PackageName),
#[error("Package `{0}` includes both a registry (`packages.wheels`) and a VCS source (`packages.vcs`)")]
WheelWithVcs(PackageName),
#[error("Package `{0}` includes both a registry (`packages.wheels`) and an archive source (`packages.archive`)")]
WheelWithArchive(PackageName),
#[error("Package `{0}` includes both a registry (`packages.sdist`) and a directory source (`packages.directory`)")]
SdistWithDirectory(PackageName),
#[error("Package `{0}` includes both a registry (`packages.sdist`) and a VCS source (`packages.vcs`)")]
SdistWithVcs(PackageName),
#[error("Package `{0}` includes both a registry (`packages.sdist`) and an archive source (`packages.archive`)")]
SdistWithArchive(PackageName),
#[error("Package `{0}` includes both a directory (`packages.directory`) and a VCS source (`packages.vcs`)")]
DirectoryWithVcs(PackageName),
#[error("Package `{0}` includes both a directory (`packages.directory`) and an archive source (`packages.archive`)")]
DirectoryWithArchive(PackageName),
#[error("Package `{0}` includes both a VCS (`packages.vcs`) and an archive source (`packages.archive`)")]
VcsWithArchive(PackageName),
#[error(
"Package `{0}` must include one of: `wheels`, `directory`, `archive`, `sdist`, or `vcs`"
)]
MissingSource(PackageName), MissingSource(PackageName),
#[error("`packages.wheel` entry for `{0}` must have a `path` or `url`")] #[error("`packages.wheel` entry for `{0}` must have a `path` or `url`")]
WheelMissingPathUrl(PackageName), WheelMissingPathUrl(PackageName),
@ -558,6 +578,47 @@ impl<'lock> PylockToml {
let root = graph.add_node(Node::Root); let root = graph.add_node(Node::Root);
for package in self.packages { for package in self.packages {
match (
package.wheels.is_some(),
package.sdist.is_some(),
package.directory.is_some(),
package.vcs.is_some(),
package.archive.is_some(),
) {
// `packages.wheels` is mutually exclusive with `packages.directory`, `packages.vcs`, and `packages.archive`.
(true, _, true, _, _) => {
return Err(PylockTomlError::WheelWithDirectory(package.name.clone()));
}
(true, _, _, true, _) => {
return Err(PylockTomlError::WheelWithVcs(package.name.clone()));
}
(true, _, _, _, true) => {
return Err(PylockTomlError::WheelWithArchive(package.name.clone()));
}
// `packages.sdist` is mutually exclusive with `packages.directory`, `packages.vcs`, and `packages.archive`.
(_, true, true, _, _) => {
return Err(PylockTomlError::SdistWithDirectory(package.name.clone()));
}
(_, true, _, true, _) => {
return Err(PylockTomlError::SdistWithVcs(package.name.clone()));
}
(_, true, _, _, true) => {
return Err(PylockTomlError::SdistWithArchive(package.name.clone()));
}
// `packages.directory` is mutually exclusive with `packages.vcs`, and `packages.archive`.
(_, _, true, true, _) => {
return Err(PylockTomlError::DirectoryWithVcs(package.name.clone()));
}
(_, _, true, _, true) => {
return Err(PylockTomlError::DirectoryWithArchive(package.name.clone()));
}
// `packages.vcs` is mutually exclusive with `packages.archive`.
(_, _, _, true, true) => {
return Err(PylockTomlError::VcsWithArchive(package.name.clone()));
}
_ => {}
}
// Omit packages that aren't relevant to the current environment. // Omit packages that aren't relevant to the current environment.
let install = package.marker.evaluate(markers, &[]); let install = package.marker.evaluate(markers, &[]);

View file

@ -11071,3 +11071,40 @@ fn pep_751_mix() -> Result<()> {
Ok(()) Ok(())
} }
#[test]
fn pep_751_multiple_sources() -> Result<()> {
let context = TestContext::new("3.12");
let pylock_toml = context.temp_dir.child("pylock.toml");
pylock_toml.write_str(r#"
# This file was autogenerated by uv via the following command:
# uv export --cache-dir [CACHE_DIR] -o pylock.toml
lock-version = "1.0"
created-by = "uv"
requires-python = ">=3.12"
[[packages]]
name = "typing-extensions"
version = "4.10.0"
index = "https://pypi.org/simple"
sdist = { url = "https://files.pythonhosted.org/packages/16/3a/0d26ce356c7465a19c9ea8814b960f8a36c3b0d07c323176620b7b483e44/typing_extensions-4.10.0.tar.gz", size = 77558, hashes = { sha256 = "b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb" } }
wheels = [{ url = "https://files.pythonhosted.org/packages/f9/de/dc04a3ea60b22624b51c703a84bbe0184abcd1d0b9bc8074b5d6b7ab90bb/typing_extensions-4.10.0-py3-none-any.whl", size = 33926, hashes = { sha256 = "69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475" } }]
archive = { path = "iniconfig-2.0.0-py3-none-any.whl", hashes = { sha256 = "c5185871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374" } }
"#)?;
uv_snapshot!(context.filters(), context.pip_install()
.arg("--preview")
.arg("-r")
.arg("pylock.toml"), @r"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: Package `typing-extensions` includes both a registry (`packages.wheels`) and an archive source (`packages.archive`)
"
);
Ok(())
}