Skip to main content

pcu/
python.rs

1use check_updates_core::Version;
2use std::process::Command;
3use std::str::FromStr;
4
5/// Information about the Python environment
6#[derive(Debug, Clone)]
7pub struct PythonInfo {
8    /// Current Python version
9    pub current: Version,
10    /// Latest available Python version (from python.org)
11    pub latest: Option<Version>,
12}
13
14impl PythonInfo {
15    /// Check if an update is available
16    pub fn has_update(&self) -> bool {
17        if let Some(ref latest) = self.latest {
18            latest > &self.current
19        } else {
20            false
21        }
22    }
23}
24
25/// Detect the current Python version
26pub fn detect_python_version() -> Option<Version> {
27    // Try python3 first, then python
28    let commands = [
29        ("python3", ["--version"]),
30        ("python", ["--version"]),
31    ];
32
33    for (cmd, args) in &commands {
34        if let Ok(output) = Command::new(cmd).args(args.as_slice()).output()
35            && output.status.success() {
36                let version_output = String::from_utf8_lossy(&output.stdout);
37                // Output is like "Python 3.11.5"
38                if let Some(version_str) = version_output
39                    .trim()
40                    .strip_prefix("Python ")
41                    && let Ok(version) = Version::from_str(version_str) {
42                        return Some(version);
43                    }
44            }
45    }
46
47    None
48}
49
50/// Fetch the latest Python version from endoflife.date
51pub async fn fetch_latest_python_version() -> Option<Version> {
52    // Use the endoflife.date API - it's well-maintained and returns clean data
53    let url = "https://endoflife.date/api/python.json";
54
55    let client = reqwest::Client::builder()
56        .timeout(std::time::Duration::from_secs(5))
57        .build()
58        .ok()?;
59
60    let response = client.get(url).send().await.ok()?;
61
62    if !response.status().is_success() {
63        return None;
64    }
65
66    let json: serde_json::Value = response.json().await.ok()?;
67
68    // The API returns an array of release cycles sorted by newest first
69    // Each cycle has a "latest" field with the latest version for that cycle
70    let cycles = json.as_array()?;
71
72    // Get the latest version from the first cycle (newest Python release line)
73    // Filter to only consider Python 3.x cycles
74    for cycle in cycles {
75        let cycle_name = cycle.get("cycle")?.as_str()?;
76        if cycle_name.starts_with("3.") {
77            let latest_str = cycle.get("latest")?.as_str()?;
78            if let Ok(version) = Version::from_str(latest_str) {
79                return Some(version);
80            }
81        }
82    }
83
84    None
85}
86
87/// Get Python info (current version and optionally latest available)
88pub async fn get_python_info(check_latest: bool) -> Option<PythonInfo> {
89    let current = detect_python_version()?;
90
91    let latest = if check_latest {
92        fetch_latest_python_version().await
93    } else {
94        None
95    };
96
97    Some(PythonInfo { current, latest })
98}
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103
104    #[test]
105    fn test_detect_python_version() {
106        // This test depends on Python being installed
107        let version = detect_python_version();
108        // We just check it returns something reasonable
109        if let Some(v) = version {
110            assert!(v.major >= 2);
111        }
112    }
113
114    #[test]
115    fn test_python_info_has_update() {
116        let info = PythonInfo {
117            current: Version::from_str("3.11.0").unwrap(),
118            latest: Some(Version::from_str("3.13.1").unwrap()),
119        };
120        assert!(info.has_update());
121
122        let info = PythonInfo {
123            current: Version::from_str("3.13.1").unwrap(),
124            latest: Some(Version::from_str("3.13.1").unwrap()),
125        };
126        assert!(!info.has_update());
127
128        let info = PythonInfo {
129            current: Version::from_str("3.11.0").unwrap(),
130            latest: None,
131        };
132        assert!(!info.has_update());
133    }
134}