From 8ce6f035ede52141e545fa2583ee6877a8e1451c Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Wed, 4 Feb 2026 17:51:07 -0800 Subject: [PATCH 1/7] fix: improve conda environment name retrieval logic for external environments --- crates/pet-conda/src/environments.rs | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/crates/pet-conda/src/environments.rs b/crates/pet-conda/src/environments.rs index ec514c9c..1b6a8639 100644 --- a/crates/pet-conda/src/environments.rs +++ b/crates/pet-conda/src/environments.rs @@ -191,10 +191,19 @@ fn get_conda_env_name( // if the conda install folder is parent of the env folder, then we can use named activation. // E.g. conda env is = /envs/ // Then we can use `/bin/conda activate -n ` - if let Some(conda_dir) = conda_dir { - if !prefix.starts_with(conda_dir) { - name = get_conda_env_name_from_history_file(env_path, prefix); - } + // + // Check the history file when: + // 1. conda_dir is known but prefix is not under it (external environment) + // 2. conda_dir is unknown (we need to check history to determine if it's name or path based) + // This ensures path-based environments (created with --prefix) return None for name, + // so activation uses the full path instead of incorrectly trying name-based activation. + let should_check_history = match conda_dir { + Some(dir) => !prefix.starts_with(dir), + None => true, + }; + + if should_check_history { + name = get_conda_env_name_from_history_file(env_path, prefix); } name From f0637efd15da8189d913accdc978ee9c122872e7 Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Wed, 4 Feb 2026 17:54:05 -0800 Subject: [PATCH 2/7] test: add regression tests for path-based and name-based conda environments --- crates/pet-conda/src/environments.rs | 63 ++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/crates/pet-conda/src/environments.rs b/crates/pet-conda/src/environments.rs index 1b6a8639..d5cb7cd7 100644 --- a/crates/pet-conda/src/environments.rs +++ b/crates/pet-conda/src/environments.rs @@ -388,4 +388,67 @@ mod tests { 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"; assert!(!is_conda_env_name_in_cmd(line.to_string(), ".conda")); } + + /// Test that path-based conda environments (created with --prefix) return None for name + /// when conda_dir is unknown. This ensures VS Code uses path-based activation. + /// Regression test for https://github.com/microsoft/python-environment-tools/issues/329 + #[test] + fn path_based_env_returns_none_name_when_conda_dir_unknown() { + // Create a temp directory structure simulating a path-based conda env + let temp_dir = std::env::temp_dir().join("pet_test_path_based_env"); + let conda_meta_dir = temp_dir.join(".conda").join("conda-meta"); + std::fs::create_dir_all(&conda_meta_dir).unwrap(); + + // Write a history file showing the env was created with --prefix (path-based) + let history_file = conda_meta_dir.join("history"); + std::fs::write( + &history_file, + "# cmd: /usr/bin/conda create --yes --prefix .conda python=3.12\n", + ) + .unwrap(); + + let env_path = temp_dir.join(".conda"); + + // When conda_dir is None and env was created with --prefix, name should be None + let name = get_conda_env_name(&env_path, &env_path, &None); + assert!( + name.is_none(), + "Path-based env should return None for name, got {:?}", + name + ); + + // Cleanup + let _ = std::fs::remove_dir_all(&temp_dir); + } + + /// Test that name-based conda environments (created with -n) return the name + /// even when conda_dir is unknown. + #[test] + fn name_based_env_returns_name_when_conda_dir_unknown() { + // Create a temp directory structure simulating a name-based conda env + let temp_dir = std::env::temp_dir().join("pet_test_name_based_env"); + let conda_meta_dir = temp_dir.join("myenv").join("conda-meta"); + std::fs::create_dir_all(&conda_meta_dir).unwrap(); + + // Write a history file showing the env was created with -n (name-based) + let history_file = conda_meta_dir.join("history"); + std::fs::write( + &history_file, + "# cmd: /usr/bin/conda create -n myenv python=3.12\n", + ) + .unwrap(); + + let env_path = temp_dir.join("myenv"); + + // When conda_dir is None but env was created with -n, name should be returned + let name = get_conda_env_name(&env_path, &env_path, &None); + assert_eq!( + name, + Some("myenv".to_string()), + "Name-based env should return the name" + ); + + // Cleanup + let _ = std::fs::remove_dir_all(&temp_dir); + } } From 7f73e80a524b946250a811b2e22e3c80507d61af Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Wed, 4 Feb 2026 18:01:00 -0800 Subject: [PATCH 3/7] fix: enhance conda environment name retrieval logic for path-based and name-based environments --- crates/pet-conda/src/environments.rs | 113 +++++++++++++++++++-------- 1 file changed, 82 insertions(+), 31 deletions(-) diff --git a/crates/pet-conda/src/environments.rs b/crates/pet-conda/src/environments.rs index d5cb7cd7..4fc4dd01 100644 --- a/crates/pet-conda/src/environments.rs +++ b/crates/pet-conda/src/environments.rs @@ -214,25 +214,62 @@ fn get_conda_env_name( * And example is `# cmd: \Scripts\conda-script.py create -n sample`` * And example is `# cmd: conda create -n sample`` * - * This function returns the name of the conda environment. + * This function returns the name of the conda environment based on how it was created: + * - If created with --prefix/-p (path-based): returns None (activation must use full path) + * - If created with --name/-n (name-based): returns the folder name (can use named activation) + * - If we can't determine: returns the folder name (preserve existing behavior) */ fn get_conda_env_name_from_history_file(env_path: &Path, prefix: &Path) -> Option { let name = prefix .file_name() .map(|name| name.to_str().unwrap_or_default().to_string()); - if let Some(name) = name { + if let Some(ref name) = name { if let Some(line) = get_conda_creation_line_from_history(env_path) { // Sample lines // # cmd: \Scripts\conda-script.py create -n samlpe1 // # cmd: \Scripts\conda-script.py create -p // # cmd: /Users/donjayamanne/miniconda3/bin/conda create -n conda1 - if is_conda_env_name_in_cmd(line, &name) { - return Some(name); + // # cmd: /usr/bin/conda create --prefix ./prefix-envs/.conda1 python=3.12 -y + + // Check if environment was created with --prefix/-p (path-based) + // In this case, return None so activation uses full path + if is_path_based_conda_env(&line) { + return None; + } + + // Check if environment was created with --name/-n (name-based) + // In this case, return the folder name for named activation + if is_name_based_conda_env(&line) { + return Some(name.clone()); } } } - None + // If we can't determine from history, return folder name (preserve existing behavior) + name +} + +/// Check if the conda create command used --prefix or -p (path-based environment) +fn is_path_based_conda_env(cmd_line: &str) -> bool { + // Look for " -p " or " --prefix " after "create" + // We need to be careful to match the flag, not just any occurrence of -p + let lower = cmd_line.to_lowercase(); + if let Some(create_idx) = lower.find(" create ") { + let after_create = &lower[create_idx..]; + return after_create.contains(" -p ") || after_create.contains(" --prefix "); + } + false +} + +/// Check if the conda create command used --name or -n (name-based environment) +fn is_name_based_conda_env(cmd_line: &str) -> bool { + // Look for " -n " or " --name " after "create" + let lower = cmd_line.to_lowercase(); + if let Some(create_idx) = lower.find(" create ") { + let after_create = &lower[create_idx..]; + return after_create.contains(" -n ") || after_create.contains(" --name "); + } + false } fn get_conda_dir_from_cmd(cmd_line: String) -> Option { @@ -297,17 +334,6 @@ fn get_conda_dir_from_cmd(cmd_line: String) -> Option { None } -fn is_conda_env_name_in_cmd(cmd_line: String, name: &str) -> bool { - // Sample lines - // # cmd: \Scripts\conda-script.py create -n samlpe1 - // # cmd: \Scripts\conda-script.py create -p - // # cmd: /Users/donjayamanne/miniconda3/bin/conda create -n conda1 - // # cmd_line: "# cmd: /usr/bin/conda create -p ./prefix-envs/.conda1 python=3.12 -y" - // Look for "-n " in the command line - cmd_line.contains(format!("-n {name}").as_str()) - || cmd_line.contains(format!("--name {name}").as_str()) -} - pub fn get_activation_command( env: &CondaEnvironment, manager: &EnvManager, @@ -371,22 +397,47 @@ mod tests { } #[test] - #[cfg(unix)] - fn verify_conda_env_name() { - 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"; - assert!(is_conda_env_name_in_cmd(line.to_string(), ".conda")); - - 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"; - assert!(is_conda_env_name_in_cmd(line.to_string(), ".conda")); - - 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"; - assert!(!is_conda_env_name_in_cmd(line.to_string(), "base")); - - 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"; - assert!(!is_conda_env_name_in_cmd(line.to_string(), "base")); + fn test_is_path_based_conda_env() { + // Path-based environments use --prefix or -p + assert!(is_path_based_conda_env( + "# cmd: /usr/bin/conda create --yes --prefix .conda python=3.12" + )); + assert!(is_path_based_conda_env( + "# cmd: /usr/bin/conda create -p .conda python=3.12" + )); + assert!(is_path_based_conda_env( + "# cmd: C:\\Users\\user\\miniconda3\\Scripts\\conda.exe create --prefix .conda python=3.9" + )); + + // Name-based environments use --name or -n + assert!(!is_path_based_conda_env( + "# cmd: /usr/bin/conda create -n myenv python=3.12" + )); + assert!(!is_path_based_conda_env( + "# cmd: /usr/bin/conda create --name myenv python=3.12" + )); + } - 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"; - assert!(!is_conda_env_name_in_cmd(line.to_string(), ".conda")); + #[test] + fn test_is_name_based_conda_env() { + // Name-based environments use --name or -n + assert!(is_name_based_conda_env( + "# cmd: /usr/bin/conda create -n myenv python=3.12" + )); + assert!(is_name_based_conda_env( + "# cmd: /usr/bin/conda create --name myenv python=3.12" + )); + assert!(is_name_based_conda_env( + "# cmd: C:\\Users\\user\\miniconda3\\Scripts\\conda.exe create -n myenv python=3.9" + )); + + // Path-based environments use --prefix or -p + assert!(!is_name_based_conda_env( + "# cmd: /usr/bin/conda create --prefix .conda python=3.12" + )); + assert!(!is_name_based_conda_env( + "# cmd: /usr/bin/conda create -p .conda python=3.12" + )); } /// Test that path-based conda environments (created with --prefix) return None for name From 8fb7ab6a9ba72f50e29cc7c4a32c948d7b9b6cef Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Wed, 4 Feb 2026 19:20:22 -0800 Subject: [PATCH 4/7] fix: improve conda environment name retrieval for path-based and external environments --- crates/pet-conda/src/environments.rs | 91 +++++++++++++++++++--------- 1 file changed, 62 insertions(+), 29 deletions(-) diff --git a/crates/pet-conda/src/environments.rs b/crates/pet-conda/src/environments.rs index 4fc4dd01..3716e31f 100644 --- a/crates/pet-conda/src/environments.rs +++ b/crates/pet-conda/src/environments.rs @@ -192,18 +192,25 @@ fn get_conda_env_name( // E.g. conda env is = /envs/ // Then we can use `/bin/conda activate -n ` // - // Check the history file when: - // 1. conda_dir is known but prefix is not under it (external environment) - // 2. conda_dir is unknown (we need to check history to determine if it's name or path based) - // This ensures path-based environments (created with --prefix) return None for name, - // so activation uses the full path instead of incorrectly trying name-based activation. - let should_check_history = match conda_dir { - Some(dir) => !prefix.starts_with(dir), - None => true, - }; - - if should_check_history { - name = get_conda_env_name_from_history_file(env_path, prefix); + // Handle three cases: + // 1. conda_dir known, prefix is under it → name-based activation works, return folder name + // 2. conda_dir known, prefix NOT under it → external env, check history to distinguish -n vs -p + // 3. conda_dir unknown → can't do name-based activation without knowing conda installation, + // return None so activation uses full path (fixes issue #329 for path-based envs) + match conda_dir { + Some(dir) => { + if !prefix.starts_with(dir) { + // External environment - check history to see if created with -n or -p + name = get_conda_env_name_from_history_file(env_path, prefix); + } + // else: env is under conda_dir/envs/, name-based activation works + } + None => { + // No known conda installation directory + // Name-based activation requires knowing which conda to use, so return None + // This ensures path-based activation is used (correct for --prefix envs too) + name = None; + } } name @@ -440,13 +447,37 @@ mod tests { )); } - /// Test that path-based conda environments (created with --prefix) return None for name - /// when conda_dir is unknown. This ensures VS Code uses path-based activation. - /// Regression test for https://github.com/microsoft/python-environment-tools/issues/329 + /// Test that when conda_dir is unknown, we return None for name. + /// This is correct because name-based activation requires knowing which conda installation + /// to use. Without conda_dir, activation must use the full path. + /// This also fixes issue #329 where path-based envs incorrectly got a name. #[test] - fn path_based_env_returns_none_name_when_conda_dir_unknown() { - // Create a temp directory structure simulating a path-based conda env - let temp_dir = std::env::temp_dir().join("pet_test_path_based_env"); + fn env_returns_none_name_when_conda_dir_unknown() { + // Create a temp directory structure simulating a conda env + let temp_dir = std::env::temp_dir().join("pet_test_conda_dir_unknown"); + let conda_meta_dir = temp_dir.join("myenv").join("conda-meta"); + std::fs::create_dir_all(&conda_meta_dir).unwrap(); + + let env_path = temp_dir.join("myenv"); + + // When conda_dir is None, name should be None (can't do named activation) + let name = get_conda_env_name(&env_path, &env_path, &None); + assert!( + name.is_none(), + "When conda_dir is unknown, name should be None for path-based activation, got {:?}", + name + ); + + // Cleanup + let _ = std::fs::remove_dir_all(&temp_dir); + } + + /// Test that external environments (not under conda_dir) created with --prefix + /// return None for name, so activation uses path instead of name. + #[test] + fn external_path_based_env_returns_none_name() { + // Create a temp directory simulating an external path-based conda env + let temp_dir = std::env::temp_dir().join("pet_test_external_path_env"); let conda_meta_dir = temp_dir.join(".conda").join("conda-meta"); std::fs::create_dir_all(&conda_meta_dir).unwrap(); @@ -459,12 +490,13 @@ mod tests { .unwrap(); let env_path = temp_dir.join(".conda"); + // conda_dir is known but env is NOT under it (external environment) + let conda_dir = Some(std::path::PathBuf::from("/some/other/conda")); - // When conda_dir is None and env was created with --prefix, name should be None - let name = get_conda_env_name(&env_path, &env_path, &None); + let name = get_conda_env_name(&env_path, &env_path, &conda_dir); assert!( name.is_none(), - "Path-based env should return None for name, got {:?}", + "Path-based external env should return None for name, got {:?}", name ); @@ -472,12 +504,12 @@ mod tests { let _ = std::fs::remove_dir_all(&temp_dir); } - /// Test that name-based conda environments (created with -n) return the name - /// even when conda_dir is unknown. + /// Test that external environments (not under conda_dir) created with -n + /// return the name for name-based activation. #[test] - fn name_based_env_returns_name_when_conda_dir_unknown() { - // Create a temp directory structure simulating a name-based conda env - let temp_dir = std::env::temp_dir().join("pet_test_name_based_env"); + fn external_name_based_env_returns_name() { + // Create a temp directory simulating an external name-based conda env + let temp_dir = std::env::temp_dir().join("pet_test_external_name_env"); let conda_meta_dir = temp_dir.join("myenv").join("conda-meta"); std::fs::create_dir_all(&conda_meta_dir).unwrap(); @@ -490,13 +522,14 @@ mod tests { .unwrap(); let env_path = temp_dir.join("myenv"); + // conda_dir is known but env is NOT under it (external environment) + let conda_dir = Some(std::path::PathBuf::from("/some/other/conda")); - // When conda_dir is None but env was created with -n, name should be returned - let name = get_conda_env_name(&env_path, &env_path, &None); + let name = get_conda_env_name(&env_path, &env_path, &conda_dir); assert_eq!( name, Some("myenv".to_string()), - "Name-based env should return the name" + "Name-based external env should return the name" ); // Cleanup From a8f0352da3cf97ea4839de0cf33d50821f3fc1b2 Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Wed, 4 Feb 2026 19:55:38 -0800 Subject: [PATCH 5/7] fix: refine conda environment name retrieval for external environments to ensure safe activation --- crates/pet-conda/src/environments.rs | 55 +++++----------------------- 1 file changed, 9 insertions(+), 46 deletions(-) diff --git a/crates/pet-conda/src/environments.rs b/crates/pet-conda/src/environments.rs index 3716e31f..80066e89 100644 --- a/crates/pet-conda/src/environments.rs +++ b/crates/pet-conda/src/environments.rs @@ -222,9 +222,12 @@ fn get_conda_env_name( * And example is `# cmd: conda create -n sample`` * * This function returns the name of the conda environment based on how it was created: - * - If created with --prefix/-p (path-based): returns None (activation must use full path) * - If created with --name/-n (name-based): returns the folder name (can use named activation) - * - If we can't determine: returns the folder name (preserve existing behavior) + * - Otherwise: returns None (activation must use full path for safety) + * + * This is used for external environments (not under conda_dir). We only return a name + * if we can positively confirm it was created with -n/--name, because that's the only + * case where name-based activation works reliably for external environments. */ fn get_conda_env_name_from_history_file(env_path: &Path, prefix: &Path) -> Option { let name = prefix @@ -239,33 +242,15 @@ fn get_conda_env_name_from_history_file(env_path: &Path, prefix: &Path) -> Optio // # cmd: /Users/donjayamanne/miniconda3/bin/conda create -n conda1 // # cmd: /usr/bin/conda create --prefix ./prefix-envs/.conda1 python=3.12 -y - // Check if environment was created with --prefix/-p (path-based) - // In this case, return None so activation uses full path - if is_path_based_conda_env(&line) { - return None; - } - - // Check if environment was created with --name/-n (name-based) - // In this case, return the folder name for named activation + // Only return a name if we can confirm it was created with -n/--name + // This matches the original behavior where we checked for the exact name in the cmd if is_name_based_conda_env(&line) { return Some(name.clone()); } } } - // If we can't determine from history, return folder name (preserve existing behavior) - name -} - -/// Check if the conda create command used --prefix or -p (path-based environment) -fn is_path_based_conda_env(cmd_line: &str) -> bool { - // Look for " -p " or " --prefix " after "create" - // We need to be careful to match the flag, not just any occurrence of -p - let lower = cmd_line.to_lowercase(); - if let Some(create_idx) = lower.find(" create ") { - let after_create = &lower[create_idx..]; - return after_create.contains(" -p ") || after_create.contains(" --prefix "); - } - false + // If we can't confirm name-based creation, return None for safe path-based activation + None } /// Check if the conda create command used --name or -n (name-based environment) @@ -403,28 +388,6 @@ mod tests { ); } - #[test] - fn test_is_path_based_conda_env() { - // Path-based environments use --prefix or -p - assert!(is_path_based_conda_env( - "# cmd: /usr/bin/conda create --yes --prefix .conda python=3.12" - )); - assert!(is_path_based_conda_env( - "# cmd: /usr/bin/conda create -p .conda python=3.12" - )); - assert!(is_path_based_conda_env( - "# cmd: C:\\Users\\user\\miniconda3\\Scripts\\conda.exe create --prefix .conda python=3.9" - )); - - // Name-based environments use --name or -n - assert!(!is_path_based_conda_env( - "# cmd: /usr/bin/conda create -n myenv python=3.12" - )); - assert!(!is_path_based_conda_env( - "# cmd: /usr/bin/conda create --name myenv python=3.12" - )); - } - #[test] fn test_is_name_based_conda_env() { // Name-based environments use --name or -n From c508fb089116dc3491b38a03bd996a4cea0eccaf Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Wed, 4 Feb 2026 19:59:47 -0800 Subject: [PATCH 6/7] test: add unit tests for conda environment name retrieval in various scenarios --- crates/pet-conda/src/environments.rs | 79 ++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) diff --git a/crates/pet-conda/src/environments.rs b/crates/pet-conda/src/environments.rs index 80066e89..a37f386f 100644 --- a/crates/pet-conda/src/environments.rs +++ b/crates/pet-conda/src/environments.rs @@ -498,4 +498,83 @@ mod tests { // Cleanup let _ = std::fs::remove_dir_all(&temp_dir); } + + /// Test that environments under conda_dir/envs/ return the folder name. + /// This is the most common case for named conda environments. + #[test] + fn env_under_conda_dir_returns_folder_name() { + // Create a temp directory simulating conda_dir/envs/myenv structure + let temp_dir = std::env::temp_dir().join("pet_test_env_under_conda"); + let conda_dir = temp_dir.join("miniconda3"); + let env_path = conda_dir.join("envs").join("myenv"); + let conda_meta_dir = env_path.join("conda-meta"); + std::fs::create_dir_all(&conda_meta_dir).unwrap(); + + // When env is under conda_dir/envs/, name should be the folder name + let name = get_conda_env_name(&env_path, &env_path, &Some(conda_dir)); + assert_eq!( + name, + Some("myenv".to_string()), + "Env under conda_dir/envs/ should return folder name" + ); + + // Cleanup + let _ = std::fs::remove_dir_all(&temp_dir); + } + + /// Test that external env with no history file returns None for name. + /// This ensures safe path-based activation when we can't determine how it was created. + #[test] + fn external_env_without_history_returns_none_name() { + // Create a temp directory simulating an external conda env without history + let temp_dir = std::env::temp_dir().join("pet_test_external_no_history"); + let conda_meta_dir = temp_dir.join("myenv").join("conda-meta"); + std::fs::create_dir_all(&conda_meta_dir).unwrap(); + // Note: NOT creating a history file + + let env_path = temp_dir.join("myenv"); + // conda_dir is known but env is NOT under it (external environment) + let conda_dir = Some(std::path::PathBuf::from("/some/other/conda")); + + let name = get_conda_env_name(&env_path, &env_path, &conda_dir); + assert!( + name.is_none(), + "External env without history should return None for safe path-based activation, got {:?}", + name + ); + + // Cleanup + let _ = std::fs::remove_dir_all(&temp_dir); + } + + /// Test that external env with history but no -n/-p flags returns None. + /// Edge case: history exists but create command doesn't clearly indicate name or path based. + #[test] + fn external_env_with_ambiguous_history_returns_none_name() { + // Create a temp directory simulating an external conda env + let temp_dir = std::env::temp_dir().join("pet_test_external_ambiguous"); + let conda_meta_dir = temp_dir.join("myenv").join("conda-meta"); + std::fs::create_dir_all(&conda_meta_dir).unwrap(); + + // Write a history file without -n or -p (edge case, maybe cloned env) + let history_file = conda_meta_dir.join("history"); + std::fs::write( + &history_file, + "# some other history content\n# no create command here\n", + ) + .unwrap(); + + let env_path = temp_dir.join("myenv"); + let conda_dir = Some(std::path::PathBuf::from("/some/other/conda")); + + let name = get_conda_env_name(&env_path, &env_path, &conda_dir); + assert!( + name.is_none(), + "External env with ambiguous history should return None, got {:?}", + name + ); + + // Cleanup + let _ = std::fs::remove_dir_all(&temp_dir); + } } From 7a5701fae71b2e91ad3bcd94031c4617b00631c5 Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Thu, 5 Feb 2026 07:56:41 -0800 Subject: [PATCH 7/7] fix: refine conda environment name retrieval logic for external environments to ensure accurate activation --- crates/pet-conda/src/environments.rs | 136 +++++++++------------------ 1 file changed, 42 insertions(+), 94 deletions(-) diff --git a/crates/pet-conda/src/environments.rs b/crates/pet-conda/src/environments.rs index a37f386f..6ec9f9e4 100644 --- a/crates/pet-conda/src/environments.rs +++ b/crates/pet-conda/src/environments.rs @@ -192,24 +192,9 @@ fn get_conda_env_name( // E.g. conda env is = /envs/ // Then we can use `/bin/conda activate -n ` // - // Handle three cases: - // 1. conda_dir known, prefix is under it → name-based activation works, return folder name - // 2. conda_dir known, prefix NOT under it → external env, check history to distinguish -n vs -p - // 3. conda_dir unknown → can't do name-based activation without knowing conda installation, - // return None so activation uses full path (fixes issue #329 for path-based envs) - match conda_dir { - Some(dir) => { - if !prefix.starts_with(dir) { - // External environment - check history to see if created with -n or -p - name = get_conda_env_name_from_history_file(env_path, prefix); - } - // else: env is under conda_dir/envs/, name-based activation works - } - None => { - // No known conda installation directory - // Name-based activation requires knowing which conda to use, so return None - // This ensures path-based activation is used (correct for --prefix envs too) - name = None; + if let Some(conda_dir) = conda_dir { + if !prefix.starts_with(conda_dir) { + name = get_conda_env_name_from_history_file(env_path, prefix); } } @@ -221,47 +206,36 @@ fn get_conda_env_name( * And example is `# cmd: \Scripts\conda-script.py create -n sample`` * And example is `# cmd: conda create -n sample`` * - * This function returns the name of the conda environment based on how it was created: - * - If created with --name/-n (name-based): returns the folder name (can use named activation) - * - Otherwise: returns None (activation must use full path for safety) - * - * This is used for external environments (not under conda_dir). We only return a name - * if we can positively confirm it was created with -n/--name, because that's the only - * case where name-based activation works reliably for external environments. + * This function returns the name of the conda environment. */ fn get_conda_env_name_from_history_file(env_path: &Path, prefix: &Path) -> Option { let name = prefix .file_name() .map(|name| name.to_str().unwrap_or_default().to_string()); - if let Some(ref name) = name { + if let Some(name) = name { if let Some(line) = get_conda_creation_line_from_history(env_path) { // Sample lines // # cmd: \Scripts\conda-script.py create -n samlpe1 // # cmd: \Scripts\conda-script.py create -p // # cmd: /Users/donjayamanne/miniconda3/bin/conda create -n conda1 - // # cmd: /usr/bin/conda create --prefix ./prefix-envs/.conda1 python=3.12 -y - - // Only return a name if we can confirm it was created with -n/--name - // This matches the original behavior where we checked for the exact name in the cmd - if is_name_based_conda_env(&line) { - return Some(name.clone()); + if is_conda_env_name_in_cmd(line, &name) { + return Some(name); } } } - // If we can't confirm name-based creation, return None for safe path-based activation None } -/// Check if the conda create command used --name or -n (name-based environment) -fn is_name_based_conda_env(cmd_line: &str) -> bool { - // Look for " -n " or " --name " after "create" - let lower = cmd_line.to_lowercase(); - if let Some(create_idx) = lower.find(" create ") { - let after_create = &lower[create_idx..]; - return after_create.contains(" -n ") || after_create.contains(" --name "); - } - false +fn is_conda_env_name_in_cmd(cmd_line: String, name: &str) -> bool { + // Sample lines + // # cmd: \Scripts\conda-script.py create -n samlpe1 + // # cmd: \Scripts\conda-script.py create -p + // # cmd: /Users/donjayamanne/miniconda3/bin/conda create -n conda1 + // # cmd_line: "# cmd: /usr/bin/conda create -p ./prefix-envs/.conda1 python=3.12 -y" + // Look for "-n " in the command line + cmd_line.contains(format!("-n {name}").as_str()) + || cmd_line.contains(format!("--name {name}").as_str()) } fn get_conda_dir_from_cmd(cmd_line: String) -> Option { @@ -389,54 +363,27 @@ mod tests { } #[test] - fn test_is_name_based_conda_env() { - // Name-based environments use --name or -n - assert!(is_name_based_conda_env( - "# cmd: /usr/bin/conda create -n myenv python=3.12" - )); - assert!(is_name_based_conda_env( - "# cmd: /usr/bin/conda create --name myenv python=3.12" - )); - assert!(is_name_based_conda_env( - "# cmd: C:\\Users\\user\\miniconda3\\Scripts\\conda.exe create -n myenv python=3.9" - )); - - // Path-based environments use --prefix or -p - assert!(!is_name_based_conda_env( - "# cmd: /usr/bin/conda create --prefix .conda python=3.12" - )); - assert!(!is_name_based_conda_env( - "# cmd: /usr/bin/conda create -p .conda python=3.12" - )); - } + #[cfg(unix)] + fn verify_conda_env_name() { + 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"; + assert!(is_conda_env_name_in_cmd(line.to_string(), ".conda")); - /// Test that when conda_dir is unknown, we return None for name. - /// This is correct because name-based activation requires knowing which conda installation - /// to use. Without conda_dir, activation must use the full path. - /// This also fixes issue #329 where path-based envs incorrectly got a name. - #[test] - fn env_returns_none_name_when_conda_dir_unknown() { - // Create a temp directory structure simulating a conda env - let temp_dir = std::env::temp_dir().join("pet_test_conda_dir_unknown"); - let conda_meta_dir = temp_dir.join("myenv").join("conda-meta"); - std::fs::create_dir_all(&conda_meta_dir).unwrap(); + 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"; + assert!(is_conda_env_name_in_cmd(line.to_string(), ".conda")); - let env_path = temp_dir.join("myenv"); + 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"; + assert!(!is_conda_env_name_in_cmd(line.to_string(), "base")); - // When conda_dir is None, name should be None (can't do named activation) - let name = get_conda_env_name(&env_path, &env_path, &None); - assert!( - name.is_none(), - "When conda_dir is unknown, name should be None for path-based activation, got {:?}", - name - ); + 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"; + assert!(!is_conda_env_name_in_cmd(line.to_string(), "base")); - // Cleanup - let _ = std::fs::remove_dir_all(&temp_dir); + 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"; + assert!(!is_conda_env_name_in_cmd(line.to_string(), ".conda")); } /// Test that external environments (not under conda_dir) created with --prefix /// return None for name, so activation uses path instead of name. + /// This is the fix for issue #329. #[test] fn external_path_based_env_returns_none_name() { // Create a temp directory simulating an external path-based conda env @@ -468,7 +415,7 @@ mod tests { } /// Test that external environments (not under conda_dir) created with -n - /// return the name for name-based activation. + /// return the name for name-based activation, but only if the folder name matches. #[test] fn external_name_based_env_returns_name() { // Create a temp directory simulating an external name-based conda env @@ -476,7 +423,8 @@ mod tests { let conda_meta_dir = temp_dir.join("myenv").join("conda-meta"); std::fs::create_dir_all(&conda_meta_dir).unwrap(); - // Write a history file showing the env was created with -n (name-based) + // Write a history file showing the env was created with -n myenv (name-based) + // Note: the folder name "myenv" matches the -n argument "myenv" let history_file = conda_meta_dir.join("history"); std::fs::write( &history_file, @@ -492,7 +440,7 @@ mod tests { assert_eq!( name, Some("myenv".to_string()), - "Name-based external env should return the name" + "Name-based external env should return the name when folder matches" ); // Cleanup @@ -547,30 +495,30 @@ mod tests { let _ = std::fs::remove_dir_all(&temp_dir); } - /// Test that external env with history but no -n/-p flags returns None. - /// Edge case: history exists but create command doesn't clearly indicate name or path based. + /// Test that external env with history but folder name doesn't match -n argument returns None. + /// This prevents wrong activation when env was moved/renamed after creation. #[test] - fn external_env_with_ambiguous_history_returns_none_name() { + fn external_env_with_mismatched_name_returns_none() { // Create a temp directory simulating an external conda env - let temp_dir = std::env::temp_dir().join("pet_test_external_ambiguous"); - let conda_meta_dir = temp_dir.join("myenv").join("conda-meta"); + let temp_dir = std::env::temp_dir().join("pet_test_external_mismatch"); + // Folder is named "renamed_env" but was created with -n "original_name" + let conda_meta_dir = temp_dir.join("renamed_env").join("conda-meta"); std::fs::create_dir_all(&conda_meta_dir).unwrap(); - // Write a history file without -n or -p (edge case, maybe cloned env) let history_file = conda_meta_dir.join("history"); std::fs::write( &history_file, - "# some other history content\n# no create command here\n", + "# cmd: /usr/bin/conda create -n original_name python=3.12\n", ) .unwrap(); - let env_path = temp_dir.join("myenv"); + let env_path = temp_dir.join("renamed_env"); let conda_dir = Some(std::path::PathBuf::from("/some/other/conda")); let name = get_conda_env_name(&env_path, &env_path, &conda_dir); assert!( name.is_none(), - "External env with ambiguous history should return None, got {:?}", + "External env with mismatched name should return None, got {:?}", name );