Skip to content

Commit

Permalink
Fork resolution when Python requirement is narrowed
Browse files Browse the repository at this point in the history
  • Loading branch information
charliermarsh committed Jul 1, 2024
1 parent b3797db commit 135fe79
Show file tree
Hide file tree
Showing 3 changed files with 57 additions and 28 deletions.
25 changes: 20 additions & 5 deletions crates/uv-resolver/src/resolver/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -555,7 +555,7 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
.map(ToString::to_string)
.join(", ")
);
assert!(forks.len() >= 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
Expand Down Expand Up @@ -2422,7 +2422,6 @@ impl Dependencies {
continue;
}
};
assert!(fork_groups.forks.len() >= 2, "expected definitive fork");
let mut new_forks: Vec<Fork> = vec![];
for group in fork_groups.forks {
let mut new_forks_for_group = forks.clone();
Expand Down Expand Up @@ -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"
Expand All @@ -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
}
Expand Down Expand Up @@ -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.
Expand Down
26 changes: 4 additions & 22 deletions crates/uv/tests/branching_urls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
"###
Expand Down Expand Up @@ -381,7 +381,7 @@ fn root_package_splits_other_dependencies_too() -> Result<()> {
----- stdout -----
----- stderr -----
Resolved 9 packages in [TIME]
Resolved 7 packages in [TIME]
"###
);

Expand All @@ -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]]
Expand Down Expand Up @@ -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]]
Expand All @@ -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"
Expand Down Expand Up @@ -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
"###
Expand Down
34 changes: 33 additions & 1 deletion crates/uv/tests/pip_compile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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]
Expand Down

0 comments on commit 135fe79

Please sign in to comment.