@@ -20,22 +20,58 @@ mod env_variables;
2020
2121fn get_pipenv_project ( env : & PythonEnv ) -> Option < PathBuf > {
2222 if let Some ( prefix) = & env. prefix {
23- get_pipenv_project_from_prefix ( prefix)
24- } else {
25- // If the parent is bin or script, then get the parent.
26- let bin = env. executable . parent ( ) ?;
27- if bin. file_name ( ) . unwrap_or_default ( ) == Path :: new ( "bin" )
23+ if let Some ( project) = get_pipenv_project_from_prefix ( prefix) {
24+ return Some ( project) ;
25+ }
26+ // If there's no .project file, but the venv lives inside the project folder
27+ // (e.g., <project>/.venv or <project>/venv), then the project is the parent
28+ // directory of the venv. Detect that by checking for a Pipfile next to the venv.
29+ if let Some ( parent) = prefix. parent ( ) {
30+ let project_folder = parent;
31+ if project_folder. join ( "Pipfile" ) . exists ( ) {
32+ return Some ( project_folder. to_path_buf ( ) ) ;
33+ }
34+ }
35+ }
36+
37+ // We can also have a venv in the workspace that has pipenv installed in it.
38+ // In such cases, the project is the workspace folder containing the venv.
39+ // Derive the project folder from the executable path when prefix isn't available.
40+ // Typical layout: <project>/.venv/{bin|Scripts}/python
41+ // So walk up to {bin|Scripts} -> venv dir -> project dir and check for Pipfile.
42+ if let Some ( bin) = env. executable . parent ( ) {
43+ let venv_dir = if bin. file_name ( ) . unwrap_or_default ( ) == Path :: new ( "bin" )
2844 || bin. file_name ( ) . unwrap_or_default ( ) == Path :: new ( "Scripts" )
2945 {
30- get_pipenv_project_from_prefix ( env . executable . parent ( ) ? . parent ( ) ? )
46+ bin . parent ( )
3147 } else {
32- get_pipenv_project_from_prefix ( env. executable . parent ( ) ?)
48+ Some ( bin)
49+ } ;
50+ if let Some ( venv_dir) = venv_dir {
51+ if let Some ( project_dir) = venv_dir. parent ( ) {
52+ if project_dir. join ( "Pipfile" ) . exists ( ) {
53+ return Some ( project_dir. to_path_buf ( ) ) ;
54+ }
55+ }
3356 }
3457 }
58+
59+ // If the parent is bin or script, then get the parent.
60+ let bin = env. executable . parent ( ) ?;
61+ if bin. file_name ( ) . unwrap_or_default ( ) == Path :: new ( "bin" )
62+ || bin. file_name ( ) . unwrap_or_default ( ) == Path :: new ( "Scripts" )
63+ {
64+ get_pipenv_project_from_prefix ( env. executable . parent ( ) ?. parent ( ) ?)
65+ } else {
66+ get_pipenv_project_from_prefix ( env. executable . parent ( ) ?)
67+ }
3568}
3669
3770fn get_pipenv_project_from_prefix ( prefix : & Path ) -> Option < PathBuf > {
3871 let project_file = prefix. join ( ".project" ) ;
72+ if !project_file. exists ( ) {
73+ return None ;
74+ }
3975 let contents = fs:: read_to_string ( project_file) . ok ( ) ?;
4076 let project_folder = norm_case ( PathBuf :: from ( contents. trim ( ) . to_string ( ) ) ) ;
4177 if project_folder. exists ( ) {
@@ -45,12 +81,44 @@ fn get_pipenv_project_from_prefix(prefix: &Path) -> Option<PathBuf> {
4581 }
4682}
4783
84+ fn is_pipenv_from_project ( env : & PythonEnv ) -> bool {
85+ // If the env prefix is inside a project folder, check that folder for a Pipfile.
86+ if let Some ( prefix) = & env. prefix {
87+ if let Some ( project_dir) = prefix. parent ( ) {
88+ if project_dir. join ( "Pipfile" ) . exists ( ) {
89+ return true ;
90+ }
91+ }
92+ }
93+ // Derive from the executable path as a fallback.
94+ if let Some ( bin) = env. executable . parent ( ) {
95+ let venv_dir = if bin. file_name ( ) . unwrap_or_default ( ) == Path :: new ( "bin" )
96+ || bin. file_name ( ) . unwrap_or_default ( ) == Path :: new ( "Scripts" )
97+ {
98+ bin. parent ( )
99+ } else {
100+ Some ( bin)
101+ } ;
102+ if let Some ( venv_dir) = venv_dir {
103+ if let Some ( project_dir) = venv_dir. parent ( ) {
104+ if project_dir. join ( "Pipfile" ) . exists ( ) {
105+ return true ;
106+ }
107+ }
108+ }
109+ }
110+ false
111+ }
112+
48113fn is_pipenv ( env : & PythonEnv , env_vars : & EnvVariables ) -> bool {
49114 if let Some ( project_path) = get_pipenv_project ( env) {
50115 if project_path. join ( env_vars. pipenv_pipfile . clone ( ) ) . exists ( ) {
51116 return true ;
52117 }
53118 }
119+ if is_pipenv_from_project ( env) {
120+ return true ;
121+ }
54122 // If we have a Pipfile, then this is a pipenv environment.
55123 // Else likely a virtualenvwrapper or the like.
56124 if let Some ( project_path) = get_pipenv_project ( env) {
@@ -119,3 +187,70 @@ impl Locator for PipEnv {
119187 //
120188 }
121189}
190+
191+ #[ cfg( test) ]
192+ mod tests {
193+ use super :: * ;
194+ use std:: time:: { SystemTime , UNIX_EPOCH } ;
195+
196+ fn unique_temp_dir ( ) -> PathBuf {
197+ let mut dir = std:: env:: temp_dir ( ) ;
198+ let nanos = SystemTime :: now ( )
199+ . duration_since ( UNIX_EPOCH )
200+ . unwrap ( )
201+ . as_nanos ( ) ;
202+ dir. push ( format ! ( "pet_pipenv_test_{}" , nanos) ) ;
203+ dir
204+ }
205+
206+ #[ test]
207+ fn infer_project_for_venv_in_project ( ) {
208+ let project_dir = unique_temp_dir ( ) ;
209+ let venv_dir = project_dir. join ( ".venv" ) ;
210+ let bin_dir = if cfg ! ( windows) {
211+ venv_dir. join ( "Scripts" )
212+ } else {
213+ venv_dir. join ( "bin" )
214+ } ;
215+ let python_exe = if cfg ! ( windows) {
216+ bin_dir. join ( "python.exe" )
217+ } else {
218+ bin_dir. join ( "python" )
219+ } ;
220+
221+ // Create directories and files
222+ std:: fs:: create_dir_all ( & bin_dir) . unwrap ( ) ;
223+ std:: fs:: write ( project_dir. join ( "Pipfile" ) , b"[[source]]\n " ) . unwrap ( ) ;
224+ // Touch python exe file
225+ std:: fs:: write ( & python_exe, b"" ) . unwrap ( ) ;
226+ // Touch pyvenv.cfg in venv root so PythonEnv::new logic would normally detect prefix
227+ std:: fs:: write ( venv_dir. join ( "pyvenv.cfg" ) , b"version = 3.12.0\n " ) . unwrap ( ) ;
228+
229+ // Construct PythonEnv directly
230+ let env = PythonEnv {
231+ executable : norm_case ( python_exe. clone ( ) ) ,
232+ prefix : Some ( norm_case ( venv_dir. clone ( ) ) ) ,
233+ version : None ,
234+ symlinks : None ,
235+ } ;
236+
237+ // Validate helper infers project
238+ let inferred = get_pipenv_project ( & env) . expect ( "expected project path" ) ;
239+ assert_eq ! ( inferred, norm_case( project_dir. clone( ) ) ) ;
240+
241+ // Validate locator populates project
242+ let locator = PipEnv {
243+ env_vars : EnvVariables {
244+ pipenv_max_depth : 3 ,
245+ pipenv_pipfile : "Pipfile" . to_string ( ) ,
246+ } ,
247+ } ;
248+ let result = locator
249+ . try_from ( & env)
250+ . expect ( "expected locator to return environment" ) ;
251+ assert_eq ! ( result. project, Some ( norm_case( project_dir. clone( ) ) ) ) ;
252+
253+ // Cleanup
254+ std:: fs:: remove_dir_all ( & project_dir) . ok ( ) ;
255+ }
256+ }
0 commit comments