Skip to content

Commit 0850160

Browse files
committed
feat(analysis): install the analysis venv with uv and wire it to Jedi
Closes #47 The per-project analysis venv was built and populated but never used: __init__ left self.virtualenv = None and never reassigned it, so SymbolTableBuilder got virtualenv=None and Jedi resolved against the default environment, ignoring the installed dependencies. Set self.virtualenv to the venv path on both a fresh build and a lazy reuse so Jedi resolves the project's third-party imports. Also install dependencies with uv (uv pip install --python <venv>) instead of pip: uv resolves and downloads in parallel with a shared global cache, which is dramatically faster for large dependency trees (e.g. Odoo). uv ships as a self-contained binary in its wheel, so it is present wherever canpy is installed (including Docker); fall back to python -m pip when uv cannot be located.
1 parent aa60bd7 commit 0850160

2 files changed

Lines changed: 40 additions & 15 deletions

File tree

codeanalyzer/core.py

Lines changed: 36 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,29 @@ def _get_base_interpreter() -> Path:
226226
f"a working Python interpreter that can create virtual environments."
227227
)
228228

229+
@staticmethod
230+
def _uv_bin() -> Optional[str]:
231+
"""Path to a uv binary: the one bundled with the ``uv`` PyPI package (a
232+
dependency, so normally always present -- including inside a Docker image),
233+
else a uv on PATH, else ``None`` (callers fall back to pip)."""
234+
try:
235+
from uv import find_uv_bin
236+
237+
return str(find_uv_bin())
238+
except Exception:
239+
return shutil.which("uv")
240+
241+
def _install_into_venv(self, venv_python: Path, args: List[str]) -> None:
242+
"""Install packages into the target venv, preferring uv for speed (parallel
243+
downloads + a shared global cache) and falling back to the venv's own pip
244+
when uv is unavailable."""
245+
uv = self._uv_bin()
246+
if uv:
247+
cmd = [uv, "pip", "install", "--python", str(venv_python), *args]
248+
else:
249+
cmd = [str(venv_python), "-m", "pip", "install", *args]
250+
self._cmd_exec_helper(cmd, cwd=self.project_dir, check=True)
251+
229252
def __enter__(self) -> "Codeanalyzer":
230253
# If no virtualenv is provided, try to create one using requirements.txt or pyproject.toml
231254
venv_path = self.cache_dir / self.project_dir.name / "virtualenv"
@@ -249,24 +272,19 @@ def __enter__(self) -> "Codeanalyzer":
249272
("test-requirements.txt", ["-r"]),
250273
]
251274

252-
for dep_file, pip_args in dependency_files:
275+
for dep_file, _ in dependency_files:
253276
if (self.project_dir / dep_file).exists():
254277
logger.info(f"Installing dependencies from {dep_file}")
255-
self._cmd_exec_helper(
256-
[str(venv_python), "-m", "pip", "install", "-U"] + pip_args + [str(self.project_dir / dep_file)],
257-
cwd=self.project_dir,
258-
check=True,
278+
self._install_into_venv(
279+
venv_python,
280+
["--upgrade", "-r", str(self.project_dir / dep_file)],
259281
)
260282

261283
# Handle Pipenv files
262284
if (self.project_dir / "Pipfile").exists():
263285
logger.info("Installing dependencies from Pipfile")
264286
# Note: This would require pipenv to be installed
265-
self._cmd_exec_helper(
266-
[str(venv_python), "-m", "pip", "install", "pipenv"],
267-
cwd=self.project_dir,
268-
check=True,
269-
)
287+
self._install_into_venv(venv_python, ["pipenv"])
270288
self._cmd_exec_helper(
271289
["pipenv", "install", "--dev"],
272290
cwd=self.project_dir,
@@ -289,14 +307,17 @@ def __enter__(self) -> "Codeanalyzer":
289307

290308
if any((self.project_dir / file).exists() for file in package_definition_files):
291309
logger.info("Installing project in editable mode")
292-
self._cmd_exec_helper(
293-
[str(venv_python), "-m", "pip", "install", "-e", str(self.project_dir)],
294-
cwd=self.project_dir,
295-
check=True,
296-
)
310+
self._install_into_venv(venv_python, ["-e", str(self.project_dir)])
297311
else:
298312
logger.warning("No package definition files found, skipping editable installation")
299313

314+
# Point Jedi at the analysis venv so it resolves the project's third-party
315+
# imports. This runs on both a fresh build and a lazy reuse of an existing
316+
# venv -- previously self.virtualenv stayed None, so the install above was
317+
# never actually used by the symbol-table builder.
318+
if venv_path.exists():
319+
self.virtualenv = venv_path
320+
300321
if self.using_codeql:
301322
logger.info(f"(Re-)initializing CodeQL analysis for {self.project_dir}")
302323

pyproject.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,10 @@ dependencies = [
4343
"ray==2.0.0; python_version < '3.11'",
4444
"ray>=2.10.0,<3.0.0; python_version >= '3.11'",
4545
"packaging>=25.0",
46+
# uv -- installs the analyzed project's deps into the analysis venv quickly.
47+
# Shipped as a self-contained binary in its wheel, so it's available wherever
48+
# canpy is pip-installed (incl. Docker); core.py falls back to pip without it.
49+
"uv>=0.5.0",
4650
]
4751

4852
[project.optional-dependencies]

0 commit comments

Comments
 (0)