Skip to content

Commit 7a5701f

Browse files
committed
fix: refine conda environment name retrieval logic for external environments to ensure accurate activation
1 parent c508fb0 commit 7a5701f

File tree

1 file changed

+42
-94
lines changed

1 file changed

+42
-94
lines changed

crates/pet-conda/src/environments.rs

Lines changed: 42 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -192,24 +192,9 @@ fn get_conda_env_name(
192192
// E.g. conda env is = <conda install>/envs/<env name>
193193
// Then we can use `<conda install>/bin/conda activate -n <env name>`
194194
//
195-
// Handle three cases:
196-
// 1. conda_dir known, prefix is under it → name-based activation works, return folder name
197-
// 2. conda_dir known, prefix NOT under it → external env, check history to distinguish -n vs -p
198-
// 3. conda_dir unknown → can't do name-based activation without knowing conda installation,
199-
// return None so activation uses full path (fixes issue #329 for path-based envs)
200-
match conda_dir {
201-
Some(dir) => {
202-
if !prefix.starts_with(dir) {
203-
// External environment - check history to see if created with -n or -p
204-
name = get_conda_env_name_from_history_file(env_path, prefix);
205-
}
206-
// else: env is under conda_dir/envs/, name-based activation works
207-
}
208-
None => {
209-
// No known conda installation directory
210-
// Name-based activation requires knowing which conda to use, so return None
211-
// This ensures path-based activation is used (correct for --prefix envs too)
212-
name = None;
195+
if let Some(conda_dir) = conda_dir {
196+
if !prefix.starts_with(conda_dir) {
197+
name = get_conda_env_name_from_history_file(env_path, prefix);
213198
}
214199
}
215200

@@ -221,47 +206,36 @@ fn get_conda_env_name(
221206
* And example is `# cmd: <conda install directory>\Scripts\conda-script.py create -n sample``
222207
* And example is `# cmd: conda create -n sample``
223208
*
224-
* This function returns the name of the conda environment based on how it was created:
225-
* - If created with --name/-n (name-based): returns the folder name (can use named activation)
226-
* - Otherwise: returns None (activation must use full path for safety)
227-
*
228-
* This is used for external environments (not under conda_dir). We only return a name
229-
* if we can positively confirm it was created with -n/--name, because that's the only
230-
* case where name-based activation works reliably for external environments.
209+
* This function returns the name of the conda environment.
231210
*/
232211
fn get_conda_env_name_from_history_file(env_path: &Path, prefix: &Path) -> Option<String> {
233212
let name = prefix
234213
.file_name()
235214
.map(|name| name.to_str().unwrap_or_default().to_string());
236215

237-
if let Some(ref name) = name {
216+
if let Some(name) = name {
238217
if let Some(line) = get_conda_creation_line_from_history(env_path) {
239218
// Sample lines
240219
// # cmd: <conda install directory>\Scripts\conda-script.py create -n samlpe1
241220
// # cmd: <conda install directory>\Scripts\conda-script.py create -p <full path>
242221
// # cmd: /Users/donjayamanne/miniconda3/bin/conda create -n conda1
243-
// # cmd: /usr/bin/conda create --prefix ./prefix-envs/.conda1 python=3.12 -y
244-
245-
// Only return a name if we can confirm it was created with -n/--name
246-
// This matches the original behavior where we checked for the exact name in the cmd
247-
if is_name_based_conda_env(&line) {
248-
return Some(name.clone());
222+
if is_conda_env_name_in_cmd(line, &name) {
223+
return Some(name);
249224
}
250225
}
251226
}
252-
// If we can't confirm name-based creation, return None for safe path-based activation
253227
None
254228
}
255229

256-
/// Check if the conda create command used --name or -n (name-based environment)
257-
fn is_name_based_conda_env(cmd_line: &str) -> bool {
258-
// Look for " -n " or " --name " after "create"
259-
let lower = cmd_line.to_lowercase();
260-
if let Some(create_idx) = lower.find(" create ") {
261-
let after_create = &lower[create_idx..];
262-
return after_create.contains(" -n ") || after_create.contains(" --name ");
263-
}
264-
false
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())
265239
}
266240

267241
fn get_conda_dir_from_cmd(cmd_line: String) -> Option<PathBuf> {
@@ -389,54 +363,27 @@ mod tests {
389363
}
390364

391365
#[test]
392-
fn test_is_name_based_conda_env() {
393-
// Name-based environments use --name or -n
394-
assert!(is_name_based_conda_env(
395-
"# cmd: /usr/bin/conda create -n myenv python=3.12"
396-
));
397-
assert!(is_name_based_conda_env(
398-
"# cmd: /usr/bin/conda create --name myenv python=3.12"
399-
));
400-
assert!(is_name_based_conda_env(
401-
"# cmd: C:\\Users\\user\\miniconda3\\Scripts\\conda.exe create -n myenv python=3.9"
402-
));
403-
404-
// Path-based environments use --prefix or -p
405-
assert!(!is_name_based_conda_env(
406-
"# cmd: /usr/bin/conda create --prefix .conda python=3.12"
407-
));
408-
assert!(!is_name_based_conda_env(
409-
"# cmd: /usr/bin/conda create -p .conda python=3.12"
410-
));
411-
}
366+
#[cfg(unix)]
367+
fn verify_conda_env_name() {
368+
let line = "# cmd: /Users/donjayamanne/.pyenv/versions/mambaforge-22.11.1-3/lib/python3.10/site-packages/conda/__main__.py create --yes --name .conda python=3.12";
369+
assert!(is_conda_env_name_in_cmd(line.to_string(), ".conda"));
412370

413-
/// Test that when conda_dir is unknown, we return None for name.
414-
/// This is correct because name-based activation requires knowing which conda installation
415-
/// to use. Without conda_dir, activation must use the full path.
416-
/// This also fixes issue #329 where path-based envs incorrectly got a name.
417-
#[test]
418-
fn env_returns_none_name_when_conda_dir_unknown() {
419-
// Create a temp directory structure simulating a conda env
420-
let temp_dir = std::env::temp_dir().join("pet_test_conda_dir_unknown");
421-
let conda_meta_dir = temp_dir.join("myenv").join("conda-meta");
422-
std::fs::create_dir_all(&conda_meta_dir).unwrap();
371+
let mut line = "# cmd: /Users/donjayamanne/.pyenv/versions/mambaforge-22.11.1-3/lib/python3.10/site-packages/conda/__main__.py create --yes -n .conda python=3.12";
372+
assert!(is_conda_env_name_in_cmd(line.to_string(), ".conda"));
423373

424-
let env_path = temp_dir.join("myenv");
374+
line = "# cmd: /Users/donjayamanne/.pyenv/versions/mambaforge-22.11.1-3/lib/python3.10/site-packages/conda/__main__.py create --yes --name .conda python=3.12";
375+
assert!(!is_conda_env_name_in_cmd(line.to_string(), "base"));
425376

426-
// When conda_dir is None, name should be None (can't do named activation)
427-
let name = get_conda_env_name(&env_path, &env_path, &None);
428-
assert!(
429-
name.is_none(),
430-
"When conda_dir is unknown, name should be None for path-based activation, got {:?}",
431-
name
432-
);
377+
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";
378+
assert!(!is_conda_env_name_in_cmd(line.to_string(), "base"));
433379

434-
// Cleanup
435-
let _ = std::fs::remove_dir_all(&temp_dir);
380+
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";
381+
assert!(!is_conda_env_name_in_cmd(line.to_string(), ".conda"));
436382
}
437383

438384
/// Test that external environments (not under conda_dir) created with --prefix
439385
/// return None for name, so activation uses path instead of name.
386+
/// This is the fix for issue #329.
440387
#[test]
441388
fn external_path_based_env_returns_none_name() {
442389
// Create a temp directory simulating an external path-based conda env
@@ -468,15 +415,16 @@ mod tests {
468415
}
469416

470417
/// Test that external environments (not under conda_dir) created with -n
471-
/// return the name for name-based activation.
418+
/// return the name for name-based activation, but only if the folder name matches.
472419
#[test]
473420
fn external_name_based_env_returns_name() {
474421
// Create a temp directory simulating an external name-based conda env
475422
let temp_dir = std::env::temp_dir().join("pet_test_external_name_env");
476423
let conda_meta_dir = temp_dir.join("myenv").join("conda-meta");
477424
std::fs::create_dir_all(&conda_meta_dir).unwrap();
478425

479-
// Write a history file showing the env was created with -n (name-based)
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"
480428
let history_file = conda_meta_dir.join("history");
481429
std::fs::write(
482430
&history_file,
@@ -492,7 +440,7 @@ mod tests {
492440
assert_eq!(
493441
name,
494442
Some("myenv".to_string()),
495-
"Name-based external env should return the name"
443+
"Name-based external env should return the name when folder matches"
496444
);
497445

498446
// Cleanup
@@ -547,30 +495,30 @@ mod tests {
547495
let _ = std::fs::remove_dir_all(&temp_dir);
548496
}
549497

550-
/// Test that external env with history but no -n/-p flags returns None.
551-
/// Edge case: history exists but create command doesn't clearly indicate name or path based.
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.
552500
#[test]
553-
fn external_env_with_ambiguous_history_returns_none_name() {
501+
fn external_env_with_mismatched_name_returns_none() {
554502
// Create a temp directory simulating an external conda env
555-
let temp_dir = std::env::temp_dir().join("pet_test_external_ambiguous");
556-
let conda_meta_dir = temp_dir.join("myenv").join("conda-meta");
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");
557506
std::fs::create_dir_all(&conda_meta_dir).unwrap();
558507

559-
// Write a history file without -n or -p (edge case, maybe cloned env)
560508
let history_file = conda_meta_dir.join("history");
561509
std::fs::write(
562510
&history_file,
563-
"# some other history content\n# no create command here\n",
511+
"# cmd: /usr/bin/conda create -n original_name python=3.12\n",
564512
)
565513
.unwrap();
566514

567-
let env_path = temp_dir.join("myenv");
515+
let env_path = temp_dir.join("renamed_env");
568516
let conda_dir = Some(std::path::PathBuf::from("/some/other/conda"));
569517

570518
let name = get_conda_env_name(&env_path, &env_path, &conda_dir);
571519
assert!(
572520
name.is_none(),
573-
"External env with ambiguous history should return None, got {:?}",
521+
"External env with mismatched name should return None, got {:?}",
574522
name
575523
);
576524

0 commit comments

Comments
 (0)