python_check_updates/
detector.rs1use std::fs;
2use std::path::{Path, PathBuf};
3
4#[derive(Debug, Clone, PartialEq, Eq, Hash)]
6pub enum PackageManager {
7 Pip,
8 Uv,
9 Poetry,
10 Pdm,
11 Conda,
12}
13
14impl std::fmt::Display for PackageManager {
15 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
16 match self {
17 PackageManager::Pip => write!(f, "pip"),
18 PackageManager::Uv => write!(f, "uv"),
19 PackageManager::Poetry => write!(f, "poetry"),
20 PackageManager::Pdm => write!(f, "pdm"),
21 PackageManager::Conda => write!(f, "conda"),
22 }
23 }
24}
25
26#[derive(Debug, Clone)]
28pub struct DetectedFile {
29 pub path: PathBuf,
30 pub package_manager: PackageManager,
31}
32
33pub struct ProjectDetector {
35 project_path: PathBuf,
36}
37
38impl ProjectDetector {
39 pub fn new(project_path: PathBuf) -> Self {
40 Self { project_path }
41 }
42
43 pub fn detect(&self) -> anyhow::Result<Vec<DetectedFile>> {
45 let mut detected_files = Vec::new();
46
47 let pyproject_path = self.project_path.join("pyproject.toml");
49 if pyproject_path.exists() {
50 if let Some(pm) = self.detect_pyproject_manager(&pyproject_path)? {
51 detected_files.push(DetectedFile {
52 path: pyproject_path,
53 package_manager: pm,
54 });
55 }
56 }
57
58 if let Ok(entries) = fs::read_dir(&self.project_path) {
60 for entry in entries.flatten() {
61 let path = entry.path();
62 if let Some(filename) = path.file_name() {
63 let filename_str = filename.to_string_lossy();
64 if filename_str.starts_with("requirements") && filename_str.ends_with(".txt") {
65 detected_files.push(DetectedFile {
66 path: path.clone(),
67 package_manager: PackageManager::Pip,
68 });
69 }
70 }
71 }
72 }
73
74 for filename in &["environment.yml", "environment.yaml"] {
76 let conda_path = self.project_path.join(filename);
77 if conda_path.exists() {
78 detected_files.push(DetectedFile {
79 path: conda_path,
80 package_manager: PackageManager::Conda,
81 });
82 }
83 }
84
85 Ok(detected_files)
86 }
87
88 fn detect_pyproject_manager(&self, pyproject_path: &Path) -> anyhow::Result<Option<PackageManager>> {
90 let contents = fs::read_to_string(pyproject_path)?;
91
92 let uv_lock = self.project_path.join("uv.lock");
94 let poetry_lock = self.project_path.join("poetry.lock");
95 let pdm_lock = self.project_path.join("pdm.lock");
96
97 let has_poetry_section = contents.contains("[tool.poetry]");
99 let has_pdm_section = contents.contains("[tool.pdm]");
100
101 if poetry_lock.exists() || has_poetry_section {
103 Ok(Some(PackageManager::Poetry))
104 } else if pdm_lock.exists() || has_pdm_section {
105 Ok(Some(PackageManager::Pdm))
106 } else if uv_lock.exists() {
107 Ok(Some(PackageManager::Uv))
108 } else {
109 if contents.contains("[project]") &&
112 (contents.contains("dependencies") || contents.contains("[project.dependencies]")) {
113 Ok(Some(PackageManager::Uv))
114 } else {
115 Ok(None)
117 }
118 }
119 }
120
121 pub fn get_sync_command(&self, pm: &PackageManager) -> &'static str {
123 match pm {
124 PackageManager::Pip => "pip install -r requirements.txt",
125 PackageManager::Uv => "uv lock",
126 PackageManager::Poetry => "poetry lock",
127 PackageManager::Pdm => "pdm lock",
128 PackageManager::Conda => "conda env update",
129 }
130 }
131}
132
133#[cfg(test)]
134mod tests {
135 use super::*;
136 use std::fs;
137 use tempfile::TempDir;
138
139 #[test]
140 fn test_detect_pip_requirements() {
141 let temp_dir = TempDir::new().unwrap();
142 let req_path = temp_dir.path().join("requirements.txt");
143 fs::write(&req_path, "requests==2.28.0\n").unwrap();
144
145 let detector = ProjectDetector::new(temp_dir.path().to_path_buf());
146 let detected = detector.detect().unwrap();
147
148 assert_eq!(detected.len(), 1);
149 assert_eq!(detected[0].package_manager, PackageManager::Pip);
150 assert_eq!(detected[0].path, req_path);
151 }
152
153 #[test]
154 fn test_detect_multiple_requirements() {
155 let temp_dir = TempDir::new().unwrap();
156 let req_path = temp_dir.path().join("requirements.txt");
157 let req_dev_path = temp_dir.path().join("requirements-dev.txt");
158 fs::write(&req_path, "requests==2.28.0\n").unwrap();
159 fs::write(&req_dev_path, "pytest==7.0.0\n").unwrap();
160
161 let detector = ProjectDetector::new(temp_dir.path().to_path_buf());
162 let detected = detector.detect().unwrap();
163
164 assert_eq!(detected.len(), 2);
165 assert!(detected.iter().all(|d| d.package_manager == PackageManager::Pip));
166 }
167
168 #[test]
169 fn test_detect_poetry() {
170 let temp_dir = TempDir::new().unwrap();
171 let pyproject_path = temp_dir.path().join("pyproject.toml");
172 let poetry_lock_path = temp_dir.path().join("poetry.lock");
173
174 fs::write(&pyproject_path, "[tool.poetry]\nname = \"test\"\n").unwrap();
175 fs::write(&poetry_lock_path, "").unwrap();
176
177 let detector = ProjectDetector::new(temp_dir.path().to_path_buf());
178 let detected = detector.detect().unwrap();
179
180 assert_eq!(detected.len(), 1);
181 assert_eq!(detected[0].package_manager, PackageManager::Poetry);
182 assert_eq!(detected[0].path, pyproject_path);
183 }
184
185 #[test]
186 fn test_detect_poetry_without_lock() {
187 let temp_dir = TempDir::new().unwrap();
188 let pyproject_path = temp_dir.path().join("pyproject.toml");
189
190 fs::write(&pyproject_path, "[tool.poetry]\nname = \"test\"\n").unwrap();
191
192 let detector = ProjectDetector::new(temp_dir.path().to_path_buf());
193 let detected = detector.detect().unwrap();
194
195 assert_eq!(detected.len(), 1);
196 assert_eq!(detected[0].package_manager, PackageManager::Poetry);
197 }
198
199 #[test]
200 fn test_detect_pdm() {
201 let temp_dir = TempDir::new().unwrap();
202 let pyproject_path = temp_dir.path().join("pyproject.toml");
203 let pdm_lock_path = temp_dir.path().join("pdm.lock");
204
205 fs::write(&pyproject_path, "[tool.pdm]\n").unwrap();
206 fs::write(&pdm_lock_path, "").unwrap();
207
208 let detector = ProjectDetector::new(temp_dir.path().to_path_buf());
209 let detected = detector.detect().unwrap();
210
211 assert_eq!(detected.len(), 1);
212 assert_eq!(detected[0].package_manager, PackageManager::Pdm);
213 }
214
215 #[test]
216 fn test_detect_uv() {
217 let temp_dir = TempDir::new().unwrap();
218 let pyproject_path = temp_dir.path().join("pyproject.toml");
219 let uv_lock_path = temp_dir.path().join("uv.lock");
220
221 fs::write(&pyproject_path, "[project]\nname = \"test\"\ndependencies = []\n").unwrap();
222 fs::write(&uv_lock_path, "").unwrap();
223
224 let detector = ProjectDetector::new(temp_dir.path().to_path_buf());
225 let detected = detector.detect().unwrap();
226
227 assert_eq!(detected.len(), 1);
228 assert_eq!(detected[0].package_manager, PackageManager::Uv);
229 }
230
231 #[test]
232 fn test_detect_uv_without_lock() {
233 let temp_dir = TempDir::new().unwrap();
234 let pyproject_path = temp_dir.path().join("pyproject.toml");
235
236 fs::write(&pyproject_path, "[project]\nname = \"test\"\ndependencies = [\"requests\"]\n").unwrap();
237
238 let detector = ProjectDetector::new(temp_dir.path().to_path_buf());
239 let detected = detector.detect().unwrap();
240
241 assert_eq!(detected.len(), 1);
242 assert_eq!(detected[0].package_manager, PackageManager::Uv);
243 }
244
245 #[test]
246 fn test_detect_conda_yml() {
247 let temp_dir = TempDir::new().unwrap();
248 let env_path = temp_dir.path().join("environment.yml");
249
250 fs::write(&env_path, "name: test\n").unwrap();
251
252 let detector = ProjectDetector::new(temp_dir.path().to_path_buf());
253 let detected = detector.detect().unwrap();
254
255 assert_eq!(detected.len(), 1);
256 assert_eq!(detected[0].package_manager, PackageManager::Conda);
257 assert_eq!(detected[0].path, env_path);
258 }
259
260 #[test]
261 fn test_detect_conda_yaml() {
262 let temp_dir = TempDir::new().unwrap();
263 let env_path = temp_dir.path().join("environment.yaml");
264
265 fs::write(&env_path, "name: test\n").unwrap();
266
267 let detector = ProjectDetector::new(temp_dir.path().to_path_buf());
268 let detected = detector.detect().unwrap();
269
270 assert_eq!(detected.len(), 1);
271 assert_eq!(detected[0].package_manager, PackageManager::Conda);
272 }
273
274 #[test]
275 fn test_detect_mixed_project() {
276 let temp_dir = TempDir::new().unwrap();
277 let req_path = temp_dir.path().join("requirements.txt");
278 let env_path = temp_dir.path().join("environment.yml");
279
280 fs::write(&req_path, "requests==2.28.0\n").unwrap();
281 fs::write(&env_path, "name: test\n").unwrap();
282
283 let detector = ProjectDetector::new(temp_dir.path().to_path_buf());
284 let detected = detector.detect().unwrap();
285
286 assert_eq!(detected.len(), 2);
287 assert!(detected.iter().any(|d| d.package_manager == PackageManager::Pip));
288 assert!(detected.iter().any(|d| d.package_manager == PackageManager::Conda));
289 }
290
291 #[test]
292 fn test_priority_poetry_over_others() {
293 let temp_dir = TempDir::new().unwrap();
294 let pyproject_path = temp_dir.path().join("pyproject.toml");
295 let poetry_lock_path = temp_dir.path().join("poetry.lock");
296 let uv_lock_path = temp_dir.path().join("uv.lock");
297
298 fs::write(&pyproject_path, "[tool.poetry]\nname = \"test\"\n").unwrap();
300 fs::write(&poetry_lock_path, "").unwrap();
301 fs::write(&uv_lock_path, "").unwrap();
302
303 let detector = ProjectDetector::new(temp_dir.path().to_path_buf());
304 let detected = detector.detect().unwrap();
305
306 assert_eq!(detected.len(), 1);
307 assert_eq!(detected[0].package_manager, PackageManager::Poetry);
308 }
309
310 #[test]
311 fn test_get_sync_command() {
312 let temp_dir = TempDir::new().unwrap();
313 let detector = ProjectDetector::new(temp_dir.path().to_path_buf());
314
315 assert_eq!(detector.get_sync_command(&PackageManager::Pip), "pip install -r requirements.txt");
316 assert_eq!(detector.get_sync_command(&PackageManager::Uv), "uv lock");
317 assert_eq!(detector.get_sync_command(&PackageManager::Poetry), "poetry lock");
318 assert_eq!(detector.get_sync_command(&PackageManager::Pdm), "pdm lock");
319 assert_eq!(detector.get_sync_command(&PackageManager::Conda), "conda env update");
320 }
321}