Skip to main content

dev_prune/adapters/
pipenv.rs

1// Copyright 2026 VKrishna04
2// SPDX-License-Identifier: Apache-2.0
3
4// Pipenv adapter for Python projects.
5//
6// Only the in-project environment is claimed — the `.venv` that appears when
7// `PIPENV_VENV_IN_PROJECT` is set. Pipenv's default is a virtualenv in a shared
8// directory under the user's home, keyed by a hash of the project path. That lives
9// outside the repository and is left alone entirely: it is where other projects'
10// dependencies are installed, not a cache, so no lockfile here can prove it recoverable.
11
12use super::{
13    BloatDir, EnforcePolicy, PackageManager, dir_size, enforce_two_tier, run_command_with_timeout,
14};
15use anyhow::Result;
16use std::path::Path;
17
18/// Pipenv package manager adapter.
19pub struct Pipenv;
20
21impl PackageManager for Pipenv {
22    fn name(&self) -> &'static str {
23        "pipenv"
24    }
25
26    fn detect(&self, path: &Path) -> bool {
27        path.join("Pipfile").exists()
28    }
29
30    fn bloat_dirs(&self, path: &Path) -> Vec<BloatDir> {
31        let venv = path.join(".venv");
32        if !venv.is_dir() {
33            return Vec::new();
34        }
35        vec![BloatDir {
36            name: ".venv".to_string(),
37            path: venv.clone(),
38            size_bytes: dir_size(&venv),
39            shared_bytes: 0,
40        }]
41    }
42
43    /// `pipenv verify` is exactly this check and nothing else: it compares the hash
44    /// `Pipfile.lock` recorded against the current `Pipfile` and exits non-zero when
45    /// they no longer match, writing nothing. `pipenv lock` is the write side.
46    fn enforce_lockfile(&self, path: &Path, policy: EnforcePolicy) -> Result<()> {
47        enforce_two_tier(
48            &path.join("Pipfile.lock"),
49            "pipenv",
50            &["verify"],
51            &["lock"],
52            path,
53            policy,
54        )
55    }
56
57    /// `--deploy` refuses rather than re-resolving when the lockfile is out of date with
58    /// the `Pipfile`, which is the right failure for a restore: putting back something
59    /// other than what was deleted is worse than reporting that it cannot be done.
60    fn restore(&self, path: &Path, timeout: std::time::Duration) -> Result<()> {
61        run_command_with_timeout("pipenv", &["install", "--deploy"], path, timeout)
62    }
63
64    fn lockfiles(&self) -> &'static [&'static str] {
65        &["Pipfile.lock"]
66    }
67}
68
69#[cfg(test)]
70mod tests {
71    use super::*;
72    use std::fs;
73    use tempfile::tempdir;
74
75    #[test]
76    fn detects_on_the_pipfile() {
77        let dir = tempdir().unwrap();
78        assert!(!Pipenv.detect(dir.path()));
79        fs::write(dir.path().join("Pipfile"), "[packages]\n").unwrap();
80        assert!(Pipenv.detect(dir.path()));
81    }
82
83    #[test]
84    fn claims_only_an_in_project_environment() {
85        let dir = tempdir().unwrap();
86        fs::write(dir.path().join("Pipfile"), "").unwrap();
87        assert!(Pipenv.bloat_dirs(dir.path()).is_empty());
88        fs::create_dir(dir.path().join(".venv")).unwrap();
89        let dirs = Pipenv.bloat_dirs(dir.path());
90        assert_eq!(dirs.len(), 1);
91        assert_eq!(dirs[0].name, ".venv");
92    }
93}