Skip to content
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

Merged
merged 4 commits into from
May 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 75 additions & 0 deletions native_locator/src/global_virtualenvs.rs
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"),
Copy link
Author

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,

] {
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
}
1 change: 1 addition & 0 deletions native_locator/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ pub mod logging;
pub mod conda;
pub mod known;
pub mod pyenv;
pub mod global_virtualenvs;
4 changes: 4 additions & 0 deletions native_locator/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@ use messaging::{create_dispatcher, MessageDispatcher};

mod common_python;
mod conda;
mod global_virtualenvs;
mod homebrew;
mod known;
mod logging;
mod messaging;
mod pipenv;
mod pyenv;
mod utils;
mod windows_python;
Expand All @@ -35,6 +37,8 @@ fn main() {

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

pipenv::find_and_report(&mut dispatcher, &environment);

#[cfg(unix)]
homebrew::find_and_report(&mut dispatcher, &environment);

Expand Down
29 changes: 29 additions & 0 deletions native_locator/src/messaging.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ pub enum PythonEnvironmentCategory {
Pyenv,
PyenvVirtualEnv,
WindowsStore,
Pipenv,
}

#[derive(Serialize, Deserialize)]
Expand All @@ -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>,
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@karthiknadig Lets discuss tomorrow.

}

impl PythonEnvironment {
Expand All @@ -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),
}
}
}
Expand Down
56 changes: 56 additions & 0 deletions native_locator/src/pipenv.rs
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
}
26 changes: 1 addition & 25 deletions native_locator/src/pyenv.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use crate::known;
use crate::messaging;
use crate::messaging::EnvManager;
use crate::utils::find_python_binary_path;
use crate::utils::parse_pyenv_cfg;

#[cfg(windows)]
fn get_home_pyenv_dir(environment: &impl known::Environment) -> Option<String> {
Expand Down Expand Up @@ -123,31 +124,6 @@ fn report_if_pure_python_environment(
Some(())
}

#[derive(Debug)]
struct PyEnvCfg {
version: String,
}

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();
for line in contents.lines() {
if let Some(captures) = version_regex.captures(line) {
if let Some(value) = captures.get(1) {
return Some(PyEnvCfg {
version: value.as_str().to_string(),
});
}
}
}
None
}

fn report_if_virtual_env_environment(
executable: PathBuf,
path: &PathBuf,
Expand Down
48 changes: 48 additions & 0 deletions native_locator/src/utils.rs
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() {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@karthiknadig i think this is correct.
E.g. virtual envs and pipenvs have their version numbers in the pyvenv.cfg file, thus we can try to avoid the spawn here.

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)")
Expand Down
2 changes: 1 addition & 1 deletion native_locator/tests/common_python_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()});
Copy link
Author

Choose a reason for hiding this comment

The 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
handcrafting JSON is messy, structs will provide better language features.
@karthiknadig FYI only, will clean this in a separate PR

assert_messages(&[expected_json], &dispatcher);
}
4 changes: 2 additions & 2 deletions native_locator/tests/conda_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,8 @@ fn finds_two_conda_envs_from_txt() {
let conda_2_exe = join_test_paths(&[conda_2.clone().as_str(), "python"]);

let expected_conda_env = json!({ "executablePath": conda_exe.clone(), "version": null});
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"]});
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"]});
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"]});
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"]});
assert_messages(
&[expected_conda_env, expected_conda_1, expected_conda_2],
&dispatcher,
Expand Down
10 changes: 5 additions & 5 deletions native_locator/tests/pyenv_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,11 +75,11 @@ fn find_pyenv_envs() {

assert_eq!(dispatcher.messages.len(), 6);
let expected_manager = json!({ "executablePath": pyenv_exe.clone(), "version": null });
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});
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});
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});
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});
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});
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});
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});
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});
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});
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});
assert_messages(
&[
expected_manager,
Expand Down
Loading
Loading