Skip to main content

pyo3/
version.rs

1/// Represents the major, minor, and patch (if any) versions of this interpreter.
2///
3/// This struct is usually created with [`Python::version_info`].
4///
5/// # Examples
6///
7/// ```rust
8/// # use pyo3::Python;
9/// Python::attach(|py| {
10///     // PyO3 supports Python 3.8 and up.
11///     assert!(py.version_info() >= (3, 8));
12///     assert!(py.version_info() >= (3, 8, 0));
13/// });
14/// ```
15///
16/// [`Python::version_info`]: crate::marker::Python::version_info
17#[derive(Debug)]
18pub struct PythonVersionInfo {
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<&'static str>,
27}
28
29impl PythonVersionInfo {
30    /// Parses a hard-coded Python interpreter version string (e.g. 3.9.0a4+).
31    pub(crate) fn from_str(
32        version_number_str: &'static str,
33    ) -> Result<PythonVersionInfo, &'static str> {
34        fn split_and_parse_number(version_part: &str) -> (u8, Option<&str>) {
35            match version_part.find(|c: char| !c.is_ascii_digit()) {
36                None => (version_part.parse().unwrap(), None),
37                Some(version_part_suffix_start) => {
38                    let (version_part, version_part_suffix) =
39                        version_part.split_at(version_part_suffix_start);
40                    (version_part.parse().unwrap(), Some(version_part_suffix))
41                }
42            }
43        }
44
45        let mut parts = version_number_str.splitn(3, '.');
46        let major_str = parts.next().ok_or("Python major version missing")?;
47        let minor_str = parts.next().ok_or("Python minor version missing")?;
48        let patch_str = parts.next();
49
50        let major = major_str
51            .parse()
52            .map_err(|_| "Python major version not an integer")?;
53        let (minor, suffix) = split_and_parse_number(minor_str);
54        if suffix.is_some() {
55            assert!(patch_str.is_none());
56            return Ok(PythonVersionInfo {
57                major,
58                minor,
59                patch: 0,
60                suffix,
61            });
62        }
63
64        let (patch, suffix) = patch_str.map(split_and_parse_number).unwrap_or_default();
65        Ok(PythonVersionInfo {
66            major,
67            minor,
68            patch,
69            suffix,
70        })
71    }
72}
73
74impl PartialEq<(u8, u8)> for PythonVersionInfo {
75    fn eq(&self, other: &(u8, u8)) -> bool {
76        self.major == other.0 && self.minor == other.1
77    }
78}
79
80impl PartialEq<(u8, u8, u8)> for PythonVersionInfo {
81    fn eq(&self, other: &(u8, u8, u8)) -> bool {
82        self.major == other.0 && self.minor == other.1 && self.patch == other.2
83    }
84}
85
86impl PartialOrd<(u8, u8)> for PythonVersionInfo {
87    fn partial_cmp(&self, other: &(u8, u8)) -> Option<std::cmp::Ordering> {
88        (self.major, self.minor).partial_cmp(other)
89    }
90}
91
92impl PartialOrd<(u8, u8, u8)> for PythonVersionInfo {
93    fn partial_cmp(&self, other: &(u8, u8, u8)) -> Option<std::cmp::Ordering> {
94        (self.major, self.minor, self.patch).partial_cmp(other)
95    }
96}
97
98#[cfg(test)]
99mod test {
100    use super::*;
101    use crate::Python;
102    #[test]
103    fn test_python_version_info() {
104        Python::attach(|py| {
105            let version = py.version_info();
106            #[cfg(Py_3_8)]
107            assert!(version >= (3, 8));
108            #[cfg(Py_3_8)]
109            assert!(version >= (3, 8, 0));
110            #[cfg(Py_3_9)]
111            assert!(version >= (3, 9));
112            #[cfg(Py_3_9)]
113            assert!(version >= (3, 9, 0));
114            #[cfg(Py_3_10)]
115            assert!(version >= (3, 10));
116            #[cfg(Py_3_10)]
117            assert!(version >= (3, 10, 0));
118            #[cfg(Py_3_11)]
119            assert!(version >= (3, 11));
120            #[cfg(Py_3_11)]
121            assert!(version >= (3, 11, 0));
122            #[cfg(Py_3_12)]
123            assert!(version >= (3, 12));
124            #[cfg(Py_3_12)]
125            assert!(version >= (3, 12, 0));
126            #[cfg(Py_3_13)]
127            assert!(version >= (3, 13));
128            #[cfg(Py_3_13)]
129            assert!(version >= (3, 13, 0));
130            #[cfg(Py_3_14)]
131            assert!(version >= (3, 14));
132            #[cfg(Py_3_14)]
133            assert!(version >= (3, 14, 0));
134        });
135    }
136
137    #[test]
138    fn test_python_version_info_parse() {
139        assert!(PythonVersionInfo::from_str("3.5.0a1").unwrap() >= (3, 5, 0));
140        assert!(PythonVersionInfo::from_str("3.5+").unwrap() >= (3, 5, 0));
141        assert_eq!(PythonVersionInfo::from_str("3.5+").unwrap(), (3, 5, 0));
142        assert_ne!(PythonVersionInfo::from_str("3.5+").unwrap(), (3, 5, 1));
143        assert!(PythonVersionInfo::from_str("3.5.2a1+").unwrap() < (3, 5, 3));
144        assert_eq!(PythonVersionInfo::from_str("3.5.2a1+").unwrap(), (3, 5, 2));
145        assert_eq!(PythonVersionInfo::from_str("3.5.2a1+").unwrap(), (3, 5));
146        assert_eq!(PythonVersionInfo::from_str("3.5+").unwrap(), (3, 5));
147        assert!(PythonVersionInfo::from_str("3.5.2a1+").unwrap() < (3, 6));
148        assert!(PythonVersionInfo::from_str("3.5.2a1+").unwrap() > (3, 4));
149        assert!(PythonVersionInfo::from_str("3.11.3+chromium.29").unwrap() >= (3, 11, 3));
150        assert_eq!(
151            PythonVersionInfo::from_str("3.11.3+chromium.29")
152                .unwrap()
153                .suffix,
154            Some("+chromium.29")
155        );
156    }
157}