Skip to main content

dev_prune/adapters/
uv.rs

1// Copyright 2026 VKrishna04
2// SPDX-License-Identifier: Apache-2.0
3
4// uv package manager adapter for Python projects.
5
6use super::{BloatDir, EnforcePolicy, PackageManager, dir_size, enforce_two_tier, run_command};
7use anyhow::Result;
8use std::fs;
9use std::path::Path;
10
11/// Adapter for uv-based Python projects.
12pub struct Uv;
13
14impl PackageManager for Uv {
15    fn name(&self) -> &'static str {
16        "uv"
17    }
18
19    fn detect(&self, path: &Path) -> bool {
20        let uv_lock = path.join("uv.lock");
21        if uv_lock.exists() {
22            return true;
23        }
24
25        let pyproject = path.join("pyproject.toml");
26        if pyproject.exists() {
27            if let Ok(content) = fs::read_to_string(&pyproject) {
28                if content.contains("[tool.uv]") {
29                    return true;
30                }
31            }
32        }
33
34        false
35    }
36
37    fn bloat_dirs(&self, path: &Path) -> Vec<BloatDir> {
38        let mut dirs = Vec::new();
39        let venv_path = path.join(".venv");
40        if venv_path.exists() {
41            dirs.push(BloatDir {
42                name: ".venv".to_string(),
43                path: venv_path.clone(),
44                size_bytes: dir_size(&venv_path),
45            });
46        }
47        dirs
48    }
49
50    /// Enforces the lockfile without writing it.
51    ///
52    /// `uv lock --locked` asserts that `uv.lock` is already up to date with
53    /// `pyproject.toml` and exits non-zero instead of rewriting it when it is not.
54    /// Plain `uv lock` is the writing form, for the case where no lockfile exists yet.
55    fn enforce_lockfile(&self, path: &Path, policy: EnforcePolicy) -> Result<()> {
56        let lockfile = path.join("uv.lock");
57        enforce_two_tier(
58            &lockfile,
59            "uv",
60            &["lock", "--locked"],
61            &["lock"],
62            path,
63            policy,
64        )
65    }
66
67    fn restore(&self, path: &Path) -> Result<()> {
68        run_command("uv", &["sync"], path)
69    }
70
71    fn lockfiles(&self) -> &'static [&'static str] {
72        &["uv.lock"]
73    }
74}
75
76#[cfg(test)]
77mod tests {
78    use super::*;
79    use std::fs::File;
80    use std::io::Write;
81    use tempfile::tempdir;
82
83    #[test]
84    fn test_name() {
85        let adapter = Uv;
86        assert_eq!(adapter.name(), "uv");
87    }
88
89    #[test]
90    fn test_detect_positive_lock() {
91        let dir = tempdir().unwrap();
92        File::create(dir.path().join("uv.lock")).unwrap();
93
94        let adapter = Uv;
95        assert!(adapter.detect(dir.path()));
96    }
97
98    #[test]
99    fn test_detect_positive_toml() {
100        let dir = tempdir().unwrap();
101        let mut file = File::create(dir.path().join("pyproject.toml")).unwrap();
102        writeln!(file, "[tool.uv]").unwrap();
103
104        let adapter = Uv;
105        assert!(adapter.detect(dir.path()));
106    }
107
108    #[test]
109    fn test_detect_negative() {
110        let dir = tempdir().unwrap();
111
112        let adapter = Uv;
113        assert!(!adapter.detect(dir.path()));
114    }
115
116    #[test]
117    fn test_bloat_dirs_present() {
118        let dir = tempdir().unwrap();
119        fs::create_dir(dir.path().join(".venv")).unwrap();
120
121        let adapter = Uv;
122        let dirs = adapter.bloat_dirs(dir.path());
123        assert_eq!(dirs.len(), 1);
124        assert_eq!(dirs[0].name, ".venv");
125    }
126
127    #[test]
128    fn test_bloat_dirs_absent() {
129        let dir = tempdir().unwrap();
130
131        let adapter = Uv;
132        let dirs = adapter.bloat_dirs(dir.path());
133        assert!(dirs.is_empty());
134    }
135}