mirror of https://github.com/astral-sh/uv
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 (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,<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 https://github.com/astral-sh/pubgrub/pull/45 Closes https://github.com/astral-sh/uv/issues/15199
This commit is contained in:
parent
1d7d7fdf00
commit
a6daab422f
|
|
@ -3109,7 +3109,7 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pubgrub"
|
name = "pubgrub"
|
||||||
version = "0.3.0"
|
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 = [
|
dependencies = [
|
||||||
"indexmap",
|
"indexmap",
|
||||||
"log",
|
"log",
|
||||||
|
|
@ -6614,7 +6614,7 @@ checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "version-ranges"
|
name = "version-ranges"
|
||||||
version = "0.1.1"
|
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 = [
|
dependencies = [
|
||||||
"smallvec",
|
"smallvec",
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -144,7 +144,7 @@ percent-encoding = { version = "2.3.1" }
|
||||||
petgraph = { version = "0.8.0" }
|
petgraph = { version = "0.8.0" }
|
||||||
proc-macro2 = { version = "1.0.86" }
|
proc-macro2 = { version = "1.0.86" }
|
||||||
procfs = { version = "0.17.0", default-features = false, features = ["flate2"] }
|
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" }
|
quote = { version = "1.0.37" }
|
||||||
rayon = { version = "1.10.0" }
|
rayon = { version = "1.10.0" }
|
||||||
ref-cast = { version = "1.0.24" }
|
ref-cast = { version = "1.0.24" }
|
||||||
|
|
@ -193,7 +193,7 @@ unicode-width = { version = "0.2.0" }
|
||||||
unscanny = { version = "0.1.0" }
|
unscanny = { version = "0.1.0" }
|
||||||
url = { version = "2.5.2", features = ["serde"] }
|
url = { version = "2.5.2", features = ["serde"] }
|
||||||
uuid = { version = "1.16.0" }
|
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" }
|
walkdir = { version = "2.5.0" }
|
||||||
which = { version = "8.0.0", features = ["regex"] }
|
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"] }
|
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"] }
|
||||||
|
|
|
||||||
|
|
@ -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<Self> {
|
||||||
|
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.
|
/// Returns the name of this PubGrub package, if it has one.
|
||||||
pub(crate) fn name(&self) -> Option<&PackageName> {
|
pub(crate) fn name(&self) -> Option<&PackageName> {
|
||||||
match &**self {
|
match &**self {
|
||||||
|
|
|
||||||
|
|
@ -152,7 +152,7 @@ impl PubGrubPriorities {
|
||||||
Some(tiebreaker) => *tiebreaker,
|
Some(tiebreaker) => *tiebreaker,
|
||||||
None => {
|
None => {
|
||||||
if cfg!(debug_assertions) {
|
if cfg!(debug_assertions) {
|
||||||
panic!("Virtual package not known: `{package}`")
|
panic!("Package not registered in prioritization: `{package:?}`")
|
||||||
} else {
|
} else {
|
||||||
PubGrubTiebreaker(Reverse(u32::MAX))
|
PubGrubTiebreaker(Reverse(u32::MAX))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2952,6 +2952,12 @@ impl ForkState {
|
||||||
|
|
||||||
// Update the package priorities.
|
// Update the package priorities.
|
||||||
self.priorities.insert(package, version, &self.fork_urls);
|
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(())
|
Ok(())
|
||||||
|
|
@ -2964,6 +2970,24 @@ impl ForkState {
|
||||||
for_version: &Version,
|
for_version: &Version,
|
||||||
dependencies: Vec<PubGrubDependency>,
|
dependencies: Vec<PubGrubDependency>,
|
||||||
) {
|
) {
|
||||||
|
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(
|
let conflict = self.pubgrub.add_package_version_dependencies(
|
||||||
self.next,
|
self.next,
|
||||||
for_version.clone(),
|
for_version.clone(),
|
||||||
|
|
|
||||||
|
|
@ -13103,7 +13103,7 @@ fn unconditional_overlapping_marker_disjoint_version_constraints() -> Result<()>
|
||||||
|
|
||||||
----- stderr -----
|
----- stderr -----
|
||||||
× No solution found when resolving dependencies:
|
× 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(())
|
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
|
success: false
|
||||||
exit_code: 1
|
exit_code: 1
|
||||||
----- stdout -----
|
----- stdout -----
|
||||||
|
|
||||||
----- stderr -----
|
----- stderr -----
|
||||||
× No solution found when resolving dependencies:
|
× 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.
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
@ -29965,40 +29965,8 @@ fn lock_conflict_for_disjoint_python_version() -> Result<()> {
|
||||||
|
|
||||||
----- stderr -----
|
----- stderr -----
|
||||||
× No solution found when resolving dependencies for split (markers: python_full_version >= '3.11'):
|
× 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:
|
╰─▶ 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.
|
||||||
numpy{python_full_version >= '3.10'}<=1.21.0
|
And because your project depends on pandas==1.5.3, we can conclude that your project's requirements are unsatisfiable.
|
||||||
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.
|
|
||||||
|
|
||||||
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`.
|
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 -----
|
----- stderr -----
|
||||||
× No solution found when resolving dependencies for split (markers: sys_platform == 'exotic'):
|
× No solution found when resolving dependencies for split (markers: sys_platform == 'exotic'):
|
||||||
╰─▶ Because only the following versions of numpy{sys_platform == 'exotic'} are available:
|
╰─▶ 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.
|
||||||
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.
|
|
||||||
|
|
||||||
hint: The resolution failed for an environment that is not the current one, consider limiting the environments with `tool.uv.environments`.
|
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(())
|
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(())
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -3148,7 +3148,7 @@ fn fork_non_local_fork_marker_direct() -> Result<()> {
|
||||||
|
|
||||||
----- stderr -----
|
----- stderr -----
|
||||||
× No solution found when resolving dependencies:
|
× 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.
|
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 -----
|
----- stderr -----
|
||||||
× No solution found when resolving dependencies:
|
× 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:
|
╰─▶ 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.
|
||||||
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.
|
|
||||||
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.
|
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.
|
||||||
"
|
"
|
||||||
);
|
);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue