pip_upgrade/
lib.rs

1use std::io::Result;
2use std::process::Command;
3
4use colored::Colorize;
5use spinoff::{spinners, Color, Spinner};
6
7use serde::{Deserialize, Serialize};
8
9#[derive(Debug, Serialize, Deserialize)]
10pub struct Package {
11    name: String,
12    version: String,
13    latest_version: String,
14}
15
16impl Package {
17    fn to_formatted(&self) -> Self {
18        Self {
19            name: self.name.clone(),
20            version: format!("v{}", self.version).bright_red().to_string(),
21            latest_version: format!("v{}", self.latest_version)
22                .bright_green()
23                .to_string(),
24        }
25    }
26}
27
28// run in shell `pip list --outdated --format=json` and parse the json output
29fn get_outdated() -> Result<Vec<Package>> {
30    let mut spinner = Spinner::new(
31        spinners::Dots,
32        "Scanning for outdated packages...",
33        Color::Cyan,
34    );
35    // get the json output
36    let output = Command::new("pip")
37        .args(["list", "--outdated", "--not-required", "--format=json"])
38        .output()?;
39
40    // parse the json output
41    let packages: Vec<Package> = match serde_json::from_slice(&output.stdout) {
42        Ok(packages) => packages,
43        Err(error) => {
44            spinner.fail(&format!("Failed to parse json:\n  {error}"));
45            return Ok(vec![]);
46        }
47    };
48
49    spinner.clear();
50
51    Ok(packages)
52}
53
54/// # Errors
55/// Will return `Err` if the command fails to execute
56///
57/// # Panics
58/// Will panic if the command fails to execute
59pub fn show_outdated() -> Result<()> {
60    let packages = get_outdated()?;
61    if packages.is_empty() {
62        return Ok(());
63    }
64    for package in packages {
65        let formatted = package.to_formatted();
66        println!(
67            "🐍 {}: {} -> {}",
68            formatted.name, formatted.version, formatted.latest_version
69        );
70    }
71
72    Ok(())
73}
74
75/// # Errors
76/// Will return `Err` if the command fails to execute
77///
78/// # Panics
79/// Will panic if the command fails to execute
80pub fn update_all() -> Result<()> {
81    // for each package, run `pip install --upgrade <package>`
82    for package in get_outdated()? {
83        let formatted = package.to_formatted();
84
85        let message = format!(
86            "Upgrading {} from {} to {}",
87            formatted.name, formatted.version, formatted.latest_version
88        );
89
90        let mut spinner = Spinner::new(spinners::Dots, message, Color::Cyan);
91
92        let output = std::process::Command::new("pip")
93            .args(["install", "--upgrade", &formatted.name])
94            .output()?;
95
96        let stdout = String::from_utf8(output.stdout).unwrap_or_default();
97        let stdout_output = stdout.lines().last().unwrap_or_default().trim();
98
99        match output.status.code().unwrap_or(1) {
100            0 => spinner.success(stdout_output),
101            1 => spinner.fail(stdout_output),
102            _ => spinner.warn(stdout_output),
103        }
104    }
105
106    Ok(())
107}