Skip to content

Commit 91203b4

Browse files
committed
feat: Add Pipenv support with configuration options and environment management
1 parent 10e21ec commit 91203b4

File tree

7 files changed

+253
-5
lines changed

7 files changed

+253
-5
lines changed

crates/pet-core/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ pub struct Configuration {
3030
pub workspace_directories: Option<Vec<PathBuf>>,
3131
pub executables: Option<Vec<PathBuf>>,
3232
pub conda_executable: Option<PathBuf>,
33+
pub pipenv_executable: Option<PathBuf>,
3334
pub poetry_executable: Option<PathBuf>,
3435
/// Custom locations where environments can be found.
3536
/// These are different from search_paths, as these are specific directories where environments are expected.

crates/pet-core/src/manager.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use std::path::PathBuf;
77
#[derive(Serialize, Deserialize, Copy, Clone, PartialEq, Eq, Debug, Hash)]
88
pub enum EnvManagerType {
99
Conda,
10+
Pipenv,
1011
Poetry,
1112
Pyenv,
1213
}

crates/pet-pipenv/src/env_variables.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Licensed under the MIT License.
33

44
use pet_core::os_environment::Environment;
5+
use std::path::PathBuf;
56

67
#[derive(Debug, Clone)]
78
// NOTE: Do not implement Default trait, as we do not want to ever forget to set the values.
@@ -10,6 +11,12 @@ pub struct EnvVariables {
1011
#[allow(dead_code)]
1112
pub pipenv_max_depth: u16,
1213
pub pipenv_pipfile: String,
14+
/// User's home directory
15+
pub home: Option<PathBuf>,
16+
/// Maps to env var `WORKON_HOME` - custom directory for virtual environments
17+
pub workon_home: Option<PathBuf>,
18+
/// Maps to env var `PATH`
19+
pub path: Option<String>,
1320
}
1421

1522
impl EnvVariables {
@@ -22,6 +29,9 @@ impl EnvVariables {
2229
pipenv_pipfile: env
2330
.get_env_var("PIPENV_PIPFILE".to_string())
2431
.unwrap_or("Pipfile".to_string()),
32+
home: env.get_user_home(),
33+
workon_home: env.get_env_var("WORKON_HOME".to_string()).map(PathBuf::from),
34+
path: env.get_env_var("PATH".to_string()),
2535
}
2636
}
2737
}

crates/pet-pipenv/src/lib.rs

Lines changed: 130 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,25 @@
22
// Licensed under the MIT License.
33

44
use env_variables::EnvVariables;
5+
use log::trace;
6+
use manager::PipenvManager;
57
use pet_core::env::PythonEnv;
68
use pet_core::os_environment::Environment;
79
use pet_core::LocatorKind;
810
use pet_core::{
911
python_environment::{PythonEnvironment, PythonEnvironmentBuilder, PythonEnvironmentKind},
1012
reporter::Reporter,
11-
Locator,
13+
Configuration, Locator,
1214
};
1315
use pet_fs::path::norm_case;
1416
use pet_python_utils::executable::find_executables;
1517
use pet_python_utils::version;
1618
use std::path::Path;
19+
use std::sync::{Arc, RwLock};
1720
use std::{fs, path::PathBuf};
1821

1922
mod env_variables;
23+
pub mod manager;
2024

2125
fn get_pipenv_project(env: &PythonEnv) -> Option<PathBuf> {
2226
if let Some(prefix) = &env.prefix {
@@ -128,21 +132,129 @@ fn is_pipenv(env: &PythonEnv, env_vars: &EnvVariables) -> bool {
128132
}
129133
}
130134

135+
/// Get the default virtualenvs directory for pipenv
136+
/// - If WORKON_HOME is set, use that
137+
/// - Linux/macOS: ~/.local/share/virtualenvs/
138+
/// - Windows: %USERPROFILE%\.virtualenvs\
139+
fn get_virtualenvs_dir(env_vars: &EnvVariables) -> Option<PathBuf> {
140+
// First check WORKON_HOME environment variable
141+
if let Some(workon_home) = &env_vars.workon_home {
142+
if workon_home.is_dir() {
143+
return Some(workon_home.clone());
144+
}
145+
}
146+
147+
// Fall back to default locations
148+
if let Some(home) = &env_vars.home {
149+
if std::env::consts::OS == "windows" {
150+
let dir = home.join(".virtualenvs");
151+
if dir.is_dir() {
152+
return Some(dir);
153+
}
154+
} else {
155+
let dir = home.join(".local").join("share").join("virtualenvs");
156+
if dir.is_dir() {
157+
return Some(dir);
158+
}
159+
}
160+
}
161+
162+
None
163+
}
164+
165+
/// Discover pipenv environments from the virtualenvs directory
166+
fn list_environments(env_vars: &EnvVariables) -> Vec<PythonEnvironment> {
167+
let mut environments = vec![];
168+
169+
if let Some(virtualenvs_dir) = get_virtualenvs_dir(env_vars) {
170+
trace!("Searching for pipenv environments in {:?}", virtualenvs_dir);
171+
172+
if let Ok(entries) = fs::read_dir(&virtualenvs_dir) {
173+
for entry in entries.flatten() {
174+
let path = entry.path();
175+
if !path.is_dir() {
176+
continue;
177+
}
178+
179+
// Check if this directory is a valid virtualenv with a .project file
180+
let project_file = path.join(".project");
181+
if !project_file.exists() {
182+
continue;
183+
}
184+
185+
// Read the project path from .project file
186+
if let Ok(project_contents) = fs::read_to_string(&project_file) {
187+
let project_path = PathBuf::from(project_contents.trim());
188+
let project_path = norm_case(project_path);
189+
190+
// Check if the project has a Pipfile
191+
if !project_path.join(&env_vars.pipenv_pipfile).exists() {
192+
continue;
193+
}
194+
195+
// Find the Python executable in the virtualenv
196+
let bin_dir = if std::env::consts::OS == "windows" {
197+
path.join("Scripts")
198+
} else {
199+
path.join("bin")
200+
};
201+
202+
let python_exe = if std::env::consts::OS == "windows" {
203+
bin_dir.join("python.exe")
204+
} else {
205+
bin_dir.join("python")
206+
};
207+
208+
if python_exe.is_file() {
209+
let symlinks = find_executables(&bin_dir);
210+
let version = version::from_creator_for_virtual_env(&path);
211+
212+
let env = PythonEnvironmentBuilder::new(Some(
213+
PythonEnvironmentKind::Pipenv,
214+
))
215+
.executable(Some(norm_case(python_exe)))
216+
.version(version)
217+
.prefix(Some(norm_case(path.clone())))
218+
.project(Some(project_path))
219+
.symlinks(Some(symlinks))
220+
.build();
221+
222+
trace!("Found pipenv environment: {:?}", env);
223+
environments.push(env);
224+
}
225+
}
226+
}
227+
}
228+
}
229+
230+
environments
231+
}
232+
131233
pub struct PipEnv {
132234
env_vars: EnvVariables,
235+
pipenv_executable: Arc<RwLock<Option<PathBuf>>>,
133236
}
134237

135238
impl PipEnv {
136239
pub fn from(environment: &dyn Environment) -> PipEnv {
137240
PipEnv {
138241
env_vars: EnvVariables::from(environment),
242+
pipenv_executable: Arc::new(RwLock::new(None)),
139243
}
140244
}
141245
}
246+
142247
impl Locator for PipEnv {
143248
fn get_kind(&self) -> LocatorKind {
144249
LocatorKind::PipEnv
145250
}
251+
252+
fn configure(&self, config: &Configuration) {
253+
if let Some(exe) = &config.pipenv_executable {
254+
self.pipenv_executable.write().unwrap().replace(exe.clone());
255+
}
256+
}
257+
146258
fn supported_categories(&self) -> Vec<PythonEnvironmentKind> {
147259
vec![PythonEnvironmentKind::Pipenv]
148260
}
@@ -183,8 +295,19 @@ impl Locator for PipEnv {
183295
)
184296
}
185297

186-
fn find(&self, _reporter: &dyn Reporter) {
187-
//
298+
fn find(&self, reporter: &dyn Reporter) {
299+
// First, find and report the pipenv manager
300+
let pipenv_exe = self.pipenv_executable.read().unwrap().clone();
301+
if let Some(manager) = PipenvManager::find(pipenv_exe, &self.env_vars) {
302+
trace!("Found pipenv manager: {:?}", manager);
303+
reporter.report_manager(&manager.to_manager());
304+
}
305+
306+
// Then discover and report pipenv environments
307+
let environments = list_environments(&self.env_vars);
308+
for env in environments {
309+
reporter.report_environment(&env);
310+
}
188311
}
189312
}
190313

@@ -243,7 +366,11 @@ mod tests {
243366
env_vars: EnvVariables {
244367
pipenv_max_depth: 3,
245368
pipenv_pipfile: "Pipfile".to_string(),
369+
home: None,
370+
workon_home: None,
371+
path: None,
246372
},
373+
pipenv_executable: Arc::new(RwLock::new(None)),
247374
};
248375
let result = locator
249376
.try_from(&env)

crates/pet-pipenv/src/manager.rs

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
use log::trace;
5+
use pet_core::manager::{EnvManager, EnvManagerType};
6+
use std::{env, path::PathBuf};
7+
8+
use crate::env_variables::EnvVariables;
9+
10+
#[derive(Clone, PartialEq, Eq, Debug)]
11+
pub struct PipenvManager {
12+
pub executable: PathBuf,
13+
}
14+
15+
impl PipenvManager {
16+
pub fn find(executable: Option<PathBuf>, env_variables: &EnvVariables) -> Option<Self> {
17+
// If an explicit executable path is provided, check if it exists
18+
if let Some(executable) = executable {
19+
if executable.is_file() {
20+
return Some(PipenvManager { executable });
21+
}
22+
}
23+
24+
// Search in common installation locations
25+
if let Some(home) = &env_variables.home {
26+
let mut search_paths = vec![
27+
// pip install --user pipenv on Linux/macOS
28+
home.join(".local").join("bin").join("pipenv"),
29+
// pipx install pipenv
30+
home.join(".local")
31+
.join("pipx")
32+
.join("venvs")
33+
.join("pipenv")
34+
.join("bin")
35+
.join("pipenv"),
36+
];
37+
38+
if std::env::consts::OS == "windows" {
39+
// pip install --user pipenv on Windows
40+
search_paths.push(
41+
home.join("AppData")
42+
.join("Roaming")
43+
.join("Python")
44+
.join("Scripts")
45+
.join("pipenv.exe"),
46+
);
47+
// Another common Windows location
48+
search_paths.push(
49+
home.join("AppData")
50+
.join("Local")
51+
.join("Programs")
52+
.join("Python")
53+
.join("Scripts")
54+
.join("pipenv.exe"),
55+
);
56+
// pipx on Windows
57+
search_paths.push(
58+
home.join(".local")
59+
.join("pipx")
60+
.join("venvs")
61+
.join("pipenv")
62+
.join("Scripts")
63+
.join("pipenv.exe"),
64+
);
65+
}
66+
67+
for executable in search_paths {
68+
if executable.is_file() {
69+
return Some(PipenvManager { executable });
70+
}
71+
}
72+
73+
// Look for pipenv in current PATH
74+
if let Some(env_path) = &env_variables.path {
75+
for each in env::split_paths(env_path) {
76+
let executable = each.join("pipenv");
77+
if executable.is_file() {
78+
return Some(PipenvManager { executable });
79+
}
80+
if std::env::consts::OS == "windows" {
81+
let executable = each.join("pipenv.exe");
82+
if executable.is_file() {
83+
return Some(PipenvManager { executable });
84+
}
85+
}
86+
}
87+
}
88+
}
89+
90+
trace!("Pipenv exe not found");
91+
None
92+
}
93+
94+
pub fn to_manager(&self) -> EnvManager {
95+
EnvManager {
96+
executable: self.executable.clone(),
97+
version: None,
98+
tool: EnvManagerType::Pipenv,
99+
}
100+
}
101+
}

crates/pet/src/jsonrpc.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ pub struct ConfigureOptions {
9696
/// Glob patterns are supported (e.g., "/home/user/projects/*").
9797
pub workspace_directories: Option<Vec<PathBuf>>,
9898
pub conda_executable: Option<PathBuf>,
99+
pub pipenv_executable: Option<PathBuf>,
99100
pub poetry_executable: Option<PathBuf>,
100101
/// Custom locations where environments can be found. Generally global locations where virtualenvs & the like can be found.
101102
/// Workspace directories should not be included into this list.
@@ -127,6 +128,7 @@ pub fn handle_configure(context: Arc<Context>, id: u32, params: Value) {
127128
.filter(|p| p.is_dir())
128129
.collect()
129130
});
131+
cfg.pipenv_executable = configure_options.pipenv_executable;
130132
cfg.poetry_executable = configure_options.poetry_executable;
131133
// We will not support changing the cache directories once set.
132134
// No point, supporting such a use case.

docs/JSONRPC.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,13 @@ interface ConfigureParams {
5757
*/
5858
condaExecutable?: string;
5959
/**
60-
* This is the path to the conda executable.
60+
* This is the path to the pipenv executable.
61+
*
62+
* Useful for VS Code so users can configure where they have installed Pipenv.
63+
*/
64+
pipenvExecutable?: string;
65+
/**
66+
* This is the path to the poetry executable.
6167
*
6268
* Useful for VS Code so users can configure where they have installed Poetry.
6369
*/
@@ -253,7 +259,7 @@ interface Manager {
253259
/**
254260
* The type of the Manager.
255261
*/
256-
tool: "Conda" | "Poetry" | "Pyenv";
262+
tool: "Conda" | "Pipenv" | "Poetry" | "Pyenv";
257263
/**
258264
* The version of the manager/tool.
259265
* In the case of conda, this is the version of conda.

0 commit comments

Comments
 (0)