Skip to content
6 changes: 2 additions & 4 deletions crates/pet-conda/tests/ci_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -269,9 +269,7 @@ fn detect_new_conda_env_created_with_p_flag_without_python() {
let env = environments
.iter()
.find(|x| x.prefix == Some(prefix.clone()))
.unwrap_or_else(|| {
panic!("New Environment ({prefix:?}) not created, detected envs {environments:?}")
});
.unwrap_or_else(|| panic!("New Environment ({prefix:?}) not created, detected envs {environments:?}"));

assert_eq!(env.prefix, prefix.clone().into());
assert_eq!(env.name, None);
Expand Down Expand Up @@ -387,7 +385,7 @@ fn create_conda_env(mode: &CondaCreateEnvNameOrPath, python_version: Option<Stri
.expect("Failed to execute command");
}

fn get_version(value: &String) -> String {
fn get_version(value: &str) -> String {
// Regex to extract just the d.d.d version from the full version string
let re = regex::Regex::new(r"\d+\.\d+\.\d+").unwrap();
let captures = re.captures(value).unwrap();
Expand Down
30 changes: 16 additions & 14 deletions crates/pet-conda/tests/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,27 +46,29 @@ pub struct TestEnvironment {
root: Option<PathBuf>,
globals_locations: Vec<PathBuf>,
}

impl Environment for TestEnvironment {
fn get_env_var(&self, key: String) -> Option<String> {
self.vars.get(&key).cloned()
}
fn get_root(&self) -> Option<PathBuf> {
self.root.clone()
}
fn get_user_home(&self) -> Option<PathBuf> {
self.home.clone()
}
fn get_know_global_search_locations(&self) -> Vec<PathBuf> {
self.globals_locations.clone()
}
}

#[allow(dead_code)]
pub fn create_test_environment(
vars: HashMap<String, String>,
home: Option<PathBuf>,
globals_locations: Vec<PathBuf>,
root: Option<PathBuf>,
) -> TestEnvironment {
impl Environment for TestEnvironment {
fn get_env_var(&self, key: String) -> Option<String> {
self.vars.get(&key).cloned()
}
fn get_root(&self) -> Option<PathBuf> {
self.root.clone()
}
fn get_user_home(&self) -> Option<PathBuf> {
self.home.clone()
}
fn get_know_global_search_locations(&self) -> Vec<PathBuf> {
self.globals_locations.clone()
}
}
TestEnvironment {
vars,
home,
Expand Down
1 change: 1 addition & 0 deletions crates/pet-core/src/python_environment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,7 @@ pub fn get_environment_key(env: &PythonEnvironment) -> Option<PathBuf> {

#[cfg(test)]
mod tests {
#[cfg(windows)]
use super::*;

#[test]
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();
}
}
30 changes: 16 additions & 14 deletions crates/pet-poetry/tests/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,25 +35,27 @@ pub struct TestEnvironment {
home: Option<PathBuf>,
root: Option<PathBuf>,
}

impl Environment for TestEnvironment {
fn get_env_var(&self, key: String) -> Option<String> {
self.vars.get(&key).cloned()
}
fn get_root(&self) -> Option<PathBuf> {
self.root.clone()
}
fn get_user_home(&self) -> Option<PathBuf> {
self.home.clone()
}
fn get_know_global_search_locations(&self) -> Vec<PathBuf> {
vec![]
}
}

#[allow(dead_code)]
pub fn create_test_environment(
vars: HashMap<String, String>,
home: Option<PathBuf>,
root: Option<PathBuf>,
) -> TestEnvironment {
impl Environment for TestEnvironment {
fn get_env_var(&self, key: String) -> Option<String> {
self.vars.get(&key).cloned()
}
fn get_root(&self) -> Option<PathBuf> {
self.root.clone()
}
fn get_user_home(&self) -> Option<PathBuf> {
self.home.clone()
}
fn get_know_global_search_locations(&self) -> Vec<PathBuf> {
vec![]
}
}
TestEnvironment { vars, home, root }
}
7 changes: 3 additions & 4 deletions crates/pet-poetry/tests/config_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
mod common;

#[cfg(unix)]
#[cfg_attr(any(feature = "ci",), test)]
#[test]
#[allow(dead_code)]
fn global_config_with_defaults() {
use common::create_env_variables;
Expand Down Expand Up @@ -37,7 +37,7 @@ fn global_config_with_defaults() {
}

#[cfg(unix)]
#[cfg_attr(any(feature = "ci",), test)]
#[test]
#[allow(dead_code)]
fn global_config_with_specific_values() {
use std::path::PathBuf;
Expand Down Expand Up @@ -81,9 +81,8 @@ fn global_config_with_specific_values() {
}

#[cfg(unix)]
#[cfg_attr(any(feature = "ci",), test)]
#[test]
#[allow(dead_code)]

fn local_config_with_specific_values() {
use std::path::PathBuf;

Expand Down
30 changes: 16 additions & 14 deletions crates/pet-pyenv/tests/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,27 +34,29 @@ pub struct TestEnvironment {
root: Option<PathBuf>,
globals_locations: Vec<PathBuf>,
}

impl Environment for TestEnvironment {
fn get_env_var(&self, key: String) -> Option<String> {
self.vars.get(&key).cloned()
}
fn get_root(&self) -> Option<PathBuf> {
self.root.clone()
}
fn get_user_home(&self) -> Option<PathBuf> {
self.home.clone()
}
fn get_know_global_search_locations(&self) -> Vec<PathBuf> {
self.globals_locations.clone()
}
}

#[allow(dead_code)]
pub fn create_test_environment(
vars: HashMap<String, String>,
home: Option<PathBuf>,
globals_locations: Vec<PathBuf>,
root: Option<PathBuf>,
) -> TestEnvironment {
impl Environment for TestEnvironment {
fn get_env_var(&self, key: String) -> Option<String> {
self.vars.get(&key).cloned()
}
fn get_root(&self) -> Option<PathBuf> {
self.root.clone()
}
fn get_user_home(&self) -> Option<PathBuf> {
self.home.clone()
}
fn get_know_global_search_locations(&self) -> Vec<PathBuf> {
self.globals_locations.clone()
}
}
TestEnvironment {
vars,
home,
Expand Down
Loading
Loading