54 lines
1.7 KiB
Python
54 lines
1.7 KiB
Python
import subprocess
|
|||
|
|
import sys
|
||
|
|
from pathlib import Path
|
||
|
|
|
||
|
|
|
||
|
|
class DependencyManager:
|
||
|
|
def __init__(self, requirements_path: Path) -> None:
|
||
|
|
self.requirements_path = requirements_path
|
||
|
|
|
||
|
|
@staticmethod
|
||
|
|
def _normalize_requirement_name(requirement_line: str) -> str:
|
||
|
|
line = requirement_line.split(";", 1)[0].strip()
|
||
|
|
for sep in ["==", ">=", "<=", "!=", "~=", ">", "<"]:
|
||
|
|
if sep in line:
|
||
|
|
line = line.split(sep, 1)[0].strip()
|
||
|
|
break
|
||
|
|
if "[" in line:
|
||
|
|
line = line.split("[", 1)[0].strip()
|
||
|
|
return line
|
||
|
|
|
||
|
|
def parse_requirements(self) -> list[str]:
|
||
|
|
if not self.requirements_path.exists():
|
||
|
|
return []
|
||
|
|
|
||
|
|
requirements: list[str] = []
|
||
|
|
for raw in self.requirements_path.read_text(encoding="utf-8").splitlines():
|
||
|
|
line = raw.strip()
|
||
|
|
if not line or line.startswith("#"):
|
||
|
|
continue
|
||
|
|
requirements.append(line)
|
||
|
|
return requirements
|
||
|
|
|
||
|
|
def _is_installed(self, requirement: str) -> bool:
|
||
|
|
package_name = self._normalize_requirement_name(requirement)
|
||
|
|
if not package_name:
|
||
|
|
return True
|
||
|
|
|
||
|
|
result = subprocess.run(
|
||
|
|
[sys.executable, "-m", "pip", "show", package_name],
|
||
|
|
stdout=subprocess.DEVNULL,
|
||
|
|
stderr=subprocess.DEVNULL,
|
||
|
|
check=False,
|
||
|
|
)
|
||
|
|
return result.returncode == 0
|
||
|
|
|
||
|
|
def install_missing(self) -> list[str]:
|
||
|
|
requirements = self.parse_requirements()
|
||
|
|
missing = [req for req in requirements if not self._is_installed(req)]
|
||
|
|
|
||
|
|
if missing:
|
||
|
|
subprocess.check_call([sys.executable, "-m", "pip", "install", *missing])
|
||
|
|
|
||
|
|
return missing
|