@@ -19,6 +19,7 @@ def __init__(self, venv_path: Optional[str] = None, debug: bool = False):
1919 self .debug = debug
2020 self ._cached_venv_path : str | None = None
2121 self ._venv_path_searched = False
22+ self ._cached_pip_command : str | None = None
2223
2324 def is_usable (self , executor : EnvironmentExecutor , working_dir : Optional [str ] = None ) -> bool :
2425 """Check if pip is usable in the environment."""
@@ -77,81 +78,125 @@ def get_dependencies(
7778 return packages , metadata
7879
7980 def _get_pip_location (self , executor : EnvironmentExecutor , working_dir : Optional [str ] = None ) -> str :
80- """Get the location of the pip environment."""
81+ """Get the location of the pip environment, properly classifying system vs project scope ."""
8182 pip_command = self ._get_pip_command (executor , working_dir )
8283 stdout , _ , exit_code = executor .execute_command (f"{ pip_command } show pip" , working_dir )
8384
8485 if exit_code == 0 :
8586 for line in stdout .split ("\n " ):
8687 if line .startswith ("Location:" ):
87- return line .split (":" , 1 )[1 ].strip ()
88+ location = line .split (":" , 1 )[1 ].strip ()
89+
90+ # Check if this is a system location
91+ if location .startswith (("/usr/lib" , "/usr/local/lib" )):
92+ return "system"
93+
94+ return location
8895
8996 return "system"
9097
9198 def _get_pip_command (self , executor : EnvironmentExecutor , working_dir : Optional [str ] = None ) -> str :
9299 """Get the appropriate pip command, activating venv if available."""
100+ if hasattr (self , "_cached_pip_command" ) and self ._cached_pip_command :
101+ return self ._cached_pip_command
102+
93103 venv_path = self ._find_venv_path (executor , working_dir )
94104 if venv_path :
95105 venv_pip = f"{ venv_path } /bin/pip"
96106 if executor .path_exists (venv_pip ):
107+ self ._cached_pip_command = venv_pip
97108 return venv_pip
109+
110+ self ._cached_pip_command = "pip"
98111 return "pip"
99112
113+ def _validate_venv_path (self , executor : EnvironmentExecutor , path : str ) -> str | None :
114+ """Check if a path contains pyvenv.cfg and return the venv path if valid."""
115+ pyvenv_cfg_path = f"{ path } /pyvenv.cfg"
116+ if executor .path_exists (pyvenv_cfg_path ):
117+ self ._cached_venv_path = path
118+ self ._venv_path_searched = True
119+ return path
120+ return None
121+
100122 def _find_venv_path (self , executor : EnvironmentExecutor , working_dir : Optional [str ] = None ) -> str | None :
101- """Find virtual environment using batch search for pyvenv.cfg files."""
123+ """Find virtual environment using priority-based search for pyvenv.cfg files."""
102124 # Return cached result if already searched
103125 if self ._venv_path_searched :
104126 return self ._cached_venv_path
105127
106- # Build search paths in priority order
107- search_paths = []
108- search_dir = working_dir or "."
109-
110- # 1. Explicit venv path (highest priority)
128+ # 1. Explicit venv path (highest priority) - check immediately
111129 if self .explicit_venv_path :
112130 stdout , _ , exit_code = executor .execute_command (f"echo { self .explicit_venv_path } " )
113131 if exit_code == 0 and stdout .strip ():
114- search_paths .append (stdout .strip ())
132+ venv_path = self ._validate_venv_path (executor , stdout .strip ())
133+ if venv_path :
134+ self ._cached_venv_path = venv_path
135+ self ._venv_path_searched = True
136+ return venv_path
115137
116- # 2. VIRTUAL_ENV environment variable (Docker containers)
138+ # 2. VIRTUAL_ENV environment variable (Docker containers) - check immediately
117139 if not isinstance (executor , HostExecutor ):
118140 stdout , _ , exit_code = executor .execute_command ("echo $VIRTUAL_ENV" , working_dir )
119141 if exit_code == 0 and stdout .strip ():
120- search_paths .append (stdout .strip ())
121-
122- # 3. Local project directory and common venv names
123- search_paths .extend (
124- [
125- search_dir ,
126- f"{ search_dir } /venv" ,
127- f"{ search_dir } /.venv" ,
128- f"{ search_dir } /env" ,
129- f"{ search_dir } /.env" ,
130- f"{ search_dir } /virtualenv" ,
131- ]
132- )
142+ venv_path = self ._validate_venv_path (executor , stdout .strip ())
143+ if venv_path :
144+ self ._cached_venv_path = venv_path
145+ self ._venv_path_searched = True
146+ return venv_path
147+
148+ # 3. Extract venv path from pip show pip location (fallback method) - check immediately
149+ # Use basic pip command to avoid circular dependency with _get_pip_command
150+ stdout , _ , exit_code = executor .execute_command ("pip show pip" , working_dir )
151+ if exit_code == 0 :
152+ for line in stdout .split ("\n " ):
153+ if line .startswith ("Location:" ):
154+ pip_location = line .split (":" , 1 )[1 ].strip ()
155+ # Skip system locations
156+ if not pip_location .startswith (("/usr/lib" , "/usr/local/lib" )) and "/lib/python" in pip_location :
157+ venv_path = pip_location .split ("/lib/python" )[0 ]
158+ # Trust this path since pip is installed there - just cache and return
159+ self ._cached_venv_path = venv_path
160+ self ._venv_path_searched = True
161+ return venv_path
162+ break
163+
164+ # 4. Batch search for remaining venv locations
165+ # Collect all paths and use single find command for efficiency (fewer executor calls)
166+ search_paths = []
167+ common_venv_names = ["venv" , ".venv" , "env" , ".env" , "virtualenv" ]
133168
134- # 4. External venv locations (if working_dir specified)
169+ # Check working directory and its subdirectories (if working_dir specified)
170+ if working_dir :
171+ search_paths .append (working_dir ) # working_dir itself
172+ for venv_name in common_venv_names :
173+ search_paths .append (f"{ working_dir } /{ venv_name } " )
174+
175+ # Check home directory subdirectories (always check)
176+ # Resolve home directory explicitly to avoid tilde expansion issues
177+ home_stdout , _ , home_exit_code = executor .execute_command ("echo $HOME" )
178+ if home_exit_code == 0 and home_stdout .strip ():
179+ home_dir = home_stdout .strip ()
180+ for venv_name in common_venv_names :
181+ search_paths .append (f"{ home_dir } /{ venv_name } " )
182+
183+ # External venv locations (project-specific, only if working_dir specified)
135184 if working_dir :
136185 resolved_working_dir = self ._resolve_absolute_path (executor , working_dir )
137186 project_name = os .path .basename (resolved_working_dir )
138187
139- # Expand external paths
140- external_locations = [
141- f"~/.virtualenvs/{ project_name } " ,
142- f"~/.local/share/virtualenvs/{ project_name } " ,
143- f"~/.cache/pypoetry/virtualenvs/{ project_name } " ,
144- f"~/.pyenv/versions/{ project_name } " ,
145- ]
146-
147- for location in external_locations :
148- stdout , _ , exit_code = executor .execute_command (f"echo { location } " )
149- if exit_code == 0 and stdout .strip ():
150- search_paths .append (stdout .strip ())
188+ # Use resolved home directory for external locations too
189+ if home_exit_code == 0 and home_stdout .strip ():
190+ home_dir = home_stdout .strip ()
191+ external_locations = [
192+ f"{ home_dir } /.virtualenvs/{ project_name } " ,
193+ f"{ home_dir } /.local/share/virtualenvs/{ project_name } " ,
194+ f"{ home_dir } /.cache/pypoetry/virtualenvs/{ project_name } " ,
195+ f"{ home_dir } /.pyenv/versions/{ project_name } " ,
196+ ]
197+ search_paths .extend (external_locations )
151198
152- # Single batch find command to locate pyvenv.cfg in all search paths
153199 if search_paths :
154- # Escape paths and create find command
155200 escaped_paths = [f"'{ path } '" for path in search_paths ]
156201 find_cmd = f"find { ' ' .join (escaped_paths )} -maxdepth 1 -name 'pyvenv.cfg' -type f 2>/dev/null | head -1"
157202
@@ -169,7 +214,7 @@ def _find_venv_path(self, executor: EnvironmentExecutor, working_dir: Optional[s
169214 print ("pip_detector performing system-wide pyvenv.cfg search in container environment" )
170215
171216 stdout , _ , exit_code = executor .execute_command (
172- "find /opt /home /usr/local -name 'pyvenv.cfg' -type f 2>/dev/null | head -1"
217+ "find /opt /home -name 'pyvenv.cfg' -type f 2>/dev/null | head -1"
173218 )
174219
175220 if exit_code == 0 and stdout .strip ():
0 commit comments