From 135fe791f74b22fb5bba50bed0c7bce25d3b8415 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Mon, 1 Jul 2024 17:23:43 -0400 Subject: [PATCH] Fork resolution when Python requirement is narrowed --- crates/uv-resolver/src/resolver/mod.rs | 25 +++++++++++++++---- crates/uv/tests/branching_urls.rs | 26 +++----------------- crates/uv/tests/pip_compile.rs | 34 +++++++++++++++++++++++++- 3 files changed, 57 insertions(+), 28 deletions(-) diff --git a/crates/uv-resolver/src/resolver/mod.rs b/crates/uv-resolver/src/resolver/mod.rs index 411e190f5a01..c1b9f8f2d68a 100644 --- a/crates/uv-resolver/src/resolver/mod.rs +++ b/crates/uv-resolver/src/resolver/mod.rs @@ -555,7 +555,7 @@ impl ResolverState= 2); + // This is a somewhat tortured technique to ensure // that our resolver state is only cloned as much // as it needs to be. We basically move the state @@ -2422,7 +2422,6 @@ impl Dependencies { continue; } }; - assert!(fork_groups.forks.len() >= 2, "expected definitive fork"); let mut new_forks: Vec = vec![]; for group in fork_groups.forks { let mut new_forks_for_group = forks.clone(); @@ -2600,7 +2599,7 @@ impl<'a> PossibleForks<'a> { let PossibleForks::PossiblyForking(ref fork_groups) = *self else { return false; }; - fork_groups.forks.len() > 1 + fork_groups.has_fork() } /// Consumes this possible set of forks and converts a "possibly forking" @@ -2613,9 +2612,8 @@ impl<'a> PossibleForks<'a> { let PossibleForks::PossiblyForking(ref fork_groups) = self else { return self; }; - if fork_groups.forks.len() == 1 { + if !fork_groups.has_fork() { self.make_no_forks_possible(); - return self; } self } @@ -2678,6 +2676,23 @@ impl<'a> PossibleForkGroups<'a> { .iter_mut() .find(|fork| fork.is_overlapping(marker)) } + + /// Returns `true` if the fork group has a fork. + fn has_fork(&self) -> bool { + if self.forks.len() > 1 { + return true; + } + + if self.forks.iter().any(|fork| { + fork.packages + .iter() + .any(|(_, markers)| requires_python_marker(markers).is_some()) + }) { + return true; + } + + false + } } /// Intermediate state representing a single possible fork. diff --git a/crates/uv/tests/branching_urls.rs b/crates/uv/tests/branching_urls.rs index c0858105fabd..6e7518243856 100644 --- a/crates/uv/tests/branching_urls.rs +++ b/crates/uv/tests/branching_urls.rs @@ -90,7 +90,7 @@ fn branching_urls_overlapping() -> Result<()> { ----- stdout ----- ----- stderr ----- - error: Requirements contain conflicting URLs for package `iniconfig`: + error: Requirements contain conflicting URLs for package `iniconfig` in split `python_version < '3.12' and python_version >= '3.11'`: - https://files.pythonhosted.org/packages/9b/dd/b3c12c6d707058fa947864b67f0c4e0c39ef8610988d7baea9578f3c48f3/iniconfig-1.1.1-py2.py3-none-any.whl - https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl "### @@ -381,7 +381,7 @@ fn root_package_splits_other_dependencies_too() -> Result<()> { ----- stdout ----- ----- stderr ----- - Resolved 9 packages in [TIME] + Resolved 7 packages in [TIME] "### ); @@ -397,7 +397,6 @@ fn root_package_splits_other_dependencies_too() -> Result<()> { { name = "anyio", version = "4.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_version < '3.12'" }, { name = "anyio", version = "4.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_version >= '3.12'" }, { name = "b1", marker = "python_version < '3.12'" }, - { name = "b2", marker = "python_version >= '3.12'" }, ] [[distribution]] @@ -431,15 +430,7 @@ fn root_package_splits_other_dependencies_too() -> Result<()> { version = "0.1.0" source = { directory = "b1" } dependencies = [ - { name = "iniconfig", version = "1.1.1", source = { registry = "https://pypi.org/simple" } }, - ] - - [[distribution]] - name = "b2" - version = "0.1.0" - source = { directory = "b2" } - dependencies = [ - { name = "iniconfig", version = "2.0.0", source = { registry = "https://pypi.org/simple" } }, + { name = "iniconfig" }, ] [[distribution]] @@ -460,15 +451,6 @@ fn root_package_splits_other_dependencies_too() -> Result<()> { { url = "https://files.pythonhosted.org/packages/9b/dd/b3c12c6d707058fa947864b67f0c4e0c39ef8610988d7baea9578f3c48f3/iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3", size = 4990 }, ] - [[distribution]] - name = "iniconfig" - version = "2.0.0" - source = { registry = "https://pypi.org/simple" } - sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } - wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, - ] - [[distribution]] name = "sniffio" version = "1.3.1" @@ -637,7 +619,7 @@ fn branching_urls_of_different_sources_conflict() -> Result<()> { ----- stdout ----- ----- stderr ----- - error: Requirements contain conflicting URLs for package `iniconfig`: + error: Requirements contain conflicting URLs for package `iniconfig` in split `python_version < '3.12' and python_version >= '3.11'`: - git+https://github.com/pytest-dev/iniconfig@93f5930e668c0d1ddf4597e38dd0dea4e2665e7a - https://files.pythonhosted.org/packages/9b/dd/b3c12c6d707058fa947864b67f0c4e0c39ef8610988d7baea9578f3c48f3/iniconfig-1.1.1-py2.py3-none-any.whl "### diff --git a/crates/uv/tests/pip_compile.rs b/crates/uv/tests/pip_compile.rs index cc557a6a6f05..6cdf82c83d84 100644 --- a/crates/uv/tests/pip_compile.rs +++ b/crates/uv/tests/pip_compile.rs @@ -6567,7 +6567,7 @@ fn universal_multi_version() -> Result<()> { /// Perform a universal resolution that requires narrowing the supported Python range in one of the /// fork branches. #[test] -fn universal_requires_python() -> Result<()> { +fn universal_requires_python_fork() -> Result<()> { let context = TestContext::new("3.12"); let requirements_in = context.temp_dir.child("requirements.in"); requirements_in.write_str(indoc::indoc! {r" @@ -6599,6 +6599,38 @@ fn universal_requires_python() -> Result<()> { Ok(()) } +/// Perform a universal resolution that requires narrowing the supported Python range without +/// forking. +#[test] +fn universal_requires_python_non_fork() -> Result<()> { + let context = TestContext::new("3.12"); + let requirements_in = context.temp_dir.child("requirements.in"); + requirements_in.write_str(indoc::indoc! {r" + numpy >=1.26 ; python_version >= '3.9' + "})?; + + uv_snapshot!(context.filters(), windows_filters=false, context.pip_compile() + .arg("requirements.in") + .arg("-p") + .arg("3.8") + .arg("--universal"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + # This file was autogenerated by uv via the following command: + # uv pip compile --cache-dir [CACHE_DIR] requirements.in -p 3.8 --universal + numpy==1.26.4 ; python_version >= '3.9' + # via -r requirements.in + + ----- stderr ----- + warning: The requested Python version 3.8 is not available; 3.12.[X] will be used to build dependencies instead. + Resolved 1 package in [TIME] + "### + ); + + Ok(()) +} + /// Resolve a package from a `requirements.in` file, with a `constraints.txt` file pinning one of /// its transitive dependencies to a specific version. #[test]