mirror of https://github.com/astral-sh/uv
Make < exclusive for non-prerelease markers (#1878)
## Summary Even when pre-releases are "allowed", per PEP 440, `pydantic<2.0.0` should _not_ include pre-releases. This PR modifies the specifier translation to treat `pydantic<2.0.0` as `pydantic<2.0.0.min0`, where `min` is an internal-only version segment that's invisible to users. Closes https://github.com/astral-sh/uv/issues/1641.
This commit is contained in:
parent
53a250714c
commit
8d706b0f2a
|
|
@ -385,6 +385,19 @@ impl Version {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the min-release part of this version, if it exists.
|
||||||
|
///
|
||||||
|
/// The "min" component is internal-only, and does not exist in PEP 440.
|
||||||
|
/// The version `1.0min0` is smaller than all other `1.0` versions,
|
||||||
|
/// like `1.0a1`, `1.0dev0`, etc.
|
||||||
|
#[inline]
|
||||||
|
pub fn min(&self) -> Option<u64> {
|
||||||
|
match *self.inner {
|
||||||
|
VersionInner::Small { ref small } => small.min(),
|
||||||
|
VersionInner::Full { ref full } => full.min,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Set the release numbers and return the updated version.
|
/// Set the release numbers and return the updated version.
|
||||||
///
|
///
|
||||||
/// Usually one can just use `Version::new` to create a new version with
|
/// Usually one can just use `Version::new` to create a new version with
|
||||||
|
|
@ -512,6 +525,22 @@ impl Version {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Set the min-release component and return the updated version.
|
||||||
|
///
|
||||||
|
/// The "min" component is internal-only, and does not exist in PEP 440.
|
||||||
|
/// The version `1.0min0` is smaller than all other `1.0` versions,
|
||||||
|
/// like `1.0a1`, `1.0dev0`, etc.
|
||||||
|
#[inline]
|
||||||
|
pub fn with_min(mut self, value: Option<u64>) -> Version {
|
||||||
|
if let VersionInner::Small { ref mut small } = Arc::make_mut(&mut self.inner) {
|
||||||
|
if small.set_min(value) {
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.make_full().min = value;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Convert this version to a "full" representation in-place and return a
|
/// Convert this version to a "full" representation in-place and return a
|
||||||
/// mutable borrow to the full type.
|
/// mutable borrow to the full type.
|
||||||
fn make_full(&mut self) -> &mut VersionFull {
|
fn make_full(&mut self) -> &mut VersionFull {
|
||||||
|
|
@ -519,6 +548,7 @@ impl Version {
|
||||||
let full = VersionFull {
|
let full = VersionFull {
|
||||||
epoch: small.epoch(),
|
epoch: small.epoch(),
|
||||||
release: small.release().to_vec(),
|
release: small.release().to_vec(),
|
||||||
|
min: small.min(),
|
||||||
pre: small.pre(),
|
pre: small.pre(),
|
||||||
post: small.post(),
|
post: small.post(),
|
||||||
dev: small.dev(),
|
dev: small.dev(),
|
||||||
|
|
@ -744,10 +774,13 @@ impl FromStr for Version {
|
||||||
/// * Bytes 5, 4 and 3 correspond to the second, third and fourth release
|
/// * Bytes 5, 4 and 3 correspond to the second, third and fourth release
|
||||||
/// segments, respectively.
|
/// segments, respectively.
|
||||||
/// * Bytes 2, 1 and 0 represent *one* of the following:
|
/// * Bytes 2, 1 and 0 represent *one* of the following:
|
||||||
/// `.devN, aN, bN, rcN, <no suffix>, .postN`. Its representation is thus:
|
/// `min, .devN, aN, bN, rcN, <no suffix>, .postN`.
|
||||||
|
/// Its representation is thus:
|
||||||
/// * The most significant 3 bits of Byte 2 corresponds to a value in
|
/// * The most significant 3 bits of Byte 2 corresponds to a value in
|
||||||
/// the range 0-5 inclusive, corresponding to dev, pre-a, pre-b, pre-rc,
|
/// the range 0-6 inclusive, corresponding to min, dev, pre-a, pre-b, pre-rc,
|
||||||
/// no-suffix or post releases, respectively.
|
/// no-suffix or post releases, respectively. `min` is a special version that
|
||||||
|
/// does not exist in PEP 440, but is used here to represent the smallest
|
||||||
|
/// possible version, preceding any `dev`, `pre`, `post` or releases.
|
||||||
/// * The low 5 bits combined with the bits in bytes 1 and 0 correspond
|
/// * The low 5 bits combined with the bits in bytes 1 and 0 correspond
|
||||||
/// to the release number of the suffix, if one exists. If there is no
|
/// to the release number of the suffix, if one exists. If there is no
|
||||||
/// suffix, then this bits are always 0.
|
/// suffix, then this bits are always 0.
|
||||||
|
|
@ -810,18 +843,19 @@ struct VersionSmall {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl VersionSmall {
|
impl VersionSmall {
|
||||||
const SUFFIX_DEV: u64 = 0;
|
const SUFFIX_MIN: u64 = 0;
|
||||||
const SUFFIX_PRE_ALPHA: u64 = 1;
|
const SUFFIX_DEV: u64 = 1;
|
||||||
const SUFFIX_PRE_BETA: u64 = 2;
|
const SUFFIX_PRE_ALPHA: u64 = 2;
|
||||||
const SUFFIX_PRE_RC: u64 = 3;
|
const SUFFIX_PRE_BETA: u64 = 3;
|
||||||
const SUFFIX_NONE: u64 = 4;
|
const SUFFIX_PRE_RC: u64 = 4;
|
||||||
const SUFFIX_POST: u64 = 5;
|
const SUFFIX_NONE: u64 = 5;
|
||||||
|
const SUFFIX_POST: u64 = 6;
|
||||||
const SUFFIX_MAX_VERSION: u64 = 0x1FFFFF;
|
const SUFFIX_MAX_VERSION: u64 = 0x1FFFFF;
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn new() -> VersionSmall {
|
fn new() -> VersionSmall {
|
||||||
VersionSmall {
|
VersionSmall {
|
||||||
repr: 0x00000000_00800000,
|
repr: 0x00000000_00A00000,
|
||||||
release: [0, 0, 0, 0],
|
release: [0, 0, 0, 0],
|
||||||
len: 0,
|
len: 0,
|
||||||
}
|
}
|
||||||
|
|
@ -888,7 +922,7 @@ impl VersionSmall {
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn set_post(&mut self, value: Option<u64>) -> bool {
|
fn set_post(&mut self, value: Option<u64>) -> bool {
|
||||||
if self.pre().is_some() || self.dev().is_some() {
|
if self.min().is_some() || self.pre().is_some() || self.dev().is_some() {
|
||||||
return value.is_none();
|
return value.is_none();
|
||||||
}
|
}
|
||||||
match value {
|
match value {
|
||||||
|
|
@ -931,7 +965,7 @@ impl VersionSmall {
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn set_pre(&mut self, value: Option<PreRelease>) -> bool {
|
fn set_pre(&mut self, value: Option<PreRelease>) -> bool {
|
||||||
if self.dev().is_some() || self.post().is_some() {
|
if self.min().is_some() || self.dev().is_some() || self.post().is_some() {
|
||||||
return value.is_none();
|
return value.is_none();
|
||||||
}
|
}
|
||||||
match value {
|
match value {
|
||||||
|
|
@ -970,7 +1004,7 @@ impl VersionSmall {
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn set_dev(&mut self, value: Option<u64>) -> bool {
|
fn set_dev(&mut self, value: Option<u64>) -> bool {
|
||||||
if self.pre().is_some() || self.post().is_some() {
|
if self.min().is_some() || self.pre().is_some() || self.post().is_some() {
|
||||||
return value.is_none();
|
return value.is_none();
|
||||||
}
|
}
|
||||||
match value {
|
match value {
|
||||||
|
|
@ -988,6 +1022,35 @@ impl VersionSmall {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn min(&self) -> Option<u64> {
|
||||||
|
if self.suffix_kind() == VersionSmall::SUFFIX_MIN {
|
||||||
|
Some(self.suffix_version())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn set_min(&mut self, value: Option<u64>) -> bool {
|
||||||
|
if self.dev().is_some() || self.pre().is_some() || self.post().is_some() {
|
||||||
|
return value.is_none();
|
||||||
|
}
|
||||||
|
match value {
|
||||||
|
None => {
|
||||||
|
self.set_suffix_kind(VersionSmall::SUFFIX_NONE);
|
||||||
|
}
|
||||||
|
Some(number) => {
|
||||||
|
if number > VersionSmall::SUFFIX_MAX_VERSION {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
self.set_suffix_kind(VersionSmall::SUFFIX_MIN);
|
||||||
|
self.set_suffix_version(number);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn local(&self) -> &[LocalSegment] {
|
fn local(&self) -> &[LocalSegment] {
|
||||||
// A "small" version is never used if the version has a non-zero number
|
// A "small" version is never used if the version has a non-zero number
|
||||||
|
|
@ -1079,6 +1142,10 @@ struct VersionFull {
|
||||||
/// > Local version labels have no specific semantics assigned, but
|
/// > Local version labels have no specific semantics assigned, but
|
||||||
/// > some syntactic restrictions are imposed.
|
/// > some syntactic restrictions are imposed.
|
||||||
local: Vec<LocalSegment>,
|
local: Vec<LocalSegment>,
|
||||||
|
/// An internal-only segment that does not exist in PEP 440, used to
|
||||||
|
/// represent the smallest possible version of a release, preceding any
|
||||||
|
/// `dev`, `pre`, `post` or releases.
|
||||||
|
min: Option<u64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A version number pattern.
|
/// A version number pattern.
|
||||||
|
|
@ -1410,7 +1477,7 @@ impl<'a> Parser<'a> {
|
||||||
| (u64::from(release[1]) << 40)
|
| (u64::from(release[1]) << 40)
|
||||||
| (u64::from(release[2]) << 32)
|
| (u64::from(release[2]) << 32)
|
||||||
| (u64::from(release[3]) << 24)
|
| (u64::from(release[3]) << 24)
|
||||||
| (0x80 << 16)
|
| (0xA0 << 16)
|
||||||
| (0x00 << 8)
|
| (0x00 << 8)
|
||||||
| (0x00 << 0),
|
| (0x00 << 0),
|
||||||
release: [
|
release: [
|
||||||
|
|
@ -2243,9 +2310,9 @@ pub(crate) fn compare_release(this: &[u64], other: &[u64]) -> Ordering {
|
||||||
/// According to [a summary of permitted suffixes and relative
|
/// According to [a summary of permitted suffixes and relative
|
||||||
/// ordering][pep440-suffix-ordering] the order of pre/post-releases is: .devN,
|
/// ordering][pep440-suffix-ordering] the order of pre/post-releases is: .devN,
|
||||||
/// aN, bN, rcN, <no suffix (final)>, .postN but also, you can have dev/post
|
/// aN, bN, rcN, <no suffix (final)>, .postN but also, you can have dev/post
|
||||||
/// releases on beta releases, so we make a three stage ordering: ({dev: 0, a:
|
/// releases on beta releases, so we make a three stage ordering: ({min: 0,
|
||||||
/// 1, b: 2, rc: 3, (): 4, post: 5}, <preN>, <postN or None as smallest>, <devN
|
/// dev: 1, a: 2, b: 3, rc: 4, (): 5, post: 6}, <preN>, <postN or None as
|
||||||
/// or Max as largest>, <local>)
|
/// smallest>, <devN or Max as largest>, <local>)
|
||||||
///
|
///
|
||||||
/// For post, any number is better than none (so None defaults to None<0),
|
/// For post, any number is better than none (so None defaults to None<0),
|
||||||
/// but for dev, no number is better (so None default to the maximum). For
|
/// but for dev, no number is better (so None default to the maximum). For
|
||||||
|
|
@ -2254,9 +2321,11 @@ pub(crate) fn compare_release(this: &[u64], other: &[u64]) -> Ordering {
|
||||||
///
|
///
|
||||||
/// [pep440-suffix-ordering]: https://peps.python.org/pep-0440/#summary-of-permitted-suffixes-and-relative-ordering
|
/// [pep440-suffix-ordering]: https://peps.python.org/pep-0440/#summary-of-permitted-suffixes-and-relative-ordering
|
||||||
fn sortable_tuple(version: &Version) -> (u64, u64, Option<u64>, u64, &[LocalSegment]) {
|
fn sortable_tuple(version: &Version) -> (u64, u64, Option<u64>, u64, &[LocalSegment]) {
|
||||||
match (version.pre(), version.post(), version.dev()) {
|
match (version.pre(), version.post(), version.dev(), version.min()) {
|
||||||
|
// min release
|
||||||
|
(_pre, post, _dev, Some(n)) => (0, 0, post, n, version.local()),
|
||||||
// dev release
|
// dev release
|
||||||
(None, None, Some(n)) => (0, 0, None, n, version.local()),
|
(None, None, Some(n), None) => (1, 0, None, n, version.local()),
|
||||||
// alpha release
|
// alpha release
|
||||||
(
|
(
|
||||||
Some(PreRelease {
|
Some(PreRelease {
|
||||||
|
|
@ -2265,7 +2334,8 @@ fn sortable_tuple(version: &Version) -> (u64, u64, Option<u64>, u64, &[LocalSegm
|
||||||
}),
|
}),
|
||||||
post,
|
post,
|
||||||
dev,
|
dev,
|
||||||
) => (1, n, post, dev.unwrap_or(u64::MAX), version.local()),
|
None,
|
||||||
|
) => (2, n, post, dev.unwrap_or(u64::MAX), version.local()),
|
||||||
// beta release
|
// beta release
|
||||||
(
|
(
|
||||||
Some(PreRelease {
|
Some(PreRelease {
|
||||||
|
|
@ -2274,7 +2344,8 @@ fn sortable_tuple(version: &Version) -> (u64, u64, Option<u64>, u64, &[LocalSegm
|
||||||
}),
|
}),
|
||||||
post,
|
post,
|
||||||
dev,
|
dev,
|
||||||
) => (2, n, post, dev.unwrap_or(u64::MAX), version.local()),
|
None,
|
||||||
|
) => (3, n, post, dev.unwrap_or(u64::MAX), version.local()),
|
||||||
// alpha release
|
// alpha release
|
||||||
(
|
(
|
||||||
Some(PreRelease {
|
Some(PreRelease {
|
||||||
|
|
@ -2283,11 +2354,14 @@ fn sortable_tuple(version: &Version) -> (u64, u64, Option<u64>, u64, &[LocalSegm
|
||||||
}),
|
}),
|
||||||
post,
|
post,
|
||||||
dev,
|
dev,
|
||||||
) => (3, n, post, dev.unwrap_or(u64::MAX), version.local()),
|
None,
|
||||||
|
) => (4, n, post, dev.unwrap_or(u64::MAX), version.local()),
|
||||||
// final release
|
// final release
|
||||||
(None, None, None) => (4, 0, None, 0, version.local()),
|
(None, None, None, None) => (5, 0, None, 0, version.local()),
|
||||||
// post release
|
// post release
|
||||||
(None, Some(post), dev) => (5, 0, Some(post), dev.unwrap_or(u64::MAX), version.local()),
|
(None, Some(post), dev, None) => {
|
||||||
|
(6, 0, Some(post), dev.unwrap_or(u64::MAX), version.local())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -3367,6 +3441,9 @@ mod tests {
|
||||||
])
|
])
|
||||||
);
|
);
|
||||||
assert_eq!(p(" \n5\n \t"), Version::new([5]));
|
assert_eq!(p(" \n5\n \t"), Version::new([5]));
|
||||||
|
|
||||||
|
// min tests
|
||||||
|
assert!(Parser::new("1.min0".as_bytes()).parse().is_err())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tests the error cases of our version parser.
|
// Tests the error cases of our version parser.
|
||||||
|
|
@ -3510,6 +3587,46 @@ mod tests {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn min_version() {
|
||||||
|
// Ensure that the `.min` suffix precedes all other suffixes.
|
||||||
|
let less = Version::new([1, 0]).with_min(Some(0));
|
||||||
|
|
||||||
|
let versions = &[
|
||||||
|
"1.dev0",
|
||||||
|
"1.0.dev456",
|
||||||
|
"1.0a1",
|
||||||
|
"1.0a2.dev456",
|
||||||
|
"1.0a12.dev456",
|
||||||
|
"1.0a12",
|
||||||
|
"1.0b1.dev456",
|
||||||
|
"1.0b2",
|
||||||
|
"1.0b2.post345.dev456",
|
||||||
|
"1.0b2.post345",
|
||||||
|
"1.0rc1.dev456",
|
||||||
|
"1.0rc1",
|
||||||
|
"1.0",
|
||||||
|
"1.0+abc.5",
|
||||||
|
"1.0+abc.7",
|
||||||
|
"1.0+5",
|
||||||
|
"1.0.post456.dev34",
|
||||||
|
"1.0.post456",
|
||||||
|
"1.0.15",
|
||||||
|
"1.1.dev1",
|
||||||
|
];
|
||||||
|
|
||||||
|
for greater in versions.iter() {
|
||||||
|
let greater = greater.parse::<Version>().unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
less.cmp(&greater),
|
||||||
|
Ordering::Less,
|
||||||
|
"less: {:?}\ngreater: {:?}",
|
||||||
|
less.as_bloated_debug(),
|
||||||
|
greater.as_bloated_debug()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Tests our bespoke u64 decimal integer parser.
|
// Tests our bespoke u64 decimal integer parser.
|
||||||
#[test]
|
#[test]
|
||||||
fn parse_number_u64() {
|
fn parse_number_u64() {
|
||||||
|
|
@ -3577,6 +3694,7 @@ mod tests {
|
||||||
.field("post", &self.0.post())
|
.field("post", &self.0.post())
|
||||||
.field("dev", &self.0.dev())
|
.field("dev", &self.0.dev())
|
||||||
.field("local", &self.0.local())
|
.field("local", &self.0.local())
|
||||||
|
.field("min", &self.0.min())
|
||||||
.finish()
|
.finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -521,7 +521,7 @@ impl CacheBucket {
|
||||||
CacheBucket::FlatIndex => "flat-index-v0",
|
CacheBucket::FlatIndex => "flat-index-v0",
|
||||||
CacheBucket::Git => "git-v0",
|
CacheBucket::Git => "git-v0",
|
||||||
CacheBucket::Interpreter => "interpreter-v0",
|
CacheBucket::Interpreter => "interpreter-v0",
|
||||||
CacheBucket::Simple => "simple-v2",
|
CacheBucket::Simple => "simple-v3",
|
||||||
CacheBucket::Wheels => "wheels-v0",
|
CacheBucket::Wheels => "wheels-v0",
|
||||||
CacheBucket::Archive => "archive-v0",
|
CacheBucket::Archive => "archive-v0",
|
||||||
}
|
}
|
||||||
|
|
@ -677,13 +677,13 @@ impl ArchiveTimestamp {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::cmp::PartialOrd for ArchiveTimestamp {
|
impl PartialOrd for ArchiveTimestamp {
|
||||||
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||||
Some(self.timestamp().cmp(&other.timestamp()))
|
Some(self.timestamp().cmp(&other.timestamp()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::cmp::Ord for ArchiveTimestamp {
|
impl Ord for ArchiveTimestamp {
|
||||||
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||||
self.timestamp().cmp(&other.timestamp())
|
self.timestamp().cmp(&other.timestamp())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -95,4 +95,15 @@ impl PreReleaseStrategy {
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns `true` if a [`PackageName`] is allowed to have pre-release versions.
|
||||||
|
pub(crate) fn allows(&self, package: &PackageName) -> bool {
|
||||||
|
match self {
|
||||||
|
Self::Disallow => false,
|
||||||
|
Self::Allow => true,
|
||||||
|
Self::IfNecessary => false,
|
||||||
|
Self::Explicit(packages) => packages.contains(package),
|
||||||
|
Self::IfNecessaryOrExplicit(packages) => packages.contains(package),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,6 @@ use rustc_hash::FxHashMap;
|
||||||
use uv_normalize::PackageName;
|
use uv_normalize::PackageName;
|
||||||
|
|
||||||
use crate::candidate_selector::CandidateSelector;
|
use crate::candidate_selector::CandidateSelector;
|
||||||
use crate::prerelease_mode::PreReleaseStrategy;
|
|
||||||
use crate::python_requirement::PythonRequirement;
|
use crate::python_requirement::PythonRequirement;
|
||||||
use crate::resolver::UnavailablePackage;
|
use crate::resolver::UnavailablePackage;
|
||||||
|
|
||||||
|
|
@ -346,25 +345,10 @@ impl PubGrubReportFormatter<'_> {
|
||||||
) -> IndexSet<PubGrubHint> {
|
) -> IndexSet<PubGrubHint> {
|
||||||
/// Returns `true` if pre-releases were allowed for a package.
|
/// Returns `true` if pre-releases were allowed for a package.
|
||||||
fn allowed_prerelease(package: &PubGrubPackage, selector: &CandidateSelector) -> bool {
|
fn allowed_prerelease(package: &PubGrubPackage, selector: &CandidateSelector) -> bool {
|
||||||
match selector.prerelease_strategy() {
|
let PubGrubPackage::Package(package, ..) = package else {
|
||||||
PreReleaseStrategy::Disallow => false,
|
return false;
|
||||||
PreReleaseStrategy::Allow => true,
|
};
|
||||||
PreReleaseStrategy::IfNecessary => false,
|
selector.prerelease_strategy().allows(package)
|
||||||
PreReleaseStrategy::Explicit(packages) => {
|
|
||||||
if let PubGrubPackage::Package(package, ..) = package {
|
|
||||||
packages.contains(package)
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
PreReleaseStrategy::IfNecessaryOrExplicit(packages) => {
|
|
||||||
if let PubGrubPackage::Package(package, ..) = package {
|
|
||||||
packages.contains(package)
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut hints = IndexSet::default();
|
let mut hints = IndexSet::default();
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@ impl TryFrom<&VersionSpecifier> for PubGrubSpecifier {
|
||||||
let [rest @ .., last, _] = specifier.version().release() else {
|
let [rest @ .., last, _] = specifier.version().release() else {
|
||||||
return Err(ResolveError::InvalidTildeEquals(specifier.clone()));
|
return Err(ResolveError::InvalidTildeEquals(specifier.clone()));
|
||||||
};
|
};
|
||||||
let upper = pep440_rs::Version::new(rest.iter().chain([&(last + 1)]))
|
let upper = Version::new(rest.iter().chain([&(last + 1)]))
|
||||||
.with_epoch(specifier.version().epoch())
|
.with_epoch(specifier.version().epoch())
|
||||||
.with_dev(Some(0));
|
.with_dev(Some(0));
|
||||||
let version = specifier.version().clone();
|
let version = specifier.version().clone();
|
||||||
|
|
@ -46,7 +46,14 @@ impl TryFrom<&VersionSpecifier> for PubGrubSpecifier {
|
||||||
}
|
}
|
||||||
Operator::LessThan => {
|
Operator::LessThan => {
|
||||||
let version = specifier.version().clone();
|
let version = specifier.version().clone();
|
||||||
|
if version.any_prerelease() {
|
||||||
Range::strictly_lower_than(version)
|
Range::strictly_lower_than(version)
|
||||||
|
} else {
|
||||||
|
// Per 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.
|
||||||
|
Range::strictly_lower_than(version.with_min(Some(0)))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Operator::LessThanEqual => {
|
Operator::LessThanEqual => {
|
||||||
let version = specifier.version().clone();
|
let version = specifier.version().clone();
|
||||||
|
|
|
||||||
|
|
@ -4268,3 +4268,77 @@ fn unsafe_package() -> Result<()> {
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Resolve a package with a strict upper bound, allowing pre-releases. Per PEP 440, pre-releases
|
||||||
|
/// that match the bound (e.g., `2.0.0rc1`) should be _not_ allowed.
|
||||||
|
#[test]
|
||||||
|
fn pre_release_upper_bound_exclude() -> Result<()> {
|
||||||
|
let context = TestContext::new("3.12");
|
||||||
|
let requirements_in = context.temp_dir.child("requirements.in");
|
||||||
|
requirements_in.write_str("flask<2.0.0")?;
|
||||||
|
|
||||||
|
uv_snapshot!(context.compile()
|
||||||
|
.arg("requirements.in")
|
||||||
|
.arg("--prerelease=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] --exclude-newer 2023-11-18T12:00:00Z requirements.in --prerelease=allow
|
||||||
|
click==7.1.2
|
||||||
|
# via flask
|
||||||
|
flask==1.1.4
|
||||||
|
itsdangerous==1.1.0
|
||||||
|
# via flask
|
||||||
|
jinja2==2.11.3
|
||||||
|
# via flask
|
||||||
|
markupsafe==2.1.3
|
||||||
|
# via jinja2
|
||||||
|
werkzeug==1.0.1
|
||||||
|
# via flask
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
Resolved 6 packages in [TIME]
|
||||||
|
"###
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resolve a package with a strict upper bound that includes a pre-release. Per PEP 440,
|
||||||
|
/// pre-releases _should_ be allowed.
|
||||||
|
#[test]
|
||||||
|
fn pre_release_upper_bound_include() -> Result<()> {
|
||||||
|
let context = TestContext::new("3.12");
|
||||||
|
let requirements_in = context.temp_dir.child("requirements.in");
|
||||||
|
requirements_in.write_str("flask<2.0.0rc4")?;
|
||||||
|
|
||||||
|
uv_snapshot!(context.compile()
|
||||||
|
.arg("requirements.in")
|
||||||
|
.arg("--prerelease=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] --exclude-newer 2023-11-18T12:00:00Z requirements.in --prerelease=allow
|
||||||
|
click==8.1.7
|
||||||
|
# via flask
|
||||||
|
flask==2.0.0rc2
|
||||||
|
itsdangerous==2.1.2
|
||||||
|
# via flask
|
||||||
|
jinja2==3.1.2
|
||||||
|
# via flask
|
||||||
|
markupsafe==2.1.3
|
||||||
|
# via
|
||||||
|
# jinja2
|
||||||
|
# werkzeug
|
||||||
|
werkzeug==3.0.1
|
||||||
|
# via flask
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
Resolved 6 packages in [TIME]
|
||||||
|
"###
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
//! DO NOT EDIT
|
//! DO NOT EDIT
|
||||||
//!
|
//!
|
||||||
//! Generated with ./scripts/scenarios/update.py
|
//! Generated with scripts/scenarios/update.py
|
||||||
//! Scenarios from <https://github.com/zanieb/packse/tree/de0bab473eeaa4445db5a8febd732c655fad3d52/scenarios>
|
//! Scenarios from <https://github.com/zanieb/packse/tree/4f39539c1b858e28268554604e75c69e25272e5a/scenarios>
|
||||||
//!
|
//!
|
||||||
#![cfg(all(feature = "python", feature = "pypi"))]
|
#![cfg(all(feature = "python", feature = "pypi"))]
|
||||||
|
|
||||||
|
|
@ -29,7 +29,7 @@ fn command(context: &TestContext, python_versions: &[&str]) -> Command {
|
||||||
.arg("--index-url")
|
.arg("--index-url")
|
||||||
.arg("https://test.pypi.org/simple")
|
.arg("https://test.pypi.org/simple")
|
||||||
.arg("--find-links")
|
.arg("--find-links")
|
||||||
.arg("https://raw.githubusercontent.com/zanieb/packse/de0bab473eeaa4445db5a8febd732c655fad3d52/vendor/links.html")
|
.arg("https://raw.githubusercontent.com/zanieb/packse/4f39539c1b858e28268554604e75c69e25272e5a/vendor/links.html")
|
||||||
.arg("--cache-dir")
|
.arg("--cache-dir")
|
||||||
.arg(context.cache_dir.path())
|
.arg(context.cache_dir.path())
|
||||||
.env("VIRTUAL_ENV", context.venv.as_os_str())
|
.env("VIRTUAL_ENV", context.venv.as_os_str())
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
//! DO NOT EDIT
|
//! DO NOT EDIT
|
||||||
//!
|
//!
|
||||||
//! Generated with ./scripts/scenarios/update.py
|
//! Generated with scripts/scenarios/update.py
|
||||||
//! Scenarios from <https://github.com/zanieb/packse/tree/de0bab473eeaa4445db5a8febd732c655fad3d52/scenarios>
|
//! Scenarios from <https://github.com/zanieb/packse/tree/4f39539c1b858e28268554604e75c69e25272e5a/scenarios>
|
||||||
//!
|
//!
|
||||||
#![cfg(all(feature = "python", feature = "pypi"))]
|
#![cfg(all(feature = "python", feature = "pypi"))]
|
||||||
|
|
||||||
|
|
@ -48,7 +48,7 @@ fn command(context: &TestContext) -> Command {
|
||||||
.arg("--index-url")
|
.arg("--index-url")
|
||||||
.arg("https://test.pypi.org/simple")
|
.arg("https://test.pypi.org/simple")
|
||||||
.arg("--find-links")
|
.arg("--find-links")
|
||||||
.arg("https://raw.githubusercontent.com/zanieb/packse/de0bab473eeaa4445db5a8febd732c655fad3d52/vendor/links.html")
|
.arg("https://raw.githubusercontent.com/zanieb/packse/4f39539c1b858e28268554604e75c69e25272e5a/vendor/links.html")
|
||||||
.arg("--cache-dir")
|
.arg("--cache-dir")
|
||||||
.arg(context.cache_dir.path())
|
.arg(context.cache_dir.path())
|
||||||
.env("VIRTUAL_ENV", context.venv.as_os_str())
|
.env("VIRTUAL_ENV", context.venv.as_os_str())
|
||||||
|
|
@ -486,7 +486,7 @@ fn dependency_excludes_range_of_compatible_versions() {
|
||||||
/// There is a non-contiguous range of compatible versions for the requested package
|
/// There is a non-contiguous range of compatible versions for the requested package
|
||||||
/// `a`, but another dependency `c` excludes the range. This is the same as
|
/// `a`, but another dependency `c` excludes the range. This is the same as
|
||||||
/// `dependency-excludes-range-of-compatible-versions` but some of the versions of
|
/// `dependency-excludes-range-of-compatible-versions` but some of the versions of
|
||||||
/// `a` are incompatible for another reason e.g. dependency on non-existent package
|
/// `a` are incompatible for another reason e.g. dependency on non-existant package
|
||||||
/// `d`.
|
/// `d`.
|
||||||
///
|
///
|
||||||
/// ```text
|
/// ```text
|
||||||
|
|
@ -2043,6 +2043,192 @@ fn transitive_prerelease_and_stable_dependency_many_versions_holes() {
|
||||||
assert_not_installed(&context.venv, "b_041e36bc", &context.temp_dir);
|
assert_not_installed(&context.venv, "b_041e36bc", &context.temp_dir);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// package-only-prereleases-boundary
|
||||||
|
///
|
||||||
|
/// The user requires a non-prerelease version of `a` which only has prerelease
|
||||||
|
/// versions available. There are pre-releases on the boundary of their range.
|
||||||
|
///
|
||||||
|
/// ```text
|
||||||
|
/// edcef999
|
||||||
|
/// ├── environment
|
||||||
|
/// │ └── python3.8
|
||||||
|
/// ├── root
|
||||||
|
/// │ └── requires a<0.2.0
|
||||||
|
/// │ └── unsatisfied: no matching version
|
||||||
|
/// └── a
|
||||||
|
/// ├── a-0.1.0a1
|
||||||
|
/// ├── a-0.2.0a1
|
||||||
|
/// └── a-0.3.0a1
|
||||||
|
/// ```
|
||||||
|
#[test]
|
||||||
|
fn package_only_prereleases_boundary() {
|
||||||
|
let context = TestContext::new("3.8");
|
||||||
|
|
||||||
|
// In addition to the standard filters, swap out package names for more realistic messages
|
||||||
|
let mut filters = INSTA_FILTERS.to_vec();
|
||||||
|
filters.push((r"a-edcef999", "albatross"));
|
||||||
|
filters.push((r"-edcef999", ""));
|
||||||
|
|
||||||
|
uv_snapshot!(filters, command(&context)
|
||||||
|
.arg("a-edcef999<0.2.0")
|
||||||
|
, @r###"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
Resolved 1 package in [TIME]
|
||||||
|
Downloaded 1 package in [TIME]
|
||||||
|
Installed 1 package in [TIME]
|
||||||
|
+ albatross==0.1.0a1
|
||||||
|
"###);
|
||||||
|
|
||||||
|
// Since there are only prerelease versions of `a` available, a prerelease is
|
||||||
|
// allowed. Since the user did not explictly request a pre-release, pre-releases at
|
||||||
|
// the boundary should not be selected.
|
||||||
|
assert_installed(&context.venv, "a_edcef999", "0.1.0a1", &context.temp_dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// package-prereleases-boundary
|
||||||
|
///
|
||||||
|
/// The user requires a non-prerelease version of `a` but has enabled pre-releases.
|
||||||
|
/// There are pre-releases on the boundary of their range.
|
||||||
|
///
|
||||||
|
/// ```text
|
||||||
|
/// 6d600873
|
||||||
|
/// ├── environment
|
||||||
|
/// │ └── python3.8
|
||||||
|
/// ├── root
|
||||||
|
/// │ └── requires a<0.2.0
|
||||||
|
/// │ └── satisfied by a-0.1.0
|
||||||
|
/// └── a
|
||||||
|
/// ├── a-0.1.0
|
||||||
|
/// ├── a-0.2.0a1
|
||||||
|
/// └── a-0.3.0
|
||||||
|
/// ```
|
||||||
|
#[test]
|
||||||
|
fn package_prereleases_boundary() {
|
||||||
|
let context = TestContext::new("3.8");
|
||||||
|
|
||||||
|
// In addition to the standard filters, swap out package names for more realistic messages
|
||||||
|
let mut filters = INSTA_FILTERS.to_vec();
|
||||||
|
filters.push((r"a-6d600873", "albatross"));
|
||||||
|
filters.push((r"-6d600873", ""));
|
||||||
|
|
||||||
|
uv_snapshot!(filters, command(&context)
|
||||||
|
.arg("--prerelease=allow")
|
||||||
|
.arg("a-6d600873<0.2.0")
|
||||||
|
, @r###"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
Resolved 1 package in [TIME]
|
||||||
|
Downloaded 1 package in [TIME]
|
||||||
|
Installed 1 package in [TIME]
|
||||||
|
+ albatross==0.1.0
|
||||||
|
"###);
|
||||||
|
|
||||||
|
// Since the user did not use a pre-release specifier, pre-releases at the boundary
|
||||||
|
// should not be selected even though pre-releases are allowed.
|
||||||
|
assert_installed(&context.venv, "a_6d600873", "0.1.0", &context.temp_dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// package-prereleases-global-boundary
|
||||||
|
///
|
||||||
|
/// The user requires a non-prerelease version of `a` but has enabled pre-releases.
|
||||||
|
/// There are pre-releases on the boundary of their range.
|
||||||
|
///
|
||||||
|
/// ```text
|
||||||
|
/// cf1b8081
|
||||||
|
/// ├── environment
|
||||||
|
/// │ └── python3.8
|
||||||
|
/// ├── root
|
||||||
|
/// │ └── requires a<0.2.0
|
||||||
|
/// │ └── satisfied by a-0.1.0
|
||||||
|
/// └── a
|
||||||
|
/// ├── a-0.1.0
|
||||||
|
/// ├── a-0.2.0a1
|
||||||
|
/// └── a-0.3.0
|
||||||
|
/// ```
|
||||||
|
#[test]
|
||||||
|
fn package_prereleases_global_boundary() {
|
||||||
|
let context = TestContext::new("3.8");
|
||||||
|
|
||||||
|
// In addition to the standard filters, swap out package names for more realistic messages
|
||||||
|
let mut filters = INSTA_FILTERS.to_vec();
|
||||||
|
filters.push((r"a-cf1b8081", "albatross"));
|
||||||
|
filters.push((r"-cf1b8081", ""));
|
||||||
|
|
||||||
|
uv_snapshot!(filters, command(&context)
|
||||||
|
.arg("--prerelease=allow")
|
||||||
|
.arg("a-cf1b8081<0.2.0")
|
||||||
|
, @r###"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
Resolved 1 package in [TIME]
|
||||||
|
Downloaded 1 package in [TIME]
|
||||||
|
Installed 1 package in [TIME]
|
||||||
|
+ albatross==0.1.0
|
||||||
|
"###);
|
||||||
|
|
||||||
|
// Since the user did not use a pre-release specifier, pre-releases at the boundary
|
||||||
|
// should not be selected even though pre-releases are allowed.
|
||||||
|
assert_installed(&context.venv, "a_cf1b8081", "0.1.0", &context.temp_dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// package-prereleases-specifier-boundary
|
||||||
|
///
|
||||||
|
/// The user requires a prerelease version of `a`. There are pre-releases on the
|
||||||
|
/// boundary of their range.
|
||||||
|
///
|
||||||
|
/// ```text
|
||||||
|
/// 357b9636
|
||||||
|
/// ├── environment
|
||||||
|
/// │ └── python3.8
|
||||||
|
/// ├── root
|
||||||
|
/// │ └── requires a<0.2.0a2
|
||||||
|
/// │ └── satisfied by a-0.1.0
|
||||||
|
/// └── a
|
||||||
|
/// ├── a-0.1.0
|
||||||
|
/// ├── a-0.2.0
|
||||||
|
/// ├── a-0.2.0a1
|
||||||
|
/// ├── a-0.2.0a2
|
||||||
|
/// ├── a-0.2.0a3
|
||||||
|
/// └── a-0.3.0
|
||||||
|
/// ```
|
||||||
|
#[test]
|
||||||
|
fn package_prereleases_specifier_boundary() {
|
||||||
|
let context = TestContext::new("3.8");
|
||||||
|
|
||||||
|
// In addition to the standard filters, swap out package names for more realistic messages
|
||||||
|
let mut filters = INSTA_FILTERS.to_vec();
|
||||||
|
filters.push((r"a-357b9636", "albatross"));
|
||||||
|
filters.push((r"-357b9636", ""));
|
||||||
|
|
||||||
|
uv_snapshot!(filters, command(&context)
|
||||||
|
.arg("a-357b9636<0.2.0a2")
|
||||||
|
, @r###"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
Resolved 1 package in [TIME]
|
||||||
|
Downloaded 1 package in [TIME]
|
||||||
|
Installed 1 package in [TIME]
|
||||||
|
+ albatross==0.2.0a1
|
||||||
|
"###);
|
||||||
|
|
||||||
|
// Since the user used a pre-release specifier, pre-releases at the boundary should
|
||||||
|
// be selected.
|
||||||
|
assert_installed(&context.venv, "a_357b9636", "0.2.0a1", &context.temp_dir);
|
||||||
|
}
|
||||||
|
|
||||||
/// requires-python-version-does-not-exist
|
/// requires-python-version-does-not-exist
|
||||||
///
|
///
|
||||||
/// The user requires a package which requires a Python version that does not exist
|
/// The user requires a package which requires a Python version that does not exist
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
apache-airflow[otel]
|
||||||
|
opentelemetry-exporter-prometheus<0.44
|
||||||
|
|
@ -45,7 +45,7 @@ import textwrap
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
PACKSE_COMMIT = "de0bab473eeaa4445db5a8febd732c655fad3d52"
|
PACKSE_COMMIT = "4f39539c1b858e28268554604e75c69e25272e5a"
|
||||||
TOOL_ROOT = Path(__file__).parent
|
TOOL_ROOT = Path(__file__).parent
|
||||||
TEMPLATES = TOOL_ROOT / "templates"
|
TEMPLATES = TOOL_ROOT / "templates"
|
||||||
INSTALL_TEMPLATE = TEMPLATES / "install.mustache"
|
INSTALL_TEMPLATE = TEMPLATES / "install.mustache"
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue