From ae335aa07696314b9d94ae3efc17fe70c86afed4 Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Tue, 2 Sep 2025 17:17:37 -0700 Subject: [PATCH 1/3] Fix to detect `venv` with `pipenv` installed in it as `pipenv` environment --- crates/pet-pipenv/src/lib.rs | 45 ++++++++++++++++++++++++++++-------- 1 file changed, 35 insertions(+), 10 deletions(-) diff --git a/crates/pet-pipenv/src/lib.rs b/crates/pet-pipenv/src/lib.rs index b727a11d..c3f5a6ea 100644 --- a/crates/pet-pipenv/src/lib.rs +++ b/crates/pet-pipenv/src/lib.rs @@ -20,22 +20,35 @@ mod env_variables; fn get_pipenv_project(env: &PythonEnv) -> Option { if let Some(prefix) = &env.prefix { - get_pipenv_project_from_prefix(prefix) - } else { - // If the parent is bin or script, then get the parent. - let bin = env.executable.parent()?; - if bin.file_name().unwrap_or_default() == Path::new("bin") - || bin.file_name().unwrap_or_default() == Path::new("Scripts") - { - get_pipenv_project_from_prefix(env.executable.parent()?.parent()?) - } else { - get_pipenv_project_from_prefix(env.executable.parent()?) + if let Some(project) = get_pipenv_project_from_prefix(prefix) { + return Some(project); + } + } + + // We can also have a venv in the workspace that has pipenv installed in it. + // In such cases, the project is the workspace folder containing the venv. + if let Some(project) = &env.project { + if project.join("Pipfile").exists() { + return Some(project.clone()); } } + + // If the parent is bin or script, then get the parent. + let bin = env.executable.parent()?; + if bin.file_name().unwrap_or_default() == Path::new("bin") + || bin.file_name().unwrap_or_default() == Path::new("Scripts") + { + get_pipenv_project_from_prefix(env.executable.parent()?.parent()?) + } else { + get_pipenv_project_from_prefix(env.executable.parent()?) + } } fn get_pipenv_project_from_prefix(prefix: &Path) -> Option { let project_file = prefix.join(".project"); + if !project_file.exists() { + return None; + } let contents = fs::read_to_string(project_file).ok()?; let project_folder = norm_case(PathBuf::from(contents.trim().to_string())); if project_folder.exists() { @@ -45,12 +58,24 @@ fn get_pipenv_project_from_prefix(prefix: &Path) -> Option { } } +fn is_pipenv_from_project(env: &PythonEnv) -> bool { + if let Some(project) = &env.project { + if project.join("Pipfile").exists() { + return true; + } + } + false +} + fn is_pipenv(env: &PythonEnv, env_vars: &EnvVariables) -> bool { if let Some(project_path) = get_pipenv_project(env) { if project_path.join(env_vars.pipenv_pipfile.clone()).exists() { return true; } } + if is_pipenv_from_project(env) { + return true; + } // If we have a Pipfile, then this is a pipenv environment. // Else likely a virtualenvwrapper or the like. if let Some(project_path) = get_pipenv_project(env) { From 8893c4a79da0d86d1460385d29e14bfc003df840 Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Tue, 2 Sep 2025 17:51:31 -0700 Subject: [PATCH 2/3] fix error --- crates/pet-pipenv/src/lib.rs | 122 +++++++++++++++++++++++++++++++++-- 1 file changed, 116 insertions(+), 6 deletions(-) diff --git a/crates/pet-pipenv/src/lib.rs b/crates/pet-pipenv/src/lib.rs index c3f5a6ea..e5e6f353 100644 --- a/crates/pet-pipenv/src/lib.rs +++ b/crates/pet-pipenv/src/lib.rs @@ -23,13 +23,36 @@ fn get_pipenv_project(env: &PythonEnv) -> Option { if let Some(project) = get_pipenv_project_from_prefix(prefix) { return Some(project); } + // If there's no .project file, but the venv lives inside the project folder + // (e.g., /.venv or /venv), then the project is the parent + // directory of the venv. Detect that by checking for a Pipfile next to the venv. + if let Some(parent) = prefix.parent() { + let project_folder = parent; + if project_folder.join("Pipfile").exists() { + return Some(project_folder.to_path_buf()); + } + } } // We can also have a venv in the workspace that has pipenv installed in it. // In such cases, the project is the workspace folder containing the venv. - if let Some(project) = &env.project { - if project.join("Pipfile").exists() { - return Some(project.clone()); + // Derive the project folder from the executable path when prefix isn't available. + // Typical layout: /.venv/{bin|Scripts}/python + // So walk up to {bin|Scripts} -> venv dir -> project dir and check for Pipfile. + if let Some(bin) = env.executable.parent() { + let venv_dir = if bin.file_name().unwrap_or_default() == Path::new("bin") + || bin.file_name().unwrap_or_default() == Path::new("Scripts") + { + bin.parent() + } else { + Some(bin) + }; + if let Some(venv_dir) = venv_dir { + if let Some(project_dir) = venv_dir.parent() { + if project_dir.join("Pipfile").exists() { + return Some(project_dir.to_path_buf()); + } + } } } @@ -59,9 +82,29 @@ fn get_pipenv_project_from_prefix(prefix: &Path) -> Option { } fn is_pipenv_from_project(env: &PythonEnv) -> bool { - if let Some(project) = &env.project { - if project.join("Pipfile").exists() { - return true; + // If the env prefix is inside a project folder, check that folder for a Pipfile. + if let Some(prefix) = &env.prefix { + if let Some(project_dir) = prefix.parent() { + if project_dir.join("Pipfile").exists() { + return true; + } + } + } + // Derive from the executable path as a fallback. + if let Some(bin) = env.executable.parent() { + let venv_dir = if bin.file_name().unwrap_or_default() == Path::new("bin") + || bin.file_name().unwrap_or_default() == Path::new("Scripts") + { + bin.parent() + } else { + Some(bin) + }; + if let Some(venv_dir) = venv_dir { + if let Some(project_dir) = venv_dir.parent() { + if project_dir.join("Pipfile").exists() { + return true; + } + } } } false @@ -144,3 +187,70 @@ impl Locator for PipEnv { // } } + +#[cfg(test)] +mod tests { + use super::*; + use std::time::{SystemTime, UNIX_EPOCH}; + + fn unique_temp_dir() -> PathBuf { + let mut dir = std::env::temp_dir(); + let nanos = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(); + dir.push(format!("pet_pipenv_test_{}", nanos)); + dir + } + + #[test] + fn infer_project_for_venv_in_project() { + let project_dir = unique_temp_dir(); + let venv_dir = project_dir.join(".venv"); + let bin_dir = if cfg!(windows) { + venv_dir.join("Scripts") + } else { + venv_dir.join("bin") + }; + let python_exe = if cfg!(windows) { + bin_dir.join("python.exe") + } else { + bin_dir.join("python") + }; + + // Create directories and files + std::fs::create_dir_all(&bin_dir).unwrap(); + std::fs::write(project_dir.join("Pipfile"), b"[[source]]\n").unwrap(); + // Touch python exe file + std::fs::write(&python_exe, b"").unwrap(); + // Touch pyvenv.cfg in venv root so PythonEnv::new logic would normally detect prefix + std::fs::write(venv_dir.join("pyvenv.cfg"), b"version = 3.12.0\n").unwrap(); + + // Construct PythonEnv directly + let env = PythonEnv { + executable: norm_case(python_exe.clone()), + prefix: Some(norm_case(venv_dir.clone())), + version: None, + symlinks: None, + }; + + // Validate helper infers project + let inferred = get_pipenv_project(&env).expect("expected project path"); + assert_eq!(inferred, norm_case(project_dir.clone())); + + // Validate locator populates project + let locator = PipEnv { + env_vars: EnvVariables { + pipenv_max_depth: 3, + pipenv_pipfile: "Pipfile".to_string(), + }, + }; + let result = locator + .try_from(&env) + .expect("expected locator to return environment"); + assert_eq!(result.project, Some(norm_case(project_dir.clone()))); + + // Cleanup + std::fs::remove_dir_all(&project_dir).ok(); + } +} From be0539bf46f71eef1dfe54b6a68f1dc8aec43474 Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Tue, 2 Sep 2025 18:29:26 -0700 Subject: [PATCH 3/3] Update min python for linux/mac --- .github/workflows/pr-check.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml index 0a5161e0..1710b3af 100644 --- a/.github/workflows/pr-check.yml +++ b/.github/workflows/pr-check.yml @@ -134,7 +134,7 @@ jobs: if: startsWith( matrix.os, 'ubuntu') || startsWith( matrix.os, 'macos') run: | pyenv install --list - pyenv install 3.13:latest 3.12:latest 3.8:latest + pyenv install 3.13:latest 3.12:latest 3.9:latest shell: bash # pyenv-win install list has not updated for a while