66from pathlib import Path
77from typing import Any , Dict , Optional , Union , List
88
9+ import time
10+
911import ray
1012from codeanalyzer .utils import logger
1113from codeanalyzer .schema import (
@@ -81,6 +83,7 @@ def _cmd_exec_helper(
8183 capture_output : bool = True ,
8284 check : bool = True ,
8385 suppress_output : bool = False ,
86+ log_on_failure : bool = True ,
8487 ) -> subprocess .CompletedProcess :
8588 """
8689 Runs a subprocess with real-time output streaming to the logger.
@@ -90,7 +93,10 @@ def _cmd_exec_helper(
9093 cwd: Working directory to run the command in.
9194 capture_output: If True, retains and returns the output.
9295 check: If True, raises CalledProcessError on non-zero exit.
93- suppress_output: If True, silences log output.
96+ suppress_output: If True, silences per-line debug output.
97+ log_on_failure: If False, suppresses the error-level log on
98+ non-zero exit (use when the caller handles the exception and
99+ will emit its own diagnostic).
94100
95101 Returns:
96102 subprocess.CompletedProcess
@@ -121,9 +127,10 @@ def _cmd_exec_helper(
121127
122128 if check and returncode != 0 :
123129 error_output = "\n " .join (output_lines )
124- logger .error (f"Command failed with exit code { returncode } : { ' ' .join (cmd )} " )
125- if error_output :
126- logger .error (f"Command output:\n { error_output } " )
130+ if log_on_failure :
131+ logger .error (f"Command failed with exit code { returncode } : { ' ' .join (cmd )} " )
132+ if error_output :
133+ logger .error (f"Command output:\n { error_output } " )
127134 raise subprocess .CalledProcessError (returncode , cmd , output = error_output )
128135
129136 return subprocess .CompletedProcess (
@@ -244,13 +251,22 @@ def _uv_bin() -> Optional[str]:
244251 def _install_into_venv (self , venv_python : Path , args : List [str ]) -> None :
245252 """Install packages into the target venv, preferring uv for speed (parallel
246253 downloads + a shared global cache) and falling back to the venv's own pip
247- when uv is unavailable."""
254+ when uv is unavailable.
255+
256+ Raises ``subprocess.CalledProcessError`` on failure; callers in
257+ ``__enter__`` catch this and warn-and-continue so a single failing
258+ package (e.g. a C extension that needs system libs) does not abort the
259+ entire analysis.
260+ """
248261 uv = self ._uv_bin ()
249262 if uv :
250263 cmd = [uv , "pip" , "install" , "--python" , str (venv_python ), * args ]
251264 else :
252265 cmd = [str (venv_python ), "-m" , "pip" , "install" , * args ]
253- self ._cmd_exec_helper (cmd , cwd = self .project_dir , check = True )
266+ self ._cmd_exec_helper (
267+ cmd , cwd = self .project_dir , check = True ,
268+ suppress_output = True , log_on_failure = False ,
269+ )
254270
255271 def __enter__ (self ) -> "Codeanalyzer" :
256272 # If no virtualenv is provided, try to create one using requirements.txt or pyproject.toml
@@ -283,20 +299,32 @@ def __enter__(self) -> "Codeanalyzer":
283299 for dep_file , _ in dependency_files :
284300 if (self .project_dir / dep_file ).exists ():
285301 logger .info (f"Installing dependencies from { dep_file } " )
286- self ._install_into_venv (
287- venv_python ,
288- ["--upgrade" , "-r" , str (self .project_dir / dep_file )],
289- )
302+ try :
303+ self ._install_into_venv (
304+ venv_python ,
305+ ["--upgrade" , "-r" , str (self .project_dir / dep_file )],
306+ )
307+ except subprocess .CalledProcessError as exc :
308+ logger .warning (
309+ f"Dependency installation from { dep_file } failed "
310+ f"(exit { exc .returncode } ) — continuing without it. "
311+ "Jedi type resolution may be incomplete."
312+ )
290313
291314 # Handle Pipenv files
292315 if (self .project_dir / "Pipfile" ).exists ():
293316 logger .info ("Installing dependencies from Pipfile" )
294- self ._install_into_venv (venv_python , ["pipenv" ])
295- self ._cmd_exec_helper (
296- ["pipenv" , "install" , "--dev" ],
297- cwd = self .project_dir ,
298- check = True ,
299- )
317+ try :
318+ self ._install_into_venv (venv_python , ["pipenv" ])
319+ self ._cmd_exec_helper (
320+ ["pipenv" , "install" , "--dev" ],
321+ cwd = self .project_dir ,
322+ check = True ,
323+ )
324+ except subprocess .CalledProcessError as exc :
325+ logger .warning (
326+ f"Pipenv installation failed (exit { exc .returncode } ) — continuing without it."
327+ )
300328
301329 # Handle conda environment files
302330 conda_files = ["conda.yml" , "environment.yml" ]
@@ -314,7 +342,13 @@ def __enter__(self) -> "Codeanalyzer":
314342
315343 if any ((self .project_dir / file ).exists () for file in package_definition_files ):
316344 logger .info ("Installing project in editable mode" )
317- self ._install_into_venv (venv_python , ["-e" , str (self .project_dir )])
345+ try :
346+ self ._install_into_venv (venv_python , ["-e" , str (self .project_dir )])
347+ except subprocess .CalledProcessError as exc :
348+ logger .warning (
349+ f"Editable install failed (exit { exc .returncode } ) — "
350+ "continuing without it. Jedi type resolution may be incomplete."
351+ )
318352 else :
319353 logger .warning ("No package definition files found, skipping editable installation" )
320354
@@ -393,8 +427,10 @@ def analyze(self) -> PyApplication:
393427 resolve_unresolved_constructors (symbol_table )
394428
395429 # Level 1: Jedi call graph.
430+ t0_jedi = time .perf_counter ()
396431 jedi_edges = jedi_call_graph_edges (symbol_table )
397432 call_graph = list (jedi_edges )
433+ logger .info ("✅ Jedi: %d edges in %.1fs" , len (call_graph ), time .perf_counter () - t0_jedi )
398434
399435 if self .analysis_level >= 2 :
400436 # Level 2: also add PyCG edges.
@@ -508,7 +544,8 @@ def _build_symbol_table(self, cached_symbol_table: Optional[Dict[str, PyModule]]
508544 Dict[str, PyModule]: A dictionary mapping file paths to PyModule objects.
509545 """
510546 symbol_table : Dict [str , PyModule ] = {}
511-
547+ t0_st = time .perf_counter ()
548+
512549 # Handle single file analysis
513550 if self .file_name is not None :
514551 single_file = self .project_dir / self .file_name
@@ -615,7 +652,10 @@ def _build_symbol_table(self, cached_symbol_table: Optional[Dict[str, PyModule]]
615652 if files_from_cache > 0 :
616653 logger .info (f"Reused { files_from_cache } files from cache, processed { files_processed } new/changed files" )
617654
618- logger .info ("✅ Symbol table generation complete." )
655+ logger .info (
656+ "✅ Symbol table: %d modules in %.1fs" ,
657+ len (symbol_table ), time .perf_counter () - t0_st ,
658+ )
619659 return symbol_table
620660
621661 def _get_pycg_call_graph (
0 commit comments