Coverage for .nox/test-3-9/lib/python3.9/site-packages/nskit/vcs/installer.py: 95%
65 statements
« prev ^ index » next coverage.py v7.4.2, created at 2024-02-25 17:38 +0000
« prev ^ index » next coverage.py v7.4.2, created at 2024-02-25 17:38 +0000
1"""Repository installers."""
2from __future__ import annotations
4from abc import ABC, abstractmethod
5from pathlib import Path
6import subprocess # nosec B404
7import sys
8from typing import List, TYPE_CHECKING
10from pydantic_settings import SettingsConfigDict
11import virtualenv
13from nskit._logging import logger_factory
14from nskit.common.configuration import BaseConfiguration
15from nskit.common.contextmanagers import ChDir
16from nskit.common.extensions import ExtensionsEnum
18if TYPE_CHECKING:
19 from nskit.vcs.codebase import Codebase
22logger = logger_factory.get_logger(__name__)
24ENTRYPOINT = 'nskit.vcs.installers'
25InstallersEnum = ExtensionsEnum.from_entrypoint('InstallersEnum', ENTRYPOINT)
28class Installer(ABC, BaseConfiguration):
29 """Abstract class for language installer.
31 Can be enabled or disabled using the boolean flag and environment variables.
32 """
34 enabled: bool = True
36 def check(self, path: Path, **kwargs):
37 """Check if the installer is enabled, and the repo matches the criteria."""
38 return self.enabled and self.check_repo(path=path, **kwargs)
40 @abstractmethod
41 def check_repo(self, path: Path, **kwargs):
42 """Check if the repo matches the installer language."""
43 raise NotImplementedError('Implement in a language specific installer. It should check for appropriate files to signal that it is a repo of that type')
45 @abstractmethod
46 def install(self, path: Path, *, codebase: Codebase | None = None, deps: bool = True, **kwargs):
47 """Install the repo into the appropriate environment."""
48 raise NotImplementedError('Implement in language specific installer. It should take in any language specific environment/executables')
51class PythonInstaller(Installer):
52 """Python language installer.
54 Can be enabled or disabled using the boolean flag and environment variables. The virtualenv config can be updated (to a custom dir/path relative to the codebase root)
55 """
57 model_config = SettingsConfigDict(env_prefix='NSKIT_PYTHON_INSTALLER_', env_file='.env')
58 virtualenv_dir: Path = Path('.venv')
59 # Include Azure DevOps seeder
60 virtualenv_args: List[str] = []
61 # For Azure Devops could set this to something like: ['--seeder', 'azdo-pip']
63 def check_repo(self, path: Path):
64 """Check if this is a python repo."""
65 logger.debug(f'{self.__class__} enabled, checking for match.')
66 result = (path/'setup.py').exists() or (path/'pyproject.toml').exists() or (path/'requirements.txt').exists()
67 logger.info(f'Matched repo to {self.__class__}.')
68 return result
70 def install(self, path: Path, *, codebase: Codebase | None = None, executable: str = 'venv', deps: bool = True):
71 """Install the repo.
73 executable can override the executable to use (e.g. a virtualenv)
74 deps controls whether dependencies are installed or not.
75 """
76 executable = self._get_executable(path, codebase, executable)
77 logger.info(f'Installing using {executable}.')
78 args = []
79 if not deps:
80 args.append('--no-deps')
81 with ChDir(path):
82 if Path('setup.py').exists() or Path('pyproject.toml').exists():
83 subprocess.check_call([str(executable), '-m', 'pip', 'install', '-e', '.[dev]']+args) # nosec B603, B607
84 elif deps and Path('requirements.txt').exists():
85 subprocess.check_call([str(executable), '-m', 'pip', 'install', '-r', 'requirements.txt']) # nosec B603, B607
87 def _get_virtualenv(self, full_virtualenv_dir: Path):
88 """Get the virtualenv executable.
90 Create's it if it doesn't exist.
91 """
92 if not full_virtualenv_dir.exists():
93 virtualenv.cli_run([str(full_virtualenv_dir)]+self.virtualenv_args)
94 if sys.platform.startswith('win'):
95 executable = full_virtualenv_dir/'Scripts'/'python.exe'
96 else:
97 executable = full_virtualenv_dir/'bin'/'python'
98 return executable.absolute()
100 def _get_executable(self, path: Path, codebase: Codebase | None = None, executable: str | None = 'venv'):
101 # Install in the current environment
102 if self.virtualenv_dir.is_absolute():
103 full_virtualenv_dir = self.virtualenv_dir
104 elif codebase:
105 full_virtualenv_dir = codebase.root_dir/self.virtualenv_dir
106 else:
107 full_virtualenv_dir = path/self.virtualenv_dir
108 if executable is None:
109 executable = sys.executable
110 elif executable == 'venv':
111 executable = self._get_virtualenv(full_virtualenv_dir=full_virtualenv_dir)
112 return executable