#[derive(Debug)]
pub struct PythonVersionInfo<'py> {
pub major: u8,
pub minor: u8,
pub patch: u8,
pub suffix: Option<&'py str>,
}
impl<'py> PythonVersionInfo<'py> {
pub(crate) fn from_str(version_number_str: &'py str) -> Result<Self, &str> {
fn split_and_parse_number(version_part: &str) -> (u8, Option<&str>) {
match version_part.find(|c: char| !c.is_ascii_digit()) {
None => (version_part.parse().unwrap(), None),
Some(version_part_suffix_start) => {
let (version_part, version_part_suffix) =
version_part.split_at(version_part_suffix_start);
(version_part.parse().unwrap(), Some(version_part_suffix))
}
}
}
let mut parts = version_number_str.split('.');
let major_str = parts.next().ok_or("Python major version missing")?;
let minor_str = parts.next().ok_or("Python minor version missing")?;
let patch_str = parts.next();
if parts.next().is_some() {
return Err("Python version string has too many parts");
};
let major = major_str
.parse()
.map_err(|_| "Python major version not an integer")?;
let (minor, suffix) = split_and_parse_number(minor_str);
if suffix.is_some() {
assert!(patch_str.is_none());
return Ok(PythonVersionInfo {
major,
minor,
patch: 0,
suffix,
});
}
let (patch, suffix) = patch_str.map(split_and_parse_number).unwrap_or_default();
Ok(PythonVersionInfo {
major,
minor,
patch,
suffix,
})
}
}
impl PartialEq<(u8, u8)> for PythonVersionInfo<'_> {
fn eq(&self, other: &(u8, u8)) -> bool {
self.major == other.0 && self.minor == other.1
}
}
impl PartialEq<(u8, u8, u8)> for PythonVersionInfo<'_> {
fn eq(&self, other: &(u8, u8, u8)) -> bool {
self.major == other.0 && self.minor == other.1 && self.patch == other.2
}
}
impl PartialOrd<(u8, u8)> for PythonVersionInfo<'_> {
fn partial_cmp(&self, other: &(u8, u8)) -> Option<std::cmp::Ordering> {
(self.major, self.minor).partial_cmp(other)
}
}
impl PartialOrd<(u8, u8, u8)> for PythonVersionInfo<'_> {
fn partial_cmp(&self, other: &(u8, u8, u8)) -> Option<std::cmp::Ordering> {
(self.major, self.minor, self.patch).partial_cmp(other)
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::Python;
#[test]
fn test_python_version_info() {
Python::with_gil(|py| {
let version = py.version_info();
#[cfg(Py_3_7)]
assert!(version >= (3, 7));
#[cfg(Py_3_7)]
assert!(version >= (3, 7, 0));
#[cfg(Py_3_8)]
assert!(version >= (3, 8));
#[cfg(Py_3_8)]
assert!(version >= (3, 8, 0));
#[cfg(Py_3_9)]
assert!(version >= (3, 9));
#[cfg(Py_3_9)]
assert!(version >= (3, 9, 0));
#[cfg(Py_3_10)]
assert!(version >= (3, 10));
#[cfg(Py_3_10)]
assert!(version >= (3, 10, 0));
#[cfg(Py_3_11)]
assert!(version >= (3, 11));
#[cfg(Py_3_11)]
assert!(version >= (3, 11, 0));
});
}
#[test]
fn test_python_version_info_parse() {
assert!(PythonVersionInfo::from_str("3.5.0a1").unwrap() >= (3, 5, 0));
assert!(PythonVersionInfo::from_str("3.5+").unwrap() >= (3, 5, 0));
assert!(PythonVersionInfo::from_str("3.5+").unwrap() == (3, 5, 0));
assert!(PythonVersionInfo::from_str("3.5+").unwrap() != (3, 5, 1));
assert!(PythonVersionInfo::from_str("3.5.2a1+").unwrap() < (3, 5, 3));
assert!(PythonVersionInfo::from_str("3.5.2a1+").unwrap() == (3, 5, 2));
assert!(PythonVersionInfo::from_str("3.5.2a1+").unwrap() == (3, 5));
assert!(PythonVersionInfo::from_str("3.5+").unwrap() == (3, 5));
assert!(PythonVersionInfo::from_str("3.5.2a1+").unwrap() < (3, 6));
assert!(PythonVersionInfo::from_str("3.5.2a1+").unwrap() > (3, 4));
}
}