Skip to content

Commit 1e7f925

Browse files
committed
Add support for pipenv environments (#23379)
1 parent c4e4ee6 commit 1e7f925

File tree

11 files changed

+235
-39
lines changed

11 files changed

+235
-39
lines changed
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
use crate::{
5+
known,
6+
utils::{find_python_binary_path, get_version},
7+
};
8+
use std::{fs, path::PathBuf};
9+
10+
fn get_global_virtualenv_dirs(environment: &impl known::Environment) -> Vec<PathBuf> {
11+
let mut venv_dirs: Vec<PathBuf> = vec![];
12+
13+
if let Some(work_on_home) = environment.get_env_var("WORKON_HOME".to_string()) {
14+
if let Ok(work_on_home) = fs::canonicalize(work_on_home) {
15+
if work_on_home.exists() {
16+
venv_dirs.push(work_on_home);
17+
}
18+
}
19+
}
20+
21+
if let Some(home) = environment.get_user_home() {
22+
let home = PathBuf::from(home);
23+
for dir in [
24+
PathBuf::from("envs"),
25+
PathBuf::from(".direnv"),
26+
PathBuf::from(".venvs"),
27+
PathBuf::from(".virtualenvs"),
28+
PathBuf::from(".local").join("share").join("virtualenvs"),
29+
] {
30+
let venv_dir = home.join(dir);
31+
if venv_dir.exists() {
32+
venv_dirs.push(venv_dir);
33+
}
34+
}
35+
if cfg!(target_os = "linux") {
36+
let envs = PathBuf::from("Envs");
37+
if envs.exists() {
38+
venv_dirs.push(envs);
39+
}
40+
}
41+
}
42+
43+
venv_dirs
44+
}
45+
46+
pub struct PythonEnv {
47+
pub path: PathBuf,
48+
pub executable: PathBuf,
49+
pub version: Option<String>,
50+
}
51+
52+
pub fn list_global_virtualenvs(environment: &impl known::Environment) -> Vec<PythonEnv> {
53+
let mut python_envs: Vec<PythonEnv> = vec![];
54+
for root_dir in get_global_virtualenv_dirs(environment).iter() {
55+
if let Ok(dirs) = fs::read_dir(root_dir) {
56+
for venv_dir in dirs {
57+
if let Ok(venv_dir) = venv_dir {
58+
let venv_dir = venv_dir.path();
59+
if !venv_dir.is_dir() {
60+
continue;
61+
}
62+
if let Some(executable) = find_python_binary_path(&venv_dir) {
63+
python_envs.push(PythonEnv {
64+
path: venv_dir,
65+
executable: executable.clone(),
66+
version: get_version(executable.to_str().unwrap()),
67+
});
68+
}
69+
}
70+
}
71+
}
72+
}
73+
74+
python_envs
75+
}

native_locator/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@ pub mod logging;
88
pub mod conda;
99
pub mod known;
1010
pub mod pyenv;
11+
pub mod global_virtualenvs;

native_locator/src/main.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,12 @@ use messaging::{create_dispatcher, MessageDispatcher};
88

99
mod common_python;
1010
mod conda;
11+
mod global_virtualenvs;
1112
mod homebrew;
1213
mod known;
1314
mod logging;
1415
mod messaging;
16+
mod pipenv;
1517
mod pyenv;
1618
mod utils;
1719
mod windows_python;
@@ -35,6 +37,8 @@ fn main() {
3537

3638
pyenv::find_and_report(&mut dispatcher, &environment);
3739

40+
pipenv::find_and_report(&mut dispatcher, &environment);
41+
3842
#[cfg(unix)]
3943
homebrew::find_and_report(&mut dispatcher, &environment);
4044

native_locator/src/messaging.rs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ pub enum PythonEnvironmentCategory {
6666
Pyenv,
6767
PyenvVirtualEnv,
6868
WindowsStore,
69+
Pipenv,
6970
}
7071

7172
#[derive(Serialize, Deserialize)]
@@ -79,6 +80,10 @@ pub struct PythonEnvironment {
7980
pub sys_prefix_path: Option<String>,
8081
pub env_manager: Option<EnvManager>,
8182
pub python_run_command: Option<Vec<String>>,
83+
/**
84+
* The project path for the Pipenv environment.
85+
*/
86+
pub project_path: Option<String>,
8287
}
8388

8489
impl PythonEnvironment {
@@ -101,6 +106,30 @@ impl PythonEnvironment {
101106
sys_prefix_path,
102107
env_manager,
103108
python_run_command,
109+
project_path: None,
110+
}
111+
}
112+
pub fn new_pipenv(
113+
python_executable_path: Option<String>,
114+
version: Option<String>,
115+
env_path: Option<String>,
116+
sys_prefix_path: Option<String>,
117+
env_manager: Option<EnvManager>,
118+
project_path: String,
119+
) -> Self {
120+
Self {
121+
name: None,
122+
python_executable_path: python_executable_path.clone(),
123+
category: PythonEnvironmentCategory::Pipenv,
124+
version,
125+
env_path,
126+
sys_prefix_path,
127+
env_manager,
128+
python_run_command: match python_executable_path {
129+
Some(exe) => Some(vec![exe]),
130+
None => None,
131+
},
132+
project_path: Some(project_path),
104133
}
105134
}
106135
}

native_locator/src/pipenv.rs

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
use crate::global_virtualenvs::{list_global_virtualenvs, PythonEnv};
5+
use crate::known;
6+
use crate::messaging::{MessageDispatcher, PythonEnvironment};
7+
use std::fs;
8+
use std::path::PathBuf;
9+
10+
fn get_project_folder(env: &PythonEnv) -> Option<String> {
11+
let project_file = env.path.join(".project");
12+
if project_file.exists() {
13+
if let Ok(contents) = fs::read_to_string(project_file) {
14+
let project_folder = PathBuf::from(contents.trim().to_string());
15+
if project_folder.exists() {
16+
return Some(project_folder.to_string_lossy().to_string());
17+
}
18+
}
19+
}
20+
21+
None
22+
}
23+
24+
pub fn find_and_report(
25+
dispatcher: &mut impl MessageDispatcher,
26+
environment: &impl known::Environment,
27+
) -> Option<()> {
28+
for env in list_global_virtualenvs(environment).iter() {
29+
if let Some(project_path) = get_project_folder(&env) {
30+
let env_path = env
31+
.path
32+
.clone()
33+
.into_os_string()
34+
.to_string_lossy()
35+
.to_string();
36+
let executable = env
37+
.executable
38+
.clone()
39+
.into_os_string()
40+
.to_string_lossy()
41+
.to_string();
42+
let env = PythonEnvironment::new_pipenv(
43+
Some(executable),
44+
env.version.clone(),
45+
Some(env_path.clone()),
46+
Some(env_path),
47+
None,
48+
project_path,
49+
);
50+
51+
dispatcher.report_environment(env);
52+
}
53+
}
54+
55+
None
56+
}

native_locator/src/pyenv.rs

Lines changed: 1 addition & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ use crate::known;
99
use crate::messaging;
1010
use crate::messaging::EnvManager;
1111
use crate::utils::find_python_binary_path;
12+
use crate::utils::parse_pyenv_cfg;
1213

1314
#[cfg(windows)]
1415
fn get_home_pyenv_dir(environment: &impl known::Environment) -> Option<String> {
@@ -123,31 +124,6 @@ fn report_if_pure_python_environment(
123124
Some(())
124125
}
125126

126-
#[derive(Debug)]
127-
struct PyEnvCfg {
128-
version: String,
129-
}
130-
131-
fn parse_pyenv_cfg(path: &PathBuf) -> Option<PyEnvCfg> {
132-
let cfg = path.join("pyvenv.cfg");
133-
if !fs::metadata(&cfg).is_ok() {
134-
return None;
135-
}
136-
137-
let contents = fs::read_to_string(cfg).ok()?;
138-
let version_regex = Regex::new(r"^version\s*=\s*(\d+\.\d+\.\d+)$").unwrap();
139-
for line in contents.lines() {
140-
if let Some(captures) = version_regex.captures(line) {
141-
if let Some(value) = captures.get(1) {
142-
return Some(PyEnvCfg {
143-
version: value.as_str().to_string(),
144-
});
145-
}
146-
}
147-
}
148-
None
149-
}
150-
151127
fn report_if_virtual_env_environment(
152128
executable: PathBuf,
153129
path: &PathBuf,

native_locator/src/utils.rs

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,60 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT License.
33

4+
use regex::Regex;
45
use std::{
6+
fs,
57
path::{Path, PathBuf},
68
process::Command,
79
};
810

11+
#[derive(Debug)]
12+
pub struct PyEnvCfg {
13+
pub version: String,
14+
}
15+
16+
pub fn parse_pyenv_cfg(path: &PathBuf) -> Option<PyEnvCfg> {
17+
let cfg = path.join("pyvenv.cfg");
18+
if !fs::metadata(&cfg).is_ok() {
19+
return None;
20+
}
21+
22+
let contents = fs::read_to_string(cfg).ok()?;
23+
let version_regex = Regex::new(r"^version\s*=\s*(\d+\.\d+\.\d+)$").unwrap();
24+
let version_info_regex = Regex::new(r"^version_info\s*=\s*(\d+\.\d+\.\d+.*)$").unwrap();
25+
for line in contents.lines() {
26+
if !line.contains("version") {
27+
continue;
28+
}
29+
if let Some(captures) = version_regex.captures(line) {
30+
if let Some(value) = captures.get(1) {
31+
return Some(PyEnvCfg {
32+
version: value.as_str().to_string(),
33+
});
34+
}
35+
}
36+
if let Some(captures) = version_info_regex.captures(line) {
37+
if let Some(value) = captures.get(1) {
38+
return Some(PyEnvCfg {
39+
version: value.as_str().to_string(),
40+
});
41+
}
42+
}
43+
}
44+
None
45+
}
46+
947
pub fn get_version(path: &str) -> Option<String> {
48+
if let Some(parent_folder) = PathBuf::from(path).parent() {
49+
if let Some(pyenv_cfg) = parse_pyenv_cfg(&parent_folder.to_path_buf()) {
50+
return Some(pyenv_cfg.version);
51+
}
52+
if let Some(parent_folder) = parent_folder.parent() {
53+
if let Some(pyenv_cfg) = parse_pyenv_cfg(&parent_folder.to_path_buf()) {
54+
return Some(pyenv_cfg.version);
55+
}
56+
}
57+
}
1058
let output = Command::new(path)
1159
.arg("-c")
1260
.arg("import sys; print(sys.version)")

native_locator/tests/common_python_test.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,6 @@ fn find_python_in_path_this() {
2727
common_python::find_and_report(&mut dispatcher, &known);
2828

2929
assert_eq!(dispatcher.messages.len(), 1);
30-
let expected_json = json!({"envManager":null,"name":null,"pythonExecutablePath":unix_python_exe.clone(),"category":"system","version":null,"pythonRunCommand":[unix_python_exe.clone()],"envPath":unix_python.clone(),"sysPrefixPath":unix_python.clone()});
30+
let expected_json = json!({"envManager":null,"projectPath": null, "name":null,"pythonExecutablePath":unix_python_exe.clone(),"category":"system","version":null,"pythonRunCommand":[unix_python_exe.clone()],"envPath":unix_python.clone(),"sysPrefixPath":unix_python.clone()});
3131
assert_messages(&[expected_json], &dispatcher);
3232
}

native_locator/tests/conda_test.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,8 +81,8 @@ fn finds_two_conda_envs_from_txt() {
8181
let conda_2_exe = join_test_paths(&[conda_2.clone().as_str(), "python"]);
8282

8383
let expected_conda_env = json!({ "executablePath": conda_exe.clone(), "version": null});
84-
let expected_conda_1 = json!({ "name": "one", "pythonExecutablePath": conda_1_exe.clone(), "category": "conda", "version": "10.0.1", "envPath": conda_1.clone(), "sysPrefixPath": conda_1.clone(), "envManager": null, "pythonRunCommand": [conda_exe.clone(), "run", "-n", "one", "python"]});
85-
let expected_conda_2 = json!({ "name": "two", "pythonExecutablePath": conda_2_exe.clone(), "category": "conda", "version": null, "envPath": conda_2.clone(), "sysPrefixPath": conda_2.clone(), "envManager": null,"pythonRunCommand": [conda_exe.clone(),"run","-n","two","python"]});
84+
let expected_conda_1 = json!({ "name": "one","projectPath": null, "pythonExecutablePath": conda_1_exe.clone(), "category": "conda", "version": "10.0.1", "envPath": conda_1.clone(), "sysPrefixPath": conda_1.clone(), "envManager": null, "pythonRunCommand": [conda_exe.clone(), "run", "-n", "one", "python"]});
85+
let expected_conda_2 = json!({ "name": "two", "projectPath": null, "pythonExecutablePath": conda_2_exe.clone(), "category": "conda", "version": null, "envPath": conda_2.clone(), "sysPrefixPath": conda_2.clone(), "envManager": null,"pythonRunCommand": [conda_exe.clone(),"run","-n","two","python"]});
8686
assert_messages(
8787
&[expected_conda_env, expected_conda_1, expected_conda_2],
8888
&dispatcher,

native_locator/tests/pyenv_test.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -75,11 +75,11 @@ fn find_pyenv_envs() {
7575

7676
assert_eq!(dispatcher.messages.len(), 6);
7777
let expected_manager = json!({ "executablePath": pyenv_exe.clone(), "version": null });
78-
let expected_3_9_9 = json!({"name": null,"pythonExecutablePath": join_test_paths(&[home.as_str(), ".pyenv/versions/3.9.9/bin/python"]), "pythonRunCommand": [join_test_paths(&[home.as_str(), ".pyenv/versions/3.9.9/bin/python"])], "category": "pyenv","version": "3.9.9","envPath": join_test_paths(&[home.as_str(), ".pyenv/versions/3.9.9"]),"sysPrefixPath": join_test_paths(&[home.as_str(), ".pyenv/versions/3.9.9"]), "envManager": expected_manager});
79-
let expected_virtual_env = json!({"name": "my-virtual-env", "version": "3.10.13", "category": "pyenvVirtualEnv", "envPath": join_test_paths(&[home.as_str(),".pyenv/versions/my-virtual-env"]), "pythonExecutablePath": join_test_paths(&[home.as_str(),".pyenv/versions/my-virtual-env/bin/python"]), "sysPrefixPath": join_test_paths(&[home.as_str(),".pyenv/versions/my-virtual-env"]), "pythonRunCommand": [join_test_paths(&[home.as_str(),".pyenv/versions/my-virtual-env/bin/python"])], "envManager": expected_manager});
80-
let expected_3_12_1 = json!({"name": null,"pythonExecutablePath": join_test_paths(&[home.as_str(), ".pyenv/versions/3.12.1/bin/python"]), "pythonRunCommand": [join_test_paths(&[home.as_str(), ".pyenv/versions/3.12.1/bin/python"])], "category": "pyenv","version": "3.12.1","envPath": join_test_paths(&[home.as_str(), ".pyenv/versions/3.12.1"]),"sysPrefixPath": join_test_paths(&[home.as_str(), ".pyenv/versions/3.12.1"]), "envManager": expected_manager});
81-
let expected_3_13_dev = json!({"name": null,"pythonExecutablePath": join_test_paths(&[home.as_str(), ".pyenv/versions/3.13-dev/bin/python"]), "pythonRunCommand": [join_test_paths(&[home.as_str(), ".pyenv/versions/3.13-dev/bin/python"])], "category": "pyenv","version": "3.13-dev","envPath": join_test_paths(&[home.as_str(), ".pyenv/versions/3.13-dev"]),"sysPrefixPath": join_test_paths(&[home.as_str(), ".pyenv/versions/3.13-dev"]), "envManager": expected_manager});
82-
let expected_3_12_1a3 = json!({"name": null,"pythonExecutablePath": join_test_paths(&[home.as_str(), ".pyenv/versions/3.12.1a3/bin/python"]), "pythonRunCommand": [join_test_paths(&[home.as_str(), ".pyenv/versions/3.12.1a3/bin/python"])], "category": "pyenv","version": "3.12.1a3","envPath": join_test_paths(&[home.as_str(), ".pyenv/versions/3.12.1a3"]),"sysPrefixPath": join_test_paths(&[home.as_str(), ".pyenv/versions/3.12.1a3"]), "envManager": expected_manager});
78+
let expected_3_9_9 = json!({"projectPath": null, "name": null,"pythonExecutablePath": join_test_paths(&[home.as_str(), ".pyenv/versions/3.9.9/bin/python"]), "pythonRunCommand": [join_test_paths(&[home.as_str(), ".pyenv/versions/3.9.9/bin/python"])], "category": "pyenv","version": "3.9.9","envPath": join_test_paths(&[home.as_str(), ".pyenv/versions/3.9.9"]),"sysPrefixPath": join_test_paths(&[home.as_str(), ".pyenv/versions/3.9.9"]), "envManager": expected_manager});
79+
let expected_virtual_env = json!({"projectPath": null, "name": "my-virtual-env", "version": "3.10.13", "category": "pyenvVirtualEnv", "envPath": join_test_paths(&[home.as_str(),".pyenv/versions/my-virtual-env"]), "pythonExecutablePath": join_test_paths(&[home.as_str(),".pyenv/versions/my-virtual-env/bin/python"]), "sysPrefixPath": join_test_paths(&[home.as_str(),".pyenv/versions/my-virtual-env"]), "pythonRunCommand": [join_test_paths(&[home.as_str(),".pyenv/versions/my-virtual-env/bin/python"])], "envManager": expected_manager});
80+
let expected_3_12_1 = json!({"projectPath": null, "name": null,"pythonExecutablePath": join_test_paths(&[home.as_str(), ".pyenv/versions/3.12.1/bin/python"]), "pythonRunCommand": [join_test_paths(&[home.as_str(), ".pyenv/versions/3.12.1/bin/python"])], "category": "pyenv","version": "3.12.1","envPath": join_test_paths(&[home.as_str(), ".pyenv/versions/3.12.1"]),"sysPrefixPath": join_test_paths(&[home.as_str(), ".pyenv/versions/3.12.1"]), "envManager": expected_manager});
81+
let expected_3_13_dev = json!({"projectPath": null, "name": null,"pythonExecutablePath": join_test_paths(&[home.as_str(), ".pyenv/versions/3.13-dev/bin/python"]), "pythonRunCommand": [join_test_paths(&[home.as_str(), ".pyenv/versions/3.13-dev/bin/python"])], "category": "pyenv","version": "3.13-dev","envPath": join_test_paths(&[home.as_str(), ".pyenv/versions/3.13-dev"]),"sysPrefixPath": join_test_paths(&[home.as_str(), ".pyenv/versions/3.13-dev"]), "envManager": expected_manager});
82+
let expected_3_12_1a3 = json!({"projectPath": null, "name": null,"pythonExecutablePath": join_test_paths(&[home.as_str(), ".pyenv/versions/3.12.1a3/bin/python"]), "pythonRunCommand": [join_test_paths(&[home.as_str(), ".pyenv/versions/3.12.1a3/bin/python"])], "category": "pyenv","version": "3.12.1a3","envPath": join_test_paths(&[home.as_str(), ".pyenv/versions/3.12.1a3"]),"sysPrefixPath": join_test_paths(&[home.as_str(), ".pyenv/versions/3.12.1a3"]), "envManager": expected_manager});
8383
assert_messages(
8484
&[
8585
expected_manager,

0 commit comments

Comments
 (0)