Skip to main content

lean_ctx_patterns/
pip.rs

1// SPDX-License-Identifier: Apache-2.0
2
3macro_rules! static_regex {
4    ($pattern:expr_2021) => {{
5        static RE: std::sync::OnceLock<regex::Regex> = std::sync::OnceLock::new();
6        RE.get_or_init(|| {
7            regex::Regex::new($pattern).expect(concat!("BUG: invalid static regex: ", $pattern))
8        })
9    }};
10}
11
12fn pip_installed_re() -> &'static regex::Regex {
13    static_regex!(r"Successfully installed\s+(.+)")
14}
15fn pip_outdated_re() -> &'static regex::Regex {
16    static_regex!(r"^(\S+)\s+(\S+)\s+(\S+)\s+(\S+)")
17}
18
19pub fn compress(command: &str, output: &str) -> Option<String> {
20    if command.contains("uninstall") {
21        return Some(compress_uninstall(output));
22    }
23    if command.contains("install") {
24        return Some(compress_install(output));
25    }
26    if command.contains("list") || command.contains("freeze") {
27        if command.contains("outdated") || command.contains("--outdated") {
28            return Some(compress_outdated(output));
29        }
30        return Some(compress_list(output));
31    }
32    if command.contains("show") {
33        return Some(compress_show(output));
34    }
35    if command.contains("check") {
36        return Some(compress_check(output));
37    }
38    None
39}
40
41fn compress_install(output: &str) -> String {
42    let trimmed = output.trim();
43    if trimmed.is_empty() {
44        return "ok".to_string();
45    }
46
47    if let Some(caps) = pip_installed_re().captures(trimmed) {
48        let packages: Vec<&str> = caps[1].split_whitespace().collect();
49        return format!("ok (+{} packages): {}", packages.len(), packages.join(", "));
50    }
51
52    if trimmed.contains("already satisfied") {
53        return "ok (already satisfied)".to_string();
54    }
55
56    compact_output(trimmed, 5)
57}
58
59fn compress_list(output: &str) -> String {
60    let lines: Vec<&str> = output.lines().collect();
61    if lines.len() <= 2 {
62        return output.to_string();
63    }
64
65    let skip = if lines[0].starts_with("Package") || lines[0].starts_with("---") {
66        2
67    } else {
68        0
69    };
70    let packages: Vec<String> = lines[skip..]
71        .iter()
72        .filter_map(|l| {
73            let parts: Vec<&str> = l.split_whitespace().collect();
74            if parts.len() >= 2 {
75                Some(format!("{}=={}", parts[0], parts[1]))
76            } else {
77                None
78            }
79        })
80        .collect();
81
82    if packages.is_empty() {
83        return output.to_string();
84    }
85    format!("{} packages:\n{}", packages.len(), packages.join("\n"))
86}
87
88fn compress_outdated(output: &str) -> String {
89    let lines: Vec<&str> = output.lines().collect();
90    let skip = if lines.first().is_some_and(|l| l.starts_with("Package")) {
91        2
92    } else {
93        0
94    };
95
96    let mut outdated = Vec::new();
97    for line in lines.iter().skip(skip) {
98        if let Some(caps) = pip_outdated_re().captures(line) {
99            let name = &caps[1];
100            let current = &caps[2];
101            let latest = &caps[3];
102            outdated.push(format!("{name}: {current} → {latest}"));
103        }
104    }
105
106    if outdated.is_empty() {
107        return "all up-to-date".to_string();
108    }
109    format!("{} outdated:\n{}", outdated.len(), outdated.join("\n"))
110}
111
112fn compress_uninstall(output: &str) -> String {
113    let trimmed = output.trim();
114    if trimmed.is_empty() {
115        return "ok".to_string();
116    }
117
118    let removed: Vec<&str> = trimmed
119        .lines()
120        .filter(|l| l.contains("Successfully uninstalled"))
121        .collect();
122
123    if removed.is_empty() {
124        return compact_output(trimmed, 3);
125    }
126
127    let names: Vec<String> = removed
128        .iter()
129        .take(30)
130        .map(|l| {
131            l.trim()
132                .strip_prefix("Successfully uninstalled ")
133                .unwrap_or(l.trim())
134                .to_string()
135        })
136        .collect();
137
138    let total = removed.len();
139    if names.is_empty() {
140        return format!("ok (removed {total} packages)");
141    }
142    format!("ok (removed {total} packages): {}", names.join(", "))
143}
144
145fn compress_show(output: &str) -> String {
146    compact_output(output, 10)
147}
148
149fn compress_check(output: &str) -> String {
150    let trimmed = output.trim();
151    if trimmed.is_empty() || trimmed.contains("No broken requirements") {
152        return "ok (no broken dependencies)".to_string();
153    }
154
155    let broken: Vec<&str> = trimmed
156        .lines()
157        .filter(|l| l.contains("requires") || l.contains("has requirement"))
158        .collect();
159
160    if broken.is_empty() {
161        return compact_output(trimmed, 5);
162    }
163    format!(
164        "{} broken dependencies:\n{}",
165        broken.len(),
166        broken.join("\n")
167    )
168}
169
170fn compact_output(text: &str, max: usize) -> String {
171    let lines: Vec<&str> = text.lines().filter(|l| !l.trim().is_empty()).collect();
172    if lines.len() <= max {
173        return lines.join("\n");
174    }
175    format!(
176        "{}\n{}",
177        lines[..max].join("\n"),
178        super::elision_marker(lines.len() - max)
179    )
180}