uv/crates/pep440-rs/src/version.rs

3302 lines
113 KiB
Rust
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

use std::{
borrow::Borrow,
cmp::Ordering,
hash::{Hash, Hasher},
str::FromStr,
sync::Arc,
};
#[cfg(feature = "pyo3")]
use pyo3::{
basic::CompareOp, exceptions::PyValueError, pyclass, pymethods, FromPyObject, IntoPy, PyAny,
PyObject, PyResult, Python,
};
#[cfg(feature = "serde")]
use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
/// One of `~=` `==` `!=` `<=` `>=` `<` `>` `===`
#[derive(Eq, PartialEq, Debug, Hash, Clone, Copy)]
#[cfg_attr(feature = "pyo3", pyclass)]
pub enum Operator {
/// `== 1.2.3`
Equal,
/// `== 1.2.*`
EqualStar,
/// `===` (discouraged)
///
/// <https://peps.python.org/pep-0440/#arbitrary-equality>
///
/// "Use of this operator is heavily discouraged and tooling MAY display a warning when it is used"
// clippy doesn't like this: #[deprecated = "Use of this operator is heavily discouraged"]
ExactEqual,
/// `!= 1.2.3`
NotEqual,
/// `!= 1.2.*`
NotEqualStar,
/// `~=`
TildeEqual,
/// `<`
LessThan,
/// `<=`
LessThanEqual,
/// `>`
GreaterThan,
/// `>=`
GreaterThanEqual,
}
impl Operator {
/// Returns true if and only if this operator can be used in a version
/// specifier with a version containing a non-empty local segment.
///
/// Specifically, this comes from the "Local version identifiers are
/// NOT permitted in this version specifier." phrasing in the version
/// specifiers [spec].
///
/// [spec]: https://packaging.python.org/en/latest/specifications/version-specifiers/
pub(crate) fn is_local_compatible(&self) -> bool {
!matches!(
*self,
Operator::GreaterThan
| Operator::GreaterThanEqual
| Operator::LessThan
| Operator::LessThanEqual
| Operator::TildeEqual
| Operator::EqualStar
| Operator::NotEqualStar
)
}
/// Returns the wildcard version of this operator, if appropriate.
///
/// This returns `None` when this operator doesn't have an analogous
/// wildcard operator.
pub(crate) fn to_star(self) -> Option<Operator> {
match self {
Operator::Equal => Some(Operator::EqualStar),
Operator::NotEqual => Some(Operator::NotEqualStar),
_ => None,
}
}
}
impl FromStr for Operator {
type Err = OperatorParseError;
/// Notably, this does not know about star versions, it just assumes the base operator
fn from_str(s: &str) -> Result<Self, Self::Err> {
let operator = match s {
"==" => Self::Equal,
"===" => {
#[cfg(feature = "tracing")]
{
tracing::warn!("Using arbitrary equality (`===`) is discouraged");
}
#[allow(deprecated)]
Self::ExactEqual
}
"!=" => Self::NotEqual,
"~=" => Self::TildeEqual,
"<" => Self::LessThan,
"<=" => Self::LessThanEqual,
">" => Self::GreaterThan,
">=" => Self::GreaterThanEqual,
other => {
return Err(OperatorParseError {
got: other.to_string(),
})
}
};
Ok(operator)
}
}
impl std::fmt::Display for Operator {
/// Note the `EqualStar` is also `==`.
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let operator = match self {
Operator::Equal => "==",
// Beware, this doesn't print the star
Operator::EqualStar => "==",
#[allow(deprecated)]
Operator::ExactEqual => "===",
Operator::NotEqual => "!=",
Operator::NotEqualStar => "!=",
Operator::TildeEqual => "~=",
Operator::LessThan => "<",
Operator::LessThanEqual => "<=",
Operator::GreaterThan => ">",
Operator::GreaterThanEqual => ">=",
};
write!(f, "{operator}")
}
}
#[cfg(feature = "pyo3")]
#[pymethods]
impl Operator {
fn __str__(&self) -> String {
self.to_string()
}
fn __repr__(&self) -> String {
self.to_string()
}
}
/// An error that occurs when parsing an invalid version specifier operator.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct OperatorParseError {
pub(crate) got: String,
}
impl std::error::Error for OperatorParseError {}
impl std::fmt::Display for OperatorParseError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(
f,
"no such comparison operator {:?}, must be one of ~= == != <= >= < > ===",
self.got
)
}
}
// NOTE: I did a little bit of experimentation to determine what most version
// numbers actually look like. The idea here is that if we know what most look
// like, then we can optimize our representation for the common case, while
// falling back to something more complete for any cases that fall outside of
// that.
//
// The experiment downloaded PyPI's distribution metadata from Google BigQuery,
// and then counted the number of versions with various qualities:
//
// total: 11264078
// release counts:
// 01: 51204 (0.45%)
// 02: 754520 (6.70%)
// 03: 9757602 (86.63%)
// 04: 527403 (4.68%)
// 05: 77994 (0.69%)
// 06: 91346 (0.81%)
// 07: 1421 (0.01%)
// 08: 205 (0.00%)
// 09: 72 (0.00%)
// 10: 2297 (0.02%)
// 11: 5 (0.00%)
// 12: 2 (0.00%)
// 13: 4 (0.00%)
// 20: 2 (0.00%)
// 39: 1 (0.00%)
// JUST release counts:
// 01: 48297 (0.43%)
// 02: 604692 (5.37%)
// 03: 8460917 (75.11%)
// 04: 465354 (4.13%)
// 05: 49293 (0.44%)
// 06: 25909 (0.23%)
// 07: 1413 (0.01%)
// 08: 192 (0.00%)
// 09: 72 (0.00%)
// 10: 2292 (0.02%)
// 11: 5 (0.00%)
// 12: 2 (0.00%)
// 13: 4 (0.00%)
// 20: 2 (0.00%)
// 39: 1 (0.00%)
// non-zero epochs: 1902 (0.02%)
// pre-releases: 752184 (6.68%)
// post-releases: 134383 (1.19%)
// dev-releases: 765099 (6.79%)
// locals: 1 (0.00%)
// fitsu8: 10388430 (92.23%)
// sweetspot: 10236089 (90.87%)
//
// The "JUST release counts" corresponds to versions that only have a release
// component and nothing else. The "fitsu8" property indicates that all numbers
// (except for local numeric segments) fit into `u8`. The "sweetspot" property
// consists of any version number with no local part, 4 or fewer parts in the
// release version and *all* numbers fit into a u8.
//
// This somewhat confirms what one might expect: the vast majority of versions
// (75%) are precisely in the format of `x.y.z`. That is, a version with only a
// release version of 3 components.
//
// ---AG
/// A version number such as `1.2.3` or `4!5.6.7-a8.post9.dev0`.
///
/// Beware that the sorting implemented with [Ord] and [Eq] is not consistent with the operators
/// from PEP 440, i.e. compare two versions in rust with `>` gives a different result than a
/// `VersionSpecifier` with `>` as operator.
///
/// Parse with [`Version::from_str`]:
///
/// ```rust
/// use std::str::FromStr;
/// use pep440_rs::Version;
///
/// let version = Version::from_str("1.19").unwrap();
/// ```
#[derive(Clone)]
pub struct Version {
inner: Arc<VersionInner>,
}
#[derive(Clone, Debug)]
enum VersionInner {
Small { small: VersionSmall },
Full { full: VersionFull },
}
impl Version {
/// Create a new version from an iterator of segments in the release part
/// of a version.
///
/// # Panics
///
/// When the iterator yields no elements.
#[inline]
pub fn new<I, R>(release_numbers: I) -> Version
where
I: IntoIterator<Item = R>,
R: Borrow<u64>,
{
Version {
inner: Arc::new(VersionInner::Small {
small: VersionSmall::new(),
}),
}
.with_release(release_numbers)
}
/// Whether this is an alpha/beta/rc or dev version
#[inline]
pub fn any_prerelease(&self) -> bool {
self.is_pre() || self.is_dev()
}
/// Whether this is an alpha/beta/rc version
#[inline]
pub fn is_pre(&self) -> bool {
self.pre().is_some()
}
/// Whether this is a dev version
#[inline]
pub fn is_dev(&self) -> bool {
self.dev().is_some()
}
/// Whether this is a post version
#[inline]
pub fn is_post(&self) -> bool {
self.post().is_some()
}
/// Whether this is a local version (e.g. `1.2.3+localsuffixesareweird`)
///
/// When true, it is guaranteed that the slice returned by
/// [`Version::local`] is non-empty.
#[inline]
pub fn is_local(&self) -> bool {
!self.local().is_empty()
}
/// Returns the epoch of this version.
#[inline]
pub fn epoch(&self) -> u64 {
match *self.inner {
VersionInner::Small { ref small } => small.epoch(),
VersionInner::Full { ref full } => full.epoch,
}
}
/// Returns the release number part of the version.
#[inline]
pub fn release(&self) -> &[u64] {
match *self.inner {
VersionInner::Small { ref small } => small.release(),
VersionInner::Full { ref full, .. } => &full.release,
}
}
/// Returns the pre-relase part of this version, if it exists.
#[inline]
pub fn pre(&self) -> Option<(PreRelease, u64)> {
match *self.inner {
VersionInner::Small { ref small } => small.pre(),
VersionInner::Full { ref full } => full.pre,
}
}
/// Returns the post-release part of this version, if it exists.
#[inline]
pub fn post(&self) -> Option<u64> {
match *self.inner {
VersionInner::Small { ref small } => small.post(),
VersionInner::Full { ref full } => full.post,
}
}
/// Returns the dev-release part of this version, if it exists.
#[inline]
pub fn dev(&self) -> Option<u64> {
match *self.inner {
VersionInner::Small { ref small } => small.dev(),
VersionInner::Full { ref full } => full.dev,
}
}
/// Returns the local segments in this version, if any exist.
#[inline]
pub fn local(&self) -> &[LocalSegment] {
match *self.inner {
VersionInner::Small { ref small } => small.local(),
VersionInner::Full { ref full } => &full.local,
}
}
/// Set the release numbers and return the updated version.
///
/// Usually one can just use `Version::new` to create a new version with
/// the updated release numbers, but this is useful when one wants to
/// preserve the other components of a version number while only changing
/// the release numbers.
///
/// # Panics
///
/// When the iterator yields no elements.
#[inline]
pub fn with_release<I, R>(mut self, release_numbers: I) -> Version
where
I: IntoIterator<Item = R>,
R: Borrow<u64>,
{
self.clear_release();
for n in release_numbers {
self.push_release(*n.borrow());
}
assert!(
!self.release().is_empty(),
"release must have non-zero size"
);
self
}
/// Push the given release number into this version. It will become the
/// last number in the release component.
#[inline]
fn push_release(&mut self, n: u64) {
if let VersionInner::Small { ref mut small } = Arc::make_mut(&mut self.inner) {
if small.push_release(n) {
return;
}
}
self.make_full().release.push(n);
}
/// Clears the release component of this version so that it has no numbers.
///
/// Generally speaking, this empty state should not be exposed to callers
/// since all versions should have at least one release number.
#[inline]
fn clear_release(&mut self) {
match Arc::make_mut(&mut self.inner) {
VersionInner::Small { ref mut small } => small.clear_release(),
VersionInner::Full { ref mut full } => {
full.release.clear();
}
}
}
/// Set the epoch and return the updated version.
#[inline]
pub fn with_epoch(mut self, value: u64) -> Version {
if let VersionInner::Small { ref mut small } = Arc::make_mut(&mut self.inner) {
if small.set_epoch(value) {
return self;
}
}
self.make_full().epoch = value;
self
}
/// Set the pre-release component and return the updated version.
#[inline]
pub fn with_pre(mut self, value: Option<(PreRelease, u64)>) -> Version {
if let VersionInner::Small { ref mut small } = Arc::make_mut(&mut self.inner) {
if small.set_pre(value) {
return self;
}
}
self.make_full().pre = value;
self
}
/// Set the post-release component and return the updated version.
#[inline]
pub fn with_post(mut self, value: Option<u64>) -> Version {
if let VersionInner::Small { ref mut small } = Arc::make_mut(&mut self.inner) {
if small.set_post(value) {
return self;
}
}
self.make_full().post = value;
self
}
/// Set the dev-release component and return the updated version.
#[inline]
pub fn with_dev(mut self, value: Option<u64>) -> Version {
if let VersionInner::Small { ref mut small } = Arc::make_mut(&mut self.inner) {
if small.set_dev(value) {
return self;
}
}
self.make_full().dev = value;
self
}
/// Set the local segments and return the updated version.
#[inline]
pub fn with_local(mut self, value: Vec<LocalSegment>) -> Version {
if value.is_empty() {
self.without_local()
} else {
self.make_full().local = value;
self
}
}
/// For PEP 440 specifier matching: "Except where specifically noted below,
/// local version identifiers MUST NOT be permitted in version specifiers,
/// and local version labels MUST be ignored entirely when checking if
/// candidate versions match a given version specifier."
#[inline]
pub fn without_local(mut self) -> Version {
// A "small" version is already guaranteed not to have a local
// component, so we only need to do anything if we have a "full"
// version.
if let VersionInner::Full { ref mut full } = Arc::make_mut(&mut self.inner) {
full.local.clear();
}
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 {
if let VersionInner::Small { ref small } = *self.inner {
let full = VersionFull {
epoch: small.epoch(),
release: small.release().to_vec(),
pre: small.pre(),
post: small.post(),
dev: small.dev(),
local: vec![],
};
*self = Version {
inner: Arc::new(VersionInner::Full { full }),
};
}
match Arc::make_mut(&mut self.inner) {
VersionInner::Full { ref mut full } => full,
VersionInner::Small { .. } => unreachable!(),
}
}
/// Performs a "slow" but complete comparison between two versions.
///
/// This comparison is done using only the public API of a `Version`, and
/// is thus independent of its specific representation. This is useful
/// to use when comparing two versions that aren't *both* the small
/// representation.
#[cold]
#[inline(never)]
fn cmp_slow(&self, other: &Version) -> Ordering {
match self.epoch().cmp(&other.epoch()) {
Ordering::Less => {
return Ordering::Less;
}
Ordering::Equal => {}
Ordering::Greater => {
return Ordering::Greater;
}
}
match compare_release(self.release(), other.release()) {
Ordering::Less => {
return Ordering::Less;
}
Ordering::Equal => {}
Ordering::Greater => {
return Ordering::Greater;
}
}
// release is equal, so compare the other parts
sortable_tuple(self).cmp(&sortable_tuple(other))
}
}
/// <https://github.com/serde-rs/serde/issues/1316#issue-332908452>
#[cfg(feature = "serde")]
impl<'de> Deserialize<'de> for Version {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
FromStr::from_str(&s).map_err(de::Error::custom)
}
}
/// <https://github.com/serde-rs/serde/issues/1316#issue-332908452>
#[cfg(feature = "serde")]
impl Serialize for Version {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.collect_str(self)
}
}
/// Shows normalized version
impl std::fmt::Display for Version {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let epoch = if self.epoch() == 0 {
String::new()
} else {
format!("{}!", self.epoch())
};
let release = self
.release()
.iter()
.map(ToString::to_string)
.collect::<Vec<String>>()
.join(".");
let pre = self
.pre()
.as_ref()
.map(|(pre_kind, pre_version)| format!("{pre_kind}{pre_version}"))
.unwrap_or_default();
let post = self
.post()
.map(|post| format!(".post{post}"))
.unwrap_or_default();
let dev = self
.dev()
.map(|dev| format!(".dev{dev}"))
.unwrap_or_default();
let local = if self.local().is_empty() {
String::new()
} else {
format!(
"+{}",
self.local()
.iter()
.map(std::string::ToString::to_string)
.collect::<Vec<String>>()
.join(".")
)
};
write!(f, "{epoch}{release}{pre}{post}{dev}{local}")
}
}
impl std::fmt::Debug for Version {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "\"{}\"", self)
}
}
impl PartialEq<Self> for Version {
#[inline]
fn eq(&self, other: &Self) -> bool {
self.cmp(other) == Ordering::Equal
}
}
impl Eq for Version {}
impl Hash for Version {
/// Custom implementation to ignoring trailing zero because `PartialEq` zero pads
#[inline]
fn hash<H: Hasher>(&self, state: &mut H) {
self.epoch().hash(state);
// Skip trailing zeros
for i in self.release().iter().rev().skip_while(|x| **x == 0) {
i.hash(state);
}
self.pre().hash(state);
self.dev().hash(state);
self.post().hash(state);
self.local().hash(state);
}
}
impl PartialOrd<Self> for Version {
#[inline]
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for Version {
/// 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.0b2-346 < 1.0c1.dev456 < 1.0c1 < 1.0rc2 < 1.0c3
/// < 1.0 < 1.0.post456.dev34 < 1.0.post456
#[inline]
fn cmp(&self, other: &Self) -> Ordering {
match (&*self.inner, &*other.inner) {
(VersionInner::Small { small: small1 }, VersionInner::Small { small: small2 }) => {
small1.repr.cmp(&small2.repr)
}
_ => self.cmp_slow(other),
}
}
}
impl FromStr for Version {
type Err = VersionParseError;
/// Parses a version such as `1.19`, `1.0a1`,`1.0+abc.5` or `1!2012.2`
///
/// Note that this doesn't allow wildcard versions.
fn from_str(version: &str) -> Result<Self, Self::Err> {
Parser::new(version.as_bytes()).parse()
}
}
/// A "small" representation of a version.
///
/// This representation is used for a (very common) subset of versions: the
/// set of all versions with ~small numbers and no local component. The
/// representation is designed to be (somewhat) compact, but also laid out in
/// a way that makes comparisons between two small versions equivalent to a
/// simple `memcmp`.
///
/// The methods on this type encapsulate the representation. Since this type
/// cannot represent the full range of all versions, setters on this type will
/// return `false` if the value could not be stored. In this case, callers
/// should generally convert a version into its "full" representation and then
/// set the value on the full type.
///
/// # Representation
///
/// At time of writing, this representation supports versions that meet all of
/// the following criteria:
///
/// * The epoch must be `0`.
/// * The release portion must have 4 or fewer segments.
/// * All release segments, except for the first, must be representable in a
/// `u8`. The first segment must be representable in a `u16`. (This permits
/// calendar versions, like `2023.03`, to be represented.)
/// * There is *at most* one of the following components: pre, dev or post.
/// * If there is a pre segment, then its numeric value is less than 64.
/// * If there is a dev or post segment, then its value is less than u8::MAX.
/// * There are zero "local" segments.
///
/// The above constraints were chosen as a balancing point between being able
/// to represent all parts of a version in a very small amount of space,
/// and for supporting as many versions in the wild as possible. There is,
/// however, another constraint in play here: comparisons between two `Version`
/// values. It turns out that we do a lot of them as part of resolution, and
/// the cheaper we can make that, the better. This constraint pushes us
/// toward using as little space as possible. Indeed, here, comparisons are
/// implemented via `u64::cmp`.
///
/// We pack versions fitting the above constraints into a `u64` in such a way
/// that it preserves the ordering between versions as prescribed in PEP 440.
/// Namely:
///
/// * Bytes 6 and 7 correspond to the first release segment as a `u16`.
/// * Bytes 5, 4 and 3 correspond to the second, third and fourth release
/// segments, respectively.
/// * Byte 2 corresponds to the post-release segment. If there is no
/// post-release segment, then byte 2 is set to 0x00. This makes "no
/// post-release" sort before "has post-release." The numeric value
/// (constrained to be <u8::MAX) has 1 added to it so that 0x00 is reserved to
/// indicate absence.
/// * Byte 1 corresponds to the pre-release segment. If there is no pre-release
/// segment, then byte 1 is set to 0xFF. This makes "no pre-release" sort
/// after "has pre-release." The most significant two bits of byte 1 encode
/// the type of pre-release (alpha, beta, rc) while the low 6 bits encode the
/// pre-release numeric value.
/// * Byte 0 corresponds to the dev-release segment. If there is no dev-release
/// segment, then byte 0 is set to 0xFF. This makes "no dev-release" sort after
/// "has dev-release." The dev-release value (constrainted to be <u8::MAX) is
/// stored in byte 0 as-is.
///
/// The order of the encoding above is significant. For example, the
/// post-release segment is encoded at a more significant byte in the `u64`
/// than the pre-release segment because `1.2.3.post1 > 1.2.3rc9999`.
///
/// Notice also that nothing about the representation inherently prohibits
/// storing any combination of pre, dev or post release components. We
/// could absolutely store all three (assuming they fit into their various
/// constraints outlined above). But, if we did that, a simple `u64::cmp` would
/// no longer be correct. For example, `1.0.post456.dev34 < 1.0.post456`, but
/// in the representation above, it would treat `1.0.post456.dev34` as greater
/// than `1.0.post456`. To make comparisons cheap for multi-component versions
/// like that, we'd need to use more space. Thankfully, such versions are
/// incredibly rare. Virtually all versions have zero or one pre, dev or post
/// release components.
#[derive(Clone, Debug)]
struct VersionSmall {
/// The representation discussed above.
repr: u64,
/// The `u64` numbers in the release component.
///
/// These are *only* used to implement the public API `Version::release`
/// method. This is necessary in order to provide a `&[u64]` to the caller.
/// If we didn't need the public API, or could re-work it, then we could
/// get rid of this extra storage. (Which is indeed duplicative of what is
/// stored in `repr`.) Note that this uses `u64` not because it can store
/// bigger numbers than what's in `repr` (it can't), but so that it permits
/// us to return a `&[u64]`.
///
/// I believe there is only one way to get rid of this extra storage:
/// change the public API so that it doesn't return a `&[u64]`. Instead,
/// we'd return a new type that conceptually represents a `&[u64]`, but may
/// use a different representation based on what kind of `Version` it came
/// from. The downside of this approach is that one loses the flexibility
/// of a simple `&[u64]`. (Which, at time of writing, is taken advantage of
/// in several places via slice patterns.) But, if we needed to change it,
/// we could do it without losing expressivity, but losing convenience.
release: [u64; 4],
/// The number of segments in the release component.
///
/// Strictly speaking, this isn't necessary since `1.2` is considered
/// equivalent to `1.2.0.0`. But in practice it's nice to be able
/// to truncate the zero components. And always filling out to 4
/// places somewhat exposes internal details, since the "full" version
/// representation would not do that.
len: u8,
}
impl VersionSmall {
#[inline]
fn new() -> VersionSmall {
VersionSmall {
repr: 0x00000000_0000FFFF,
release: [0, 0, 0, 0],
len: 0,
}
}
#[inline]
fn epoch(&self) -> u64 {
0
}
#[inline]
fn set_epoch(&mut self, value: u64) -> bool {
if value != 0 {
return false;
}
true
}
#[inline]
fn release(&self) -> &[u64] {
&self.release[..usize::from(self.len)]
}
#[inline]
fn clear_release(&mut self) {
self.repr &= !0xFFFFFFFF_FF000000;
self.release = [0, 0, 0, 0];
self.len = 0;
}
#[inline]
fn push_release(&mut self, n: u64) -> bool {
if self.len == 0 {
if n > u64::from(u16::MAX) {
return false;
}
self.repr |= n << 48;
self.release[0] = n;
self.len = 1;
true
} else {
if n > u64::from(u8::MAX) {
return false;
}
if self.len >= 4 {
return false;
}
let shift = 48 - (usize::from(self.len) * 8);
self.repr |= n << shift;
self.release[usize::from(self.len)] = n;
self.len += 1;
true
}
}
#[inline]
fn post(&self) -> Option<u64> {
let v = (self.repr >> 16) & 0xFF;
if v == 0 {
None
} else {
Some(v - 1)
}
}
#[inline]
fn set_post(&mut self, value: Option<u64>) -> bool {
if value.is_some() && (self.pre().is_some() || self.dev().is_some()) {
return false;
}
match value {
None => {
self.repr &= !(0xFF << 16);
}
Some(number) => {
if number > 0b1111_1110 {
return false;
}
self.repr &= !(0xFF << 16);
self.repr |= (number + 1) << 16;
}
}
true
}
#[inline]
fn pre(&self) -> Option<(PreRelease, u64)> {
let v = (self.repr >> 8) & 0xFF;
if v == 0xFF {
return None;
}
let number = v & 0b0011_1111;
let kind = match v >> 6 {
0 => PreRelease::Alpha,
1 => PreRelease::Beta,
2 => PreRelease::Rc,
_ => unreachable!(),
};
Some((kind, number))
}
#[inline]
fn set_pre(&mut self, value: Option<(PreRelease, u64)>) -> bool {
if value.is_some() && (self.post().is_some() || self.dev().is_some()) {
return false;
}
match value {
None => {
self.repr |= 0xFF << 8;
}
Some((kind, number)) => {
if number > 0b0011_1111 {
return false;
}
let kind = match kind {
PreRelease::Alpha => 0,
PreRelease::Beta => 1,
PreRelease::Rc => 2,
};
self.repr &= !(0xFF << 8);
self.repr |= ((kind << 6) | number) << 8;
}
}
true
}
#[inline]
fn dev(&self) -> Option<u64> {
let v = self.repr & 0xFF;
if v == 0xFF {
None
} else {
Some(v)
}
}
#[inline]
fn set_dev(&mut self, value: Option<u64>) -> bool {
if value.is_some() && (self.pre().is_some() || self.post().is_some()) {
return false;
}
match value {
None => {
self.repr |= 0xFF;
}
Some(number) => {
if number > 0b1111_1110 {
return false;
}
self.repr &= !0xFF;
self.repr |= number;
}
}
true
}
#[inline]
fn local(&self) -> &[LocalSegment] {
// A "small" version is never used if the version has a non-zero number
// of local segments.
&[]
}
}
/// The "full" representation of a version.
///
/// This can represent all possible versions, but is a bit beefier because of
/// it. It also uses some indirection for variable length data such as the
/// release numbers and the local segments.
///
/// In general, the "full" representation is rarely used in practice since most
/// versions will fit into the "small" representation.
#[derive(Clone, Debug)]
struct VersionFull {
/// The [versioning
/// epoch](https://peps.python.org/pep-0440/#version-epochs). Normally
/// just 0, but you can increment it if you switched the versioning
/// scheme.
epoch: u64,
/// The normal number part of the version (["final
/// release"](https://peps.python.org/pep-0440/#final-releases)), such
/// a `1.2.3` in `4!1.2.3-a8.post9.dev1`
///
/// Note that we drop the * placeholder by moving it to `Operator`
release: Vec<u64>,
/// The [prerelease](https://peps.python.org/pep-0440/#pre-releases),
/// i.e. alpha, beta or rc plus a number
///
/// Note that whether this is Some influences the version range
/// matching since normally we exclude all prerelease versions
pre: Option<(PreRelease, u64)>,
/// The [Post release
/// version](https://peps.python.org/pep-0440/#post-releases), higher
/// post version are preferred over lower post or none-post versions
post: Option<u64>,
/// The [developmental
/// release](https://peps.python.org/pep-0440/#developmental-releases),
/// if any
dev: Option<u64>,
/// A [local version
/// identifier](https://peps.python.org/pep-0440/#local-version-identif
/// iers) such as `+deadbeef` in `1.2.3+deadbeef`
///
/// > They consist of a normal public version identifier (as defined
/// > in the previous section), along with an arbitrary “local version
/// > label”, separated from the public version identifier by a plus.
/// > Local version labels have no specific semantics assigned, but
/// > some syntactic restrictions are imposed.
local: Vec<LocalSegment>,
}
/// A version number pattern.
///
/// A version pattern appears in a
/// [`VersionSpecifier`](crate::VersionSpecifier). It is just like a version,
/// except that it permits a trailing `*` (wildcard) at the end of the version
/// number. The wildcard indicates that any version with the same prefix should
/// match.
///
/// A `VersionPattern` cannot do any matching itself. Instead,
/// it needs to be paired with an [`Operator`] to create a
/// [`VersionSpecifier`](crate::VersionSpecifier).
///
/// Here are some valid and invalid examples:
///
/// * `1.2.3` -> verbatim pattern
/// * `1.2.3.*` -> wildcard pattern
/// * `1.2.*.4` -> invalid
/// * `1.0-dev1.*` -> invalid
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct VersionPattern {
version: Version,
wildcard: bool,
}
impl VersionPattern {
/// Creates a new verbatim version pattern that matches the given
/// version exactly.
#[inline]
pub fn verbatim(version: Version) -> VersionPattern {
VersionPattern {
version,
wildcard: false,
}
}
/// Creates a new wildcard version pattern that matches any version with
/// the given version as a prefix.
#[inline]
pub fn wildcard(version: Version) -> VersionPattern {
VersionPattern {
version,
wildcard: true,
}
}
/// Returns the underlying version.
#[inline]
pub fn version(&self) -> &Version {
&self.version
}
/// Consumes this pattern and returns ownership of the underlying version.
#[inline]
pub fn into_version(self) -> Version {
self.version
}
/// Returns true if and only if this pattern contains a wildcard.
#[inline]
pub fn is_wildcard(&self) -> bool {
self.wildcard
}
}
impl FromStr for VersionPattern {
type Err = VersionPatternParseError;
fn from_str(version: &str) -> Result<VersionPattern, VersionPatternParseError> {
Parser::new(version.as_bytes()).parse_pattern()
}
}
/// Optional prerelease modifier (alpha, beta or release candidate) appended to version
///
/// <https://peps.python.org/pep-0440/#pre-releases>
#[derive(PartialEq, Eq, Debug, Hash, Clone, Copy, Ord, PartialOrd)]
#[cfg_attr(feature = "pyo3", pyclass)]
pub enum PreRelease {
/// alpha prerelease
Alpha,
/// beta prerelease
Beta,
/// release candidate prerelease
Rc,
}
impl std::fmt::Display for PreRelease {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Alpha => write!(f, "a"),
Self::Beta => write!(f, "b"),
Self::Rc => write!(f, "rc"),
}
}
}
/// A part of the [local version identifier](<https://peps.python.org/pep-0440/#local-version-identifiers>)
///
/// Local versions are a mess:
///
/// > Comparison and ordering of local versions considers each segment of the local version
/// > (divided by a .) separately. If a segment consists entirely of ASCII digits then that section
/// > should be considered an integer for comparison purposes and if a segment contains any ASCII
/// > letters then that segment is compared lexicographically with case insensitivity. When
/// > comparing a numeric and lexicographic segment, the numeric section always compares as greater
/// > than the lexicographic segment. Additionally a local version with a great number of segments
/// > will always compare as greater than a local version with fewer segments, as long as the
/// > shorter local versions segments match the beginning of the longer local versions segments
/// > exactly.
///
/// Luckily the default `Ord` implementation for `Vec<LocalSegment>` matches the PEP 440 rules.
#[derive(Eq, PartialEq, Debug, Clone, Hash)]
pub enum LocalSegment {
/// Not-parseable as integer segment of local version
String(String),
/// Inferred integer segment of local version
Number(u64),
}
impl std::fmt::Display for LocalSegment {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::String(string) => write!(f, "{string}"),
Self::Number(number) => write!(f, "{number}"),
}
}
}
impl PartialOrd for LocalSegment {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for LocalSegment {
fn cmp(&self, other: &Self) -> Ordering {
// <https://peps.python.org/pep-0440/#local-version-identifiers>
match (self, other) {
(Self::Number(n1), Self::Number(n2)) => n1.cmp(n2),
(Self::String(s1), Self::String(s2)) => s1.cmp(s2),
(Self::Number(_), Self::String(_)) => Ordering::Greater,
(Self::String(_), Self::Number(_)) => Ordering::Less,
}
}
}
/// The state used for [parsing a version][pep440].
///
/// This parses the most "flexible" format of a version as described in the
/// "normalization" section of PEP 440.
///
/// This can also parse a version "pattern," which essentially is just like
/// parsing a version, but permits a trailing wildcard. e.g., `1.2.*`.
///
/// [pep440]: https://packaging.python.org/en/latest/specifications/version-specifiers/
#[derive(Debug)]
struct Parser<'a> {
/// The version string we are parsing.
v: &'a [u8],
/// The current position of the parser.
i: usize,
/// The epoch extracted from the version.
epoch: u64,
/// The release numbers extracted from the version.
release: ReleaseNumbers,
/// The pre-release version, if any.
pre: Option<(PreRelease, u64)>,
/// The post-release version, if any.
post: Option<u64>,
/// The dev release, if any.
dev: Option<u64>,
/// The local segments, if any.
local: Vec<LocalSegment>,
/// Whether a wildcard at the end of the version was found or not.
///
/// This is only valid when a version pattern is being parsed.
wildcard: bool,
}
impl<'a> Parser<'a> {
/// The "separators" that are allowed in several different parts of a
/// version.
const SEPARATOR: ByteSet = ByteSet::new(&[b'.', b'_', b'-']);
/// Create a new `Parser` for parsing the version in the given byte string.
fn new(version: &'a [u8]) -> Parser<'a> {
Parser {
v: version,
i: 0,
epoch: 0,
release: ReleaseNumbers::new(),
pre: None,
post: None,
dev: None,
local: vec![],
wildcard: false,
}
}
/// Parse a verbatim version.
///
/// If a version pattern is found, then an error is returned.
fn parse(self) -> Result<Version, VersionParseError> {
match self.parse_pattern() {
Ok(vpat) => {
if !vpat.is_wildcard() {
Ok(vpat.into_version())
} else {
Err(ErrorKind::Wildcard.into())
}
}
// If we get an error when parsing a version pattern, then
// usually it will actually just be a VersionParseError.
// But if it's specific to version patterns, and since
// we are expecting a verbatim version here, we can just
// return a generic "wildcards not allowed" error in that
// case.
Err(err) => match *err.kind {
PatternErrorKind::Version(err) => Err(err),
PatternErrorKind::WildcardNotTrailing => Err(ErrorKind::Wildcard.into()),
},
}
}
/// Parse a version pattern, which may be a verbatim version.
fn parse_pattern(mut self) -> Result<VersionPattern, VersionPatternParseError> {
if let Some(vpat) = self.parse_fast() {
return Ok(vpat);
}
self.bump_while(|byte| byte.is_ascii_whitespace());
self.bump_if("v");
self.parse_epoch_and_initial_release()?;
self.parse_rest_of_release()?;
if self.parse_wildcard()? {
return Ok(self.into_pattern());
}
self.parse_pre()?;
self.parse_post()?;
self.parse_dev()?;
self.parse_local()?;
self.bump_while(|byte| byte.is_ascii_whitespace());
if !self.is_done() {
let remaining = String::from_utf8_lossy(&self.v[self.i..]).into_owned();
let version = self.into_pattern().version;
return Err(ErrorKind::UnexpectedEnd { version, remaining }.into());
}
Ok(self.into_pattern())
}
/// Attempts to do a "fast parse" of a version.
///
/// This looks for versions of the form `w[.x[.y[.z]]]` while
/// simultaneously parsing numbers. This format corresponds to the
/// overwhelming majority of all version strings and can avoid most of the
/// work done in the more general parser.
///
/// If the version string is not in the format of `w[.x[.y[.z]]]`, then
/// this returns `None`.
fn parse_fast(&self) -> Option<VersionPattern> {
let (mut prev_digit, mut cur, mut release, mut len) = (false, 0u8, [0u8; 4], 0u8);
for &byte in self.v {
if byte == b'.' {
if !prev_digit {
return None;
}
prev_digit = false;
*release.get_mut(usize::from(len))? = cur;
len += 1;
cur = 0;
} else {
let digit = byte.checked_sub(b'0')?;
if digit > 9 {
return None;
}
prev_digit = true;
cur = cur.checked_mul(10)?.checked_add(digit)?;
}
}
if !prev_digit {
return None;
}
*release.get_mut(usize::from(len))? = cur;
len += 1;
let small = VersionSmall {
// Clippy warns about no-ops like `(0x00 << 16)`, but I
// think it makes the bit logic much clearer, and makes it
// explicit that nothing was forgotten.
#[allow(clippy::identity_op)]
repr: (u64::from(release[0]) << 48)
| (u64::from(release[1]) << 40)
| (u64::from(release[2]) << 32)
| (u64::from(release[3]) << 24)
| (0x00 << 16)
| (0xFF << 8)
| (0xFF << 0),
release: [
u64::from(release[0]),
u64::from(release[1]),
u64::from(release[2]),
u64::from(release[3]),
],
len,
};
let inner = Arc::new(VersionInner::Small { small });
let version = Version { inner };
Some(VersionPattern {
version,
wildcard: false,
})
}
/// Parses an optional initial epoch number and the first component of the
/// release part of a version number. In all cases, the first part of a
/// version must be a single number, and if one isn't found, an error is
/// returned.
///
/// Upon success, the epoch is possibly set and the release has exactly one
/// number in it. The parser will be positioned at the beginning of the
/// next component, which is usually a `.`, indicating the start of the
/// second number in the release component. It could however point to the
/// end of input, in which case, a valid version should be returned.
fn parse_epoch_and_initial_release(&mut self) -> Result<(), VersionPatternParseError> {
let first_number = self.parse_number()?.ok_or(ErrorKind::NoLeadingNumber)?;
let first_release_number = if self.bump_if("!") {
self.epoch = first_number;
self.parse_number()?
.ok_or(ErrorKind::NoLeadingReleaseNumber)?
} else {
first_number
};
self.release.push(first_release_number);
Ok(())
}
/// This parses the rest of the numbers in the release component of
/// the version. Upon success, the release part of this parser will be
/// completely finished, and the parser will be positioned at the first
/// character after the last number in the release component. This position
/// may point to a `.`, for example, the second dot in `1.2.*` or `1.2.a5`
/// or `1.2.dev5`. It may also point to the end of the input, in which
/// case, the caller should return the current version.
///
/// Callers should use this after the initial optional epoch and the first
/// release number have been parsed.
fn parse_rest_of_release(&mut self) -> Result<(), VersionPatternParseError> {
while self.bump_if(".") {
let Some(n) = self.parse_number()? else {
self.unbump();
break;
};
self.release.push(n);
}
Ok(())
}
/// Attempts to parse a trailing wildcard after the numbers in the release
/// component. Upon success, this returns `true` and positions the parser
/// immediately after the `.*` (which must necessarily be the end of
/// input), or leaves it unchanged if no wildcard was found. It is an error
/// if a `.*` is found and there is still more input after the `.*`.
///
/// Callers should use this immediately after parsing all of the numbers in
/// the release component of the version.
fn parse_wildcard(&mut self) -> Result<bool, VersionPatternParseError> {
if !self.bump_if(".*") {
return Ok(false);
}
if !self.is_done() {
return Err(PatternErrorKind::WildcardNotTrailing.into());
}
self.wildcard = true;
Ok(true)
}
/// Parses the pre-release component of a version.
///
/// If this version has no pre-release component, then this is a no-op.
/// Otherwise, it sets `self.pre` and positions the parser to the first
/// byte immediately following the pre-release.
fn parse_pre(&mut self) -> Result<(), VersionPatternParseError> {
// SPELLINGS and MAP are in correspondence. SPELLINGS is used to look
// for what spelling is used in the version string (if any), and
// the index of the element found is used to lookup which type of
// PreRelease it is.
//
// Note also that the order of the strings themselves matters. If 'pre'
// were before 'preview' for example, then 'preview' would never match
// since the strings are matched in order.
const SPELLINGS: StringSet =
StringSet::new(&["alpha", "beta", "preview", "pre", "rc", "a", "b", "c"]);
const MAP: &[PreRelease] = &[
PreRelease::Alpha,
PreRelease::Beta,
PreRelease::Rc,
PreRelease::Rc,
PreRelease::Rc,
PreRelease::Alpha,
PreRelease::Beta,
PreRelease::Rc,
];
let oldpos = self.i;
self.bump_if_byte_set(&Parser::SEPARATOR);
let Some(spelling) = self.bump_if_string_set(&SPELLINGS) else {
// We might see a separator (or not) and then something
// that isn't a pre-release. At this stage, we can't tell
// whether it's invalid or not. So we back-up and let the
// caller try something else.
self.reset(oldpos);
return Ok(());
};
let kind = MAP[spelling];
self.bump_if_byte_set(&Parser::SEPARATOR);
// Under the normalization rules, a pre-release without an
// explicit number defaults to `0`.
let number = self.parse_number()?.unwrap_or(0);
self.pre = Some((kind, number));
Ok(())
}
/// Parses the post-release component of a version.
///
/// If this version has no post-release component, then this is a no-op.
/// Otherwise, it sets `self.post` and positions the parser to the first
/// byte immediately following the post-release.
fn parse_post(&mut self) -> Result<(), VersionPatternParseError> {
const SPELLINGS: StringSet = StringSet::new(&["post", "rev", "r"]);
let oldpos = self.i;
if self.bump_if("-") {
if let Some(n) = self.parse_number()? {
self.post = Some(n);
return Ok(());
}
self.reset(oldpos);
}
self.bump_if_byte_set(&Parser::SEPARATOR);
if self.bump_if_string_set(&SPELLINGS).is_none() {
// As with pre-releases, if we don't see post|rev|r here, we can't
// yet determine whether the version as a whole is invalid since
// post-releases are optional.
self.reset(oldpos);
return Ok(());
}
self.bump_if_byte_set(&Parser::SEPARATOR);
// Under the normalization rules, a post-release without an
// explicit number defaults to `0`.
self.post = Some(self.parse_number()?.unwrap_or(0));
Ok(())
}
/// Parses the dev-release component of a version.
///
/// If this version has no dev-release component, then this is a no-op.
/// Otherwise, it sets `self.dev` and positions the parser to the first
/// byte immediately following the post-release.
fn parse_dev(&mut self) -> Result<(), VersionPatternParseError> {
let oldpos = self.i;
self.bump_if_byte_set(&Parser::SEPARATOR);
if !self.bump_if("dev") {
// As with pre-releases, if we don't see dev here, we can't
// yet determine whether the version as a whole is invalid
// since dev-releases are optional.
self.reset(oldpos);
return Ok(());
}
self.bump_if_byte_set(&Parser::SEPARATOR);
// Under the normalization rules, a post-release without an
// explicit number defaults to `0`.
self.dev = Some(self.parse_number()?.unwrap_or(0));
Ok(())
}
/// Parses the local component of a version.
///
/// If this version has no local component, then this is a no-op.
/// Otherwise, it adds to `self.local` and positions the parser to the
/// first byte immediately following the local component. (Which ought to
/// be the end of the version since the local component is the last thing
/// that can appear in a version.)
fn parse_local(&mut self) -> Result<(), VersionPatternParseError> {
if !self.bump_if("+") {
return Ok(());
}
let mut precursor = '+';
loop {
let first = self.bump_while(|byte| byte.is_ascii_alphanumeric());
if first.is_empty() {
return Err(ErrorKind::LocalEmpty { precursor }.into());
}
self.local.push(if let Ok(number) = parse_u64(first) {
LocalSegment::Number(number)
} else {
let string = String::from_utf8(first.to_ascii_lowercase())
.expect("ASCII alphanumerics are always valid UTF-8");
LocalSegment::String(string)
});
let Some(byte) = self.bump_if_byte_set(&Parser::SEPARATOR) else {
break;
};
precursor = char::from(byte);
}
Ok(())
}
/// Consumes input from the current position while the characters are ASCII
/// digits, and then attempts to parse what was consumed as a decimal
/// number.
///
/// If nothing was consumed, then `Ok(None)` is returned. Otherwise, if the
/// digits consumed do not form a valid decimal number that fits into a
/// `u64`, then an error is returned.
fn parse_number(&mut self) -> Result<Option<u64>, VersionPatternParseError> {
let digits = self.bump_while(|ch| ch.is_ascii_digit());
if digits.is_empty() {
return Ok(None);
}
Ok(Some(parse_u64(digits)?))
}
/// Turns whatever state has been gathered into a `VersionPattern`.
///
/// # Panics
///
/// When `self.release` is empty. Callers must ensure at least one part
/// of the release component has been successfully parsed. Otherwise, the
/// version itself is invalid.
fn into_pattern(self) -> VersionPattern {
assert!(
self.release.len() > 0,
"version with no release numbers is invalid"
);
let version = Version::new(self.release.as_slice())
.with_epoch(self.epoch)
.with_pre(self.pre)
.with_post(self.post)
.with_dev(self.dev)
.with_local(self.local);
VersionPattern {
version,
wildcard: self.wildcard,
}
}
/// Consumes input from this parser while the given predicate returns true.
/// The resulting input (which may be empty) is returned.
///
/// Once returned, the parser is positioned at the first position where the
/// predicate returns `false`. (This may be the position at the end of the
/// input such that [`Parser::is_done`] returns `true`.)
fn bump_while(&mut self, mut predicate: impl FnMut(u8) -> bool) -> &'a [u8] {
let start = self.i;
while !self.is_done() && predicate(self.byte()) {
self.i = self.i.saturating_add(1);
}
&self.v[start..self.i]
}
/// Consumes `bytes.len()` bytes from the current position of the parser if
/// and only if `bytes` is a prefix of the input starting at the current
/// position. Otherwise, this is a no-op. Returns true when consumption was
/// successful.
fn bump_if(&mut self, string: &str) -> bool {
if self.is_done() {
return false;
}
if starts_with_ignore_ascii_case(string.as_bytes(), &self.v[self.i..]) {
self.i = self
.i
.checked_add(string.len())
.expect("valid offset because of prefix");
true
} else {
false
}
}
/// Like [`Parser::bump_if`], but attempts each string in the ordered set
/// given. If one is successfully consumed from the start of the current
/// position in the input, then it is returned.
fn bump_if_string_set(&mut self, set: &StringSet) -> Option<usize> {
let index = set.starts_with(&self.v[self.i..])?;
let found = &set.strings[index];
self.i = self
.i
.checked_add(found.len())
.expect("valid offset because of prefix");
Some(index)
}
/// Like [`Parser::bump_if`], but attempts each byte in the set
/// given. If one is successfully consumed from the start of the
/// current position in the input.
fn bump_if_byte_set(&mut self, set: &ByteSet) -> Option<u8> {
let found = set.starts_with(&self.v[self.i..])?;
self.i = self
.i
.checked_add(1)
.expect("valid offset because of prefix");
Some(found)
}
/// Moves the parser back one byte. i.e., ungetch.
///
/// This is useful when one has bumped the parser "too far" and wants to
/// back-up. This tends to help with composition among parser routines.
///
/// # Panics
///
/// When the parser is already positioned at the beginning.
fn unbump(&mut self) {
self.i = self.i.checked_sub(1).expect("not at beginning of input");
}
/// Resets the parser to the given position.
///
/// # Panics
///
/// When `offset` is greater than `self.v.len()`.
fn reset(&mut self, offset: usize) {
assert!(offset <= self.v.len());
self.i = offset;
}
/// Returns the byte at the current position of the parser.
///
/// # Panics
///
/// When `Parser::is_done` returns `true`.
fn byte(&self) -> u8 {
self.v[self.i]
}
/// Returns true if and only if there is no more input to consume.
fn is_done(&self) -> bool {
self.i >= self.v.len()
}
}
/// Stores the numbers found in the release portion of a version.
///
/// We use this in the version parser to avoid allocating in the 90+% case.
#[derive(Debug)]
enum ReleaseNumbers {
Inline { numbers: [u64; 4], len: usize },
Vec(Vec<u64>),
}
impl ReleaseNumbers {
/// Create a new empty set of release numbers.
fn new() -> ReleaseNumbers {
ReleaseNumbers::Inline {
numbers: [0; 4],
len: 0,
}
}
/// Push a new release number. This automatically switches over to the heap
/// when the lengths grow too big.
fn push(&mut self, n: u64) {
match *self {
ReleaseNumbers::Inline {
ref mut numbers,
ref mut len,
} => {
assert!(*len <= 4);
if *len == 4 {
let mut numbers = numbers.to_vec();
numbers.push(n);
*self = ReleaseNumbers::Vec(numbers.to_vec());
} else {
numbers[*len] = n;
*len += 1;
}
}
ReleaseNumbers::Vec(ref mut numbers) => {
numbers.push(n);
}
}
}
/// Returns the number of components in this release component.
fn len(&self) -> usize {
self.as_slice().len()
}
/// Returns the release components as a slice.
fn as_slice(&self) -> &[u64] {
match *self {
ReleaseNumbers::Inline { ref numbers, len } => &numbers[..len],
ReleaseNumbers::Vec(ref vec) => vec,
}
}
}
/// Represents a set of strings for prefix searching.
///
/// This can be built as a constant and is useful for quickly looking for one
/// of a number of matching literal strings while ignoring ASCII case.
struct StringSet {
/// A set of the first bytes of each string in this set. We use this to
/// quickly bail out of searching if the first byte of our haystack doesn't
/// match any element in this set.
first_byte: ByteSet,
/// The strings in this set. They are matched in order.
strings: &'static [&'static str],
}
impl StringSet {
/// Create a new string set for prefix searching from the given strings.
///
/// # Panics
///
/// When the number of strings is too big.
const fn new(strings: &'static [&'static str]) -> StringSet {
assert!(
strings.len() <= 20,
"only a small number of strings are supported"
);
let (mut firsts, mut firsts_len) = ([0u8; 20], 0);
let mut i = 0;
while i < strings.len() {
assert!(
!strings[i].is_empty(),
"every string in set should be non-empty",
);
firsts[firsts_len] = strings[i].as_bytes()[0];
firsts_len += 1;
i += 1;
}
let first_byte = ByteSet::new(&firsts);
StringSet {
first_byte,
strings,
}
}
/// Returns the index of the first string in this set that is a prefix of
/// the given haystack, or `None` if no elements are a prefix.
fn starts_with(&self, haystack: &[u8]) -> Option<usize> {
let first_byte = self.first_byte.starts_with(haystack)?;
for (i, &string) in self.strings.iter().enumerate() {
let bytes = string.as_bytes();
if bytes[0].eq_ignore_ascii_case(&first_byte)
&& starts_with_ignore_ascii_case(bytes, haystack)
{
return Some(i);
}
}
None
}
}
/// A set of bytes for searching case insensitively (ASCII only).
struct ByteSet {
set: [bool; 256],
}
impl ByteSet {
/// Create a new byte set for searching from the given bytes.
const fn new(bytes: &[u8]) -> ByteSet {
let mut set = [false; 256];
let mut i = 0;
while i < bytes.len() {
set[bytes[i].to_ascii_uppercase() as usize] = true;
set[bytes[i].to_ascii_lowercase() as usize] = true;
i += 1;
}
ByteSet { set }
}
/// Returns the first byte in the haystack if and only if that byte is in
/// this set (ignoring ASCII case).
fn starts_with(&self, haystack: &[u8]) -> Option<u8> {
let byte = *haystack.first()?;
if self.contains(byte) {
Some(byte)
} else {
None
}
}
/// Returns true if and only if the given byte is in this set.
fn contains(&self, byte: u8) -> bool {
self.set[usize::from(byte)]
}
}
impl std::fmt::Debug for ByteSet {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
let mut set = f.debug_set();
for byte in 0..=255 {
if self.contains(byte) {
set.entry(&char::from(byte));
}
}
set.finish()
}
}
/// An error that occurs when parsing a [`Version`] string fails.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct VersionParseError {
kind: Box<ErrorKind>,
}
impl std::error::Error for VersionParseError {}
impl std::fmt::Display for VersionParseError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match *self.kind {
ErrorKind::Wildcard => write!(f, "wildcards are not allowed in a version"),
ErrorKind::InvalidDigit { got } if got.is_ascii() => {
write!(f, "expected ASCII digit, but found {:?}", char::from(got))
}
ErrorKind::InvalidDigit { got } => {
write!(
f,
"expected ASCII digit, but found non-ASCII byte \\x{:02X}",
got
)
}
ErrorKind::NumberTooBig { ref bytes } => {
let string = match std::str::from_utf8(bytes) {
Ok(v) => v,
Err(err) => {
std::str::from_utf8(&bytes[..err.valid_up_to()]).expect("valid UTF-8")
}
};
write!(
f,
"expected number less than or equal to {}, \
but number found in {string:?} exceeds it",
u64::MAX,
)
}
ErrorKind::NoLeadingNumber => {
write!(
f,
"expected version to start with a number, \
but no leading ASCII digits were found"
)
}
ErrorKind::NoLeadingReleaseNumber => {
write!(
f,
"expected version to have a non-empty release component after an epoch, \
but no ASCII digits after the epoch were found"
)
}
ErrorKind::LocalEmpty { precursor } => {
write!(
f,
"found a `{precursor}` indicating the start of a local \
component in a version, but did not find any alpha-numeric \
ASCII segment following the `{precursor}`",
)
}
ErrorKind::UnexpectedEnd {
ref version,
ref remaining,
} => {
write!(
f,
"after parsing {version}, found {remaining:?} after it, \
which is not part of a valid version",
)
}
}
}
}
/// The kind of error that occurs when parsing a `Version`.
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) enum ErrorKind {
/// Occurs when a version pattern is found but a normal verbatim version is
/// expected.
Wildcard,
/// Occurs when an ASCII digit was expected, but something else was found.
InvalidDigit {
/// The (possibly non-ASCII) byte that was seen instead of [0-9].
got: u8,
},
/// Occurs when a number was found that exceeds what can fit into a u64.
NumberTooBig {
/// The bytes that were being parsed as a number. These may contain
/// invalid digits or even invalid UTF-8.
bytes: Vec<u8>,
},
/// Occurs when a version does not start with a leading number.
NoLeadingNumber,
/// Occurs when an epoch version does not have a number after the `!`.
NoLeadingReleaseNumber,
/// Occurs when a `+` (or a `.` after the first local segment) is seen
/// (indicating a local component of a version), but no alpha-numeric ASCII
/// string is found following it.
LocalEmpty {
/// Either a `+` or a `[-_.]` indicating what was found that demands a
/// non-empty local segment following it.
precursor: char,
},
/// Occurs when a version has been parsed but there is some unexpected
/// trailing data in the string.
UnexpectedEnd {
/// The version that has been parsed so far.
version: Version,
/// The bytes that were remaining and not parsed.
remaining: String,
},
}
impl From<ErrorKind> for VersionParseError {
fn from(kind: ErrorKind) -> VersionParseError {
VersionParseError {
kind: Box::new(kind),
}
}
}
/// An error that occurs when parsing a [`VersionPattern`] string fails.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct VersionPatternParseError {
kind: Box<PatternErrorKind>,
}
impl std::error::Error for VersionPatternParseError {}
impl std::fmt::Display for VersionPatternParseError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match *self.kind {
PatternErrorKind::Version(ref err) => err.fmt(f),
PatternErrorKind::WildcardNotTrailing => {
write!(f, "wildcards in versions must be at the end")
}
}
}
}
/// The kind of error that occurs when parsing a `VersionPattern`.
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) enum PatternErrorKind {
Version(VersionParseError),
WildcardNotTrailing,
}
impl From<PatternErrorKind> for VersionPatternParseError {
fn from(kind: PatternErrorKind) -> VersionPatternParseError {
VersionPatternParseError {
kind: Box::new(kind),
}
}
}
impl From<ErrorKind> for VersionPatternParseError {
fn from(kind: ErrorKind) -> VersionPatternParseError {
VersionPatternParseError::from(VersionParseError::from(kind))
}
}
impl From<VersionParseError> for VersionPatternParseError {
fn from(err: VersionParseError) -> VersionPatternParseError {
VersionPatternParseError {
kind: Box::new(PatternErrorKind::Version(err)),
}
}
}
/// Workaround for <https://github.com/PyO3/pyo3/pull/2786>
#[cfg(feature = "pyo3")]
#[derive(Clone, Debug)]
#[pyclass(name = "Version")]
pub struct PyVersion(pub Version);
#[cfg(feature = "pyo3")]
#[pymethods]
impl PyVersion {
/// The [versioning epoch](https://peps.python.org/pep-0440/#version-epochs). Normally just 0,
/// but you can increment it if you switched the versioning scheme.
#[getter]
pub fn epoch(&self) -> u64 {
self.0.epoch()
}
/// The normal number part of the version
/// (["final release"](https://peps.python.org/pep-0440/#final-releases)),
/// such a `1.2.3` in `4!1.2.3-a8.post9.dev1`
///
/// Note that we drop the * placeholder by moving it to `Operator`
#[getter]
pub fn release(&self) -> Vec<u64> {
self.0.release().to_vec()
}
/// The [prerelease](https://peps.python.org/pep-0440/#pre-releases), i.e. alpha, beta or rc
/// plus a number
///
/// Note that whether this is Some influences the version
/// range matching since normally we exclude all prerelease versions
#[getter]
pub fn pre(&self) -> Option<(PreRelease, u64)> {
self.0.pre()
}
/// The [Post release version](https://peps.python.org/pep-0440/#post-releases),
/// higher post version are preferred over lower post or none-post versions
#[getter]
pub fn post(&self) -> Option<u64> {
self.0.post()
}
/// The [developmental release](https://peps.python.org/pep-0440/#developmental-releases),
/// if any
#[getter]
pub fn dev(&self) -> Option<u64> {
self.0.dev()
}
/// The first item of release or 0 if unavailable.
#[getter]
#[allow(clippy::get_first)]
pub fn major(&self) -> u64 {
self.0.release().get(0).copied().unwrap_or_default()
}
/// The second item of release or 0 if unavailable.
#[getter]
pub fn minor(&self) -> u64 {
self.0.release().get(1).copied().unwrap_or_default()
}
/// The third item of release or 0 if unavailable.
#[getter]
pub fn micro(&self) -> u64 {
self.0.release().get(2).copied().unwrap_or_default()
}
/// Parses a PEP 440 version string
#[cfg(feature = "pyo3")]
#[new]
pub fn parse(version: &str) -> PyResult<Self> {
Ok(Self(
Version::from_str(version).map_err(|e| PyValueError::new_err(e.to_string()))?,
))
}
// Maps the error type
/// Parse a PEP 440 version optionally ending with `.*`
#[cfg(feature = "pyo3")]
#[staticmethod]
pub fn parse_star(version_specifier: &str) -> PyResult<(Self, bool)> {
version_specifier
.parse::<VersionPattern>()
.map_err(|e| PyValueError::new_err(e.to_string()))
.map(|VersionPattern { version, wildcard }| (Self(version), wildcard))
}
/// Returns the normalized representation
#[cfg(feature = "pyo3")]
pub fn __str__(&self) -> String {
self.0.to_string()
}
/// Returns the normalized representation
#[cfg(feature = "pyo3")]
pub fn __repr__(&self) -> String {
format!(r#"<Version("{}")>"#, self.0)
}
/// Returns the normalized representation
#[cfg(feature = "pyo3")]
pub fn __hash__(&self) -> u64 {
let mut hasher = std::collections::hash_map::DefaultHasher::new();
self.0.hash(&mut hasher);
hasher.finish()
}
#[cfg(feature = "pyo3")]
fn __richcmp__(&self, other: &Self, op: CompareOp) -> bool {
op.matches(self.0.cmp(&other.0))
}
fn any_prerelease(&self) -> bool {
self.0.any_prerelease()
}
}
#[cfg(feature = "pyo3")]
impl IntoPy<PyObject> for Version {
fn into_py(self, py: Python<'_>) -> PyObject {
PyVersion(self).into_py(py)
}
}
#[cfg(feature = "pyo3")]
impl<'source> FromPyObject<'source> for Version {
fn extract(ob: &'source PyAny) -> PyResult<Self> {
Ok(ob.extract::<PyVersion>()?.0)
}
}
/// Compare the release parts of two versions, e.g. `4.3.1` > `4.2`, `1.1.0` ==
/// `1.1` and `1.16` < `1.19`
pub(crate) fn compare_release(this: &[u64], other: &[u64]) -> Ordering {
if this.len() == other.len() {
return this.cmp(other);
}
// "When comparing release segments with different numbers of components, the shorter segment
// is padded out with additional zeros as necessary"
for (this, other) in this.iter().chain(std::iter::repeat(&0)).zip(
other
.iter()
.chain(std::iter::repeat(&0))
.take(this.len().max(other.len())),
) {
match this.cmp(other) {
Ordering::Less => {
return Ordering::Less;
}
Ordering::Equal => {}
Ordering::Greater => {
return Ordering::Greater;
}
}
}
Ordering::Equal
}
/// Compare the parts attached after the release, given equal release
///
/// 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>)
///
/// 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
/// local the Option<Vec<T>> luckily already has the correct default Ord
/// implementation
///
/// [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()) {
// dev release
(None, None, Some(n)) => (0, 0, None, n, version.local()),
// alpha release
(Some((PreRelease::Alpha, n)), post, dev) => {
(1, n, post, dev.unwrap_or(u64::MAX), version.local())
}
// beta release
(Some((PreRelease::Beta, n)), post, dev) => {
(2, n, post, dev.unwrap_or(u64::MAX), version.local())
}
// alpha release
(Some((PreRelease::Rc, n)), post, dev) => {
(3, n, post, dev.unwrap_or(u64::MAX), version.local())
}
// final release
(None, None, None) => (4, 0, None, 0, version.local()),
// post release
(None, Some(post), dev) => (5, 0, Some(post), dev.unwrap_or(u64::MAX), version.local()),
}
}
/// Returns true only when, ignoring ASCII case, `needle` is a prefix of
/// `haystack`.
fn starts_with_ignore_ascii_case(needle: &[u8], haystack: &[u8]) -> bool {
needle.len() <= haystack.len()
&& std::iter::zip(needle, haystack).all(|(b1, b2)| b1.eq_ignore_ascii_case(b2))
}
/// Parses a u64 number from the given slice of ASCII digit characters.
///
/// If any byte in the given slice is not [0-9], then this returns an error.
/// Similarly, if the number parsed does not fit into a `u64`, then this
/// returns an error.
///
/// # Motivation
///
/// We hand-write this for a couple reasons. Firstly, the standard library's
/// FromStr impl for parsing integers requires UTF-8 validation first. We
/// don't need that for version parsing since we stay in the realm of ASCII.
/// Secondly, std's version is a little more flexible because it supports
/// signed integers. So for example, it permits a leading `+` before the actual
/// integer. We don't need that for version parsing.
fn parse_u64(bytes: &[u8]) -> Result<u64, VersionParseError> {
let mut n: u64 = 0;
for &byte in bytes {
let digit = match byte.checked_sub(b'0') {
None => return Err(ErrorKind::InvalidDigit { got: byte }.into()),
Some(digit) if digit > 9 => return Err(ErrorKind::InvalidDigit { got: byte }.into()),
Some(digit) => {
debug_assert!((0..=9).contains(&digit));
u64::from(digit)
}
};
n = n
.checked_mul(10)
.and_then(|n| n.checked_add(digit))
.ok_or_else(|| ErrorKind::NumberTooBig {
bytes: bytes.to_vec(),
})?;
}
Ok(n)
}
/// The minimum version that can be represented by a [`Version`].
pub static MIN_VERSION: once_cell::sync::Lazy<Version> =
once_cell::sync::Lazy::new(|| Version::from_str("0a0.dev0").unwrap());
#[cfg(feature = "pubgrub")]
impl pubgrub::version::Version for Version {
fn lowest() -> Self {
MIN_VERSION.to_owned()
}
fn bump(&self) -> Self {
let mut next = self.clone();
if let Some(dev) = next.dev() {
next = next.with_dev(Some(dev + 1));
} else if let Some(post) = next.post() {
next = next.with_post(Some(post + 1));
} else {
next = next.with_post(Some(0)).with_dev(Some(0));
}
next
}
}
#[cfg(test)]
mod tests {
use std::str::FromStr;
#[cfg(feature = "pyo3")]
use pyo3::pyfunction;
use crate::VersionSpecifier;
use super::*;
/// <https://github.com/pypa/packaging/blob/237ff3aa348486cf835a980592af3a59fccd6101/tests/test_version.py#L24-L81>
#[test]
fn test_packaging_versions() {
let versions = [
// Implicit epoch of 0
("1.0.dev456", Version::new([1, 0]).with_dev(Some(456))),
(
"1.0a1",
Version::new([1, 0]).with_pre(Some((PreRelease::Alpha, 1))),
),
(
"1.0a2.dev456",
Version::new([1, 0])
.with_pre(Some((PreRelease::Alpha, 2)))
.with_dev(Some(456)),
),
(
"1.0a12.dev456",
Version::new([1, 0])
.with_pre(Some((PreRelease::Alpha, 12)))
.with_dev(Some(456)),
),
(
"1.0a12",
Version::new([1, 0]).with_pre(Some((PreRelease::Alpha, 12))),
),
(
"1.0b1.dev456",
Version::new([1, 0])
.with_pre(Some((PreRelease::Beta, 1)))
.with_dev(Some(456)),
),
(
"1.0b2",
Version::new([1, 0]).with_pre(Some((PreRelease::Beta, 2))),
),
(
"1.0b2.post345.dev456",
Version::new([1, 0])
.with_pre(Some((PreRelease::Beta, 2)))
.with_dev(Some(456))
.with_post(Some(345)),
),
(
"1.0b2.post345",
Version::new([1, 0])
.with_pre(Some((PreRelease::Beta, 2)))
.with_post(Some(345)),
),
(
"1.0b2-346",
Version::new([1, 0])
.with_pre(Some((PreRelease::Beta, 2)))
.with_post(Some(346)),
),
(
"1.0c1.dev456",
Version::new([1, 0])
.with_pre(Some((PreRelease::Rc, 1)))
.with_dev(Some(456)),
),
(
"1.0c1",
Version::new([1, 0]).with_pre(Some((PreRelease::Rc, 1))),
),
(
"1.0rc2",
Version::new([1, 0]).with_pre(Some((PreRelease::Rc, 2))),
),
(
"1.0c3",
Version::new([1, 0]).with_pre(Some((PreRelease::Rc, 3))),
),
("1.0", Version::new([1, 0])),
(
"1.0.post456.dev34",
Version::new([1, 0]).with_post(Some(456)).with_dev(Some(34)),
),
("1.0.post456", Version::new([1, 0]).with_post(Some(456))),
("1.1.dev1", Version::new([1, 1]).with_dev(Some(1))),
(
"1.2+123abc",
Version::new([1, 2]).with_local(vec![LocalSegment::String("123abc".to_string())]),
),
(
"1.2+123abc456",
Version::new([1, 2])
.with_local(vec![LocalSegment::String("123abc456".to_string())]),
),
(
"1.2+abc",
Version::new([1, 2]).with_local(vec![LocalSegment::String("abc".to_string())]),
),
(
"1.2+abc123",
Version::new([1, 2]).with_local(vec![LocalSegment::String("abc123".to_string())]),
),
(
"1.2+abc123def",
Version::new([1, 2])
.with_local(vec![LocalSegment::String("abc123def".to_string())]),
),
(
"1.2+1234.abc",
Version::new([1, 2]).with_local(vec![
LocalSegment::Number(1234),
LocalSegment::String("abc".to_string()),
]),
),
(
"1.2+123456",
Version::new([1, 2]).with_local(vec![LocalSegment::Number(123456)]),
),
(
"1.2.r32+123456",
Version::new([1, 2])
.with_post(Some(32))
.with_local(vec![LocalSegment::Number(123456)]),
),
(
"1.2.rev33+123456",
Version::new([1, 2])
.with_post(Some(33))
.with_local(vec![LocalSegment::Number(123456)]),
),
// Explicit epoch of 1
(
"1!1.0.dev456",
Version::new([1, 0]).with_epoch(1).with_dev(Some(456)),
),
(
"1!1.0a1",
Version::new([1, 0])
.with_epoch(1)
.with_pre(Some((PreRelease::Alpha, 1))),
),
(
"1!1.0a2.dev456",
Version::new([1, 0])
.with_epoch(1)
.with_pre(Some((PreRelease::Alpha, 2)))
.with_dev(Some(456)),
),
(
"1!1.0a12.dev456",
Version::new([1, 0])
.with_epoch(1)
.with_pre(Some((PreRelease::Alpha, 12)))
.with_dev(Some(456)),
),
(
"1!1.0a12",
Version::new([1, 0])
.with_epoch(1)
.with_pre(Some((PreRelease::Alpha, 12))),
),
(
"1!1.0b1.dev456",
Version::new([1, 0])
.with_epoch(1)
.with_pre(Some((PreRelease::Beta, 1)))
.with_dev(Some(456)),
),
(
"1!1.0b2",
Version::new([1, 0])
.with_epoch(1)
.with_pre(Some((PreRelease::Beta, 2))),
),
(
"1!1.0b2.post345.dev456",
Version::new([1, 0])
.with_epoch(1)
.with_pre(Some((PreRelease::Beta, 2)))
.with_post(Some(345))
.with_dev(Some(456)),
),
(
"1!1.0b2.post345",
Version::new([1, 0])
.with_epoch(1)
.with_pre(Some((PreRelease::Beta, 2)))
.with_post(Some(345)),
),
(
"1!1.0b2-346",
Version::new([1, 0])
.with_epoch(1)
.with_pre(Some((PreRelease::Beta, 2)))
.with_post(Some(346)),
),
(
"1!1.0c1.dev456",
Version::new([1, 0])
.with_epoch(1)
.with_pre(Some((PreRelease::Rc, 1)))
.with_dev(Some(456)),
),
(
"1!1.0c1",
Version::new([1, 0])
.with_epoch(1)
.with_pre(Some((PreRelease::Rc, 1))),
),
(
"1!1.0rc2",
Version::new([1, 0])
.with_epoch(1)
.with_pre(Some((PreRelease::Rc, 2))),
),
(
"1!1.0c3",
Version::new([1, 0])
.with_epoch(1)
.with_pre(Some((PreRelease::Rc, 3))),
),
("1!1.0", Version::new([1, 0]).with_epoch(1)),
(
"1!1.0.post456.dev34",
Version::new([1, 0])
.with_epoch(1)
.with_post(Some(456))
.with_dev(Some(34)),
),
(
"1!1.0.post456",
Version::new([1, 0]).with_epoch(1).with_post(Some(456)),
),
(
"1!1.1.dev1",
Version::new([1, 1]).with_epoch(1).with_dev(Some(1)),
),
(
"1!1.2+123abc",
Version::new([1, 2])
.with_epoch(1)
.with_local(vec![LocalSegment::String("123abc".to_string())]),
),
(
"1!1.2+123abc456",
Version::new([1, 2])
.with_epoch(1)
.with_local(vec![LocalSegment::String("123abc456".to_string())]),
),
(
"1!1.2+abc",
Version::new([1, 2])
.with_epoch(1)
.with_local(vec![LocalSegment::String("abc".to_string())]),
),
(
"1!1.2+abc123",
Version::new([1, 2])
.with_epoch(1)
.with_local(vec![LocalSegment::String("abc123".to_string())]),
),
(
"1!1.2+abc123def",
Version::new([1, 2])
.with_epoch(1)
.with_local(vec![LocalSegment::String("abc123def".to_string())]),
),
(
"1!1.2+1234.abc",
Version::new([1, 2]).with_epoch(1).with_local(vec![
LocalSegment::Number(1234),
LocalSegment::String("abc".to_string()),
]),
),
(
"1!1.2+123456",
Version::new([1, 2])
.with_epoch(1)
.with_local(vec![LocalSegment::Number(123456)]),
),
(
"1!1.2.r32+123456",
Version::new([1, 2])
.with_epoch(1)
.with_post(Some(32))
.with_local(vec![LocalSegment::Number(123456)]),
),
(
"1!1.2.rev33+123456",
Version::new([1, 2])
.with_epoch(1)
.with_post(Some(33))
.with_local(vec![LocalSegment::Number(123456)]),
),
(
"98765!1.2.rev33+123456",
Version::new([1, 2])
.with_epoch(98765)
.with_post(Some(33))
.with_local(vec![LocalSegment::Number(123456)]),
),
];
for (string, structured) in versions {
match Version::from_str(string) {
Err(err) => {
unreachable!(
"expected {string:?} to parse as {structured:?}, but got {err:?}",
structured = structured.as_bloated_debug(),
)
}
Ok(v) => assert!(
v == structured,
"for {string:?}, expected {structured:?} but got {v:?}",
structured = structured.as_bloated_debug(),
v = v.as_bloated_debug(),
),
}
let spec = format!("=={string}");
match VersionSpecifier::from_str(&spec) {
Err(err) => {
unreachable!(
"expected version in {spec:?} to parse as {structured:?}, but got {err:?}",
structured = structured.as_bloated_debug(),
)
}
Ok(v) => assert!(
v.version() == &structured,
"for {string:?}, expected {structured:?} but got {v:?}",
structured = structured.as_bloated_debug(),
v = v.version.as_bloated_debug(),
),
}
}
}
/// <https://github.com/pypa/packaging/blob/237ff3aa348486cf835a980592af3a59fccd6101/tests/test_version.py#L91-L100>
#[test]
fn test_packaging_failures() {
let versions = [
// Versions with invalid local versions
"1.0+a+",
"1.0++",
"1.0+_foobar",
"1.0+foo&asd",
"1.0+1+1",
// Nonsensical versions should also be invalid
"french toast",
"==french toast",
];
for version in versions {
assert!(Version::from_str(version).is_err());
assert!(VersionSpecifier::from_str(&format!("=={version}")).is_err());
}
}
#[test]
fn test_equality_and_normalization() {
let versions = [
// Various development release incarnations
("1.0dev", "1.0.dev0"),
("1.0.dev", "1.0.dev0"),
("1.0dev1", "1.0.dev1"),
("1.0dev", "1.0.dev0"),
("1.0-dev", "1.0.dev0"),
("1.0-dev1", "1.0.dev1"),
("1.0DEV", "1.0.dev0"),
("1.0.DEV", "1.0.dev0"),
("1.0DEV1", "1.0.dev1"),
("1.0DEV", "1.0.dev0"),
("1.0.DEV1", "1.0.dev1"),
("1.0-DEV", "1.0.dev0"),
("1.0-DEV1", "1.0.dev1"),
// Various alpha incarnations
("1.0a", "1.0a0"),
("1.0.a", "1.0a0"),
("1.0.a1", "1.0a1"),
("1.0-a", "1.0a0"),
("1.0-a1", "1.0a1"),
("1.0alpha", "1.0a0"),
("1.0.alpha", "1.0a0"),
("1.0.alpha1", "1.0a1"),
("1.0-alpha", "1.0a0"),
("1.0-alpha1", "1.0a1"),
("1.0A", "1.0a0"),
("1.0.A", "1.0a0"),
("1.0.A1", "1.0a1"),
("1.0-A", "1.0a0"),
("1.0-A1", "1.0a1"),
("1.0ALPHA", "1.0a0"),
("1.0.ALPHA", "1.0a0"),
("1.0.ALPHA1", "1.0a1"),
("1.0-ALPHA", "1.0a0"),
("1.0-ALPHA1", "1.0a1"),
// Various beta incarnations
("1.0b", "1.0b0"),
("1.0.b", "1.0b0"),
("1.0.b1", "1.0b1"),
("1.0-b", "1.0b0"),
("1.0-b1", "1.0b1"),
("1.0beta", "1.0b0"),
("1.0.beta", "1.0b0"),
("1.0.beta1", "1.0b1"),
("1.0-beta", "1.0b0"),
("1.0-beta1", "1.0b1"),
("1.0B", "1.0b0"),
("1.0.B", "1.0b0"),
("1.0.B1", "1.0b1"),
("1.0-B", "1.0b0"),
("1.0-B1", "1.0b1"),
("1.0BETA", "1.0b0"),
("1.0.BETA", "1.0b0"),
("1.0.BETA1", "1.0b1"),
("1.0-BETA", "1.0b0"),
("1.0-BETA1", "1.0b1"),
// Various release candidate incarnations
("1.0c", "1.0rc0"),
("1.0.c", "1.0rc0"),
("1.0.c1", "1.0rc1"),
("1.0-c", "1.0rc0"),
("1.0-c1", "1.0rc1"),
("1.0rc", "1.0rc0"),
("1.0.rc", "1.0rc0"),
("1.0.rc1", "1.0rc1"),
("1.0-rc", "1.0rc0"),
("1.0-rc1", "1.0rc1"),
("1.0C", "1.0rc0"),
("1.0.C", "1.0rc0"),
("1.0.C1", "1.0rc1"),
("1.0-C", "1.0rc0"),
("1.0-C1", "1.0rc1"),
("1.0RC", "1.0rc0"),
("1.0.RC", "1.0rc0"),
("1.0.RC1", "1.0rc1"),
("1.0-RC", "1.0rc0"),
("1.0-RC1", "1.0rc1"),
// Various post release incarnations
("1.0post", "1.0.post0"),
("1.0.post", "1.0.post0"),
("1.0post1", "1.0.post1"),
("1.0post", "1.0.post0"),
("1.0-post", "1.0.post0"),
("1.0-post1", "1.0.post1"),
("1.0POST", "1.0.post0"),
("1.0.POST", "1.0.post0"),
("1.0POST1", "1.0.post1"),
("1.0POST", "1.0.post0"),
("1.0r", "1.0.post0"),
("1.0rev", "1.0.post0"),
("1.0.POST1", "1.0.post1"),
("1.0.r1", "1.0.post1"),
("1.0.rev1", "1.0.post1"),
("1.0-POST", "1.0.post0"),
("1.0-POST1", "1.0.post1"),
("1.0-5", "1.0.post5"),
("1.0-r5", "1.0.post5"),
("1.0-rev5", "1.0.post5"),
// Local version case insensitivity
("1.0+AbC", "1.0+abc"),
// Integer Normalization
("1.01", "1.1"),
("1.0a05", "1.0a5"),
("1.0b07", "1.0b7"),
("1.0c056", "1.0rc56"),
("1.0rc09", "1.0rc9"),
("1.0.post000", "1.0.post0"),
("1.1.dev09000", "1.1.dev9000"),
("00!1.2", "1.2"),
("0100!0.0", "100!0.0"),
// Various other normalizations
("v1.0", "1.0"),
(" v1.0\t\n", "1.0"),
];
for (version_str, normalized_str) in versions {
let version = Version::from_str(version_str).unwrap();
let normalized = Version::from_str(normalized_str).unwrap();
// Just test version parsing again
assert_eq!(version, normalized, "{version_str} {normalized_str}");
// Test version normalization
assert_eq!(
version.to_string(),
normalized.to_string(),
"{version_str} {normalized_str}"
);
}
}
/// <https://github.com/pypa/packaging/blob/237ff3aa348486cf835a980592af3a59fccd6101/tests/test_version.py#L229-L277>
#[test]
fn test_equality_and_normalization2() {
let versions = [
("1.0.dev456", "1.0.dev456"),
("1.0a1", "1.0a1"),
("1.0a2.dev456", "1.0a2.dev456"),
("1.0a12.dev456", "1.0a12.dev456"),
("1.0a12", "1.0a12"),
("1.0b1.dev456", "1.0b1.dev456"),
("1.0b2", "1.0b2"),
("1.0b2.post345.dev456", "1.0b2.post345.dev456"),
("1.0b2.post345", "1.0b2.post345"),
("1.0rc1.dev456", "1.0rc1.dev456"),
("1.0rc1", "1.0rc1"),
("1.0", "1.0"),
("1.0.post456.dev34", "1.0.post456.dev34"),
("1.0.post456", "1.0.post456"),
("1.0.1", "1.0.1"),
("0!1.0.2", "1.0.2"),
("1.0.3+7", "1.0.3+7"),
("0!1.0.4+8.0", "1.0.4+8.0"),
("1.0.5+9.5", "1.0.5+9.5"),
("1.2+1234.abc", "1.2+1234.abc"),
("1.2+123456", "1.2+123456"),
("1.2+123abc", "1.2+123abc"),
("1.2+123abc456", "1.2+123abc456"),
("1.2+abc", "1.2+abc"),
("1.2+abc123", "1.2+abc123"),
("1.2+abc123def", "1.2+abc123def"),
("1.1.dev1", "1.1.dev1"),
("7!1.0.dev456", "7!1.0.dev456"),
("7!1.0a1", "7!1.0a1"),
("7!1.0a2.dev456", "7!1.0a2.dev456"),
("7!1.0a12.dev456", "7!1.0a12.dev456"),
("7!1.0a12", "7!1.0a12"),
("7!1.0b1.dev456", "7!1.0b1.dev456"),
("7!1.0b2", "7!1.0b2"),
("7!1.0b2.post345.dev456", "7!1.0b2.post345.dev456"),
("7!1.0b2.post345", "7!1.0b2.post345"),
("7!1.0rc1.dev456", "7!1.0rc1.dev456"),
("7!1.0rc1", "7!1.0rc1"),
("7!1.0", "7!1.0"),
("7!1.0.post456.dev34", "7!1.0.post456.dev34"),
("7!1.0.post456", "7!1.0.post456"),
("7!1.0.1", "7!1.0.1"),
("7!1.0.2", "7!1.0.2"),
("7!1.0.3+7", "7!1.0.3+7"),
("7!1.0.4+8.0", "7!1.0.4+8.0"),
("7!1.0.5+9.5", "7!1.0.5+9.5"),
("7!1.1.dev1", "7!1.1.dev1"),
];
for (version_str, normalized_str) in versions {
let version = Version::from_str(version_str).unwrap();
let normalized = Version::from_str(normalized_str).unwrap();
assert_eq!(version, normalized, "{version_str} {normalized_str}");
// Test version normalization
assert_eq!(
version.to_string(),
normalized_str,
"{version_str} {normalized_str}"
);
// Since we're already at it
assert_eq!(
version.to_string(),
normalized.to_string(),
"{version_str} {normalized_str}"
);
}
}
#[test]
fn test_star_fixed_version() {
let result = Version::from_str("0.9.1.*");
assert_eq!(result.unwrap_err(), ErrorKind::Wildcard.into());
}
#[test]
fn test_invalid_word() {
let result = Version::from_str("blergh");
assert_eq!(result.unwrap_err(), ErrorKind::NoLeadingNumber.into());
}
#[test]
fn test_from_version_star() {
let p = |s: &str| -> Result<VersionPattern, _> { s.parse() };
assert!(!p("1.2.3").unwrap().is_wildcard());
assert!(p("1.2.3.*").unwrap().is_wildcard());
assert_eq!(
p("1.2.*.4.*").unwrap_err(),
PatternErrorKind::WildcardNotTrailing.into(),
);
assert_eq!(
p("1.0-dev1.*").unwrap_err(),
ErrorKind::UnexpectedEnd {
version: Version::new([1, 0]).with_dev(Some(1)),
remaining: ".*".to_string()
}
.into(),
);
assert_eq!(
p("1.0a1.*").unwrap_err(),
ErrorKind::UnexpectedEnd {
version: Version::new([1, 0]).with_pre(Some((PreRelease::Alpha, 1))),
remaining: ".*".to_string()
}
.into(),
);
assert_eq!(
p("1.0.post1.*").unwrap_err(),
ErrorKind::UnexpectedEnd {
version: Version::new([1, 0]).with_post(Some(1)),
remaining: ".*".to_string()
}
.into(),
);
assert_eq!(
p("1.0+lolwat.*").unwrap_err(),
ErrorKind::LocalEmpty { precursor: '.' }.into(),
);
}
// Tests the valid cases of our version parser. These were written
// in tandem with the parser.
//
// They are meant to be additional (but in some cases likely redundant)
// with some of the above tests.
#[test]
fn parse_version_valid() {
let p = |s: &str| match Parser::new(s.as_bytes()).parse() {
Ok(v) => v,
Err(err) => unreachable!("expected valid version, but got error: {err:?}"),
};
// release-only tests
assert_eq!(p("5"), Version::new([5]));
assert_eq!(p("5.6"), Version::new([5, 6]));
assert_eq!(p("5.6.7"), Version::new([5, 6, 7]));
assert_eq!(p("512.623.734"), Version::new([512, 623, 734]));
assert_eq!(p("1.2.3.4"), Version::new([1, 2, 3, 4]));
assert_eq!(p("1.2.3.4.5"), Version::new([1, 2, 3, 4, 5]));
// epoch tests
assert_eq!(p("4!5"), Version::new([5]).with_epoch(4));
assert_eq!(p("4!5.6"), Version::new([5, 6]).with_epoch(4));
// pre-release tests
assert_eq!(
p("5a1"),
Version::new([5]).with_pre(Some((PreRelease::Alpha, 1)))
);
assert_eq!(
p("5alpha1"),
Version::new([5]).with_pre(Some((PreRelease::Alpha, 1)))
);
assert_eq!(
p("5b1"),
Version::new([5]).with_pre(Some((PreRelease::Beta, 1)))
);
assert_eq!(
p("5beta1"),
Version::new([5]).with_pre(Some((PreRelease::Beta, 1)))
);
assert_eq!(
p("5rc1"),
Version::new([5]).with_pre(Some((PreRelease::Rc, 1)))
);
assert_eq!(
p("5c1"),
Version::new([5]).with_pre(Some((PreRelease::Rc, 1)))
);
assert_eq!(
p("5preview1"),
Version::new([5]).with_pre(Some((PreRelease::Rc, 1)))
);
assert_eq!(
p("5pre1"),
Version::new([5]).with_pre(Some((PreRelease::Rc, 1)))
);
assert_eq!(
p("5.6.7pre1"),
Version::new([5, 6, 7]).with_pre(Some((PreRelease::Rc, 1)))
);
assert_eq!(
p("5alpha789"),
Version::new([5]).with_pre(Some((PreRelease::Alpha, 789)))
);
assert_eq!(
p("5.alpha789"),
Version::new([5]).with_pre(Some((PreRelease::Alpha, 789)))
);
assert_eq!(
p("5-alpha789"),
Version::new([5]).with_pre(Some((PreRelease::Alpha, 789)))
);
assert_eq!(
p("5_alpha789"),
Version::new([5]).with_pre(Some((PreRelease::Alpha, 789)))
);
assert_eq!(
p("5alpha.789"),
Version::new([5]).with_pre(Some((PreRelease::Alpha, 789)))
);
assert_eq!(
p("5alpha-789"),
Version::new([5]).with_pre(Some((PreRelease::Alpha, 789)))
);
assert_eq!(
p("5alpha_789"),
Version::new([5]).with_pre(Some((PreRelease::Alpha, 789)))
);
assert_eq!(
p("5ALPHA789"),
Version::new([5]).with_pre(Some((PreRelease::Alpha, 789)))
);
assert_eq!(
p("5aLpHa789"),
Version::new([5]).with_pre(Some((PreRelease::Alpha, 789)))
);
assert_eq!(
p("5alpha"),
Version::new([5]).with_pre(Some((PreRelease::Alpha, 0)))
);
// post-release tests
assert_eq!(p("5post2"), Version::new([5]).with_post(Some(2)));
assert_eq!(p("5rev2"), Version::new([5]).with_post(Some(2)));
assert_eq!(p("5r2"), Version::new([5]).with_post(Some(2)));
assert_eq!(p("5.post2"), Version::new([5]).with_post(Some(2)));
assert_eq!(p("5-post2"), Version::new([5]).with_post(Some(2)));
assert_eq!(p("5_post2"), Version::new([5]).with_post(Some(2)));
assert_eq!(p("5.post.2"), Version::new([5]).with_post(Some(2)));
assert_eq!(p("5.post-2"), Version::new([5]).with_post(Some(2)));
assert_eq!(p("5.post_2"), Version::new([5]).with_post(Some(2)));
assert_eq!(
p("5.6.7.post_2"),
Version::new([5, 6, 7]).with_post(Some(2))
);
assert_eq!(p("5-2"), Version::new([5]).with_post(Some(2)));
assert_eq!(p("5.6.7-2"), Version::new([5, 6, 7]).with_post(Some(2)));
assert_eq!(p("5POST2"), Version::new([5]).with_post(Some(2)));
assert_eq!(p("5PoSt2"), Version::new([5]).with_post(Some(2)));
assert_eq!(p("5post"), Version::new([5]).with_post(Some(0)));
// dev-release tests
assert_eq!(p("5dev2"), Version::new([5]).with_dev(Some(2)));
assert_eq!(p("5.dev2"), Version::new([5]).with_dev(Some(2)));
assert_eq!(p("5-dev2"), Version::new([5]).with_dev(Some(2)));
assert_eq!(p("5_dev2"), Version::new([5]).with_dev(Some(2)));
assert_eq!(p("5.dev.2"), Version::new([5]).with_dev(Some(2)));
assert_eq!(p("5.dev-2"), Version::new([5]).with_dev(Some(2)));
assert_eq!(p("5.dev_2"), Version::new([5]).with_dev(Some(2)));
assert_eq!(p("5.6.7.dev_2"), Version::new([5, 6, 7]).with_dev(Some(2)));
assert_eq!(p("5DEV2"), Version::new([5]).with_dev(Some(2)));
assert_eq!(p("5dEv2"), Version::new([5]).with_dev(Some(2)));
assert_eq!(p("5DeV2"), Version::new([5]).with_dev(Some(2)));
assert_eq!(p("5dev"), Version::new([5]).with_dev(Some(0)));
// local tests
assert_eq!(
p("5+2"),
Version::new([5]).with_local(vec![LocalSegment::Number(2)])
);
assert_eq!(
p("5+a"),
Version::new([5]).with_local(vec![LocalSegment::String("a".to_string())])
);
assert_eq!(
p("5+abc.123"),
Version::new([5]).with_local(vec![
LocalSegment::String("abc".to_string()),
LocalSegment::Number(123),
])
);
assert_eq!(
p("5+123.abc"),
Version::new([5]).with_local(vec![
LocalSegment::Number(123),
LocalSegment::String("abc".to_string()),
])
);
assert_eq!(
p("5+18446744073709551615.abc"),
Version::new([5]).with_local(vec![
LocalSegment::Number(18446744073709551615),
LocalSegment::String("abc".to_string()),
])
);
assert_eq!(
p("5+18446744073709551616.abc"),
Version::new([5]).with_local(vec![
LocalSegment::String("18446744073709551616".to_string()),
LocalSegment::String("abc".to_string()),
])
);
assert_eq!(
p("5+ABC.123"),
Version::new([5]).with_local(vec![
LocalSegment::String("abc".to_string()),
LocalSegment::Number(123),
])
);
assert_eq!(
p("5+ABC-123.4_5_xyz-MNO"),
Version::new([5]).with_local(vec![
LocalSegment::String("abc".to_string()),
LocalSegment::Number(123),
LocalSegment::Number(4),
LocalSegment::Number(5),
LocalSegment::String("xyz".to_string()),
LocalSegment::String("mno".to_string()),
])
);
assert_eq!(
p("5.6.7+abc-00123"),
Version::new([5, 6, 7]).with_local(vec![
LocalSegment::String("abc".to_string()),
LocalSegment::Number(123),
])
);
assert_eq!(
p("5.6.7+abc-foo00123"),
Version::new([5, 6, 7]).with_local(vec![
LocalSegment::String("abc".to_string()),
LocalSegment::String("foo00123".to_string()),
])
);
assert_eq!(
p("5.6.7+abc-00123a"),
Version::new([5, 6, 7]).with_local(vec![
LocalSegment::String("abc".to_string()),
LocalSegment::String("00123a".to_string()),
])
);
// {pre-release, post-release} tests
assert_eq!(
p("5a2post3"),
Version::new([5])
.with_pre(Some((PreRelease::Alpha, 2)))
.with_post(Some(3))
);
assert_eq!(
p("5.a-2_post-3"),
Version::new([5])
.with_pre(Some((PreRelease::Alpha, 2)))
.with_post(Some(3))
);
assert_eq!(
p("5a2-3"),
Version::new([5])
.with_pre(Some((PreRelease::Alpha, 2)))
.with_post(Some(3))
);
// Ignoring a no-op 'v' prefix.
assert_eq!(p("v5"), Version::new([5]));
assert_eq!(p("V5"), Version::new([5]));
assert_eq!(p("v5.6.7"), Version::new([5, 6, 7]));
// Ignoring leading and trailing whitespace.
assert_eq!(p(" v5 "), Version::new([5]));
assert_eq!(p(" 5 "), Version::new([5]));
assert_eq!(
p(" 5.6.7+abc.123.xyz "),
Version::new([5, 6, 7]).with_local(vec![
LocalSegment::String("abc".to_string()),
LocalSegment::Number(123),
LocalSegment::String("xyz".to_string())
])
);
assert_eq!(p(" \n5\n \t"), Version::new([5]));
}
// Tests the error cases of our version parser.
//
// I wrote these with the intent to cover every possible error
// case.
//
// They are meant to be additional (but in some cases likely redundant)
// with some of the above tests.
#[test]
fn parse_version_invalid() {
let p = |s: &str| match Parser::new(s.as_bytes()).parse() {
Err(err) => err,
Ok(v) => unreachable!(
"expected version parser error, but got: {v:?}",
v = v.as_bloated_debug()
),
};
assert_eq!(p(""), ErrorKind::NoLeadingNumber.into());
assert_eq!(p("a"), ErrorKind::NoLeadingNumber.into());
assert_eq!(p("v 5"), ErrorKind::NoLeadingNumber.into());
assert_eq!(p("V 5"), ErrorKind::NoLeadingNumber.into());
assert_eq!(p("x 5"), ErrorKind::NoLeadingNumber.into());
assert_eq!(
p("18446744073709551616"),
ErrorKind::NumberTooBig {
bytes: b"18446744073709551616".to_vec()
}
.into()
);
assert_eq!(p("5!"), ErrorKind::NoLeadingReleaseNumber.into());
assert_eq!(
p("5.6./"),
ErrorKind::UnexpectedEnd {
version: Version::new([5, 6]),
remaining: "./".to_string()
}
.into()
);
assert_eq!(
p("5.6.-alpha2"),
ErrorKind::UnexpectedEnd {
version: Version::new([5, 6]),
remaining: ".-alpha2".to_string()
}
.into()
);
assert_eq!(
p("1.2.3a18446744073709551616"),
ErrorKind::NumberTooBig {
bytes: b"18446744073709551616".to_vec()
}
.into()
);
assert_eq!(p("5+"), ErrorKind::LocalEmpty { precursor: '+' }.into());
assert_eq!(p("5+ "), ErrorKind::LocalEmpty { precursor: '+' }.into());
assert_eq!(p("5+abc."), ErrorKind::LocalEmpty { precursor: '.' }.into());
assert_eq!(p("5+abc-"), ErrorKind::LocalEmpty { precursor: '-' }.into());
assert_eq!(p("5+abc_"), ErrorKind::LocalEmpty { precursor: '_' }.into());
assert_eq!(
p("5+abc. "),
ErrorKind::LocalEmpty { precursor: '.' }.into()
);
assert_eq!(
p("5.6-"),
ErrorKind::UnexpectedEnd {
version: Version::new([5, 6]),
remaining: "-".to_string()
}
.into()
);
}
#[test]
fn parse_version_pattern_valid() {
let p = |s: &str| match Parser::new(s.as_bytes()).parse_pattern() {
Ok(v) => v,
Err(err) => unreachable!("expected valid version, but got error: {err:?}"),
};
assert_eq!(p("5.*"), VersionPattern::wildcard(Version::new([5])));
assert_eq!(p("5.6.*"), VersionPattern::wildcard(Version::new([5, 6])));
assert_eq!(
p("2!5.6.*"),
VersionPattern::wildcard(Version::new([5, 6]).with_epoch(2))
);
}
#[test]
fn parse_version_pattern_invalid() {
let p = |s: &str| match Parser::new(s.as_bytes()).parse_pattern() {
Err(err) => err,
Ok(vpat) => unreachable!("expected version pattern parser error, but got: {vpat:?}"),
};
assert_eq!(p("*"), ErrorKind::NoLeadingNumber.into());
assert_eq!(p("2!*"), ErrorKind::NoLeadingReleaseNumber.into());
}
// Tests that the ordering between versions is correct.
//
// The ordering example used here was taken from PEP 440:
// https://packaging.python.org/en/latest/specifications/version-specifiers/#summary-of-permitted-suffixes-and-relative-ordering
#[test]
fn ordering() {
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 pair in versions.windows(2) {
let less = pair[0].parse::<Version>().unwrap();
let greater = pair[1].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() {
let p = |s: &str| parse_u64(s.as_bytes());
assert_eq!(p("0"), Ok(0));
assert_eq!(p("00"), Ok(0));
assert_eq!(p("1"), Ok(1));
assert_eq!(p("01"), Ok(1));
assert_eq!(p("9"), Ok(9));
assert_eq!(p("10"), Ok(10));
assert_eq!(p("18446744073709551615"), Ok(18446744073709551615));
assert_eq!(p("018446744073709551615"), Ok(18446744073709551615));
assert_eq!(p("000000018446744073709551615"), Ok(18446744073709551615));
assert_eq!(p("10a"), Err(ErrorKind::InvalidDigit { got: b'a' }.into()));
assert_eq!(p("10["), Err(ErrorKind::InvalidDigit { got: b'[' }.into()));
assert_eq!(p("10/"), Err(ErrorKind::InvalidDigit { got: b'/' }.into()));
assert_eq!(
p("18446744073709551616"),
Err(ErrorKind::NumberTooBig {
bytes: b"18446744073709551616".to_vec()
}
.into())
);
assert_eq!(
p("18446744073799551615abc"),
Err(ErrorKind::NumberTooBig {
bytes: b"18446744073799551615abc".to_vec()
}
.into())
);
assert_eq!(
parse_u64(b"18446744073799551615\xFF"),
Err(ErrorKind::NumberTooBig {
bytes: b"18446744073799551615\xFF".to_vec()
}
.into())
);
}
#[cfg(feature = "pyo3")]
#[pyfunction]
fn _convert_in_and_out(version: Version) -> Version {
version
}
/// Wraps a `Version` and provides a more "bloated" debug but standard
/// representation.
///
/// We don't do this by default because it takes up a ton of space, and
/// just printing out the display version of the version is quite a bit
/// simpler.
///
/// Nevertheless, when *testing* version parsing, you really want to
/// be able to peek at all of its constituent parts. So we use this in
/// assertion failure messages.
struct VersionBloatedDebug<'a>(&'a Version);
impl<'a> std::fmt::Debug for VersionBloatedDebug<'a> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Version")
.field("epoch", &self.0.epoch())
.field("release", &self.0.release())
.field("pre", &self.0.pre())
.field("post", &self.0.post())
.field("dev", &self.0.dev())
.field("local", &self.0.local())
.finish()
}
}
impl Version {
pub(crate) fn as_bloated_debug(&self) -> impl std::fmt::Debug + '_ {
VersionBloatedDebug(self)
}
}
}