From a6daab422f6597bd8b32e22a4901859562e12258 Mon Sep 17 00:00:00 2001 From: konsti Date: Mon, 22 Sep 2025 13:26:08 +0200 Subject: [PATCH] Add incompatibility from proxy to base package (#15200) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add an incompatibility that lets pubgrub skip of marker packages when the base package already has an incompatible version to improve the error messages (https://github.com/astral-sh/uv/issues/15199). The change is also a small perf improvement. Overall this should be able to improve performance in slow cases by avoiding trying proxy package versions that are impossible anyway, for a (ideally very small cost) for tracking the additional incompatibility and tracking the base package for each proxy package. ``` $ hhyperfine --warmup 2 "uv pip compile --universal scripts/requirements/airflow.in" "target/release/uv pip compile --universal scripts/requirements/airflow.in" Benchmark 1: uv pip compile --universal scripts/requirements/airflow.in Time (mean ± σ): 145.5 ms ± 3.9 ms [User: 154.7 ms, System: 140.7 ms] Range (min … max): 139.2 ms … 153.4 ms 20 runs Benchmark 2: target/release/uv pip compile --universal scripts/requirements/airflow.in Time (mean ± σ): 128.7 ms ± 5.5 ms [User: 141.9 ms, System: 137.3 ms] Range (min … max): 121.8 ms … 142.0 ms 23 runs Summary target/release/uv pip compile --universal scripts/requirements/airflow.in ran 1.13 ± 0.06 times faster than uv pip compile --universal scripts/requirements/airflow.in ``` This implementation is the basic version: When we see a proxy `foo{...}>=x,=x, `foo>=x,=x, `foo!=z`. This avoids trying any version of the proxy package except the version that matches our previous selection. Another is that if we see a dependency `foo>=x,=x,y` -> `foo>=x,=x,=x, `foo>=x, the package that depended of `foo>=x, 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. " );