Skip to content

Commit 3e561e7

Browse files
committed
Validate that PEP 751 entries don't include multiple sources
1 parent ffdac60 commit 3e561e7

File tree

2 files changed

+99
-1
lines changed

2 files changed

+99
-1
lines changed

crates/uv-resolver/src/lock/export/pylock_toml.rs

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,27 @@ use crate::{Installable, LockError, RequiresPython};
3434

3535
#[derive(Debug, thiserror::Error)]
3636
pub enum PylockTomlError {
37-
#[error("`packages` entry for `{0}` must contain one of: `wheels`, `directory`, `archive`, `sdist`, or `vcs`")]
37+
#[error("Package `{0}` includes both a registry (`packages.wheels`) and a directory source (`packages.directory`)")]
38+
WheelWithDirectory(PackageName),
39+
#[error("Package `{0}` includes both a registry (`packages.wheels`) and a VCS source (`packages.vcs`)")]
40+
WheelWithVcs(PackageName),
41+
#[error("Package `{0}` includes both a registry (`packages.wheels`) and an archive source (`packages.archive`)")]
42+
WheelWithArchive(PackageName),
43+
#[error("Package `{0}` includes both a registry (`packages.sdist`) and a directory source (`packages.directory`)")]
44+
SdistWithDirectory(PackageName),
45+
#[error("Package `{0}` includes both a registry (`packages.sdist`) and a VCS source (`packages.vcs`)")]
46+
SdistWithVcs(PackageName),
47+
#[error("Package `{0}` includes both a registry (`packages.sdist`) and an archive source (`packages.archive`)")]
48+
SdistWithArchive(PackageName),
49+
#[error("Package `{0}` includes both a directory (`packages.directory`) and a VCS source (`packages.vcs`)")]
50+
DirectoryWithVcs(PackageName),
51+
#[error("Package `{0}` includes both a directory (`packages.directory`) and an archive source (`packages.archive`)")]
52+
DirectoryWithArchive(PackageName),
53+
#[error("Package `{0}` includes both a VCS (`packages.vcs`) and an archive source (`packages.archive`)")]
54+
VcsWithArchive(PackageName),
55+
#[error(
56+
"Package `{0}` must include one of: `wheels`, `directory`, `archive`, `sdist`, or `vcs`"
57+
)]
3858
MissingSource(PackageName),
3959
#[error("`packages.wheel` entry for `{0}` must have a `path` or `url`")]
4060
WheelMissingPathUrl(PackageName),
@@ -538,6 +558,47 @@ impl<'lock> PylockToml {
538558
let root = graph.add_node(Node::Root);
539559

540560
for package in self.packages {
561+
match (
562+
package.wheels.is_some(),
563+
package.sdist.is_some(),
564+
package.directory.is_some(),
565+
package.vcs.is_some(),
566+
package.archive.is_some(),
567+
) {
568+
// `packages.wheels` is mutually exclusive with `packages.directory`, `packages.vcs`, and `packages.archive`.
569+
(true, _, true, _, _) => {
570+
return Err(PylockTomlError::WheelWithDirectory(package.name.clone()));
571+
}
572+
(true, _, _, true, _) => {
573+
return Err(PylockTomlError::WheelWithVcs(package.name.clone()));
574+
}
575+
(true, _, _, _, true) => {
576+
return Err(PylockTomlError::WheelWithArchive(package.name.clone()));
577+
}
578+
// `packages.sdist` is mutually exclusive with `packages.directory`, `packages.vcs`, and `packages.archive`.
579+
(_, true, true, _, _) => {
580+
return Err(PylockTomlError::SdistWithDirectory(package.name.clone()));
581+
}
582+
(_, true, _, true, _) => {
583+
return Err(PylockTomlError::SdistWithVcs(package.name.clone()));
584+
}
585+
(_, true, _, _, true) => {
586+
return Err(PylockTomlError::SdistWithArchive(package.name.clone()));
587+
}
588+
// `packages.directory` is mutually exclusive with `packages.vcs`, and `packages.archive`.
589+
(_, _, true, true, _) => {
590+
return Err(PylockTomlError::DirectoryWithVcs(package.name.clone()));
591+
}
592+
(_, _, true, _, true) => {
593+
return Err(PylockTomlError::DirectoryWithArchive(package.name.clone()));
594+
}
595+
// `packages.vcs` is mutually exclusive with `packages.archive`.
596+
(_, _, _, true, true) => {
597+
return Err(PylockTomlError::VcsWithArchive(package.name.clone()));
598+
}
599+
_ => {}
600+
}
601+
541602
// Omit packages that aren't relevant to the current environment.
542603
let install = package.marker.evaluate(markers, &[]);
543604

crates/uv/tests/it/pip_install.rs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11071,3 +11071,40 @@ fn pep_751_mix() -> Result<()> {
1107111071

1107211072
Ok(())
1107311073
}
11074+
11075+
#[test]
11076+
fn pep_751_multiple_sources() -> Result<()> {
11077+
let context = TestContext::new("3.12");
11078+
11079+
let pylock_toml = context.temp_dir.child("pylock.toml");
11080+
pylock_toml.write_str(r#"
11081+
# This file was autogenerated by uv via the following command:
11082+
# uv export --cache-dir [CACHE_DIR] -o pylock.toml
11083+
lock-version = "1.0"
11084+
created-by = "uv"
11085+
requires-python = ">=3.12"
11086+
11087+
[[packages]]
11088+
name = "typing-extensions"
11089+
version = "4.10.0"
11090+
index = "https://pypi.org/simple"
11091+
sdist = { url = "https://files.pythonhosted.org/packages/16/3a/0d26ce356c7465a19c9ea8814b960f8a36c3b0d07c323176620b7b483e44/typing_extensions-4.10.0.tar.gz", size = 77558, hashes = { sha256 = "b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb" } }
11092+
wheels = [{ url = "https://files.pythonhosted.org/packages/f9/de/dc04a3ea60b22624b51c703a84bbe0184abcd1d0b9bc8074b5d6b7ab90bb/typing_extensions-4.10.0-py3-none-any.whl", size = 33926, hashes = { sha256 = "69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475" } }]
11093+
archive = { path = "iniconfig-2.0.0-py3-none-any.whl", hashes = { sha256 = "c5185871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374" } }
11094+
"#)?;
11095+
11096+
uv_snapshot!(context.filters(), context.pip_install()
11097+
.arg("--preview")
11098+
.arg("-r")
11099+
.arg("pylock.toml"), @r"
11100+
success: false
11101+
exit_code: 2
11102+
----- stdout -----
11103+
11104+
----- stderr -----
11105+
error: Package `typing-extensions` includes both a registry (`packages.wheels`) and an archive source (`packages.archive`)
11106+
"
11107+
);
11108+
11109+
Ok(())
11110+
}

0 commit comments

Comments
 (0)