Skip to content

Commit 52b4e23

Browse files
authored
Get conda env name from history file (#217)
Fixes #212
1 parent 9636b82 commit 52b4e23

File tree

2 files changed

+113
-53
lines changed

2 files changed

+113
-53
lines changed

crates/pet-conda/src/environments.rs

Lines changed: 87 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -32,23 +32,8 @@ impl CondaEnvironment {
3232

3333
pub fn to_python_environment(&self, conda_manager: Option<EnvManager>) -> PythonEnvironment {
3434
#[allow(unused_assignments)]
35-
let mut name: Option<String> = None;
36-
if is_conda_install(&self.prefix) {
37-
name = Some("base".to_string());
38-
} else {
39-
name = self
40-
.prefix
41-
.file_name()
42-
.map(|name| name.to_str().unwrap_or_default().to_string());
43-
}
44-
// if the conda install folder is parent of the env folder, then we can use named activation.
45-
// E.g. conda env is = <conda install>/envs/<env name>
46-
// Then we can use `<conda install>/bin/conda activate -n <env name>`
47-
if let Some(conda_dir) = &self.conda_dir {
48-
if !self.prefix.starts_with(conda_dir) {
49-
name = None;
50-
}
51-
}
35+
let name = get_conda_env_name(&self.prefix, &self.prefix, &self.conda_dir);
36+
5237
// This is a root env.
5338
let builder = PythonEnvironmentBuilder::new(Some(PythonEnvironmentKind::Conda))
5439
.executable(self.executable.clone())
@@ -143,29 +128,93 @@ pub fn get_conda_installation_used_to_create_conda_env(env_path: &Path) -> Optio
143128

144129
// First look for the conda-meta/history file in the environment folder.
145130
// This could be a conda envirment (not root) but has `conda` installed in it.
131+
if let Some(line) = get_conda_creation_line_from_history(env_path) {
132+
// Sample lines
133+
// # cmd: <conda install directory>\Scripts\conda-script.py create -n samlpe1
134+
// # cmd: <conda install directory>\Scripts\conda-script.py create -p <full path>
135+
// # cmd: /Users/donjayamanne/miniconda3/bin/conda create -n conda1
136+
if let Some(conda_dir) = get_conda_dir_from_cmd(line) {
137+
if is_conda_install(&conda_dir) {
138+
return Some(conda_dir);
139+
}
140+
}
141+
}
142+
143+
// Possible the env_path is the root conda install folder.
144+
if is_conda_install(env_path) {
145+
Some(env_path.to_path_buf())
146+
} else {
147+
None
148+
}
149+
}
150+
151+
pub fn get_conda_creation_line_from_history(env_path: &Path) -> Option<String> {
146152
let conda_meta_history = env_path.join("conda-meta").join("history");
147153
if let Ok(reader) = std::fs::read_to_string(conda_meta_history.clone()) {
148154
if let Some(line) = reader.lines().map(|l| l.trim()).find(|l| {
149155
l.to_lowercase().starts_with("# cmd:") && l.to_lowercase().contains(" create -")
150156
}) {
157+
trace!(
158+
"Conda creation line for {:?} is from history file is {:?}",
159+
env_path,
160+
line
161+
);
162+
return Some(line.into());
163+
}
164+
}
165+
166+
None
167+
}
168+
169+
fn get_conda_env_name(
170+
env_path: &Path,
171+
prefix: &Path,
172+
conda_dir: &Option<PathBuf>,
173+
) -> Option<String> {
174+
let mut name: Option<String>;
175+
if is_conda_install(prefix) {
176+
name = Some("base".to_string());
177+
} else {
178+
name = prefix
179+
.file_name()
180+
.map(|name| name.to_str().unwrap_or_default().to_string());
181+
}
182+
// if the conda install folder is parent of the env folder, then we can use named activation.
183+
// E.g. conda env is = <conda install>/envs/<env name>
184+
// Then we can use `<conda install>/bin/conda activate -n <env name>`
185+
if let Some(conda_dir) = conda_dir {
186+
if !prefix.starts_with(conda_dir) {
187+
name = get_conda_env_name_from_history_file(env_path, prefix);
188+
}
189+
}
190+
191+
name
192+
}
193+
194+
/**
195+
* The conda-meta/history file in conda environments contain the command used to create the conda environment.
196+
* And example is `# cmd: <conda install directory>\Scripts\conda-script.py create -n sample``
197+
* And example is `# cmd: conda create -n sample``
198+
*
199+
* This function returns the name of the conda environment.
200+
*/
201+
fn get_conda_env_name_from_history_file(env_path: &Path, prefix: &Path) -> Option<String> {
202+
let name = prefix
203+
.file_name()
204+
.map(|name| name.to_str().unwrap_or_default().to_string());
205+
206+
if let Some(name) = name {
207+
if let Some(line) = get_conda_creation_line_from_history(env_path) {
151208
// Sample lines
152209
// # cmd: <conda install directory>\Scripts\conda-script.py create -n samlpe1
153210
// # cmd: <conda install directory>\Scripts\conda-script.py create -p <full path>
154211
// # cmd: /Users/donjayamanne/miniconda3/bin/conda create -n conda1
155-
if let Some(conda_dir) = get_conda_dir_from_cmd(line.into()) {
156-
if is_conda_install(&conda_dir) {
157-
return Some(conda_dir);
158-
}
212+
if is_conda_env_name_in_cmd(line, &name) {
213+
return Some(name);
159214
}
160215
}
161216
}
162-
163-
// Possible the env_path is the root conda install folder.
164-
if is_conda_install(env_path) {
165-
Some(env_path.to_path_buf())
166-
} else {
167-
None
168-
}
217+
None
169218
}
170219

171220
fn get_conda_dir_from_cmd(cmd_line: String) -> Option<PathBuf> {
@@ -229,6 +278,16 @@ fn get_conda_dir_from_cmd(cmd_line: String) -> Option<PathBuf> {
229278
None
230279
}
231280

281+
fn is_conda_env_name_in_cmd(cmd_line: String, name: &str) -> bool {
282+
// Sample lines
283+
// # cmd: <conda install directory>\Scripts\conda-script.py create -n samlpe1
284+
// # cmd: <conda install directory>\Scripts\conda-script.py create -p <full path>
285+
// # cmd: /Users/donjayamanne/miniconda3/bin/conda create -n conda1
286+
// # cmd_line: "# cmd: /usr/bin/conda create -p ./prefix-envs/.conda1 python=3.12 -y"
287+
// Look for "-n <name>" in the command line
288+
cmd_line.contains(format!("-n {:?}", name).as_str())
289+
}
290+
232291
pub fn get_activation_command(
233292
env: &CondaEnvironment,
234293
manager: &EnvManager,

crates/pet/tests/ci_test.rs

Lines changed: 26 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ fn verify_validity_of_discovered_envs() {
9292
);
9393

9494
let environments = reporter.environments.lock().unwrap().clone();
95-
let mut threads = vec![];
95+
// let mut threads = vec![];
9696
for environment in environments {
9797
if environment.executable.is_none() {
9898
continue;
@@ -101,32 +101,32 @@ fn verify_validity_of_discovered_envs() {
101101
// For each enviornment verify the accuracy of sys.prefix and sys.version
102102
// by spawning the Python executable
103103
let e = environment.clone();
104-
threads.push(thread::spawn(move || {
105-
verify_validity_of_interpreter_info(e);
106-
}));
104+
// threads.push(thread::spawn(move || {
105+
verify_validity_of_interpreter_info(e);
106+
// }));
107107
let e = environment.clone();
108-
threads.push(thread::spawn(move || {
109-
for exe in &e.clone().symlinks.unwrap_or_default() {
110-
// Verification 2:
111-
// For each enviornment, given the executable verify we can get the exact same information
112-
// Using the `locator.try_from` method (without having to find all environments).
113-
// I.e. we should be able to get the same information using only the executable.
114-
//
115-
// Verification 3:
116-
// Similarly for each environment use one of the known symlinks and verify we can get the same information.
117-
verify_we_can_get_same_env_info_using_from_with_exe(exe, environment.clone());
118-
// Verification 4 & 5:
119-
// Similarly for each environment use resolve method and verify we get the exact same information.
120-
verify_we_can_get_same_env_info_using_resolve_with_exe(exe, environment.clone());
121-
// Verification 6:
122-
// Given the exe, verify we can use the `find` method in JSON RPC to get the details, without spawning Python.
123-
verify_we_can_get_same_env_info_using_find_with_exe(exe, environment.clone());
124-
}
125-
}));
126-
}
127-
for thread in threads {
128-
thread.join().unwrap();
108+
// threads.push(thread::spawn(move || {
109+
for exe in &e.clone().symlinks.unwrap_or_default() {
110+
// Verification 2:
111+
// For each enviornment, given the executable verify we can get the exact same information
112+
// Using the `locator.try_from` method (without having to find all environments).
113+
// I.e. we should be able to get the same information using only the executable.
114+
//
115+
// Verification 3:
116+
// Similarly for each environment use one of the known symlinks and verify we can get the same information.
117+
verify_we_can_get_same_env_info_using_from_with_exe(exe, environment.clone());
118+
// Verification 4 & 5:
119+
// Similarly for each environment use resolve method and verify we get the exact same information.
120+
verify_we_can_get_same_env_info_using_resolve_with_exe(exe, environment.clone());
121+
// Verification 6:
122+
// Given the exe, verify we can use the `find` method in JSON RPC to get the details, without spawning Python.
123+
verify_we_can_get_same_env_info_using_find_with_exe(exe, environment.clone());
124+
}
125+
// }));
129126
}
127+
// for thread in threads {
128+
// thread.join().unwrap();
129+
// }
130130
}
131131

132132
#[cfg(unix)]
@@ -767,6 +767,7 @@ fn get_python_interpreter_info(cli: &Vec<String>) -> InterpreterInfo {
767767
.output()
768768
.expect(format!("Failed to execute command {cli:?}").as_str());
769769
let output = String::from_utf8(output.stdout).unwrap();
770+
trace!("Get Interpreter Info: {:?} => {:?}", cli, output);
770771
let output = output
771772
.split_once("503bebe7-c838-4cea-a1bc-0f2963bcb657")
772773
.unwrap()

0 commit comments

Comments
 (0)