Skip to content

Commit b35e3e4

Browse files
committed
fix: ensure workspace environments are found when searchKind is provided
1 parent 563a076 commit b35e3e4

File tree

2 files changed

+124
-31
lines changed

2 files changed

+124
-31
lines changed

crates/pet/src/find.rs

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -653,4 +653,88 @@ mod tests {
653653
"canonicalize() would resolve to target, but path() does not"
654654
);
655655
}
656+
657+
/// Test for https://github.com/microsoft/python-environment-tools/issues/151
658+
/// Verifies that refresh with searchKind (e.g., "Venv") still finds environments
659+
/// in workspace directories, not just global locations.
660+
///
661+
/// The bug was that when searchKind was provided, workspace_directories was cleared,
662+
/// preventing discovery of workspace-based environments like venvs.
663+
#[test]
664+
fn test_search_kind_finds_workspace_venvs() {
665+
use super::{find_and_report_envs, SearchScope};
666+
use crate::locators::create_locators;
667+
use pet_conda::Conda;
668+
use pet_core::os_environment::EnvironmentApi;
669+
use pet_core::python_environment::PythonEnvironmentKind;
670+
use pet_core::Configuration;
671+
use pet_poetry::Poetry;
672+
use pet_reporter::collect;
673+
use std::sync::Arc;
674+
675+
let tmp = TempDir::new().expect("Failed to create temp dir");
676+
let workspace = tmp.path().to_path_buf();
677+
678+
// Create a venv structure in the workspace
679+
let venv_dir = workspace.join(".venv");
680+
#[cfg(windows)]
681+
let bin_dir = venv_dir.join("Scripts");
682+
#[cfg(unix)]
683+
let bin_dir = venv_dir.join("bin");
684+
fs::create_dir_all(&bin_dir).expect("Failed to create bin dir");
685+
686+
// Create pyvenv.cfg (required for venv detection)
687+
fs::write(
688+
venv_dir.join("pyvenv.cfg"),
689+
"home = /usr/bin\nversion = 3.11.0\n",
690+
)
691+
.expect("Failed to create pyvenv.cfg");
692+
693+
// Create python executable
694+
#[cfg(windows)]
695+
let python_exe = bin_dir.join("python.exe");
696+
#[cfg(unix)]
697+
let python_exe = bin_dir.join("python");
698+
fs::write(&python_exe, "fake python").expect("Failed to create python exe");
699+
700+
// Set up locators and configuration
701+
let environment = EnvironmentApi::new();
702+
let conda_locator = Arc::new(Conda::from(&environment));
703+
let poetry_locator = Arc::new(Poetry::from(&environment));
704+
let locators = create_locators(conda_locator, poetry_locator, &environment);
705+
706+
let config = Configuration {
707+
workspace_directories: Some(vec![workspace.clone()]),
708+
..Default::default()
709+
};
710+
for locator in locators.iter() {
711+
locator.configure(&config);
712+
}
713+
714+
let reporter = Arc::new(collect::create_reporter());
715+
716+
// Call find_and_report_envs with SearchScope::Global(Venv)
717+
// This simulates the behavior when refresh is called with searchKind: "Venv"
718+
find_and_report_envs(
719+
reporter.as_ref(),
720+
config,
721+
&locators,
722+
&environment,
723+
Some(SearchScope::Global(PythonEnvironmentKind::Venv)),
724+
);
725+
726+
let environments = reporter.environments.lock().unwrap();
727+
728+
// The venv should be discovered even when searching by kind
729+
let venv_found = environments.iter().any(|env| {
730+
env.kind == Some(PythonEnvironmentKind::Venv)
731+
&& env.prefix.as_ref().map(|p| p == &venv_dir).unwrap_or(false)
732+
});
733+
734+
assert!(
735+
venv_found,
736+
"Venv in workspace should be found when searching by kind. Found environments: {:?}",
737+
*environments
738+
);
739+
}
656740
}

crates/pet/src/jsonrpc.rs

Lines changed: 40 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -211,43 +211,52 @@ pub fn handle_refresh(context: Arc<Context>, id: u32, params: Value) {
211211

212212
let mut search_scope = None;
213213

214-
// If search kind is provided and no search_paths, then we will only search in the global locations.
215-
if refresh_options.search_kind.is_some() || refresh_options.search_paths.is_some() {
216-
// Always clear this, as we will either serach in specified folder or a specific kind in global locations.
214+
// If search_paths is provided, limit search to those paths.
215+
// If only search_kind is provided (without search_paths), we still search
216+
// workspace directories because many environment types (like Venv, VirtualEnv)
217+
// don't have global locations - they only exist in workspace folders.
218+
// The reporter will filter results to only report the requested kind.
219+
if let Some(search_paths) = refresh_options.search_paths {
220+
// Clear workspace directories when explicit search paths are provided.
217221
config.workspace_directories = None;
218-
if let Some(search_paths) = refresh_options.search_paths {
219-
// Expand any glob patterns in the search paths
220-
let expanded_paths = expand_glob_patterns(&search_paths);
221-
trace!(
222-
"Expanded {} search paths to {} paths",
223-
search_paths.len(),
224-
expanded_paths.len()
225-
);
226-
// These workspace folders are only for this refresh.
227-
config.workspace_directories = Some(
228-
expanded_paths
229-
.iter()
230-
.filter(|p| p.is_dir())
231-
.cloned()
232-
.collect(),
233-
);
234-
config.executables = Some(
235-
expanded_paths
236-
.iter()
237-
.filter(|p| p.is_file())
238-
.cloned()
239-
.collect(),
240-
);
241-
search_scope = Some(SearchScope::Workspace);
242-
} else if let Some(search_kind) = refresh_options.search_kind {
243-
config.executables = None;
244-
search_scope = Some(SearchScope::Global(search_kind));
245-
}
222+
// Expand any glob patterns in the search paths
223+
let expanded_paths = expand_glob_patterns(&search_paths);
224+
trace!(
225+
"Expanded {} search paths to {} paths",
226+
search_paths.len(),
227+
expanded_paths.len()
228+
);
229+
// These workspace folders are only for this refresh.
230+
config.workspace_directories = Some(
231+
expanded_paths
232+
.iter()
233+
.filter(|p| p.is_dir())
234+
.cloned()
235+
.collect(),
236+
);
237+
config.executables = Some(
238+
expanded_paths
239+
.iter()
240+
.filter(|p| p.is_file())
241+
.cloned()
242+
.collect(),
243+
);
244+
search_scope = Some(SearchScope::Workspace);
246245

247246
// Configure the locators with the modified config.
248247
for locator in context.locators.iter() {
249248
locator.configure(&config);
250249
}
250+
} else if let Some(search_kind) = refresh_options.search_kind {
251+
// When only search_kind is provided, keep workspace directories so that
252+
// workspace-based environments (Venv, VirtualEnv, etc.) can be found.
253+
// The search_scope tells find_and_report_envs to filter locators by kind.
254+
search_scope = Some(SearchScope::Global(search_kind));
255+
256+
// Re-configure the locators (config unchanged, but ensures consistency).
257+
for locator in context.locators.iter() {
258+
locator.configure(&config);
259+
}
251260
} else {
252261
// Re-configure the locators with an un-modified config.
253262
// Possible we congirued the locators with a modified config in the in the previous request.

0 commit comments

Comments
 (0)