@@ -6,7 +6,8 @@ use pyo3::prelude::*;
66use std:: env;
77use std:: fmt;
88use std:: path:: { Path , PathBuf } ;
9- use which:: which;
9+
10+ mod finder;
1011
1112#[ derive( Debug ) ]
1213pub struct DjangoProject {
@@ -126,7 +127,6 @@ impl PythonEnvironment {
126127 #[ cfg( windows) ]
127128 let python_path = prefix. join ( "Scripts" ) . join ( "python.exe" ) ;
128129
129- // Check if the *prefix* and the *binary* exist.
130130 if !prefix. is_dir ( ) || !python_path. exists ( ) {
131131 return None ;
132132 }
@@ -137,10 +137,9 @@ impl PythonEnvironment {
137137 let bin_dir = prefix. join ( "Scripts" ) ;
138138
139139 let mut sys_path = Vec :: new ( ) ;
140- sys_path. push ( bin_dir) ; // Add bin/ or Scripts/
140+ sys_path. push ( bin_dir) ;
141141
142142 if let Some ( site_packages) = Self :: find_site_packages ( prefix) {
143- // Check existence inside the if let, as find_site_packages might return a path that doesn't exist
144143 if site_packages. is_dir ( ) {
145144 sys_path. push ( site_packages) ;
146145 }
@@ -167,12 +166,10 @@ impl PythonEnvironment {
167166 }
168167
169168 fn from_system_python ( ) -> Option < Self > {
170- let python_path = match which ( "python" ) {
169+ let python_path = match finder :: find ( "python" ) {
171170 Ok ( p) => p,
172171 Err ( _) => return None ,
173172 } ;
174- // which() might return a path inside a bin/Scripts dir, or directly the executable
175- // We need the prefix, which is usually two levels up from the executable in standard layouts
176173 let bin_dir = python_path. parent ( ) ?;
177174 let prefix = bin_dir. parent ( ) ?;
178175
@@ -237,6 +234,8 @@ mod tests {
237234
238235 mod env_discovery {
239236 use super :: * ;
237+ use super :: finder:: mock:: { self as exec_mock, MockGuard } ;
238+ use which:: Error as WhichError ;
240239
241240 fn create_mock_venv ( dir : & Path , version : Option < & str > ) -> PathBuf {
242241 let prefix = dir. to_path_buf ( ) ;
@@ -291,29 +290,6 @@ mod tests {
291290 }
292291 }
293292
294- // Guard struct to temporarily modify the PATH environment variable
295- struct PathGuard {
296- original_path : Option < String > ,
297- }
298-
299- impl PathGuard {
300- fn set ( new_path_val : & str ) -> Self {
301- let original_path = env:: var ( "PATH" ) . ok ( ) ;
302- env:: set_var ( "PATH" , new_path_val) ;
303- Self { original_path }
304- }
305- }
306-
307- impl Drop for PathGuard {
308- fn drop ( & mut self ) {
309- // Restore original PATH, or remove if it wasn't set initially
310- match self . original_path . as_deref ( ) {
311- Some ( val) => env:: set_var ( "PATH" , val) ,
312- None => env:: remove_var ( "PATH" ) ,
313- }
314- }
315- }
316-
317293 #[ test]
318294 fn test_explicit_venv_path_found ( ) {
319295 let project_dir = tempdir ( ) . unwrap ( ) ;
@@ -485,16 +461,13 @@ mod tests {
485461 let site_packages_path = mock_sys_python_prefix. join ( site_packages_rel_path) ;
486462 fs:: create_dir_all ( & site_packages_path) . unwrap ( ) ;
487463
488- // --- Manipulate PATH ---
489- // Completely overwrite PATH to only include the mock bin directory
490- let canonical_bin_dir =
491- bin_dir. canonicalize ( ) . expect ( "Failed to canonicalize mock bin dir" ) ;
492- let new_path = canonical_bin_dir. to_str ( ) . unwrap ( ) . to_string ( ) ;
493- let _path_guard = PathGuard :: set ( & new_path) ;
464+ // --- Set up executable finder mock ---
465+ let _guard = MockGuard ; // Ensure mocks are cleared after test
466+ exec_mock:: set_mock_path ( "python" , python_path. clone ( ) ) ;
494467
495468 // We don't create any venvs in project_dir
496469
497- // This test assumes `which python` works and points to a standard layout
470+ // This test now assumes the mock is correctly configured
498471 let system_env = PythonEnvironment :: new ( project_dir. path ( ) , None ) ;
499472
500473 assert ! (
@@ -503,10 +476,12 @@ mod tests {
503476 ) ;
504477
505478 if let Some ( env) = system_env {
506- assert_eq ! ( env. python_path, python_path, "Python path should match mock" ) ;
507479 assert_eq ! (
508- env. sys_prefix,
509- mock_sys_python_prefix,
480+ env. python_path, python_path,
481+ "Python path should match mock"
482+ ) ;
483+ assert_eq ! (
484+ env. sys_prefix, mock_sys_python_prefix,
510485 "Sys prefix should match mock prefix"
511486 ) ;
512487 assert ! (
@@ -528,17 +503,24 @@ mod tests {
528503
529504 // Ensure no explicit path, no project venvs, and set VIRTUAL_ENV to invalid.
530505 let invalid_virtual_env_path = project_dir. path ( ) . join ( "non_existent_venv_no_python" ) ;
531- let _guard =
506+ let _guard_venv =
532507 VirtualEnvGuard :: set ( "VIRTUAL_ENV" , invalid_virtual_env_path. to_str ( ) . unwrap ( ) ) ;
533508
534- // Overwrite PATH with an empty value to ensure `which("python")` fails.
535- let _path_guard = PathGuard :: set ( "" ) ;
509+ // --- Set up executable finder mock to fail ---
510+ let _guard_mock = MockGuard ;
511+ exec_mock:: set_mock_error (
512+ "python" ,
513+ WhichError :: CannotFindBinaryPath ,
514+ ) ;
536515
537516 // Call the function under test
538517 let env = PythonEnvironment :: new ( project_dir. path ( ) , None ) ;
539518
540519 // Assert that no environment was found
541- assert ! ( env. is_none( ) , "Expected no environment to be found when all discovery methods fail" ) ;
520+ assert ! (
521+ env. is_none( ) ,
522+ "Expected no environment to be found when all discovery methods fail"
523+ ) ;
542524 }
543525
544526 #[ test]
0 commit comments