Skip to content

Commit a6daab4

Browse files
authored
Add incompatibility from proxy to base package (#15200)
Add an incompatibility that lets pubgrub skip of marker packages when the base package already has an incompatible version to improve the error messages (#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,<y` we add a dependency edge `foo{...}>=x,<y` -> `foo>=x,<y`. There are several way to extend this, which likely help more with performance than with error messages. One idea is that if we see `foo{...}>=x,<y` but we already made a selection for `foo==z` outside that range, we can insert a dependency `foo{...}!=z` -> `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,<y`, we also add `foo{...}>=x,y` -> `foo>=x,<y`. This allows backtracking beyond `foo` immediately if all version of `foo{...}>=x,<y` are incompatible, since `foo{...}>=x,<y` incompatible -> `foo>=x,<y` incompatible -> the package that depended of `foo>=x,<y` is incompatible. The cost for each of these operations is tracking an additional incompatibility per virtual package. An alternative approach is to only add the incompatibility lazily, only when we've tried several version of the virtual package already. This needs to be weighed of with the better error messages that the incompatibility gives, we unfortunately have only few large reference examples. Requires astral-sh/pubgrub#45 Closes #15199
1 parent 1d7d7fd commit a6daab4

File tree

7 files changed

+99
-61
lines changed

7 files changed

+99
-61
lines changed

Cargo.lock

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ percent-encoding = { version = "2.3.1" }
144144
petgraph = { version = "0.8.0" }
145145
proc-macro2 = { version = "1.0.86" }
146146
procfs = { version = "0.17.0", default-features = false, features = ["flate2"] }
147-
pubgrub = { git = "https://github.com/astral-sh/pubgrub", rev = "06ec5a5f59ffaeb6cf5079c6cb184467da06c9db" }
147+
pubgrub = { git = "https://github.com/astral-sh/pubgrub", rev = "d8efd77673c9a90792da9da31b6c0da7ea8a324b" }
148148
quote = { version = "1.0.37" }
149149
rayon = { version = "1.10.0" }
150150
ref-cast = { version = "1.0.24" }
@@ -193,7 +193,7 @@ unicode-width = { version = "0.2.0" }
193193
unscanny = { version = "0.1.0" }
194194
url = { version = "2.5.2", features = ["serde"] }
195195
uuid = { version = "1.16.0" }
196-
version-ranges = { git = "https://github.com/astral-sh/pubgrub", rev = "06ec5a5f59ffaeb6cf5079c6cb184467da06c9db" }
196+
version-ranges = { git = "https://github.com/astral-sh/pubgrub", rev = "d8efd77673c9a90792da9da31b6c0da7ea8a324b" }
197197
walkdir = { version = "2.5.0" }
198198
which = { version = "8.0.0", features = ["regex"] }
199199
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"] }

crates/uv-resolver/src/pubgrub/package.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,32 @@ impl PubGrubPackage {
133133
}
134134
}
135135

136+
/// If this package is a proxy package, return the base package it depends on.
137+
///
138+
/// While dependency groups may be attached to a package, we don't consider them here as
139+
/// there is no (mandatory) dependency from a dependency group to the package.
140+
pub(crate) fn base_package(&self) -> Option<Self> {
141+
match &**self {
142+
PubGrubPackageInner::Root(_)
143+
| PubGrubPackageInner::Python(_)
144+
| PubGrubPackageInner::System(_)
145+
| PubGrubPackageInner::Package { .. } => None,
146+
PubGrubPackageInner::Group { .. } => {
147+
// The dependency groups of a package do not by themselves require the package
148+
// itself.
149+
None
150+
}
151+
PubGrubPackageInner::Extra { name, .. } | PubGrubPackageInner::Marker { name, .. } => {
152+
Some(Self::from_package(
153+
name.clone(),
154+
None,
155+
None,
156+
MarkerTree::TRUE,
157+
))
158+
}
159+
}
160+
}
161+
136162
/// Returns the name of this PubGrub package, if it has one.
137163
pub(crate) fn name(&self) -> Option<&PackageName> {
138164
match &**self {

crates/uv-resolver/src/pubgrub/priority.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ impl PubGrubPriorities {
152152
Some(tiebreaker) => *tiebreaker,
153153
None => {
154154
if cfg!(debug_assertions) {
155-
panic!("Virtual package not known: `{package}`")
155+
panic!("Package not registered in prioritization: `{package:?}`")
156156
} else {
157157
PubGrubTiebreaker(Reverse(u32::MAX))
158158
}

crates/uv-resolver/src/resolver/mod.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2952,6 +2952,12 @@ impl ForkState {
29522952

29532953
// Update the package priorities.
29542954
self.priorities.insert(package, version, &self.fork_urls);
2955+
// As we're adding an incompatibility from the proxy package to the base package,
2956+
// we need to register the base package.
2957+
if let Some(base_package) = package.base_package() {
2958+
self.priorities
2959+
.insert(&base_package, version, &self.fork_urls);
2960+
}
29552961
}
29562962

29572963
Ok(())
@@ -2964,6 +2970,24 @@ impl ForkState {
29642970
for_version: &Version,
29652971
dependencies: Vec<PubGrubDependency>,
29662972
) {
2973+
for dependency in &dependencies {
2974+
let PubGrubDependency {
2975+
package,
2976+
version,
2977+
parent: _,
2978+
url: _,
2979+
} = dependency;
2980+
2981+
let Some(base_package) = package.base_package() else {
2982+
continue;
2983+
};
2984+
2985+
let proxy_package = self.pubgrub.package_store.alloc(package.clone());
2986+
let base_package_id = self.pubgrub.package_store.alloc(base_package.clone());
2987+
self.pubgrub
2988+
.add_proxy_package(proxy_package, base_package_id, version.clone());
2989+
}
2990+
29672991
let conflict = self.pubgrub.add_package_version_dependencies(
29682992
self.next,
29692993
for_version.clone(),

crates/uv/tests/it/lock.rs

Lines changed: 42 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -13103,7 +13103,7 @@ fn unconditional_overlapping_marker_disjoint_version_constraints() -> Result<()>
1310313103

1310413104
----- stderr -----
1310513105
× No solution found when resolving dependencies:
13106-
╰─▶ 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.
13106+
╰─▶ Because your project depends on datasets<2.19 and datasets>=2.19, we can conclude that your project's requirements are unsatisfiable.
1310713107
");
1310813108

1310913109
Ok(())
@@ -26786,17 +26786,17 @@ fn lock_self_marker_incompatible() -> Result<()> {
2678626786
"#,
2678726787
)?;
2678826788

26789-
uv_snapshot!(context.filters(), context.lock(), @r###"
26789+
uv_snapshot!(context.filters(), context.lock(), @r"
2679026790
success: false
2679126791
exit_code: 1
2679226792
----- stdout -----
2679326793

2679426794
----- stderr -----
2679526795
× No solution found when resolving dependencies:
26796-
╰─▶ 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.
26796+
╰─▶ 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.
2679726797

2679826798
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.
26799-
"###);
26799+
");
2680026800

2680126801
Ok(())
2680226802
}
@@ -29965,40 +29965,8 @@ fn lock_conflict_for_disjoint_python_version() -> Result<()> {
2996529965

2996629966
----- stderr -----
2996729967
× No solution found when resolving dependencies for split (markers: python_full_version >= '3.11'):
29968-
╰─▶ Because only the following versions of numpy{python_full_version >= '3.10'} are available:
29969-
numpy{python_full_version >= '3.10'}<=1.21.0
29970-
numpy{python_full_version >= '3.10'}==1.21.1
29971-
numpy{python_full_version >= '3.10'}==1.21.2
29972-
numpy{python_full_version >= '3.10'}==1.21.3
29973-
numpy{python_full_version >= '3.10'}==1.21.4
29974-
numpy{python_full_version >= '3.10'}==1.21.5
29975-
numpy{python_full_version >= '3.10'}==1.21.6
29976-
numpy{python_full_version >= '3.10'}==1.22.0
29977-
numpy{python_full_version >= '3.10'}==1.22.1
29978-
numpy{python_full_version >= '3.10'}==1.22.2
29979-
numpy{python_full_version >= '3.10'}==1.22.3
29980-
numpy{python_full_version >= '3.10'}==1.22.4
29981-
numpy{python_full_version >= '3.10'}==1.23.0
29982-
numpy{python_full_version >= '3.10'}==1.23.1
29983-
numpy{python_full_version >= '3.10'}==1.23.2
29984-
numpy{python_full_version >= '3.10'}==1.23.3
29985-
numpy{python_full_version >= '3.10'}==1.23.4
29986-
numpy{python_full_version >= '3.10'}==1.23.5
29987-
numpy{python_full_version >= '3.10'}==1.24.0
29988-
numpy{python_full_version >= '3.10'}==1.24.1
29989-
numpy{python_full_version >= '3.10'}==1.24.2
29990-
numpy{python_full_version >= '3.10'}==1.24.3
29991-
numpy{python_full_version >= '3.10'}==1.24.4
29992-
numpy{python_full_version >= '3.10'}==1.25.0
29993-
numpy{python_full_version >= '3.10'}==1.25.1
29994-
numpy{python_full_version >= '3.10'}==1.25.2
29995-
numpy{python_full_version >= '3.10'}==1.26.0
29996-
numpy{python_full_version >= '3.10'}==1.26.1
29997-
numpy{python_full_version >= '3.10'}==1.26.2
29998-
numpy{python_full_version >= '3.10'}==1.26.3
29999-
numpy{python_full_version >= '3.10'}==1.26.4
30000-
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.
30001-
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.
29968+
╰─▶ 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.
29969+
And because your project depends on pandas==1.5.3, we can conclude that your project's requirements are unsatisfiable.
3000229970

3000329971
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`.
3000429972
");
@@ -30219,18 +30187,7 @@ fn lock_conflict_for_disjoint_platform() -> Result<()> {
3021930187

3022030188
----- stderr -----
3022130189
× No solution found when resolving dependencies for split (markers: sys_platform == 'exotic'):
30222-
╰─▶ Because only the following versions of numpy{sys_platform == 'exotic'} are available:
30223-
numpy{sys_platform == 'exotic'}<=1.24.0
30224-
numpy{sys_platform == 'exotic'}==1.24.1
30225-
numpy{sys_platform == 'exotic'}==1.24.2
30226-
numpy{sys_platform == 'exotic'}==1.24.3
30227-
numpy{sys_platform == 'exotic'}==1.24.4
30228-
numpy{sys_platform == 'exotic'}==1.25.0
30229-
numpy{sys_platform == 'exotic'}==1.25.1
30230-
numpy{sys_platform == 'exotic'}==1.25.2
30231-
numpy{sys_platform == 'exotic'}>1.26
30232-
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.
30233-
And because your project depends on numpy>=1.26, we can conclude that your project's requirements are unsatisfiable.
30190+
╰─▶ 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.
3023430191

3023530192
hint: The resolution failed for an environment that is not the current one, consider limiting the environments with `tool.uv.environments`.
3023630193
");
@@ -31700,3 +31657,38 @@ fn lock_required_intersection() -> Result<()> {
3170031657

3170131658
Ok(())
3170231659
}
31660+
31661+
/// Ensure conflicts on virtual packages (such as markers) give good error messages.
31662+
#[test]
31663+
fn collapsed_error_with_marker_packages() -> Result<()> {
31664+
let context = TestContext::new("3.12");
31665+
31666+
let pyproject_toml = indoc! {r#"
31667+
[project]
31668+
name = "test-project"
31669+
version = "1.0.0"
31670+
requires-python = ">=3.12"
31671+
dependencies = [
31672+
"anyio<=4.3.0; sys_platform == 'other'",
31673+
"anyio>=4.4.0; python_version < '3.14'",
31674+
]
31675+
"#};
31676+
context
31677+
.temp_dir
31678+
.child("pyproject.toml")
31679+
.write_str(pyproject_toml)?;
31680+
31681+
uv_snapshot!(context.filters(), context.lock(), @r"
31682+
success: false
31683+
exit_code: 1
31684+
----- stdout -----
31685+
31686+
----- stderr -----
31687+
× No solution found when resolving dependencies for split (markers: python_full_version < '3.14' and sys_platform == 'other'):
31688+
╰─▶ 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.
31689+
31690+
hint: The resolution failed for an environment that is not the current one, consider limiting the environments with `tool.uv.environments`.
31691+
");
31692+
31693+
Ok(())
31694+
}

crates/uv/tests/it/lock_scenarios.rs

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3148,7 +3148,7 @@ fn fork_non_local_fork_marker_direct() -> Result<()> {
31483148
31493149
----- stderr -----
31503150
× No solution found when resolving dependencies:
3151-
╰─▶ 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.
3151+
╰─▶ 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.
31523152
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.
31533153
"
31543154
);
@@ -3220,11 +3220,7 @@ fn fork_non_local_fork_marker_transitive() -> Result<()> {
32203220
32213221
----- stderr -----
32223222
× No solution found when resolving dependencies:
3223-
╰─▶ 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:
3224-
package-c{sys_platform == 'linux'}==1.0.0
3225-
package-c{sys_platform == 'linux'}>2.0.0
3226-
we can conclude that package-a==1.0.0 depends on package-c{sys_platform == 'linux'}==1.0.0.
3227-
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.
3223+
╰─▶ 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.
32283224
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.
32293225
"
32303226
);

0 commit comments

Comments
 (0)