Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/pr-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
149 changes: 142 additions & 7 deletions crates/pet-pipenv/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,22 +20,58 @@ mod env_variables;

fn get_pipenv_project(env: &PythonEnv) -> Option<PathBuf> {
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")
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., <project>/.venv or <project>/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.
// Derive the project folder from the executable path when prefix isn't available.
// Typical layout: <project>/.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")
{
get_pipenv_project_from_prefix(env.executable.parent()?.parent()?)
bin.parent()
} else {
get_pipenv_project_from_prefix(env.executable.parent()?)
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());
}
}
}
}

// 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<PathBuf> {
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() {
Expand All @@ -45,12 +81,44 @@ fn get_pipenv_project_from_prefix(prefix: &Path) -> Option<PathBuf> {
}
}

fn is_pipenv_from_project(env: &PythonEnv) -> bool {
// 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
}

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) {
Expand Down Expand Up @@ -119,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();
}
}
Loading