Skip to content

Commit

Permalink
Respect pre-release preferences from input files (astral-sh#5736)
Browse files Browse the repository at this point in the history
## Summary

Right now, if you have a `requirements.txt` with a pre-release, but the
`requirements.in` does not have a pre-release marker for that dependency
we drop the pre-release. (In the selector, we end up returning
`AllowPrerelease::IfNecessary`, the default.)

I played with a few ways of solving this... The first was to remove that
guard altogether. But if we do that,
`universal_transitive_disjoint_prerelease_requirement` fails (we use
`1.17.0rc1` in both forks, when it should only apply to one of the two).

The second was to do that, but also avoid pushing pre-releases as
preferences when we solve a fork. But then
`universal_disjoint_prereleases` fails, because we return a different
pre-release in each fork.

Finally, I settled on allowing existing pre-releases in forks if they
have no markers on them, i.e., they are "global" preferences. I believe
this is true IFF the preference came from an existing lockfile.

Closes astral-sh#5729.
  • Loading branch information
charliermarsh authored Aug 3, 2024
1 parent 030d477 commit 35b9824
Show file tree
Hide file tree
Showing 3 changed files with 181 additions and 12 deletions.
19 changes: 13 additions & 6 deletions crates/uv-resolver/src/candidate_selector.rs
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ impl CandidateSelector {
exclusions: &Exclusions,
resolver_markers: &ResolverMarkers,
) -> Option<Candidate<'a>> {
for (_marker, version) in preferences {
for (marker, version) in preferences {
// Respect the version range for this requirement.
if !range.contains(version) {
continue;
Expand Down Expand Up @@ -240,13 +240,20 @@ impl CandidateSelector {
}

// Respect the pre-release strategy for this fork.
if version.any_prerelease()
&& self
if version.any_prerelease() {
let allow = match self
.prerelease_strategy
.allows(package_name, resolver_markers)
!= AllowPrerelease::Yes
{
continue;
{
AllowPrerelease::Yes => true,
AllowPrerelease::No => false,
// If the pre-release is "global" (i.e., provided via a lockfile, rather than
// a fork), accept it unless pre-releases are completely banned.
AllowPrerelease::IfNecessary => marker.is_none(),
};
if !allow {
continue;
}
}

// Check for a remote distribution that matches the preferred version
Expand Down
2 changes: 1 addition & 1 deletion crates/uv-resolver/src/requires_python.rs
Original file line number Diff line number Diff line change
Expand Up @@ -494,7 +494,7 @@ mod tests {
];
for (i, v1) in versions.iter().enumerate() {
for v2 in &versions[i + 1..] {
assert_eq!(v1.cmp(v2), Ordering::Less, "less: {v1:?}\ngreater: {v2:?}",);
assert_eq!(v1.cmp(v2), Ordering::Less, "less: {v1:?}\ngreater: {v2:?}");
}
}
}
Expand Down
172 changes: 167 additions & 5 deletions crates/uv/tests/pip_compile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7522,6 +7522,46 @@ fn universal_nested_disjoint_local_requirement() -> Result<()> {
Ok(())
}

/// Respect an existing pre-release preference, even if preferences aren't enabled.
#[test]
fn existing_prerelease_preference() -> Result<()> {
static EXCLUDE_NEWER: &str = "2024-07-17T00:00:00Z";

let context = TestContext::new("3.12");
let requirements_in = context.temp_dir.child("requirements.in");
requirements_in.write_str(indoc::indoc! {r"
cffi
"})?;

let requirements_txt = context.temp_dir.child("requirements.txt");
requirements_txt.write_str(indoc::indoc! {r"
cffi==1.17.0rc1
pyparser==2.22
"})?;

uv_snapshot!(context.filters(), windows_filters=false, context.pip_compile()
.env("UV_EXCLUDE_NEWER", EXCLUDE_NEWER)
.arg("requirements.in")
.arg("-o")
.arg("requirements.txt"), @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 -o requirements.txt
cffi==1.17.0rc1
# via -r requirements.in
pycparser==2.22
# via cffi
----- stderr -----
Resolved 2 packages in [TIME]
"###
);

Ok(())
}

/// Requested distinct pre-release strategies with disjoint markers.
#[test]
fn universal_disjoint_prereleases() -> Result<()> {
Expand All @@ -7530,8 +7570,8 @@ fn universal_disjoint_prereleases() -> Result<()> {
let context = TestContext::new("3.12");
let requirements_in = context.temp_dir.child("requirements.in");
requirements_in.write_str(indoc::indoc! {r"
cffi ; os_name == 'linux'
cffi >= 1.17.0rc1 ; os_name != 'linux'
cffi >= 1.16.0rc1 ; os_name != 'linux'
cffi >= 1.16.0rc1, <1.16.0rc2 ; os_name == 'linux'
"})?;

uv_snapshot!(context.filters(), windows_filters=false, context.pip_compile()
Expand All @@ -7543,15 +7583,137 @@ fn universal_disjoint_prereleases() -> Result<()> {
----- stdout -----
# This file was autogenerated by uv via the following command:
# uv pip compile --cache-dir [CACHE_DIR] requirements.in --universal
cffi==1.16.0 ; os_name == 'linux'
cffi==1.16.0rc1
# via -r requirements.in
cffi==1.17.0rc1 ; os_name != 'linux'
pycparser==2.22
# via cffi
----- stderr -----
Resolved 2 packages in [TIME]
"###
);

Ok(())
}

/// Requested distinct pre-release strategies with disjoint markers.
#[test]
fn universal_disjoint_prereleases_preference() -> Result<()> {
static EXCLUDE_NEWER: &str = "2024-07-17T00:00:00Z";

let context = TestContext::new("3.12");
let requirements_in = context.temp_dir.child("requirements.in");
requirements_in.write_str(indoc::indoc! {r"
cffi ; os_name != 'linux'
cffi > 1.16.0 ; os_name == 'linux'
"})?;

let requirements_txt = context.temp_dir.child("requirements.txt");
requirements_txt.write_str(indoc::indoc! {r"
cffi==1.17.0rc1
pyparser==2.22
"})?;

uv_snapshot!(context.filters(), windows_filters=false, context.pip_compile()
.env("UV_EXCLUDE_NEWER", EXCLUDE_NEWER)
.arg("requirements.in")
.arg("-o")
.arg("requirements.txt")
.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 -o requirements.txt --universal
cffi==1.17.0rc1
# via -r requirements.in
pycparser==2.22
# via cffi
----- stderr -----
Resolved 3 packages in [TIME]
Resolved 2 packages in [TIME]
"###
);

Ok(())
}

/// Requested distinct pre-release strategies with disjoint markers.
///
/// TODO(charlie): This should resolve to two different `cffi` versions, one for each fork.
#[test]
fn universal_disjoint_prereleases_preference_marker() -> Result<()> {
static EXCLUDE_NEWER: &str = "2024-07-17T00:00:00Z";

let context = TestContext::new("3.12");
let requirements_in = context.temp_dir.child("requirements.in");
requirements_in.write_str(indoc::indoc! {r"
cffi ; os_name != 'linux'
cffi >= 1.16.0rc1 ; os_name == 'linux'
"})?;

let requirements_txt = context.temp_dir.child("requirements.txt");
requirements_txt.write_str(indoc::indoc! {r"
cffi==1.16.0 ; os_name != 'linux'
cffi==1.16.0rc1 ; os_name == 'linux'
pyparser==2.22
"})?;

uv_snapshot!(context.filters(), windows_filters=false, context.pip_compile()
.env("UV_EXCLUDE_NEWER", EXCLUDE_NEWER)
.arg("requirements.in")
.arg("-o")
.arg("requirements.txt")
.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 -o requirements.txt --universal
cffi==1.16.0
# via -r requirements.in
pycparser==2.22
# via cffi
----- stderr -----
Resolved 2 packages in [TIME]
"###
);

Ok(())
}

/// Resolve to a single version as `--prerelease=allow` is provided, even though the first branch
/// doesn't include a pre-release marker.
#[test]
fn universal_disjoint_prereleases_allow() -> Result<()> {
static EXCLUDE_NEWER: &str = "2024-07-17T00:00:00Z";

let context = TestContext::new("3.12");
let requirements_in = context.temp_dir.child("requirements.in");
requirements_in.write_str(indoc::indoc! {r"
cffi >= 1.15.0, < 1.17.0 ; os_name == 'linux'
cffi >= 1.15.0, <= 1.16.0rc2 ; os_name != 'linux'
"})?;

uv_snapshot!(context.filters(), windows_filters=false, context.pip_compile()
.env("UV_EXCLUDE_NEWER", EXCLUDE_NEWER)
.arg("requirements.in")
.arg("--universal")
.arg("--prerelease")
.arg("allow"), @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 --universal --prerelease allow
cffi==1.16.0rc2
# via -r requirements.in
pycparser==2.22
# via cffi
----- stderr -----
Resolved 2 packages in [TIME]
"###
);

Expand Down

0 comments on commit 35b9824

Please sign in to comment.