diff --git a/crates/pep440-rs/src/version.rs b/crates/pep440-rs/src/version.rs index 105886c5250c..2cc80b190953 100644 --- a/crates/pep440-rs/src/version.rs +++ b/crates/pep440-rs/src/version.rs @@ -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 { + 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) -> 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, , .postN`. Its representation is thus: +/// `min, .devN, aN, bN, rcN, , .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) -> 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) -> 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) -> 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 { + if self.suffix_kind() == VersionSmall::SUFFIX_MIN { + Some(self.suffix_version()) + } else { + None + } + } + + #[inline] + fn set_min(&mut self, value: Option) -> 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, + /// 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, } /// 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, , .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}, , , , ) +/// releases on beta releases, so we make a three stage ordering: ({min: 0, +/// dev: 1, a: 2, b: 3, rc: 4, (): 5, post: 6}, , , , ) /// /// 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, &[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, &[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, &[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, &[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::().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() } } diff --git a/crates/uv-cache/src/lib.rs b/crates/uv-cache/src/lib.rs index d3069785fafb..fb48a13a2304 100644 --- a/crates/uv-cache/src/lib.rs +++ b/crates/uv-cache/src/lib.rs @@ -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 { 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()) } diff --git a/crates/uv-resolver/src/prerelease_mode.rs b/crates/uv-resolver/src/prerelease_mode.rs index 5f5a84188334..e0ec0f6041e2 100644 --- a/crates/uv-resolver/src/prerelease_mode.rs +++ b/crates/uv-resolver/src/prerelease_mode.rs @@ -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), + } + } } diff --git a/crates/uv-resolver/src/pubgrub/report.rs b/crates/uv-resolver/src/pubgrub/report.rs index f3149638788a..2091b0158789 100644 --- a/crates/uv-resolver/src/pubgrub/report.rs +++ b/crates/uv-resolver/src/pubgrub/report.rs @@ -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 { /// 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(); diff --git a/crates/uv-resolver/src/pubgrub/specifier.rs b/crates/uv-resolver/src/pubgrub/specifier.rs index dad945feef1d..4fd65f3bcc00 100644 --- a/crates/uv-resolver/src/pubgrub/specifier.rs +++ b/crates/uv-resolver/src/pubgrub/specifier.rs @@ -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 { let version = specifier.version().clone(); diff --git a/crates/uv/tests/pip_compile.rs b/crates/uv/tests/pip_compile.rs index 0b6dea6ddca3..4d4db4285043 100644 --- a/crates/uv/tests/pip_compile.rs +++ b/crates/uv/tests/pip_compile.rs @@ -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(()) +} diff --git a/crates/uv/tests/pip_compile_scenarios.rs b/crates/uv/tests/pip_compile_scenarios.rs index f2ca6a9b9209..2cbece6dddd6 100644 --- a/crates/uv/tests/pip_compile_scenarios.rs +++ b/crates/uv/tests/pip_compile_scenarios.rs @@ -1,7 +1,7 @@ //! DO NOT EDIT //! -//! Generated with ./scripts/scenarios/update.py -//! Scenarios from +//! Generated with scripts/scenarios/update.py +//! Scenarios from //! #![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()) diff --git a/crates/uv/tests/pip_install_scenarios.rs b/crates/uv/tests/pip_install_scenarios.rs index f7edfd6216a4..e0ad34ecba84 100644 --- a/crates/uv/tests/pip_install_scenarios.rs +++ b/crates/uv/tests/pip_install_scenarios.rs @@ -1,7 +1,7 @@ //! DO NOT EDIT //! -//! Generated with ./scripts/scenarios/update.py -//! Scenarios from +//! Generated with scripts/scenarios/update.py +//! Scenarios from //! #![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 diff --git a/requirements.in b/requirements.in new file mode 100644 index 000000000000..d572c51bc716 --- /dev/null +++ b/requirements.in @@ -0,0 +1,2 @@ +apache-airflow[otel] +opentelemetry-exporter-prometheus<0.44 diff --git a/scripts/scenarios/update.py b/scripts/scenarios/update.py index dc8c5b5b71b3..e97fcccca1ce 100755 --- a/scripts/scenarios/update.py +++ b/scripts/scenarios/update.py @@ -5,7 +5,7 @@ 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 @@ from pathlib import Path -PACKSE_COMMIT = "de0bab473eeaa4445db5a8febd732c655fad3d52" +PACKSE_COMMIT = "4f39539c1b858e28268554604e75c69e25272e5a" TOOL_ROOT = Path(__file__).parent TEMPLATES = TOOL_ROOT / "templates" INSTALL_TEMPLATE = TEMPLATES / "install.mustache"