-
Notifications
You must be signed in to change notification settings - Fork 1.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add support for pipenv environments #23379
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
// Copyright (c) Microsoft Corporation. | ||
// Licensed under the MIT License. | ||
|
||
use crate::{ | ||
known, | ||
utils::{find_python_binary_path, get_version}, | ||
}; | ||
use std::{fs, path::PathBuf}; | ||
|
||
fn get_global_virtualenv_dirs(environment: &impl known::Environment) -> Vec<PathBuf> { | ||
let mut venv_dirs: Vec<PathBuf> = vec![]; | ||
|
||
if let Some(work_on_home) = environment.get_env_var("WORKON_HOME".to_string()) { | ||
if let Ok(work_on_home) = fs::canonicalize(work_on_home) { | ||
if work_on_home.exists() { | ||
venv_dirs.push(work_on_home); | ||
} | ||
} | ||
} | ||
|
||
if let Some(home) = environment.get_user_home() { | ||
let home = PathBuf::from(home); | ||
for dir in [ | ||
PathBuf::from("envs"), | ||
PathBuf::from(".direnv"), | ||
PathBuf::from(".venvs"), | ||
PathBuf::from(".virtualenvs"), | ||
PathBuf::from(".local").join("share").join("virtualenvs"), | ||
] { | ||
let venv_dir = home.join(dir); | ||
if venv_dir.exists() { | ||
venv_dirs.push(venv_dir); | ||
} | ||
} | ||
if cfg!(target_os = "linux") { | ||
let envs = PathBuf::from("Envs"); | ||
if envs.exists() { | ||
venv_dirs.push(envs); | ||
} | ||
} | ||
} | ||
|
||
venv_dirs | ||
} | ||
|
||
pub struct PythonEnv { | ||
pub path: PathBuf, | ||
pub executable: PathBuf, | ||
pub version: Option<String>, | ||
} | ||
|
||
pub fn list_global_virtualenvs(environment: &impl known::Environment) -> Vec<PythonEnv> { | ||
let mut python_envs: Vec<PythonEnv> = vec![]; | ||
for root_dir in get_global_virtualenv_dirs(environment).iter() { | ||
if let Ok(dirs) = fs::read_dir(root_dir) { | ||
for venv_dir in dirs { | ||
if let Ok(venv_dir) = venv_dir { | ||
let venv_dir = venv_dir.path(); | ||
if !venv_dir.is_dir() { | ||
continue; | ||
} | ||
if let Some(executable) = find_python_binary_path(&venv_dir) { | ||
python_envs.push(PythonEnv { | ||
path: venv_dir, | ||
executable: executable.clone(), | ||
version: get_version(executable.to_str().unwrap()), | ||
}); | ||
} | ||
} | ||
} | ||
} | ||
} | ||
|
||
python_envs | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,3 +8,4 @@ pub mod logging; | |
pub mod conda; | ||
pub mod known; | ||
pub mod pyenv; | ||
pub mod global_virtualenvs; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -66,6 +66,7 @@ pub enum PythonEnvironmentCategory { | |
Pyenv, | ||
PyenvVirtualEnv, | ||
WindowsStore, | ||
Pipenv, | ||
} | ||
|
||
#[derive(Serialize, Deserialize)] | ||
|
@@ -79,6 +80,10 @@ pub struct PythonEnvironment { | |
pub sys_prefix_path: Option<String>, | ||
pub env_manager: Option<EnvManager>, | ||
pub python_run_command: Option<Vec<String>>, | ||
/** | ||
* The project path for the Pipenv environment. | ||
*/ | ||
pub project_path: Option<String>, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @karthiknadig Lets discuss tomorrow. |
||
} | ||
|
||
impl PythonEnvironment { | ||
|
@@ -101,6 +106,30 @@ impl PythonEnvironment { | |
sys_prefix_path, | ||
env_manager, | ||
python_run_command, | ||
project_path: None, | ||
} | ||
} | ||
pub fn new_pipenv( | ||
python_executable_path: Option<String>, | ||
version: Option<String>, | ||
env_path: Option<String>, | ||
sys_prefix_path: Option<String>, | ||
env_manager: Option<EnvManager>, | ||
project_path: String, | ||
) -> Self { | ||
Self { | ||
name: None, | ||
python_executable_path: python_executable_path.clone(), | ||
category: PythonEnvironmentCategory::Pipenv, | ||
version, | ||
env_path, | ||
sys_prefix_path, | ||
env_manager, | ||
python_run_command: match python_executable_path { | ||
Some(exe) => Some(vec![exe]), | ||
None => None, | ||
}, | ||
project_path: Some(project_path), | ||
} | ||
} | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
// Copyright (c) Microsoft Corporation. | ||
// Licensed under the MIT License. | ||
|
||
use crate::global_virtualenvs::{list_global_virtualenvs, PythonEnv}; | ||
use crate::known; | ||
use crate::messaging::{MessageDispatcher, PythonEnvironment}; | ||
use std::fs; | ||
use std::path::PathBuf; | ||
|
||
fn get_project_folder(env: &PythonEnv) -> Option<String> { | ||
let project_file = env.path.join(".project"); | ||
if project_file.exists() { | ||
if let Ok(contents) = fs::read_to_string(project_file) { | ||
let project_folder = PathBuf::from(contents.trim().to_string()); | ||
if project_folder.exists() { | ||
return Some(project_folder.to_string_lossy().to_string()); | ||
} | ||
} | ||
} | ||
|
||
None | ||
} | ||
|
||
pub fn find_and_report( | ||
dispatcher: &mut impl MessageDispatcher, | ||
environment: &impl known::Environment, | ||
) -> Option<()> { | ||
for env in list_global_virtualenvs(environment).iter() { | ||
if let Some(project_path) = get_project_folder(&env) { | ||
let env_path = env | ||
.path | ||
.clone() | ||
.into_os_string() | ||
.to_string_lossy() | ||
.to_string(); | ||
let executable = env | ||
.executable | ||
.clone() | ||
.into_os_string() | ||
.to_string_lossy() | ||
.to_string(); | ||
let env = PythonEnvironment::new_pipenv( | ||
Some(executable), | ||
env.version.clone(), | ||
Some(env_path.clone()), | ||
Some(env_path), | ||
None, | ||
project_path, | ||
); | ||
|
||
dispatcher.report_environment(env); | ||
} | ||
} | ||
|
||
None | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,12 +1,60 @@ | ||
// Copyright (c) Microsoft Corporation. All rights reserved. | ||
// Licensed under the MIT License. | ||
|
||
use regex::Regex; | ||
use std::{ | ||
fs, | ||
path::{Path, PathBuf}, | ||
process::Command, | ||
}; | ||
|
||
#[derive(Debug)] | ||
pub struct PyEnvCfg { | ||
pub version: String, | ||
} | ||
|
||
pub fn parse_pyenv_cfg(path: &PathBuf) -> Option<PyEnvCfg> { | ||
let cfg = path.join("pyvenv.cfg"); | ||
if !fs::metadata(&cfg).is_ok() { | ||
return None; | ||
} | ||
|
||
let contents = fs::read_to_string(cfg).ok()?; | ||
let version_regex = Regex::new(r"^version\s*=\s*(\d+\.\d+\.\d+)$").unwrap(); | ||
let version_info_regex = Regex::new(r"^version_info\s*=\s*(\d+\.\d+\.\d+.*)$").unwrap(); | ||
for line in contents.lines() { | ||
if !line.contains("version") { | ||
continue; | ||
} | ||
if let Some(captures) = version_regex.captures(line) { | ||
if let Some(value) = captures.get(1) { | ||
return Some(PyEnvCfg { | ||
version: value.as_str().to_string(), | ||
}); | ||
} | ||
} | ||
if let Some(captures) = version_info_regex.captures(line) { | ||
if let Some(value) = captures.get(1) { | ||
return Some(PyEnvCfg { | ||
version: value.as_str().to_string(), | ||
}); | ||
} | ||
} | ||
} | ||
None | ||
} | ||
|
||
pub fn get_version(path: &str) -> Option<String> { | ||
if let Some(parent_folder) = PathBuf::from(path).parent() { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @karthiknadig i think this is correct. |
||
if let Some(pyenv_cfg) = parse_pyenv_cfg(&parent_folder.to_path_buf()) { | ||
return Some(pyenv_cfg.version); | ||
} | ||
if let Some(parent_folder) = parent_folder.parent() { | ||
if let Some(pyenv_cfg) = parse_pyenv_cfg(&parent_folder.to_path_buf()) { | ||
return Some(pyenv_cfg.version); | ||
} | ||
} | ||
} | ||
let output = Command::new(path) | ||
.arg("-c") | ||
.arg("import sys; print(sys.version)") | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -27,6 +27,6 @@ fn find_python_in_path_this() { | |
common_python::find_and_report(&mut dispatcher, &known); | ||
|
||
assert_eq!(dispatcher.messages.len(), 1); | ||
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()}); | ||
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()}); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'll update the tests to remove the JSON and just build the structs by hand, then convert to JSOn and compare |
||
assert_messages(&[expected_json], &dispatcher); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@karthiknadig lets discuss tomorrow
Right now I'm only looking for pipenv envs, but all other envs even if global will be ignored.
I think we should just list them as well,