simics_python_utils/
version.rs1use anyhow::{anyhow, Result};
5use std::path::Path;
6
7#[derive(Debug, Clone, PartialEq, Eq)]
9pub struct PythonVersion {
10 pub major: u32,
12 pub minor: u32,
14 pub patch: u32,
16}
17
18impl PythonVersion {
19 pub fn new(major: u32, minor: u32, patch: u32) -> Self {
21 Self {
22 major,
23 minor,
24 patch,
25 }
26 }
27
28 pub fn py_limited_api_hex(&self) -> String {
33 format!("0x{:02x}{:02x}0000", self.major, self.minor)
34 }
35
36 pub fn parse_from_include_dir<P: AsRef<Path>>(include_dir: P) -> Result<Self> {
40 let dir_name = include_dir
41 .as_ref()
42 .file_name()
43 .ok_or_else(|| anyhow!("Failed to get include directory filename"))?
44 .to_str()
45 .ok_or_else(|| anyhow!("Failed to convert include directory name to string"))?;
46
47 let version_str = dir_name.strip_prefix("python").ok_or_else(|| {
49 anyhow!(
50 "Include directory name does not start with 'python': {}",
51 dir_name
52 )
53 })?;
54
55 let components: Result<Vec<u32>> = version_str
56 .split('.')
57 .map(|s| {
58 s.parse::<u32>()
59 .map_err(|e| anyhow!("Failed to parse version component '{}': {}", s, e))
60 })
61 .collect();
62
63 let components = components?;
64
65 match components.len() {
66 2 => Ok(Self::new(components[0], components[1], 0)),
67 3 => Ok(Self::new(components[0], components[1], components[2])),
68 _ => Err(anyhow!(
69 "Invalid Python version format '{}', expected 'X.Y' or 'X.Y.Z'",
70 version_str
71 )),
72 }
73 }
74}
75
76impl std::fmt::Display for PythonVersion {
77 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
78 if self.patch == 0 {
79 write!(f, "{}.{}", self.major, self.minor)
80 } else {
81 write!(f, "{}.{}.{}", self.major, self.minor, self.patch)
82 }
83 }
84}
85
86#[cfg(test)]
87mod tests {
88 use super::*;
89 use std::path::PathBuf;
90
91 #[test]
92 fn test_py_limited_api_hex() {
93 let version = PythonVersion::new(3, 9, 10);
94 assert_eq!(version.py_limited_api_hex(), "0x03090000");
95
96 let version = PythonVersion::new(3, 8, 0);
97 assert_eq!(version.py_limited_api_hex(), "0x03080000");
98 }
99
100 #[test]
101 fn test_parse_from_include_dir() {
102 let path = PathBuf::from("/some/path/python3.9");
104 let version = PythonVersion::parse_from_include_dir(&path).unwrap();
105 assert_eq!(version, PythonVersion::new(3, 9, 0));
106
107 let path = PathBuf::from("/some/path/python3.9.10");
109 let version = PythonVersion::parse_from_include_dir(&path).unwrap();
110 assert_eq!(version, PythonVersion::new(3, 9, 10));
111 }
112
113 #[test]
114 fn test_display() {
115 let version = PythonVersion::new(3, 9, 0);
116 assert_eq!(version.to_string(), "3.9");
117
118 let version = PythonVersion::new(3, 9, 10);
119 assert_eq!(version.to_string(), "3.9.10");
120 }
121}