From eb6687ceca79faed89f0af2fc15ca33cdc12d645 Mon Sep 17 00:00:00 2001 From: Chuck Date: Thu, 11 Jun 2026 16:21:56 -0400 Subject: [PATCH] fix(install): correctly detect already-installed dateutil/websocket-client Address remaining coderabbitai findings on PR #369: - check_package_installed() did __import__(package_name) directly, but python-dateutil and websocket-client import as dateutil/websocket. Both always failed the "already installed" check and were reinstalled on every run. Add an IMPORT_NAME_MAP for the mismatched names. - _run() still read the entire temp file into memory before slicing the tail. Stream it line-by-line into a deque(maxlen=ERROR_TAIL_LINES) instead so memory use stays bounded for very chatty commands. --- scripts/install_dependencies_apt.py | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/scripts/install_dependencies_apt.py b/scripts/install_dependencies_apt.py index 18671662..a12d0b30 100644 --- a/scripts/install_dependencies_apt.py +++ b/scripts/install_dependencies_apt.py @@ -8,6 +8,7 @@ import subprocess import sys import tempfile import warnings +from collections import deque from pathlib import Path # How many trailing lines of a failed command's output to keep for the @@ -27,8 +28,13 @@ def _run(cmd): with tempfile.TemporaryFile(mode='w+b') as f: result = subprocess.run(cmd, stdout=f, stderr=subprocess.STDOUT) # nosec B603 B607 - hardcoded apt/pip args # nosemgrep f.seek(0) - lines = f.read().decode('utf-8', errors='replace').splitlines() - return result.returncode == 0, '\n'.join(lines[-ERROR_TAIL_LINES:]) + # Stream line-by-line so only the last ERROR_TAIL_LINES are ever held + # in memory, regardless of how much output the command produced. + tail = deque( + (line.decode('utf-8', errors='replace').rstrip('\n') for line in f), + maxlen=ERROR_TAIL_LINES, + ) + return result.returncode == 0, '\n'.join(tail) def install_via_apt(package_name): @@ -84,14 +90,22 @@ def install_via_pip(package_name): return False, output +# Distribution (pip/apt) names whose importable module name differs. +IMPORT_NAME_MAP = { + 'python-dateutil': 'dateutil', + 'websocket-client': 'websocket', +} + + def check_package_installed(package_name): """Check if a package is already installed.""" + import_name = IMPORT_NAME_MAP.get(package_name, package_name) # Suppress deprecation warnings when checking if packages are installed # (we're just checking, not using them) with warnings.catch_warnings(): warnings.filterwarnings('ignore', category=DeprecationWarning) try: - __import__(package_name) + __import__(import_name) return True except ImportError: return False