1#[derive(Debug)]
18pub struct PythonVersionInfo<'a> {
19 pub major: u8,
21 pub minor: u8,
23 pub patch: u8,
25 pub suffix: Option<&'a str>,
27}
28
29impl<'a> PythonVersionInfo<'a> {
30 pub(crate) fn from_str(version_number_str: &'a str) -> Result<PythonVersionInfo<'a>, &'a str> {
32 fn split_and_parse_number(version_part: &str) -> (u8, Option<&str>) {
33 match version_part.find(|c: char| !c.is_ascii_digit()) {
34 None => (version_part.parse().unwrap(), None),
35 Some(version_part_suffix_start) => {
36 let (version_part, version_part_suffix) =
37 version_part.split_at(version_part_suffix_start);
38 (version_part.parse().unwrap(), Some(version_part_suffix))
39 }
40 }
41 }
42
43 let mut parts = version_number_str.splitn(3, '.');
44 let major_str = parts.next().ok_or("Python major version missing")?;
45 let minor_str = parts.next().ok_or("Python minor version missing")?;
46 let patch_str = parts.next();
47
48 let major = major_str
49 .parse()
50 .map_err(|_| "Python major version not an integer")?;
51 let (minor, suffix) = split_and_parse_number(minor_str);
52 if suffix.is_some() {
53 assert!(patch_str.is_none());
54 return Ok(PythonVersionInfo {
55 major,
56 minor,
57 patch: 0,
58 suffix,
59 });
60 }
61
62 let (patch, suffix) = patch_str.map(split_and_parse_number).unwrap_or_default();
63 Ok(PythonVersionInfo {
64 major,
65 minor,
66 patch,
67 suffix,
68 })
69 }
70}
71
72impl PartialEq<(u8, u8)> for PythonVersionInfo<'_> {
73 fn eq(&self, other: &(u8, u8)) -> bool {
74 self.major == other.0 && self.minor == other.1
75 }
76}
77
78impl PartialEq<(u8, u8, u8)> for PythonVersionInfo<'_> {
79 fn eq(&self, other: &(u8, u8, u8)) -> bool {
80 self.major == other.0 && self.minor == other.1 && self.patch == other.2
81 }
82}
83
84impl PartialOrd<(u8, u8)> for PythonVersionInfo<'_> {
85 fn partial_cmp(&self, other: &(u8, u8)) -> Option<std::cmp::Ordering> {
86 (self.major, self.minor).partial_cmp(other)
87 }
88}
89
90impl PartialOrd<(u8, u8, u8)> for PythonVersionInfo<'_> {
91 fn partial_cmp(&self, other: &(u8, u8, u8)) -> Option<std::cmp::Ordering> {
92 (self.major, self.minor, self.patch).partial_cmp(other)
93 }
94}
95
96#[cfg(test)]
97mod test {
98 use super::*;
99 use crate::Python;
100 #[test]
101 fn test_python_version_info() {
102 Python::with_gil(|py| {
103 let version = py.version_info();
104 #[cfg(Py_3_7)]
105 assert!(version >= (3, 7));
106 #[cfg(Py_3_7)]
107 assert!(version >= (3, 7, 0));
108 #[cfg(Py_3_8)]
109 assert!(version >= (3, 8));
110 #[cfg(Py_3_8)]
111 assert!(version >= (3, 8, 0));
112 #[cfg(Py_3_9)]
113 assert!(version >= (3, 9));
114 #[cfg(Py_3_9)]
115 assert!(version >= (3, 9, 0));
116 #[cfg(Py_3_10)]
117 assert!(version >= (3, 10));
118 #[cfg(Py_3_10)]
119 assert!(version >= (3, 10, 0));
120 #[cfg(Py_3_11)]
121 assert!(version >= (3, 11));
122 #[cfg(Py_3_11)]
123 assert!(version >= (3, 11, 0));
124 });
125 }
126
127 #[test]
128 fn test_python_version_info_parse() {
129 assert!(PythonVersionInfo::from_str("3.5.0a1").unwrap() >= (3, 5, 0));
130 assert!(PythonVersionInfo::from_str("3.5+").unwrap() >= (3, 5, 0));
131 assert!(PythonVersionInfo::from_str("3.5+").unwrap() == (3, 5, 0));
132 assert!(PythonVersionInfo::from_str("3.5+").unwrap() != (3, 5, 1));
133 assert!(PythonVersionInfo::from_str("3.5.2a1+").unwrap() < (3, 5, 3));
134 assert!(PythonVersionInfo::from_str("3.5.2a1+").unwrap() == (3, 5, 2));
135 assert!(PythonVersionInfo::from_str("3.5.2a1+").unwrap() == (3, 5));
136 assert!(PythonVersionInfo::from_str("3.5+").unwrap() == (3, 5));
137 assert!(PythonVersionInfo::from_str("3.5.2a1+").unwrap() < (3, 6));
138 assert!(PythonVersionInfo::from_str("3.5.2a1+").unwrap() > (3, 4));
139 assert!(PythonVersionInfo::from_str("3.11.3+chromium.29").unwrap() >= (3, 11, 3));
140 assert_eq!(
141 PythonVersionInfo::from_str("3.11.3+chromium.29")
142 .unwrap()
143 .suffix,
144 Some("+chromium.29")
145 );
146 }
147}