diff --git a/crates/uv-resolver/src/lock/export/pylock_toml.rs b/crates/uv-resolver/src/lock/export/pylock_toml.rs index a27440c2c..70d87c9d1 100644 --- a/crates/uv-resolver/src/lock/export/pylock_toml.rs +++ b/crates/uv-resolver/src/lock/export/pylock_toml.rs @@ -37,7 +37,27 @@ use crate::{Installable, LockError, RequiresPython}; #[derive(Debug, thiserror::Error)] 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), #[error("`packages.wheel` entry for `{0}` must have a `path` or `url`")] WheelMissingPathUrl(PackageName), @@ -558,6 +578,47 @@ impl<'lock> PylockToml { let root = graph.add_node(Node::Root); 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. let install = package.marker.evaluate(markers, &[]); diff --git a/crates/uv/tests/it/pip_install.rs b/crates/uv/tests/it/pip_install.rs index ae2c9b96b..2682385a7 100644 --- a/crates/uv/tests/it/pip_install.rs +++ b/crates/uv/tests/it/pip_install.rs @@ -11071,3 +11071,40 @@ fn pep_751_mix() -> Result<()> { 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(()) +}