mirror of
				https://github.com/astral-sh/uv.git
				synced 2025-11-03 21:23:54 +00:00 
			
		
		
		
	Avoid installing duplicate dependencies across conflicting groups (#11653)
## Summary We need to compute the set of activated groups prior to evaluating the conflict markers on the groups' dependencies. Closes https://github.com/astral-sh/uv/issues/11648.
This commit is contained in:
		
							parent
							
								
									3365eb4a1c
								
							
						
					
					
						commit
						b588a8ea2f
					
				
					 2 changed files with 137 additions and 5 deletions
				
			
		| 
						 | 
				
			
			@ -53,6 +53,43 @@ pub trait Installable<'lock> {
 | 
			
		|||
 | 
			
		||||
        let root = petgraph.add_node(Node::Root);
 | 
			
		||||
 | 
			
		||||
        // Determine the set of activated extras and groups, from the root.
 | 
			
		||||
        //
 | 
			
		||||
        // TODO(charlie): This isn't quite right. Below, when we add the dependency groups to the
 | 
			
		||||
        // graph, we rely on the activated extras and dependency groups, to evaluate the conflict
 | 
			
		||||
        // marker. But at that point, we don't know the full set of activated extras; this is only
 | 
			
		||||
        // computed below. We somehow need to add the dependency groups _after_ we've computed all
 | 
			
		||||
        // enabled extras, but the groups themselves could depend on the set of enabled extras.
 | 
			
		||||
        if !self.lock().conflicts().is_empty() {
 | 
			
		||||
            for root_name in self.roots() {
 | 
			
		||||
                let dist = self
 | 
			
		||||
                    .lock()
 | 
			
		||||
                    .find_by_name(root_name)
 | 
			
		||||
                    .map_err(|_| LockErrorKind::MultipleRootPackages {
 | 
			
		||||
                        name: root_name.clone(),
 | 
			
		||||
                    })?
 | 
			
		||||
                    .ok_or_else(|| LockErrorKind::MissingRootPackage {
 | 
			
		||||
                        name: root_name.clone(),
 | 
			
		||||
                    })?;
 | 
			
		||||
 | 
			
		||||
                // Track the activated extras.
 | 
			
		||||
                if dev.prod() {
 | 
			
		||||
                    for extra in extras.extra_names(dist.optional_dependencies.keys()) {
 | 
			
		||||
                        activated_extras.push((&dist.id.name, extra));
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                // Track the activated groups.
 | 
			
		||||
                for group in dist
 | 
			
		||||
                    .dependency_groups
 | 
			
		||||
                    .keys()
 | 
			
		||||
                    .filter(|group| dev.contains(group))
 | 
			
		||||
                {
 | 
			
		||||
                    activated_groups.push((&dist.id.name, group));
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Add the workspace packages to the queue.
 | 
			
		||||
        for root_name in self.roots() {
 | 
			
		||||
            let dist = self
 | 
			
		||||
| 
						 | 
				
			
			@ -77,12 +114,10 @@ pub trait Installable<'lock> {
 | 
			
		|||
            petgraph.add_edge(root, index, Edge::Prod(MarkerTree::TRUE));
 | 
			
		||||
 | 
			
		||||
            if dev.prod() {
 | 
			
		||||
                // Push its dependencies on the queue and track
 | 
			
		||||
                // activated extras.
 | 
			
		||||
                // Push its dependencies onto the queue.
 | 
			
		||||
                queue.push_back((dist, None));
 | 
			
		||||
                for extra in extras.extra_names(dist.optional_dependencies.keys()) {
 | 
			
		||||
                    queue.push_back((dist, Some(extra)));
 | 
			
		||||
                    activated_extras.push((&dist.id.name, extra));
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -99,10 +134,13 @@ pub trait Installable<'lock> {
 | 
			
		|||
                })
 | 
			
		||||
                .flatten()
 | 
			
		||||
            {
 | 
			
		||||
                if !dep.complexified_marker.evaluate_no_extras(marker_env) {
 | 
			
		||||
                if !dep.complexified_marker.evaluate(
 | 
			
		||||
                    marker_env,
 | 
			
		||||
                    activated_extras.iter().copied(),
 | 
			
		||||
                    activated_groups.iter().copied(),
 | 
			
		||||
                ) {
 | 
			
		||||
                    continue;
 | 
			
		||||
                }
 | 
			
		||||
                activated_groups.push((&dist.id.name, group));
 | 
			
		||||
 | 
			
		||||
                let dep_dist = self.lock().find_by_id(&dep.package_id);
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -7714,3 +7714,97 @@ fn unsupported_git_scheme() -> Result<()> {
 | 
			
		|||
    "###);
 | 
			
		||||
    Ok(())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// See: <https://github.com/astral-sh/uv/issues/11648>
 | 
			
		||||
#[test]
 | 
			
		||||
fn multiple_group_conflicts() -> Result<()> {
 | 
			
		||||
    let context = TestContext::new("3.12");
 | 
			
		||||
 | 
			
		||||
    let pyproject_toml = context.temp_dir.child("pyproject.toml");
 | 
			
		||||
    pyproject_toml.write_str(
 | 
			
		||||
        r#"
 | 
			
		||||
        [project]
 | 
			
		||||
        name = "project"
 | 
			
		||||
        version = "0.1.0"
 | 
			
		||||
        requires-python = ">=3.12"
 | 
			
		||||
        dependencies = []
 | 
			
		||||
 | 
			
		||||
        [dependency-groups]
 | 
			
		||||
        foo = [
 | 
			
		||||
            "iniconfig>=2",
 | 
			
		||||
        ]
 | 
			
		||||
        bar = [
 | 
			
		||||
            "iniconfig<2",
 | 
			
		||||
        ]
 | 
			
		||||
        baz = [
 | 
			
		||||
            "iniconfig",
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
        [tool.uv]
 | 
			
		||||
        conflicts = [
 | 
			
		||||
          [
 | 
			
		||||
            { group = "foo" },
 | 
			
		||||
            { group = "bar" },
 | 
			
		||||
          ],
 | 
			
		||||
        ]
 | 
			
		||||
        "#,
 | 
			
		||||
    )?;
 | 
			
		||||
 | 
			
		||||
    uv_snapshot!(context.filters(), context.sync(), @r###"
 | 
			
		||||
    success: true
 | 
			
		||||
    exit_code: 0
 | 
			
		||||
    ----- stdout -----
 | 
			
		||||
 | 
			
		||||
    ----- stderr -----
 | 
			
		||||
    Resolved 3 packages in [TIME]
 | 
			
		||||
    Audited in [TIME]
 | 
			
		||||
    "###);
 | 
			
		||||
 | 
			
		||||
    uv_snapshot!(context.filters(), context.sync().arg("--group").arg("baz"), @r###"
 | 
			
		||||
    success: true
 | 
			
		||||
    exit_code: 0
 | 
			
		||||
    ----- stdout -----
 | 
			
		||||
 | 
			
		||||
    ----- stderr -----
 | 
			
		||||
    Resolved 3 packages in [TIME]
 | 
			
		||||
    Prepared 1 package in [TIME]
 | 
			
		||||
    Installed 1 package in [TIME]
 | 
			
		||||
     + iniconfig==2.0.0
 | 
			
		||||
    "###);
 | 
			
		||||
 | 
			
		||||
    uv_snapshot!(context.filters(), context.sync().arg("--group").arg("foo").arg("--group").arg("baz"), @r###"
 | 
			
		||||
    success: true
 | 
			
		||||
    exit_code: 0
 | 
			
		||||
    ----- stdout -----
 | 
			
		||||
 | 
			
		||||
    ----- stderr -----
 | 
			
		||||
    Resolved 3 packages in [TIME]
 | 
			
		||||
    Audited 1 package in [TIME]
 | 
			
		||||
    "###);
 | 
			
		||||
 | 
			
		||||
    uv_snapshot!(context.filters(), context.sync().arg("--group").arg("bar").arg("--group").arg("baz"), @r###"
 | 
			
		||||
    success: true
 | 
			
		||||
    exit_code: 0
 | 
			
		||||
    ----- stdout -----
 | 
			
		||||
 | 
			
		||||
    ----- stderr -----
 | 
			
		||||
    Resolved 3 packages in [TIME]
 | 
			
		||||
    Prepared 1 package in [TIME]
 | 
			
		||||
    Uninstalled 1 package in [TIME]
 | 
			
		||||
    Installed 1 package in [TIME]
 | 
			
		||||
     - iniconfig==2.0.0
 | 
			
		||||
     + iniconfig==1.1.1
 | 
			
		||||
    "###);
 | 
			
		||||
 | 
			
		||||
    uv_snapshot!(context.filters(), context.sync().arg("--group").arg("foo").arg("--group").arg("bar"), @r###"
 | 
			
		||||
    success: false
 | 
			
		||||
    exit_code: 2
 | 
			
		||||
    ----- stdout -----
 | 
			
		||||
 | 
			
		||||
    ----- stderr -----
 | 
			
		||||
    Resolved 3 packages in [TIME]
 | 
			
		||||
    error: Groups `foo` and `bar` are incompatible with the declared conflicts: {`project:foo`, `project:bar`}
 | 
			
		||||
    "###);
 | 
			
		||||
 | 
			
		||||
    Ok(())
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue