From 7db8448ce9bcc9338c3d4c091eca27710cf1cebb Mon Sep 17 00:00:00 2001 From: Zsolt Dollenstein Date: Wed, 24 Jun 2026 19:28:17 +0100 Subject: [PATCH 1/2] Consolidate locked dependency selection --- crates/uv-resolver/src/lib.rs | 6 +- crates/uv-resolver/src/lock/mod.rs | 296 +++++++++++++------- crates/uv/src/commands/project/toolchain.rs | 25 +- 3 files changed, 209 insertions(+), 118 deletions(-) diff --git a/crates/uv-resolver/src/lib.rs b/crates/uv-resolver/src/lib.rs index 1cab6365ae0..b0e95f3769e 100644 --- a/crates/uv-resolver/src/lib.rs +++ b/crates/uv-resolver/src/lib.rs @@ -9,9 +9,9 @@ pub use exclusions::Exclusions; pub use flat_index::{FlatDistributions, FlatIndex}; pub use fork_strategy::ForkStrategy; pub use lock::{ - Installable, Lock, LockError, LockVersion, Metadata, Package, PackageMap, PylockToml, - PylockTomlError, PylockTomlErrorKind, RequirementsTxtExport, ResolverManifest, SatisfiesResult, - TreeDisplay, VERSION, cyclonedx_json, + DependencySelection, Installable, Lock, LockError, LockVersion, Metadata, Package, PackageMap, + PylockToml, PylockTomlError, PylockTomlErrorKind, RequirementsTxtExport, ResolverManifest, + SatisfiesResult, TreeDisplay, VERSION, cyclonedx_json, }; pub use manifest::Manifest; pub use options::{Flexibility, Options, OptionsBuilder}; diff --git a/crates/uv-resolver/src/lock/mod.rs b/crates/uv-resolver/src/lock/mod.rs index 6b32d5e0734..38ebe6fd67f 100644 --- a/crates/uv-resolver/src/lock/mod.rs +++ b/crates/uv-resolver/src/lock/mod.rs @@ -303,6 +303,34 @@ pub struct Lock { manifest: ResolverManifest, } +/// Package selections from a [`Lock`] for a named direct dependency. +/// +/// The dependency can come from a dependency group, the production packages, or both. +#[derive(Debug)] +pub struct DependencySelection<'lock> { + production: Option<&'lock Package>, + groups: BTreeMap<&'lock GroupName, &'lock Package>, +} + +impl<'lock> DependencySelection<'lock> { + /// Returns the package selected by the production dependency. + pub fn production(&self) -> Option<&'lock Package> { + self.production + } + + /// Returns the package selected by the given dependency group. + pub fn group(&self, group: &GroupName) -> Option<&'lock Package> { + self.groups.get(group).copied() + } + + /// Returns the dependency groups and their package selections. + pub fn groups(&self) -> impl Iterator + '_ { + self.groups + .iter() + .map(|(group, package)| (*group, *package)) + } +} + impl Lock { /// Initialize a [`Lock`] from a [`ResolverOutput`]. pub fn from_resolution( @@ -796,119 +824,79 @@ impl Lock { &self.manifest.dependency_groups } - /// Returns the package selected by a direct dependency in a dependency group. + /// Returns the environment-specific direct dependency selections for a lock target. /// - /// If `project_name` is provided, the dependency group attached to that package is used. - /// Otherwise, the dependency group attached directly to the lock manifest is used. - pub fn find_dependency_group_package( - &self, + /// If `project_name` is provided, dependencies attached to that package are used. Otherwise, + /// dependency groups attached directly to the lock manifest are used. + pub fn dependency_selection<'lock>( + &'lock self, project_name: Option<&PackageName>, - group: &GroupName, dependency_name: &PackageName, marker_environment: &MarkerEnvironment, - ) -> Result, String> { - match project_name { - Some(project_name) => self.find_project_dependency_group_package( - project_name, - group, - dependency_name, - marker_environment, - ), - None => self.find_virtual_root_dependency_group_package( - group, - dependency_name, - marker_environment, - ), - } - } - - /// Returns `true` if the package is selected by an enabled dependency group. - pub fn is_package_in_dependency_groups( - &self, - project_name: Option<&PackageName>, - package: &Package, - marker_environment: &MarkerEnvironment, - groups: &DependencyGroupsWithDefaults, - ) -> Result { - match project_name { - Some(project_name) => { - let Some(project) = self.find_by_name(project_name)? else { - return Ok(false); - }; - for group in project - .resolved_dependency_groups() - .keys() - .filter(|group| groups.contains(group)) - { - if self.find_project_dependency_group_package( - project_name, - group, - package.name(), - marker_environment, - )? == Some(package) - { - return Ok(true); - } - } - } - None => { - for group in self - .manifest - .dependency_groups - .keys() - .filter(|group| groups.contains(group)) - { - if self.find_virtual_root_dependency_group_package( - group, - package.name(), - marker_environment, - )? == Some(package) - { - return Ok(true); - } + ) -> Result, String> { + let (production, groups) = if let Some(project_name) = project_name { + let Some(project) = self.find_by_name(project_name)? else { + return Ok(DependencySelection { + production: None, + groups: BTreeMap::new(), + }); + }; + let production = + self.find_project_dependency_package(project, dependency_name, marker_environment)?; + let mut groups = BTreeMap::new(); + for group in project.resolved_dependency_groups().keys() { + if let Some(package) = self.find_project_dependency_group_package( + project, + group, + dependency_name, + marker_environment, + )? { + groups.insert(group, package); } } - } - Ok(false) - } - - /// Returns the package selected by a dependency group on a virtual workspace root. - fn find_virtual_root_dependency_group_package( - &self, - group: &GroupName, - dependency_name: &PackageName, - marker_environment: &MarkerEnvironment, - ) -> Result, String> { - let Some(requirements) = self.manifest.dependency_groups.get(group) else { - return Ok(None); + (production, groups) + } else { + // Lock-manifest dependency groups only record requirements, not resolved package IDs. + // Select the environment-specific package once, then associate it with each group that + // has an applicable direct requirement. + let mut applicable_groups = self + .manifest + .dependency_groups + .iter() + .filter_map(|(group, requirements)| { + requirements + .iter() + .any(|requirement| { + &requirement.name == dependency_name + && requirement.marker.evaluate(marker_environment, &[]) + }) + .then_some(group) + }) + .peekable(); + let groups = if applicable_groups.peek().is_some() + && let Some(package) = self.find_by_markers(dependency_name, marker_environment)? + { + applicable_groups.map(|group| (group, package)).collect() + } else { + BTreeMap::new() + }; + (None, groups) }; - - // Confirm that the requested direct dependency applies to this environment before - // selecting a package with the same name from the universal lock. For example, - // `foo; python_version < '3.12'` must not select a locked `foo` on Python 3.12. - if !requirements.iter().any(|requirement| { - &requirement.name == dependency_name - && requirement.marker.evaluate(marker_environment, &[]) - }) { - return Ok(None); - } - self.find_by_markers(dependency_name, marker_environment) + Ok(DependencySelection { production, groups }) } /// Returns the package selected by a dependency group on a non-virtual project. fn find_project_dependency_group_package( &self, - project_name: &PackageName, + project: &Package, group: &GroupName, dependency_name: &PackageName, marker_environment: &MarkerEnvironment, ) -> Result, String> { - let Some(project) = self.find_by_name(project_name)? else { - return Ok(None); - }; let Some(dependencies) = project.resolved_dependency_groups().get(group) else { return Ok(None); }; + let project_name = project.name(); let mut selected = None; for dependency in dependencies @@ -944,15 +932,13 @@ impl Lock { } /// Returns the package selected by a production dependency on a non-virtual project. - pub fn find_dependency_package( + fn find_project_dependency_package( &self, - project_name: &PackageName, + project: &Package, dependency_name: &PackageName, marker_environment: &MarkerEnvironment, ) -> Result, String> { - let Some(project) = self.find_by_name(project_name)? else { - return Ok(None); - }; + let project_name = project.name(); let mut selected = None; for dependency in project @@ -7218,6 +7204,7 @@ pub(crate) fn is_wheel_unreachable( #[cfg(test)] mod tests { + use uv_pep508::MarkerEnvironmentBuilder; use uv_warnings::anstream; use super::*; @@ -7231,6 +7218,117 @@ mod tests { }}; } + fn marker_environment() -> MarkerEnvironment { + MarkerEnvironment::try_from(MarkerEnvironmentBuilder { + implementation_name: "cpython", + implementation_version: "3.12.0", + os_name: "posix", + platform_machine: "arm64", + platform_python_implementation: "CPython", + platform_release: "23.0.0", + platform_system: "Darwin", + platform_version: "test", + python_full_version: "3.12.0", + python_version: "3.12", + sys_platform: "darwin", + }) + .expect("valid marker environment") + } + + #[test] + fn dependency_selection_resolves_included_groups_to_same_package() { + let lock: Lock = toml::from_str( + r#" +version = 1 +revision = 3 +requires-python = ">=3.12" + +[[package]] +name = "project" +version = "0.1.0" +source = { virtual = "." } +dependencies = [{ name = "ty" }] + +[package.dependency-groups] +dev = [{ name = "ty" }] +typing = [{ name = "ty" }] + +[[package]] +name = "ty" +version = "1.0.0" +source = { registry = "https://example.com/simple" } +"#, + ) + .expect("valid lock"); + let project_name = PackageName::from_str("project").expect("valid package name"); + let dependency_name = PackageName::from_str("ty").expect("valid package name"); + let dev = GroupName::from_str("dev").expect("valid group name"); + let typing = GroupName::from_str("typing").expect("valid group name"); + let marker_environment = marker_environment(); + + let selection = lock + .dependency_selection(Some(&project_name), &dependency_name, &marker_environment) + .expect("unique project package"); + assert_eq!( + selection + .groups() + .map(|(group, _package)| group) + .collect::>(), + [&dev, &typing] + ); + + let preferred = selection.group(&dev).expect("dev dependency"); + let included = selection.group(&typing).expect("typing dependency"); + let production = selection.production().expect("production dependency"); + + assert!(std::ptr::eq(preferred, included)); + assert!(std::ptr::eq(preferred, production)); + } + + #[test] + fn dependency_selection_returns_any_selection_error() { + let lock: Lock = toml::from_str( + r#" +version = 1 +revision = 3 +requires-python = ">=3.12" + +[[package]] +name = "project" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "ty", version = "1.0.0", source = { registry = "https://example.com/simple" } }, + { name = "ty", version = "2.0.0", source = { registry = "https://example.com/simple" } }, +] + +[package.dependency-groups] +dev = [ + { name = "ty", version = "1.0.0", source = { registry = "https://example.com/simple" } }, +] + +[[package]] +name = "ty" +version = "1.0.0" +source = { registry = "https://example.com/simple" } + +[[package]] +name = "ty" +version = "2.0.0" +source = { registry = "https://example.com/simple" } +"#, + ) + .expect("valid lock"); + let project_name = PackageName::from_str("project").expect("valid package name"); + let dependency_name = PackageName::from_str("ty").expect("valid package name"); + let marker_environment = marker_environment(); + + let error = lock + .dependency_selection(Some(&project_name), &dependency_name, &marker_environment) + .expect_err("ambiguous production selection"); + insta::assert_snapshot!(error, @"found multiple packages matching production dependency `ty` for `project`"); + } + #[test] fn missing_dependency_source_unambiguous() { let data = r#" diff --git a/crates/uv/src/commands/project/toolchain.rs b/crates/uv/src/commands/project/toolchain.rs index 1016eb589b8..593763d6650 100644 --- a/crates/uv/src/commands/project/toolchain.rs +++ b/crates/uv/src/commands/project/toolchain.rs @@ -39,19 +39,16 @@ pub(crate) fn find_locked_tool<'lock>( groups: &DependencyGroupsWithDefaults, ) -> Result>> { let marker_environment = interpreter.resolver_marker_environment(); - let group_package = lock - .find_dependency_group_package( + let selection = lock + .dependency_selection( project.project_name(), - dependency_group, package_name, marker_environment.markers(), ) .map_err(anyhow::Error::msg)?; - let production_package = if group_package.is_none() - && let Some(project_name) = project.project_name() - { - lock.find_dependency_package(project_name, package_name, marker_environment.markers()) - .map_err(anyhow::Error::msg)? + let group_package = selection.group(dependency_group); + let production_package = if group_package.is_none() { + selection.production() } else { None }; @@ -60,14 +57,10 @@ pub(crate) fn find_locked_tool<'lock>( }; let installed_by_production = groups.prod() && production_package == Some(package); - let installed_by_group = lock - .is_package_in_dependency_groups( - project.project_name(), - package, - marker_environment.markers(), - groups, - ) - .map_err(anyhow::Error::msg)?; + // Require the exact package: `dev` may select ty 0.0.17 while an enabled conflicting group selects 0.0.16. + let installed_by_group = selection + .groups() + .any(|(group, selected)| groups.contains(group) && selected == package); Ok(Some(LockedTool { package, From b332bc1099af90e38748e72a45f41f49c8f50106 Mon Sep 17 00:00:00 2001 From: Zsolt Dollenstein Date: Thu, 25 Jun 2026 20:42:22 +0100 Subject: [PATCH 2/2] Simplify locked tool environment selection --- crates/uv-resolver/src/lock/mod.rs | 15 ----------- crates/uv/src/commands/project/toolchain.rs | 18 ++++--------- crates/uv/tests/project/check.rs | 28 ++++++++++++++++++++- 3 files changed, 32 insertions(+), 29 deletions(-) diff --git a/crates/uv-resolver/src/lock/mod.rs b/crates/uv-resolver/src/lock/mod.rs index 38ebe6fd67f..9d794f4af74 100644 --- a/crates/uv-resolver/src/lock/mod.rs +++ b/crates/uv-resolver/src/lock/mod.rs @@ -322,13 +322,6 @@ impl<'lock> DependencySelection<'lock> { pub fn group(&self, group: &GroupName) -> Option<&'lock Package> { self.groups.get(group).copied() } - - /// Returns the dependency groups and their package selections. - pub fn groups(&self) -> impl Iterator + '_ { - self.groups - .iter() - .map(|(group, package)| (*group, *package)) - } } impl Lock { @@ -7269,14 +7262,6 @@ source = { registry = "https://example.com/simple" } let selection = lock .dependency_selection(Some(&project_name), &dependency_name, &marker_environment) .expect("unique project package"); - assert_eq!( - selection - .groups() - .map(|(group, _package)| group) - .collect::>(), - [&dev, &typing] - ); - let preferred = selection.group(&dev).expect("dev dependency"); let included = selection.group(&typing).expect("typing dependency"); let production = selection.production().expect("production dependency"); diff --git a/crates/uv/src/commands/project/toolchain.rs b/crates/uv/src/commands/project/toolchain.rs index 593763d6650..60b297d2068 100644 --- a/crates/uv/src/commands/project/toolchain.rs +++ b/crates/uv/src/commands/project/toolchain.rs @@ -46,25 +46,17 @@ pub(crate) fn find_locked_tool<'lock>( marker_environment.markers(), ) .map_err(anyhow::Error::msg)?; - let group_package = selection.group(dependency_group); - let production_package = if group_package.is_none() { - selection.production() + let (package, installed) = if let Some(package) = selection.group(dependency_group) { + (package, groups.contains(dependency_group)) + } else if let Some(package) = selection.production() { + (package, groups.prod()) } else { - None - }; - let Some(package) = group_package.or(production_package) else { return Ok(None); }; - let installed_by_production = groups.prod() && production_package == Some(package); - // Require the exact package: `dev` may select ty 0.0.17 while an enabled conflicting group selects 0.0.16. - let installed_by_group = selection - .groups() - .any(|(group, selected)| groups.contains(group) && selected == package); - Ok(Some(LockedTool { package, - requires_separate_environment: !(installed_by_production || installed_by_group), + requires_separate_environment: !installed, })) } diff --git a/crates/uv/tests/project/check.rs b/crates/uv/tests/project/check.rs index e76d2d282a1..58a40df516f 100644 --- a/crates/uv/tests/project/check.rs +++ b/crates/uv/tests/project/check.rs @@ -519,13 +519,14 @@ fn check_uses_exact_ty_version_from_selected_included_group() -> Result<()> { "#})?; context.temp_dir.child("main.py").write_str("x = 1")?; + // `dev` includes `typing`, so the selected tool is installed in the project environment. uv_snapshot!( context.filters(), context .check() .arg("--no-default-groups") .arg("--group") - .arg("typing") + .arg("dev") .arg("--exclude-newer") .arg("2026-02-15T00:00:00Z") .arg("--show-version"), @@ -545,6 +546,31 @@ fn check_uses_exact_ty_version_from_selected_included_group() -> Result<()> { assert!(context.temp_dir.child("uv.lock").exists()); assert!(context.site_packages().join("ty").exists()); + // The preferred `dev` group is not enabled, so the tool uses a cached environment even though + // the enabled `typing` group selects the same package. + uv_snapshot!( + context.filters(), + context + .check() + .arg("--no-default-groups") + .arg("--group") + .arg("typing") + .arg("--exclude-newer") + .arg("2026-02-15T00:00:00Z") + .arg("--show-version"), + @" + success: true + exit_code: 0 + ----- stdout ----- + All checks passed! + + ----- stderr ----- + warning: `uv check` is experimental and may change without warning. Pass `--preview-features check-command` to disable this warning. + Installed 1 package in [TIME] + Using ty 0.0.17 + " + ); + Ok(()) }