Avoid installing transitive dev dependencies (#7318)

## Summary

This is arguably breaking, arguably a bug... Today, if project A depends
on project B, and you install A with dev dependencies enabled, you also
get B's dev dependencies. I think this is incorrect. Just like you
shouldn't be importing B's dependencies from A, you shouldn't be using
B's dev dependencies when developing on A.

Closes #7310.
This commit is contained in:
Charlie Marsh 2024-09-12 09:20:43 -04:00 committed by GitHub
parent f22e5ef69a
commit d52af0ccdd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 102 additions and 10 deletions

View File

@ -575,6 +575,23 @@ impl Lock {
}
}
}
// Add any dev dependencies.
for group in dev {
for dep in root.dev_dependencies.get(group).into_iter().flatten() {
if dep.complexified_marker.evaluate(marker_env, &[]) {
let dep_dist = self.find_by_id(&dep.package_id);
if seen.insert((&dep.package_id, None)) {
queue.push_back((dep_dist, None));
}
for extra in &dep.extra {
if seen.insert((&dep.package_id, Some(extra))) {
queue.push_back((dep_dist, Some(extra)));
}
}
}
}
}
}
// Add any dependency groups that are exclusive to the workspace root (e.g., dev
@ -606,15 +623,10 @@ impl Lock {
let mut map = BTreeMap::default();
let mut hashes = BTreeMap::default();
while let Some((dist, extra)) = queue.pop_front() {
let deps =
if let Some(extra) = extra {
let deps = if let Some(extra) = extra {
Either::Left(dist.optional_dependencies.get(extra).into_iter().flatten())
} else {
Either::Right(dist.dependencies.iter().chain(
dev.iter().flat_map(|group| {
dist.dev_dependencies.get(group).into_iter().flatten()
}),
))
Either::Right(dist.dependencies.iter())
};
for dep in deps {
if dep.complexified_marker.evaluate(marker_env, &[]) {

View File

@ -2248,3 +2248,83 @@ fn sync_wheel_path_source_error() -> Result<()> {
Ok(())
}
/// Avoid installing dev dependencies of transitive dependencies.
#[test]
fn transitive_dev() -> Result<()> {
let context = TestContext::new("3.12");
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(
r#"
[project]
name = "root"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["child"]
[build-system]
requires = ["setuptools>=42"]
build-backend = "setuptools.build_meta"
[tool.uv]
dev-dependencies = ["anyio>3"]
[tool.uv.sources]
child = { workspace = true }
[tool.uv.workspace]
members = ["child"]
"#,
)?;
let src = context.temp_dir.child("src").child("albatross");
src.create_dir_all()?;
let init = src.child("__init__.py");
init.touch()?;
let child = context.temp_dir.child("child");
fs_err::create_dir_all(&child)?;
let pyproject_toml = child.child("pyproject.toml");
pyproject_toml.write_str(
r#"
[project]
name = "child"
version = "0.1.0"
requires-python = ">=3.12"
[build-system]
requires = ["setuptools>=42"]
build-backend = "setuptools.build_meta"
[tool.uv]
dev-dependencies = ["iniconfig>1"]
"#,
)?;
let src = child.child("src").child("albatross");
src.create_dir_all()?;
let init = src.child("__init__.py");
init.touch()?;
uv_snapshot!(context.filters(), context.sync().arg("--dev"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 6 packages in [TIME]
Prepared 5 packages in [TIME]
Installed 5 packages in [TIME]
+ anyio==4.3.0
+ child==0.1.0 (from file://[TEMP_DIR]/child)
+ idna==3.6
+ root==0.1.0 (from file://[TEMP_DIR]/)
+ sniffio==1.3.1
"###);
Ok(())
}