@@ -23,13 +23,36 @@ fn get_pipenv_project(env: &PythonEnv) -> Option<PathBuf> {
2323 if let Some ( project) = get_pipenv_project_from_prefix ( prefix) {
2424 return Some ( project) ;
2525 }
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+ }
2635 }
2736
2837 // We can also have a venv in the workspace that has pipenv installed in it.
2938 // In such cases, the project is the workspace folder containing the venv.
30- if let Some ( project) = & env. project {
31- if project. join ( "Pipfile" ) . exists ( ) {
32- return Some ( project. clone ( ) ) ;
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" )
44+ || bin. file_name ( ) . unwrap_or_default ( ) == Path :: new ( "Scripts" )
45+ {
46+ bin. parent ( )
47+ } else {
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 }
3558
@@ -59,9 +82,29 @@ fn get_pipenv_project_from_prefix(prefix: &Path) -> Option<PathBuf> {
5982}
6083
6184fn is_pipenv_from_project ( env : & PythonEnv ) -> bool {
62- if let Some ( project) = & env. project {
63- if project. join ( "Pipfile" ) . exists ( ) {
64- return true ;
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+ }
65108 }
66109 }
67110 false
@@ -144,3 +187,70 @@ impl Locator for PipEnv {
144187 //
145188 }
146189}
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