From d52af0ccdd4e90be55f4999b7af1c62fdabf954f Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Thu, 12 Sep 2024 09:20:43 -0400 Subject: [PATCH] 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. --- crates/uv-resolver/src/lock/mod.rs | 32 ++++++++---- crates/uv/tests/sync.rs | 80 ++++++++++++++++++++++++++++++ 2 files changed, 102 insertions(+), 10 deletions(-) diff --git a/crates/uv-resolver/src/lock/mod.rs b/crates/uv-resolver/src/lock/mod.rs index 171c82cf9..f0e615c5e 100644 --- a/crates/uv-resolver/src/lock/mod.rs +++ b/crates/uv-resolver/src/lock/mod.rs @@ -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,16 +623,11 @@ 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 { - 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() - }), - )) - }; + let deps = if let Some(extra) = extra { + Either::Left(dist.optional_dependencies.get(extra).into_iter().flatten()) + } else { + Either::Right(dist.dependencies.iter()) + }; for dep in deps { if dep.complexified_marker.evaluate(marker_env, &[]) { let dep_dist = self.find_by_id(&dep.package_id); diff --git a/crates/uv/tests/sync.rs b/crates/uv/tests/sync.rs index 1646f424a..bff1a3def 100644 --- a/crates/uv/tests/sync.rs +++ b/crates/uv/tests/sync.rs @@ -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(()) +}