Skip to content

Commit e15723c

Browse files
authored
Add Pipenv support with configuration options and environment management (#323)
Fixes #322 Introduce support for Pipenv, including configuration options for the executable path and environment management. This enhancement allows users to specify Pipenv as an environment manager alongside existing options. Additional functionality includes searching for the Pipenv executable in common installation locations.
1 parent eaef718 commit e15723c

File tree

7 files changed

+255
-8
lines changed

7 files changed

+255
-8
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: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,13 @@ pub struct EnvVariables {
1111
#[allow(dead_code)]
1212
pub pipenv_max_depth: u16,
1313
pub pipenv_pipfile: String,
14+
/// User's home directory
1415
pub home: Option<PathBuf>,
15-
pub xdg_data_home: Option<String>,
16+
/// Maps to env var `WORKON_HOME` - custom directory for virtual environments
1617
pub workon_home: Option<PathBuf>,
18+
pub xdg_data_home: Option<String>,
19+
/// Maps to env var `PATH`
20+
pub path: Option<String>,
1721
}
1822

1923
impl EnvVariables {
@@ -31,6 +35,7 @@ impl EnvVariables {
3135
workon_home: env
3236
.get_env_var("WORKON_HOME".to_string())
3337
.map(PathBuf::from),
38+
path: env.get_env_var("PATH".to_string()),
3439
}
3540
}
3641
}

crates/pet-pipenv/src/lib.rs

Lines changed: 136 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,24 @@
33

44
use env_variables::EnvVariables;
55
use log::trace;
6+
use manager::PipenvManager;
67
use pet_core::env::PythonEnv;
78
use pet_core::os_environment::Environment;
89
use pet_core::LocatorKind;
910
use pet_core::{
1011
python_environment::{PythonEnvironment, PythonEnvironmentBuilder, PythonEnvironmentKind},
1112
reporter::Reporter,
12-
Locator,
13+
Configuration, Locator,
1314
};
1415
use pet_fs::path::norm_case;
1516
use pet_python_utils::executable::find_executables;
1617
use pet_python_utils::version;
1718
use std::path::Path;
19+
use std::sync::{Arc, RwLock};
1820
use std::{fs, path::PathBuf};
1921

2022
mod env_variables;
23+
pub mod manager;
2124

2225
/// Returns the list of directories where pipenv stores centralized virtual environments.
2326
/// These are the known locations where pipenv creates virtualenvs when not using in-project mode.
@@ -272,21 +275,128 @@ fn is_pipenv(env: &PythonEnv, env_vars: &EnvVariables) -> bool {
272275
false
273276
}
274277

278+
/// Get the default virtualenvs directory for pipenv
279+
/// - If WORKON_HOME is set, use that
280+
/// - Linux/macOS: ~/.local/share/virtualenvs/
281+
/// - Windows: %USERPROFILE%\.virtualenvs\
282+
fn get_virtualenvs_dir(env_vars: &EnvVariables) -> Option<PathBuf> {
283+
// First check WORKON_HOME environment variable
284+
if let Some(workon_home) = &env_vars.workon_home {
285+
if workon_home.is_dir() {
286+
return Some(workon_home.clone());
287+
}
288+
}
289+
290+
// Fall back to default locations
291+
if let Some(home) = &env_vars.home {
292+
if std::env::consts::OS == "windows" {
293+
let dir = home.join(".virtualenvs");
294+
if dir.is_dir() {
295+
return Some(dir);
296+
}
297+
} else {
298+
let dir = home.join(".local").join("share").join("virtualenvs");
299+
if dir.is_dir() {
300+
return Some(dir);
301+
}
302+
}
303+
}
304+
305+
None
306+
}
307+
308+
/// Discover pipenv environments from the virtualenvs directory
309+
fn list_environments(env_vars: &EnvVariables) -> Vec<PythonEnvironment> {
310+
let mut environments = vec![];
311+
312+
if let Some(virtualenvs_dir) = get_virtualenvs_dir(env_vars) {
313+
trace!("Searching for pipenv environments in {:?}", virtualenvs_dir);
314+
315+
if let Ok(entries) = fs::read_dir(&virtualenvs_dir) {
316+
for entry in entries.flatten() {
317+
let path = entry.path();
318+
if !path.is_dir() {
319+
continue;
320+
}
321+
322+
// Check if this directory is a valid virtualenv with a .project file
323+
let project_file = path.join(".project");
324+
if !project_file.exists() {
325+
continue;
326+
}
327+
328+
// Read the project path from .project file
329+
if let Ok(project_contents) = fs::read_to_string(&project_file) {
330+
let project_path = PathBuf::from(project_contents.trim());
331+
let project_path = norm_case(project_path);
332+
333+
// Check if the project has a Pipfile
334+
if !project_path.join(&env_vars.pipenv_pipfile).exists() {
335+
continue;
336+
}
337+
338+
// Find the Python executable in the virtualenv
339+
let bin_dir = if std::env::consts::OS == "windows" {
340+
path.join("Scripts")
341+
} else {
342+
path.join("bin")
343+
};
344+
345+
let python_exe = if std::env::consts::OS == "windows" {
346+
bin_dir.join("python.exe")
347+
} else {
348+
bin_dir.join("python")
349+
};
350+
351+
if python_exe.is_file() {
352+
let symlinks = find_executables(&bin_dir);
353+
let version = version::from_creator_for_virtual_env(&path);
354+
355+
let env =
356+
PythonEnvironmentBuilder::new(Some(PythonEnvironmentKind::Pipenv))
357+
.executable(Some(norm_case(python_exe)))
358+
.version(version)
359+
.prefix(Some(norm_case(path.clone())))
360+
.project(Some(project_path))
361+
.symlinks(Some(symlinks))
362+
.build();
363+
364+
trace!("Found pipenv environment: {:?}", env);
365+
environments.push(env);
366+
}
367+
}
368+
}
369+
}
370+
}
371+
372+
environments
373+
}
374+
275375
pub struct PipEnv {
276376
env_vars: EnvVariables,
377+
pipenv_executable: Arc<RwLock<Option<PathBuf>>>,
277378
}
278379

279380
impl PipEnv {
280381
pub fn from(environment: &dyn Environment) -> PipEnv {
281382
PipEnv {
282383
env_vars: EnvVariables::from(environment),
384+
pipenv_executable: Arc::new(RwLock::new(None)),
283385
}
284386
}
285387
}
388+
286389
impl Locator for PipEnv {
287390
fn get_kind(&self) -> LocatorKind {
288391
LocatorKind::PipEnv
289392
}
393+
394+
fn configure(&self, config: &Configuration) {
395+
if let Some(exe) = &config.pipenv_executable {
396+
self.pipenv_executable.write().unwrap().replace(exe.clone());
397+
}
398+
}
399+
290400
fn supported_categories(&self) -> Vec<PythonEnvironmentKind> {
291401
vec![PythonEnvironmentKind::Pipenv]
292402
}
@@ -334,8 +444,19 @@ impl Locator for PipEnv {
334444
)
335445
}
336446

337-
fn find(&self, _reporter: &dyn Reporter) {
338-
//
447+
fn find(&self, reporter: &dyn Reporter) {
448+
// First, find and report the pipenv manager
449+
let pipenv_exe = self.pipenv_executable.read().unwrap().clone();
450+
if let Some(manager) = PipenvManager::find(pipenv_exe, &self.env_vars) {
451+
trace!("Found pipenv manager: {:?}", manager);
452+
reporter.report_manager(&manager.to_manager());
453+
}
454+
455+
// Then discover and report pipenv environments
456+
let environments = list_environments(&self.env_vars);
457+
for env in environments {
458+
reporter.report_environment(&env);
459+
}
339460
}
340461
}
341462

@@ -361,6 +482,7 @@ mod tests {
361482
home,
362483
xdg_data_home: None,
363484
workon_home: None,
485+
path: None,
364486
}
365487
}
366488

@@ -402,6 +524,7 @@ mod tests {
402524
// Validate locator populates project
403525
let locator = PipEnv {
404526
env_vars: create_test_env_vars(None),
527+
pipenv_executable: Arc::new(RwLock::new(None)),
405528
};
406529
let result = locator
407530
.try_from(&env)
@@ -460,6 +583,7 @@ mod tests {
460583
home: Some(temp_home.clone()),
461584
xdg_data_home: None,
462585
workon_home: None,
586+
path: None,
463587
};
464588

465589
// Validate is_in_pipenv_centralized_dir detects it
@@ -475,7 +599,10 @@ mod tests {
475599
);
476600

477601
// Validate locator returns the environment
478-
let locator = PipEnv { env_vars };
602+
let locator = PipEnv {
603+
env_vars,
604+
pipenv_executable: Arc::new(RwLock::new(None)),
605+
};
479606
let result = locator
480607
.try_from(&env)
481608
.expect("expected locator to return environment");
@@ -525,6 +652,7 @@ mod tests {
525652
home: Some(temp_home.clone()),
526653
xdg_data_home: None,
527654
workon_home: None,
655+
path: None,
528656
};
529657

530658
// Should still be detected as pipenv (centralized directory + .project file)
@@ -538,7 +666,10 @@ mod tests {
538666
);
539667

540668
// Locator should return the environment, but project will point to non-existent path
541-
let locator = PipEnv { env_vars };
669+
let locator = PipEnv {
670+
env_vars,
671+
pipenv_executable: Arc::new(RwLock::new(None)),
672+
};
542673
let result = locator
543674
.try_from(&env)
544675
.expect("expected locator to return environment");

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
@@ -100,6 +100,7 @@ pub struct ConfigureOptions {
100100
/// Glob patterns are supported (e.g., "/home/user/projects/*").
101101
pub workspace_directories: Option<Vec<PathBuf>>,
102102
pub conda_executable: Option<PathBuf>,
103+
pub pipenv_executable: Option<PathBuf>,
103104
pub poetry_executable: Option<PathBuf>,
104105
/// Custom locations where environments can be found. Generally global locations where virtualenvs & the like can be found.
105106
/// Workspace directories should not be included into this list.
@@ -131,6 +132,7 @@ pub fn handle_configure(context: Arc<Context>, id: u32, params: Value) {
131132
.filter(|p| p.is_dir())
132133
.collect()
133134
});
135+
cfg.pipenv_executable = configure_options.pipenv_executable;
134136
cfg.poetry_executable = configure_options.poetry_executable;
135137
// We will not support changing the cache directories once set.
136138
// No point, supporting such a use case.

0 commit comments

Comments
 (0)