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:
Charlie Marsh 2024-02-24 18:02:03 -05:00 committed by GitHub
parent 53a250714c
commit 8d706b0f2a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 440 additions and 58 deletions

View file

@ -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.
///
/// Usually one can just use `Version::new` to create a new version with
@ -512,6 +525,22 @@ impl Version {
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
/// mutable borrow to the full type.
fn make_full(&mut self) -> &mut VersionFull {
@ -519,6 +548,7 @@ impl Version {
let full = VersionFull {
epoch: small.epoch(),
release: small.release().to_vec(),
min: small.min(),
pre: small.pre(),
post: small.post(),
dev: small.dev(),
@ -744,10 +774,13 @@ impl FromStr for Version {
/// * Bytes 5, 4 and 3 correspond to the second, third and fourth release
/// segments, respectively.
/// * 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 range 0-5 inclusive, corresponding to dev, pre-a, pre-b, pre-rc,
/// no-suffix or post releases, respectively.
/// the range 0-6 inclusive, corresponding to min, dev, pre-a, pre-b, pre-rc,
/// 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
/// to the release number of the suffix, if one exists. If there is no
/// suffix, then this bits are always 0.
@ -810,18 +843,19 @@ struct VersionSmall {
}
impl VersionSmall {
const SUFFIX_DEV: u64 = 0;
const SUFFIX_PRE_ALPHA: u64 = 1;
const SUFFIX_PRE_BETA: u64 = 2;
const SUFFIX_PRE_RC: u64 = 3;
const SUFFIX_NONE: u64 = 4;
const SUFFIX_POST: u64 = 5;
const SUFFIX_MIN: u64 = 0;
const SUFFIX_DEV: u64 = 1;
const SUFFIX_PRE_ALPHA: u64 = 2;
const SUFFIX_PRE_BETA: u64 = 3;
const SUFFIX_PRE_RC: u64 = 4;
const SUFFIX_NONE: u64 = 5;
const SUFFIX_POST: u64 = 6;
const SUFFIX_MAX_VERSION: u64 = 0x1FFFFF;
#[inline]
fn new() -> VersionSmall {
VersionSmall {
repr: 0x00000000_00800000,
repr: 0x00000000_00A00000,
release: [0, 0, 0, 0],
len: 0,
}
@ -888,7 +922,7 @@ impl VersionSmall {
#[inline]
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();
}
match value {
@ -931,7 +965,7 @@ impl VersionSmall {
#[inline]
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();
}
match value {
@ -970,7 +1004,7 @@ impl VersionSmall {
#[inline]
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();
}
match value {
@ -988,6 +1022,35 @@ impl VersionSmall {
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]
fn local(&self) -> &[LocalSegment] {
// 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
/// > some syntactic restrictions are imposed.
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.
@ -1410,7 +1477,7 @@ impl<'a> Parser<'a> {
| (u64::from(release[1]) << 40)
| (u64::from(release[2]) << 32)
| (u64::from(release[3]) << 24)
| (0x80 << 16)
| (0xA0 << 16)
| (0x00 << 8)
| (0x00 << 0),
release: [
@ -2243,9 +2310,9 @@ pub(crate) fn compare_release(this: &[u64], other: &[u64]) -> Ordering {
/// According to [a summary of permitted suffixes and relative
/// 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
/// releases on beta releases, so we make a three stage ordering: ({dev: 0, a:
/// 1, b: 2, rc: 3, (): 4, post: 5}, <preN>, <postN or None as smallest>, <devN
/// or Max as largest>, <local>)
/// releases on beta releases, so we make a three stage ordering: ({min: 0,
/// dev: 1, a: 2, b: 3, rc: 4, (): 5, post: 6}, <preN>, <postN or None as
/// smallest>, <devN or Max as largest>, <local>)
///
/// 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
@ -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
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
(None, None, Some(n)) => (0, 0, None, n, version.local()),
(None, None, Some(n), None) => (1, 0, None, n, version.local()),
// alpha release
(
Some(PreRelease {
@ -2265,7 +2334,8 @@ fn sortable_tuple(version: &Version) -> (u64, u64, Option<u64>, u64, &[LocalSegm
}),
post,
dev,
) => (1, n, post, dev.unwrap_or(u64::MAX), version.local()),
None,
) => (2, n, post, dev.unwrap_or(u64::MAX), version.local()),
// beta release
(
Some(PreRelease {
@ -2274,7 +2344,8 @@ fn sortable_tuple(version: &Version) -> (u64, u64, Option<u64>, u64, &[LocalSegm
}),
post,
dev,
) => (2, n, post, dev.unwrap_or(u64::MAX), version.local()),
None,
) => (3, n, post, dev.unwrap_or(u64::MAX), version.local()),
// alpha release
(
Some(PreRelease {
@ -2283,11 +2354,14 @@ fn sortable_tuple(version: &Version) -> (u64, u64, Option<u64>, u64, &[LocalSegm
}),
post,
dev,
) => (3, n, post, dev.unwrap_or(u64::MAX), version.local()),
None,
) => (4, n, post, dev.unwrap_or(u64::MAX), version.local()),
// final release
(None, None, None) => (4, 0, None, 0, version.local()),
(None, None, None, None) => (5, 0, None, 0, version.local()),
// 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]));
// min tests
assert!(Parser::new("1.min0".as_bytes()).parse().is_err())
}
// 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.
#[test]
fn parse_number_u64() {
@ -3577,6 +3694,7 @@ mod tests {
.field("post", &self.0.post())
.field("dev", &self.0.dev())
.field("local", &self.0.local())
.field("min", &self.0.min())
.finish()
}
}

View file

@ -521,7 +521,7 @@ impl CacheBucket {
CacheBucket::FlatIndex => "flat-index-v0",
CacheBucket::Git => "git-v0",
CacheBucket::Interpreter => "interpreter-v0",
CacheBucket::Simple => "simple-v2",
CacheBucket::Simple => "simple-v3",
CacheBucket::Wheels => "wheels-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> {
Some(self.timestamp().cmp(&other.timestamp()))
}
}
impl std::cmp::Ord for ArchiveTimestamp {
impl Ord for ArchiveTimestamp {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.timestamp().cmp(&other.timestamp())
}

View file

@ -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),
}
}
}

View file

@ -16,7 +16,6 @@ use rustc_hash::FxHashMap;
use uv_normalize::PackageName;
use crate::candidate_selector::CandidateSelector;
use crate::prerelease_mode::PreReleaseStrategy;
use crate::python_requirement::PythonRequirement;
use crate::resolver::UnavailablePackage;
@ -346,25 +345,10 @@ impl PubGrubReportFormatter<'_> {
) -> IndexSet<PubGrubHint> {
/// Returns `true` if pre-releases were allowed for a package.
fn allowed_prerelease(package: &PubGrubPackage, selector: &CandidateSelector) -> bool {
match selector.prerelease_strategy() {
PreReleaseStrategy::Disallow => false,
PreReleaseStrategy::Allow => true,
PreReleaseStrategy::IfNecessary => false,
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 PubGrubPackage::Package(package, ..) = package else {
return false;
};
selector.prerelease_strategy().allows(package)
}
let mut hints = IndexSet::default();

View file

@ -38,7 +38,7 @@ impl TryFrom<&VersionSpecifier> for PubGrubSpecifier {
let [rest @ .., last, _] = specifier.version().release() else {
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_dev(Some(0));
let version = specifier.version().clone();
@ -46,7 +46,14 @@ impl TryFrom<&VersionSpecifier> for PubGrubSpecifier {
}
Operator::LessThan => {
let version = specifier.version().clone();
Range::strictly_lower_than(version)
if version.any_prerelease() {
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 => {
let version = specifier.version().clone();

View file

@ -4268,3 +4268,77 @@ fn unsafe_package() -> Result<()> {
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(())
}

View file

@ -1,7 +1,7 @@
//! DO NOT EDIT
//!
//! Generated with ./scripts/scenarios/update.py
//! Scenarios from <https://github.com/zanieb/packse/tree/de0bab473eeaa4445db5a8febd732c655fad3d52/scenarios>
//! Generated with scripts/scenarios/update.py
//! Scenarios from <https://github.com/zanieb/packse/tree/4f39539c1b858e28268554604e75c69e25272e5a/scenarios>
//!
#![cfg(all(feature = "python", feature = "pypi"))]
@ -29,7 +29,7 @@ fn command(context: &TestContext, python_versions: &[&str]) -> Command {
.arg("--index-url")
.arg("https://test.pypi.org/simple")
.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(context.cache_dir.path())
.env("VIRTUAL_ENV", context.venv.as_os_str())

View file

@ -1,7 +1,7 @@
//! DO NOT EDIT
//!
//! Generated with ./scripts/scenarios/update.py
//! Scenarios from <https://github.com/zanieb/packse/tree/de0bab473eeaa4445db5a8febd732c655fad3d52/scenarios>
//! Generated with scripts/scenarios/update.py
//! Scenarios from <https://github.com/zanieb/packse/tree/4f39539c1b858e28268554604e75c69e25272e5a/scenarios>
//!
#![cfg(all(feature = "python", feature = "pypi"))]
@ -48,7 +48,7 @@ fn command(context: &TestContext) -> Command {
.arg("--index-url")
.arg("https://test.pypi.org/simple")
.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(context.cache_dir.path())
.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
/// `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
/// `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`.
///
/// ```text
@ -2043,6 +2043,192 @@ fn transitive_prerelease_and_stable_dependency_many_versions_holes() {
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
///
/// The user requires a package which requires a Python version that does not exist

2
requirements.in Normal file
View file

@ -0,0 +1,2 @@
apache-airflow[otel]
opentelemetry-exporter-prometheus<0.44

View file

@ -5,7 +5,7 @@ Generates and updates snapshot test cases from packse scenarios.
Usage:
Regenerate the scenario test file:
$ ./scripts/scenarios/update.py
Scenarios are pinned to a specific commit. Change the `PACKSE_COMMIT` constant to update them.
@ -45,7 +45,7 @@ import textwrap
from pathlib import Path
PACKSE_COMMIT = "de0bab473eeaa4445db5a8febd732c655fad3d52"
PACKSE_COMMIT = "4f39539c1b858e28268554604e75c69e25272e5a"
TOOL_ROOT = Path(__file__).parent
TEMPLATES = TOOL_ROOT / "templates"
INSTALL_TEMPLATE = TEMPLATES / "install.mustache"