Skip to content

Commit 0799828

Browse files
committed
Add timing/progress, fix double-bar render, fix venv warn-and-continue
Timing logs (INFO, visible at -vv) — consistent ✅ Phase: N unit in X.Xs: PyCG shard progress bars (sequential and Ray-parallel modes) matching The Ray collection loop is restructured from a single ray.wait(N) call to a deadline-based ray.wait(1) loop. Fix double progress-bar render. Fix venv warn-and-continue: _install_into_venv callers now catch CalledProcessError and emit a WARNING, so a failing pip install (e.g. psycopg2 needing compiled C extensions on odoo) no longer aborts the analysis Signed-off-by: Saurabh Sinha <sinha108@gmail.com>
1 parent 10afac1 commit 0799828

4 files changed

Lines changed: 182 additions & 98 deletions

File tree

codeanalyzer/core.py

Lines changed: 59 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
from pathlib import Path
77
from typing import Any, Dict, Optional, Union, List
88

9+
import time
10+
911
import ray
1012
from codeanalyzer.utils import logger
1113
from 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

Comments
 (0)