Fix pre-release exclusive comparison operator in uv-pep440 (#12836)

From PEP 440:

> The exclusive ordered comparison <V MUST NOT allow a pre-release of
the specified version unless the specified version is itself a
pre-release. Allowing pre-releases that are earlier than, but not equal
to a specific pre-release may be accomplished by using <V.rc1 or
similar.

We had an additional check that would block this even if the specifier
did have a pre-release.

This likely didn't show up earlier because `Ranges` uses different code
in the resolver.

I checked these changes against `packaging` to verify their behavior:

```python
print(SpecifierSet("<1").contains("1a1", prereleases=True)) # False
print(SpecifierSet("<1a2").contains("1a1", prereleases=True)) # True
print(SpecifierSet("<1").contains("1dev1", prereleases=True)) # False
print(SpecifierSet("<1dev2").contains("1dev1", prereleases=True)) # True
print(SpecifierSet("<1a2").contains("1dev1", prereleases=True)) # True
```

Closes #12834
This commit is contained in:
konsti 2025-04-12 22:57:06 +02:00 committed by GitHub
parent dd788a0f47
commit 6b7f60c1ea
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
1 changed files with 16 additions and 17 deletions

View File

@ -554,16 +554,9 @@ impl VersionSpecifier {
other.as_ref() >= this other.as_ref() >= this
} }
Operator::GreaterThan => Self::greater_than(this, &other), Operator::GreaterThan => Self::greater_than(this, &other),
Operator::GreaterThanEqual => { Operator::GreaterThanEqual => other.as_ref() >= this,
Self::greater_than(this, &other) || other.as_ref() >= this Operator::LessThan => Self::less_than(this, &other),
} Operator::LessThanEqual => other.as_ref() <= this,
Operator::LessThan => {
Self::less_than(this, &other)
&& !(version::compare_release(&this.release(), &other.release())
== Ordering::Equal
&& other.any_prerelease())
}
Operator::LessThanEqual => Self::less_than(this, &other) || other.as_ref() <= this,
} }
} }
@ -572,13 +565,13 @@ impl VersionSpecifier {
return true; return true;
} }
// This special case is here so that, unless the specifier itself // The exclusive ordered comparison <V MUST NOT allow a pre-release of the specified
// includes is a pre-release version, that we do not accept pre-release // version unless the specified version is itself a pre-release. E.g., <3.1 should
// versions for the version mentioned in the specifier (e.g. <3.1 should // not match 3.1.dev0, but should match both 3.0.dev0 and 3.0, while <3.1.dev1 does match
// not match 3.1.dev0, but should match 3.0.dev0). // 3.1.dev0, 3.0.dev0 and 3.0.
if !this.any_prerelease() if version::compare_release(&this.release(), &other.release()) == Ordering::Equal
&& other.is_pre() && !this.any_prerelease()
&& version::compare_release(&this.release(), &other.release()) == Ordering::Equal && other.any_prerelease()
{ {
return false; return false;
} }
@ -1172,10 +1165,15 @@ mod tests {
("2.0.1", ">2"), ("2.0.1", ">2"),
("2.1.post1", ">2"), ("2.1.post1", ">2"),
("2.1+local.version", ">2"), ("2.1+local.version", ">2"),
("2.post2", ">2.post1"),
// Test the less than operation // Test the less than operation
("1", "<2"), ("1", "<2"),
("2.0", "<2.1"), ("2.0", "<2.1"),
("2.0.dev0", "<2.1"), ("2.0.dev0", "<2.1"),
// https://github.com/astral-sh/uv/issues/12834
("0.1a1", "<0.1a2"),
("0.1dev1", "<0.1dev2"),
("0.1dev1", "<0.1a1"),
// Test the compatibility operation // Test the compatibility operation
("1", "~=1.0"), ("1", "~=1.0"),
("1.0.1", "~=1.0"), ("1.0.1", "~=1.0"),
@ -1271,6 +1269,7 @@ mod tests {
("2.0c1.post1.dev1", ">2"), ("2.0c1.post1.dev1", ">2"),
("2.0rc1", ">2"), ("2.0rc1", ">2"),
("2.0", ">2"), ("2.0", ">2"),
("2.post2", ">2"),
("2.0.post1", ">2"), ("2.0.post1", ">2"),
("2.0.post1.dev1", ">2"), ("2.0.post1.dev1", ">2"),
("2.0+local.version", ">2"), ("2.0+local.version", ">2"),