diff --git a/Cargo.lock b/Cargo.lock index 0fda7be7a..daec0a3b1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3109,7 +3109,7 @@ dependencies = [ [[package]] name = "pubgrub" version = "0.3.0" -source = "git+https://github.com/astral-sh/pubgrub?rev=06ec5a5f59ffaeb6cf5079c6cb184467da06c9db#06ec5a5f59ffaeb6cf5079c6cb184467da06c9db" +source = "git+https://github.com/astral-sh/pubgrub?rev=d8efd77673c9a90792da9da31b6c0da7ea8a324b#d8efd77673c9a90792da9da31b6c0da7ea8a324b" dependencies = [ "indexmap", "log", @@ -6614,7 +6614,7 @@ checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" [[package]] name = "version-ranges" version = "0.1.1" -source = "git+https://github.com/astral-sh/pubgrub?rev=06ec5a5f59ffaeb6cf5079c6cb184467da06c9db#06ec5a5f59ffaeb6cf5079c6cb184467da06c9db" +source = "git+https://github.com/astral-sh/pubgrub?rev=d8efd77673c9a90792da9da31b6c0da7ea8a324b#d8efd77673c9a90792da9da31b6c0da7ea8a324b" dependencies = [ "smallvec", ] diff --git a/Cargo.toml b/Cargo.toml index eb27c294b..972f57cf0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -144,7 +144,7 @@ percent-encoding = { version = "2.3.1" } petgraph = { version = "0.8.0" } proc-macro2 = { version = "1.0.86" } procfs = { version = "0.17.0", default-features = false, features = ["flate2"] } -pubgrub = { git = "https://github.com/astral-sh/pubgrub", rev = "06ec5a5f59ffaeb6cf5079c6cb184467da06c9db" } +pubgrub = { git = "https://github.com/astral-sh/pubgrub", rev = "d8efd77673c9a90792da9da31b6c0da7ea8a324b" } quote = { version = "1.0.37" } rayon = { version = "1.10.0" } ref-cast = { version = "1.0.24" } @@ -193,7 +193,7 @@ unicode-width = { version = "0.2.0" } unscanny = { version = "0.1.0" } url = { version = "2.5.2", features = ["serde"] } uuid = { version = "1.16.0" } -version-ranges = { git = "https://github.com/astral-sh/pubgrub", rev = "06ec5a5f59ffaeb6cf5079c6cb184467da06c9db" } +version-ranges = { git = "https://github.com/astral-sh/pubgrub", rev = "d8efd77673c9a90792da9da31b6c0da7ea8a324b" } walkdir = { version = "2.5.0" } which = { version = "8.0.0", features = ["regex"] } windows = { version = "0.59.0", features = ["Win32_Globalization", "Win32_Security", "Win32_System_Console", "Win32_System_Kernel", "Win32_System_Diagnostics_Debug", "Win32_Storage_FileSystem", "Win32_System_Registry", "Win32_System_IO", "Win32_System_Ioctl"] } diff --git a/crates/uv-resolver/src/pubgrub/package.rs b/crates/uv-resolver/src/pubgrub/package.rs index b0693b01b..ecc0c9c1c 100644 --- a/crates/uv-resolver/src/pubgrub/package.rs +++ b/crates/uv-resolver/src/pubgrub/package.rs @@ -133,6 +133,32 @@ impl PubGrubPackage { } } + /// If this package is a proxy package, return the base package it depends on. + /// + /// While dependency groups may be attached to a package, we don't consider them here as + /// there is no (mandatory) dependency from a dependency group to the package. + pub(crate) fn base_package(&self) -> Option { + match &**self { + PubGrubPackageInner::Root(_) + | PubGrubPackageInner::Python(_) + | PubGrubPackageInner::System(_) + | PubGrubPackageInner::Package { .. } => None, + PubGrubPackageInner::Group { .. } => { + // The dependency groups of a package do not by themselves require the package + // itself. + None + } + PubGrubPackageInner::Extra { name, .. } | PubGrubPackageInner::Marker { name, .. } => { + Some(Self::from_package( + name.clone(), + None, + None, + MarkerTree::TRUE, + )) + } + } + } + /// Returns the name of this PubGrub package, if it has one. pub(crate) fn name(&self) -> Option<&PackageName> { match &**self { diff --git a/crates/uv-resolver/src/pubgrub/priority.rs b/crates/uv-resolver/src/pubgrub/priority.rs index adfd044f0..7b219b476 100644 --- a/crates/uv-resolver/src/pubgrub/priority.rs +++ b/crates/uv-resolver/src/pubgrub/priority.rs @@ -152,7 +152,7 @@ impl PubGrubPriorities { Some(tiebreaker) => *tiebreaker, None => { if cfg!(debug_assertions) { - panic!("Virtual package not known: `{package}`") + panic!("Package not registered in prioritization: `{package:?}`") } else { PubGrubTiebreaker(Reverse(u32::MAX)) } diff --git a/crates/uv-resolver/src/resolver/mod.rs b/crates/uv-resolver/src/resolver/mod.rs index c7d4a2523..e8efb1438 100644 --- a/crates/uv-resolver/src/resolver/mod.rs +++ b/crates/uv-resolver/src/resolver/mod.rs @@ -2952,6 +2952,12 @@ impl ForkState { // Update the package priorities. self.priorities.insert(package, version, &self.fork_urls); + // As we're adding an incompatibility from the proxy package to the base package, + // we need to register the base package. + if let Some(base_package) = package.base_package() { + self.priorities + .insert(&base_package, version, &self.fork_urls); + } } Ok(()) @@ -2964,6 +2970,24 @@ impl ForkState { for_version: &Version, dependencies: Vec, ) { + for dependency in &dependencies { + let PubGrubDependency { + package, + version, + parent: _, + url: _, + } = dependency; + + let Some(base_package) = package.base_package() else { + continue; + }; + + let proxy_package = self.pubgrub.package_store.alloc(package.clone()); + let base_package_id = self.pubgrub.package_store.alloc(base_package.clone()); + self.pubgrub + .add_proxy_package(proxy_package, base_package_id, version.clone()); + } + let conflict = self.pubgrub.add_package_version_dependencies( self.next, for_version.clone(), diff --git a/crates/uv/tests/it/lock.rs b/crates/uv/tests/it/lock.rs index 4991617c0..70477868b 100644 --- a/crates/uv/tests/it/lock.rs +++ b/crates/uv/tests/it/lock.rs @@ -13103,7 +13103,7 @@ fn unconditional_overlapping_marker_disjoint_version_constraints() -> Result<()> ----- stderr ----- × No solution found when resolving dependencies: - ╰─▶ Because only datasets<=2.18.0 is available and your project depends on datasets>=2.19, we can conclude that your project's requirements are unsatisfiable. + ╰─▶ Because your project depends on datasets<2.19 and datasets>=2.19, we can conclude that your project's requirements are unsatisfiable. "); Ok(()) @@ -26786,17 +26786,17 @@ fn lock_self_marker_incompatible() -> Result<()> { "#, )?; - uv_snapshot!(context.filters(), context.lock(), @r###" + uv_snapshot!(context.filters(), context.lock(), @r" success: false exit_code: 1 ----- stdout ----- ----- stderr ----- × No solution found when resolving dependencies: - ╰─▶ Because only project{sys_platform == 'win32'}<=0.1 is available and your project depends on itself at an incompatible version (project{sys_platform == 'win32'}>0.1), we can conclude that your project's requirements are unsatisfiable. + ╰─▶ Because your project depends on itself at an incompatible version (project{sys_platform == 'win32'}>0.1), we can conclude that your project's requirements are unsatisfiable. hint: The project `project` depends on itself at an incompatible version. This is likely a mistake. If you intended to depend on a third-party package named `project`, consider renaming the project `project` to avoid creating a conflict. - "###); + "); Ok(()) } @@ -29965,40 +29965,8 @@ fn lock_conflict_for_disjoint_python_version() -> Result<()> { ----- stderr ----- × No solution found when resolving dependencies for split (markers: python_full_version >= '3.11'): - ╰─▶ Because only the following versions of numpy{python_full_version >= '3.10'} are available: - numpy{python_full_version >= '3.10'}<=1.21.0 - numpy{python_full_version >= '3.10'}==1.21.1 - numpy{python_full_version >= '3.10'}==1.21.2 - numpy{python_full_version >= '3.10'}==1.21.3 - numpy{python_full_version >= '3.10'}==1.21.4 - numpy{python_full_version >= '3.10'}==1.21.5 - numpy{python_full_version >= '3.10'}==1.21.6 - numpy{python_full_version >= '3.10'}==1.22.0 - numpy{python_full_version >= '3.10'}==1.22.1 - numpy{python_full_version >= '3.10'}==1.22.2 - numpy{python_full_version >= '3.10'}==1.22.3 - numpy{python_full_version >= '3.10'}==1.22.4 - numpy{python_full_version >= '3.10'}==1.23.0 - numpy{python_full_version >= '3.10'}==1.23.1 - numpy{python_full_version >= '3.10'}==1.23.2 - numpy{python_full_version >= '3.10'}==1.23.3 - numpy{python_full_version >= '3.10'}==1.23.4 - numpy{python_full_version >= '3.10'}==1.23.5 - numpy{python_full_version >= '3.10'}==1.24.0 - numpy{python_full_version >= '3.10'}==1.24.1 - numpy{python_full_version >= '3.10'}==1.24.2 - numpy{python_full_version >= '3.10'}==1.24.3 - numpy{python_full_version >= '3.10'}==1.24.4 - numpy{python_full_version >= '3.10'}==1.25.0 - numpy{python_full_version >= '3.10'}==1.25.1 - numpy{python_full_version >= '3.10'}==1.25.2 - numpy{python_full_version >= '3.10'}==1.26.0 - numpy{python_full_version >= '3.10'}==1.26.1 - numpy{python_full_version >= '3.10'}==1.26.2 - numpy{python_full_version >= '3.10'}==1.26.3 - numpy{python_full_version >= '3.10'}==1.26.4 - and pandas==1.5.3 depends on numpy{python_full_version >= '3.10'}>=1.21.0, we can conclude that pandas==1.5.3 depends on numpy>=1.21.0. - And because your project depends on numpy==1.20.3 and pandas==1.5.3, we can conclude that your project's requirements are unsatisfiable. + ╰─▶ Because pandas==1.5.3 depends on numpy{python_full_version >= '3.10'}>=1.21.0 and your project depends on numpy==1.20.3, we can conclude that your project and pandas==1.5.3 are incompatible. + And because your project depends on pandas==1.5.3, we can conclude that your project's requirements are unsatisfiable. hint: While the active Python version is 3.9, the resolution failed for other Python versions supported by your project. Consider limiting your project's supported Python versions using `requires-python`. "); @@ -30219,18 +30187,7 @@ fn lock_conflict_for_disjoint_platform() -> Result<()> { ----- stderr ----- × No solution found when resolving dependencies for split (markers: sys_platform == 'exotic'): - ╰─▶ Because only the following versions of numpy{sys_platform == 'exotic'} are available: - numpy{sys_platform == 'exotic'}<=1.24.0 - numpy{sys_platform == 'exotic'}==1.24.1 - numpy{sys_platform == 'exotic'}==1.24.2 - numpy{sys_platform == 'exotic'}==1.24.3 - numpy{sys_platform == 'exotic'}==1.24.4 - numpy{sys_platform == 'exotic'}==1.25.0 - numpy{sys_platform == 'exotic'}==1.25.1 - numpy{sys_platform == 'exotic'}==1.25.2 - numpy{sys_platform == 'exotic'}>1.26 - and your project depends on numpy{sys_platform == 'exotic'}>=1.24,<1.26, we can conclude that your project depends on numpy>=1.24.0,<=1.25.2. - And because your project depends on numpy>=1.26, we can conclude that your project's requirements are unsatisfiable. + ╰─▶ Because your project depends on numpy{sys_platform == 'exotic'}>=1.24,<1.26 and numpy>=1.26, we can conclude that your project's requirements are unsatisfiable. hint: The resolution failed for an environment that is not the current one, consider limiting the environments with `tool.uv.environments`. "); @@ -31700,3 +31657,38 @@ fn lock_required_intersection() -> Result<()> { Ok(()) } + +/// Ensure conflicts on virtual packages (such as markers) give good error messages. +#[test] +fn collapsed_error_with_marker_packages() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = indoc! {r#" + [project] + name = "test-project" + version = "1.0.0" + requires-python = ">=3.12" + dependencies = [ + "anyio<=4.3.0; sys_platform == 'other'", + "anyio>=4.4.0; python_version < '3.14'", + ] + "#}; + context + .temp_dir + .child("pyproject.toml") + .write_str(pyproject_toml)?; + + uv_snapshot!(context.filters(), context.lock(), @r" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + × No solution found when resolving dependencies for split (markers: python_full_version < '3.14' and sys_platform == 'other'): + ╰─▶ Because your project depends on anyio{sys_platform == 'other'} and anyio{python_full_version < '3.14'}>=4.4.0, we can conclude that your project's requirements are unsatisfiable. + + hint: The resolution failed for an environment that is not the current one, consider limiting the environments with `tool.uv.environments`. + "); + + Ok(()) +} diff --git a/crates/uv/tests/it/lock_scenarios.rs b/crates/uv/tests/it/lock_scenarios.rs index fe2bea4a2..a58380ae3 100644 --- a/crates/uv/tests/it/lock_scenarios.rs +++ b/crates/uv/tests/it/lock_scenarios.rs @@ -3148,7 +3148,7 @@ fn fork_non_local_fork_marker_direct() -> Result<()> { ----- stderr ----- × No solution found when resolving dependencies: - ╰─▶ Because package-a==1.0.0 depends on package-c<2.0.0 and package-b==1.0.0 depends on package-c>=2.0.0, we can conclude that package-a{sys_platform == 'linux'}==1.0.0 and package-b{sys_platform == 'darwin'}==1.0.0 are incompatible. + ╰─▶ Because package-a==1.0.0 depends on package-c<2.0.0 and package-b==1.0.0 depends on package-c>=2.0.0, we can conclude that package-b==1.0.0 and package-a{sys_platform == 'linux'}==1.0.0 are incompatible. And because your project depends on package-a{sys_platform == 'linux'}==1.0.0 and package-b{sys_platform == 'darwin'}==1.0.0, we can conclude that your project's requirements are unsatisfiable. " ); @@ -3220,11 +3220,7 @@ fn fork_non_local_fork_marker_transitive() -> Result<()> { ----- stderr ----- × No solution found when resolving dependencies: - ╰─▶ Because package-a==1.0.0 depends on package-c{sys_platform == 'linux'}<2.0.0 and only the following versions of package-c{sys_platform == 'linux'} are available: - package-c{sys_platform == 'linux'}==1.0.0 - package-c{sys_platform == 'linux'}>2.0.0 - we can conclude that package-a==1.0.0 depends on package-c{sys_platform == 'linux'}==1.0.0. - And because only package-c{sys_platform == 'darwin'}<=2.0.0 is available and package-b==1.0.0 depends on package-c{sys_platform == 'darwin'}>=2.0.0, we can conclude that package-a==1.0.0 and package-b==1.0.0 are incompatible. + ╰─▶ Because package-a==1.0.0 depends on package-c{sys_platform == 'linux'}<2.0.0 and package-b==1.0.0 depends on package-c{sys_platform == 'darwin'}>=2.0.0, we can conclude that package-a==1.0.0 and package-b==1.0.0 are incompatible. And because your project depends on package-a==1.0.0 and package-b==1.0.0, we can conclude that your project's requirements are unsatisfiable. " );