Skip to content

Commit f6a4b18

Browse files
authored
Improve conda environment name retrieval logic for external environments (#331)
Fixes #329 Enhance the logic for retrieving conda environment names to correctly handle external environments and ensure proper activation. This change checks the history file when the conda directory is unknown or when the prefix does not match the conda directory.
1 parent 7c7340a commit f6a4b18

File tree

1 file changed

+157
-11
lines changed

1 file changed

+157
-11
lines changed

crates/pet-conda/src/environments.rs

Lines changed: 157 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,7 @@ fn get_conda_env_name(
191191
// if the conda install folder is parent of the env folder, then we can use named activation.
192192
// E.g. conda env is = <conda install>/envs/<env name>
193193
// Then we can use `<conda install>/bin/conda activate -n <env name>`
194+
//
194195
if let Some(conda_dir) = conda_dir {
195196
if !prefix.starts_with(conda_dir) {
196197
name = get_conda_env_name_from_history_file(env_path, prefix);
@@ -226,6 +227,17 @@ fn get_conda_env_name_from_history_file(env_path: &Path, prefix: &Path) -> Optio
226227
None
227228
}
228229

230+
fn is_conda_env_name_in_cmd(cmd_line: String, name: &str) -> bool {
231+
// Sample lines
232+
// # cmd: <conda install directory>\Scripts\conda-script.py create -n samlpe1
233+
// # cmd: <conda install directory>\Scripts\conda-script.py create -p <full path>
234+
// # cmd: /Users/donjayamanne/miniconda3/bin/conda create -n conda1
235+
// # cmd_line: "# cmd: /usr/bin/conda create -p ./prefix-envs/.conda1 python=3.12 -y"
236+
// Look for "-n <name>" in the command line
237+
cmd_line.contains(format!("-n {name}").as_str())
238+
|| cmd_line.contains(format!("--name {name}").as_str())
239+
}
240+
229241
fn get_conda_dir_from_cmd(cmd_line: String) -> Option<PathBuf> {
230242
// Sample lines
231243
// # cmd: <conda install directory>\Scripts\conda-script.py create -n samlpe1
@@ -288,17 +300,6 @@ fn get_conda_dir_from_cmd(cmd_line: String) -> Option<PathBuf> {
288300
None
289301
}
290302

291-
fn is_conda_env_name_in_cmd(cmd_line: String, name: &str) -> bool {
292-
// Sample lines
293-
// # cmd: <conda install directory>\Scripts\conda-script.py create -n samlpe1
294-
// # cmd: <conda install directory>\Scripts\conda-script.py create -p <full path>
295-
// # cmd: /Users/donjayamanne/miniconda3/bin/conda create -n conda1
296-
// # cmd_line: "# cmd: /usr/bin/conda create -p ./prefix-envs/.conda1 python=3.12 -y"
297-
// Look for "-n <name>" in the command line
298-
cmd_line.contains(format!("-n {name}").as_str())
299-
|| cmd_line.contains(format!("--name {name}").as_str())
300-
}
301-
302303
pub fn get_activation_command(
303304
env: &CondaEnvironment,
304305
manager: &EnvManager,
@@ -379,4 +380,149 @@ mod tests {
379380
line = "# cmd: /Users/donjayamanne/.pyenv/versions/mambaforge-22.11.1-3/lib/python3.10/site-packages/conda/__main__.py create --yes -p .conda python=3.12";
380381
assert!(!is_conda_env_name_in_cmd(line.to_string(), ".conda"));
381382
}
383+
384+
/// Test that external environments (not under conda_dir) created with --prefix
385+
/// return None for name, so activation uses path instead of name.
386+
/// This is the fix for issue #329.
387+
#[test]
388+
fn external_path_based_env_returns_none_name() {
389+
// Create a temp directory simulating an external path-based conda env
390+
let temp_dir = std::env::temp_dir().join("pet_test_external_path_env");
391+
let conda_meta_dir = temp_dir.join(".conda").join("conda-meta");
392+
std::fs::create_dir_all(&conda_meta_dir).unwrap();
393+
394+
// Write a history file showing the env was created with --prefix (path-based)
395+
let history_file = conda_meta_dir.join("history");
396+
std::fs::write(
397+
&history_file,
398+
"# cmd: /usr/bin/conda create --yes --prefix .conda python=3.12\n",
399+
)
400+
.unwrap();
401+
402+
let env_path = temp_dir.join(".conda");
403+
// conda_dir is known but env is NOT under it (external environment)
404+
let conda_dir = Some(std::path::PathBuf::from("/some/other/conda"));
405+
406+
let name = get_conda_env_name(&env_path, &env_path, &conda_dir);
407+
assert!(
408+
name.is_none(),
409+
"Path-based external env should return None for name, got {:?}",
410+
name
411+
);
412+
413+
// Cleanup
414+
let _ = std::fs::remove_dir_all(&temp_dir);
415+
}
416+
417+
/// Test that external environments (not under conda_dir) created with -n
418+
/// return the name for name-based activation, but only if the folder name matches.
419+
#[test]
420+
fn external_name_based_env_returns_name() {
421+
// Create a temp directory simulating an external name-based conda env
422+
let temp_dir = std::env::temp_dir().join("pet_test_external_name_env");
423+
let conda_meta_dir = temp_dir.join("myenv").join("conda-meta");
424+
std::fs::create_dir_all(&conda_meta_dir).unwrap();
425+
426+
// Write a history file showing the env was created with -n myenv (name-based)
427+
// Note: the folder name "myenv" matches the -n argument "myenv"
428+
let history_file = conda_meta_dir.join("history");
429+
std::fs::write(
430+
&history_file,
431+
"# cmd: /usr/bin/conda create -n myenv python=3.12\n",
432+
)
433+
.unwrap();
434+
435+
let env_path = temp_dir.join("myenv");
436+
// conda_dir is known but env is NOT under it (external environment)
437+
let conda_dir = Some(std::path::PathBuf::from("/some/other/conda"));
438+
439+
let name = get_conda_env_name(&env_path, &env_path, &conda_dir);
440+
assert_eq!(
441+
name,
442+
Some("myenv".to_string()),
443+
"Name-based external env should return the name when folder matches"
444+
);
445+
446+
// Cleanup
447+
let _ = std::fs::remove_dir_all(&temp_dir);
448+
}
449+
450+
/// Test that environments under conda_dir/envs/ return the folder name.
451+
/// This is the most common case for named conda environments.
452+
#[test]
453+
fn env_under_conda_dir_returns_folder_name() {
454+
// Create a temp directory simulating conda_dir/envs/myenv structure
455+
let temp_dir = std::env::temp_dir().join("pet_test_env_under_conda");
456+
let conda_dir = temp_dir.join("miniconda3");
457+
let env_path = conda_dir.join("envs").join("myenv");
458+
let conda_meta_dir = env_path.join("conda-meta");
459+
std::fs::create_dir_all(&conda_meta_dir).unwrap();
460+
461+
// When env is under conda_dir/envs/, name should be the folder name
462+
let name = get_conda_env_name(&env_path, &env_path, &Some(conda_dir));
463+
assert_eq!(
464+
name,
465+
Some("myenv".to_string()),
466+
"Env under conda_dir/envs/ should return folder name"
467+
);
468+
469+
// Cleanup
470+
let _ = std::fs::remove_dir_all(&temp_dir);
471+
}
472+
473+
/// Test that external env with no history file returns None for name.
474+
/// This ensures safe path-based activation when we can't determine how it was created.
475+
#[test]
476+
fn external_env_without_history_returns_none_name() {
477+
// Create a temp directory simulating an external conda env without history
478+
let temp_dir = std::env::temp_dir().join("pet_test_external_no_history");
479+
let conda_meta_dir = temp_dir.join("myenv").join("conda-meta");
480+
std::fs::create_dir_all(&conda_meta_dir).unwrap();
481+
// Note: NOT creating a history file
482+
483+
let env_path = temp_dir.join("myenv");
484+
// conda_dir is known but env is NOT under it (external environment)
485+
let conda_dir = Some(std::path::PathBuf::from("/some/other/conda"));
486+
487+
let name = get_conda_env_name(&env_path, &env_path, &conda_dir);
488+
assert!(
489+
name.is_none(),
490+
"External env without history should return None for safe path-based activation, got {:?}",
491+
name
492+
);
493+
494+
// Cleanup
495+
let _ = std::fs::remove_dir_all(&temp_dir);
496+
}
497+
498+
/// Test that external env with history but folder name doesn't match -n argument returns None.
499+
/// This prevents wrong activation when env was moved/renamed after creation.
500+
#[test]
501+
fn external_env_with_mismatched_name_returns_none() {
502+
// Create a temp directory simulating an external conda env
503+
let temp_dir = std::env::temp_dir().join("pet_test_external_mismatch");
504+
// Folder is named "renamed_env" but was created with -n "original_name"
505+
let conda_meta_dir = temp_dir.join("renamed_env").join("conda-meta");
506+
std::fs::create_dir_all(&conda_meta_dir).unwrap();
507+
508+
let history_file = conda_meta_dir.join("history");
509+
std::fs::write(
510+
&history_file,
511+
"# cmd: /usr/bin/conda create -n original_name python=3.12\n",
512+
)
513+
.unwrap();
514+
515+
let env_path = temp_dir.join("renamed_env");
516+
let conda_dir = Some(std::path::PathBuf::from("/some/other/conda"));
517+
518+
let name = get_conda_env_name(&env_path, &env_path, &conda_dir);
519+
assert!(
520+
name.is_none(),
521+
"External env with mismatched name should return None, got {:?}",
522+
name
523+
);
524+
525+
// Cleanup
526+
let _ = std::fs::remove_dir_all(&temp_dir);
527+
}
382528
}

0 commit comments

Comments
 (0)