1use std::cmp::Ordering;
7use std::fmt;
8use std::str::FromStr;
9
10use crate::{Error, Result};
11
12#[derive(Debug, Clone, PartialEq, Eq, Hash)]
14pub struct PythonVersion {
15 pub major: u32,
16 pub minor: u32,
17 pub patch: Option<u32>,
18}
19
20impl PythonVersion {
21 pub fn new(major: u32, minor: u32, patch: Option<u32>) -> Self {
23 Self {
24 major,
25 minor,
26 patch,
27 }
28 }
29
30 pub fn parse(s: &str) -> Result<Self> {
32 let parts: Vec<&str> = s.trim().split('.').collect();
33
34 match parts.len() {
35 2 => {
36 let major: u32 = parts[0]
37 .parse()
38 .map_err(|_| Error::InvalidVersion(format!("invalid major version: {}", s)))?;
39 let minor: u32 = parts[1]
40 .parse()
41 .map_err(|_| Error::InvalidVersion(format!("invalid minor version: {}", s)))?;
42 Ok(Self::new(major, minor, None))
43 }
44 3 => {
45 let major: u32 = parts[0]
46 .parse()
47 .map_err(|_| Error::InvalidVersion(format!("invalid major version: {}", s)))?;
48 let minor: u32 = parts[1]
49 .parse()
50 .map_err(|_| Error::InvalidVersion(format!("invalid minor version: {}", s)))?;
51 let patch: u32 = parts[2]
52 .parse()
53 .map_err(|_| Error::InvalidVersion(format!("invalid patch version: {}", s)))?;
54 Ok(Self::new(major, minor, Some(patch)))
55 }
56 _ => Err(Error::InvalidVersion(format!(
57 "invalid version format: {} (expected X.Y or X.Y.Z)",
58 s
59 ))),
60 }
61 }
62
63 pub fn matches(&self, spec: &PythonVersion) -> bool {
65 if self.major != spec.major || self.minor != spec.minor {
66 return false;
67 }
68
69 match spec.patch {
72 None => true,
73 Some(p) => self.patch == Some(p),
74 }
75 }
76
77 pub fn to_string_full(&self) -> String {
79 match self.patch {
80 Some(p) => format!("{}.{}.{}", self.major, self.minor, p),
81 None => format!("{}.{}", self.major, self.minor),
82 }
83 }
84
85 pub fn to_string_short(&self) -> String {
87 format!("{}.{}", self.major, self.minor)
88 }
89}
90
91impl fmt::Display for PythonVersion {
92 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
93 match self.patch {
94 Some(p) => write!(f, "{}.{}.{}", self.major, self.minor, p),
95 None => write!(f, "{}.{}", self.major, self.minor),
96 }
97 }
98}
99
100impl FromStr for PythonVersion {
101 type Err = Error;
102
103 fn from_str(s: &str) -> Result<Self> {
104 Self::parse(s)
105 }
106}
107
108impl Ord for PythonVersion {
109 fn cmp(&self, other: &Self) -> Ordering {
110 match self.major.cmp(&other.major) {
111 Ordering::Equal => match self.minor.cmp(&other.minor) {
112 Ordering::Equal => {
113 let self_patch = self.patch.unwrap_or(0);
114 let other_patch = other.patch.unwrap_or(0);
115 self_patch.cmp(&other_patch)
116 }
117 ord => ord,
118 },
119 ord => ord,
120 }
121 }
122}
123
124impl PartialOrd for PythonVersion {
125 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
126 Some(self.cmp(other))
127 }
128}
129
130#[derive(Debug, Clone)]
132pub struct AvailableVersion {
133 pub version: PythonVersion,
134 pub release_tag: String,
136}
137
138impl AvailableVersion {
139 pub fn new(version: PythonVersion, release_tag: impl Into<String>) -> Self {
140 Self {
141 version,
142 release_tag: release_tag.into(),
143 }
144 }
145}
146
147pub fn available_versions() -> Vec<AvailableVersion> {
152 vec![
155 AvailableVersion::new(PythonVersion::new(3, 13, Some(1)), "20250115"),
157 AvailableVersion::new(PythonVersion::new(3, 13, Some(0)), "20241206"),
158 AvailableVersion::new(PythonVersion::new(3, 12, Some(8)), "20250115"),
160 AvailableVersion::new(PythonVersion::new(3, 12, Some(7)), "20241206"),
161 AvailableVersion::new(PythonVersion::new(3, 12, Some(6)), "20240909"),
162 AvailableVersion::new(PythonVersion::new(3, 12, Some(5)), "20240814"),
163 AvailableVersion::new(PythonVersion::new(3, 11, Some(11)), "20250115"),
165 AvailableVersion::new(PythonVersion::new(3, 11, Some(10)), "20241016"),
166 AvailableVersion::new(PythonVersion::new(3, 11, Some(9)), "20240814"),
167 AvailableVersion::new(PythonVersion::new(3, 10, Some(16)), "20250115"),
169 AvailableVersion::new(PythonVersion::new(3, 10, Some(15)), "20241016"),
170 AvailableVersion::new(PythonVersion::new(3, 10, Some(14)), "20240814"),
171 AvailableVersion::new(PythonVersion::new(3, 9, Some(21)), "20250115"),
173 AvailableVersion::new(PythonVersion::new(3, 9, Some(20)), "20241016"),
174 ]
175}
176
177pub fn find_matching_version(spec: &PythonVersion) -> Option<AvailableVersion> {
179 available_versions()
180 .into_iter()
181 .filter(|v| v.version.matches(spec))
182 .max_by(|a, b| a.version.cmp(&b.version))
183}
184
185pub fn get_versions_for_minor(major: u32, minor: u32) -> Vec<AvailableVersion> {
187 let spec = PythonVersion::new(major, minor, None);
188 available_versions()
189 .into_iter()
190 .filter(|v| v.version.matches(&spec))
191 .collect()
192}
193
194#[cfg(test)]
195mod tests {
196 use super::*;
197
198 #[test]
199 fn test_parse_short_version() {
200 let v = PythonVersion::parse("3.12").unwrap();
201 assert_eq!(v.major, 3);
202 assert_eq!(v.minor, 12);
203 assert_eq!(v.patch, None);
204 }
205
206 #[test]
207 fn test_parse_full_version() {
208 let v = PythonVersion::parse("3.12.1").unwrap();
209 assert_eq!(v.major, 3);
210 assert_eq!(v.minor, 12);
211 assert_eq!(v.patch, Some(1));
212 }
213
214 #[test]
215 fn test_parse_invalid() {
216 assert!(PythonVersion::parse("3").is_err());
217 assert!(PythonVersion::parse("3.12.1.2").is_err());
218 assert!(PythonVersion::parse("abc").is_err());
219 }
220
221 #[test]
222 fn test_matches() {
223 let spec = PythonVersion::new(3, 12, None);
224 let full = PythonVersion::new(3, 12, Some(1));
225
226 assert!(full.matches(&spec));
227 assert!(spec.matches(&spec));
228
229 let different = PythonVersion::new(3, 11, Some(1));
230 assert!(!different.matches(&spec));
231 }
232
233 #[test]
234 fn test_version_ordering() {
235 let v1 = PythonVersion::new(3, 11, Some(1));
236 let v2 = PythonVersion::new(3, 12, Some(0));
237 let v3 = PythonVersion::new(3, 12, Some(1));
238
239 assert!(v1 < v2);
240 assert!(v2 < v3);
241 assert!(v1 < v3);
242 }
243
244 #[test]
245 fn test_find_matching() {
246 let spec = PythonVersion::new(3, 12, None);
247 let found = find_matching_version(&spec);
248 assert!(found.is_some());
249 assert_eq!(found.unwrap().version.minor, 12);
250 }
251
252 #[test]
253 fn test_display() {
254 let v = PythonVersion::new(3, 12, Some(1));
255 assert_eq!(format!("{}", v), "3.12.1");
256
257 let v = PythonVersion::new(3, 12, None);
258 assert_eq!(format!("{}", v), "3.12");
259 }
260}