1/// Represents the major, minor, and patch (if any) versions of this interpreter.
2///
3/// This struct is usually created with [`Python::version`].
4///
5/// # Examples
6///
7/// ```rust
8/// # use pyo3::Python;
9/// Python::with_gil(|py| {
10/// // PyO3 supports Python 3.7 and up.
11/// assert!(py.version_info() >= (3, 7));
12/// assert!(py.version_info() >= (3, 7, 0));
13/// });
14/// ```
15///
16/// [`Python::version`]: crate::marker::Python::version
17#[derive(Debug)]
18pub struct PythonVersionInfo<'py> {
19 /// Python major version (e.g. `3`).
20 pub major: u8,
21 /// Python minor version (e.g. `11`).
22 pub minor: u8,
23 /// Python patch version (e.g. `0`).
24 pub patch: u8,
25 /// Python version suffix, if applicable (e.g. `a0`).
26 pub suffix: Option<&'py str>,
27}
28
29impl<'py> PythonVersionInfo<'py> {
30 /// Parses a hard-coded Python interpreter version string (e.g. 3.9.0a4+).
31 pub(crate) fn from_str(version_number_str: &'py str) -> Result<Self, &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.split('.');
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 if parts.next().is_some() {
48 return Err("Python version string has too many parts");
49 };
50
51 let major = major_str
52 .parse()
53 .map_err(|_| "Python major version not an integer")?;
54 let (minor, suffix) = split_and_parse_number(minor_str);
55 if suffix.is_some() {
56 assert!(patch_str.is_none());
57 return Ok(PythonVersionInfo {
58 major,
59 minor,
60 patch: 0,
61 suffix,
62 });
63 }
64
65 let (patch, suffix) = patch_str.map(split_and_parse_number).unwrap_or_default();
66 Ok(PythonVersionInfo {
67 major,
68 minor,
69 patch,
70 suffix,
71 })
72 }
73}
74
75impl PartialEq<(u8, u8)> for PythonVersionInfo<'_> {
76 fn eq(&self, other: &(u8, u8)) -> bool {
77 self.major == other.0 && self.minor == other.1
78 }
79}
80
81impl PartialEq<(u8, u8, u8)> for PythonVersionInfo<'_> {
82 fn eq(&self, other: &(u8, u8, u8)) -> bool {
83 self.major == other.0 && self.minor == other.1 && self.patch == other.2
84 }
85}
86
87impl PartialOrd<(u8, u8)> for PythonVersionInfo<'_> {
88 fn partial_cmp(&self, other: &(u8, u8)) -> Option<std::cmp::Ordering> {
89 (self.major, self.minor).partial_cmp(other)
90 }
91}
92
93impl PartialOrd<(u8, u8, u8)> for PythonVersionInfo<'_> {
94 fn partial_cmp(&self, other: &(u8, u8, u8)) -> Option<std::cmp::Ordering> {
95 (self.major, self.minor, self.patch).partial_cmp(other)
96 }
97}
98
99#[cfg(test)]
100mod test {
101 use super::*;
102 use crate::Python;
103 #[test]
104 fn test_python_version_info() {
105 Python::with_gil(|py| {
106 let version = py.version_info();
107 #[cfg(Py_3_7)]
108 assert!(version >= (3, 7));
109 #[cfg(Py_3_7)]
110 assert!(version >= (3, 7, 0));
111 #[cfg(Py_3_8)]
112 assert!(version >= (3, 8));
113 #[cfg(Py_3_8)]
114 assert!(version >= (3, 8, 0));
115 #[cfg(Py_3_9)]
116 assert!(version >= (3, 9));
117 #[cfg(Py_3_9)]
118 assert!(version >= (3, 9, 0));
119 #[cfg(Py_3_10)]
120 assert!(version >= (3, 10));
121 #[cfg(Py_3_10)]
122 assert!(version >= (3, 10, 0));
123 #[cfg(Py_3_11)]
124 assert!(version >= (3, 11));
125 #[cfg(Py_3_11)]
126 assert!(version >= (3, 11, 0));
127 });
128 }
129
130 #[test]
131 fn test_python_version_info_parse() {
132 assert!(PythonVersionInfo::from_str("3.5.0a1").unwrap() >= (3, 5, 0));
133 assert!(PythonVersionInfo::from_str("3.5+").unwrap() >= (3, 5, 0));
134 assert!(PythonVersionInfo::from_str("3.5+").unwrap() == (3, 5, 0));
135 assert!(PythonVersionInfo::from_str("3.5+").unwrap() != (3, 5, 1));
136 assert!(PythonVersionInfo::from_str("3.5.2a1+").unwrap() < (3, 5, 3));
137 assert!(PythonVersionInfo::from_str("3.5.2a1+").unwrap() == (3, 5, 2));
138 assert!(PythonVersionInfo::from_str("3.5.2a1+").unwrap() == (3, 5));
139 assert!(PythonVersionInfo::from_str("3.5+").unwrap() == (3, 5));
140 assert!(PythonVersionInfo::from_str("3.5.2a1+").unwrap() < (3, 6));
141 assert!(PythonVersionInfo::from_str("3.5.2a1+").unwrap() > (3, 4));
142 }
143}
144