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

1"""Repository installers.""" 

2from __future__ import annotations 

3 

4from abc import ABC, abstractmethod 

5from pathlib import Path 

6import subprocess # nosec B404 

7import sys 

8from typing import List, TYPE_CHECKING 

9 

10from pydantic_settings import SettingsConfigDict 

11import virtualenv 

12 

13from nskit._logging import logger_factory 

14from nskit.common.configuration import BaseConfiguration 

15from nskit.common.contextmanagers import ChDir 

16from nskit.common.extensions import ExtensionsEnum 

17 

18if TYPE_CHECKING: 

19 from nskit.vcs.codebase import Codebase 

20 

21 

22logger = logger_factory.get_logger(__name__) 

23 

24ENTRYPOINT = 'nskit.vcs.installers' 

25InstallersEnum = ExtensionsEnum.from_entrypoint('InstallersEnum', ENTRYPOINT) 

26 

27 

28class Installer(ABC, BaseConfiguration): 

29 """Abstract class for language installer. 

30 

31 Can be enabled or disabled using the boolean flag and environment variables. 

32 """ 

33 

34 enabled: bool = True 

35 

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) 

39 

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') 

44 

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') 

49 

50 

51class PythonInstaller(Installer): 

52 """Python language installer. 

53 

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 """ 

56 

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'] 

62 

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 

69 

70 def install(self, path: Path, *, codebase: Codebase | None = None, executable: str = 'venv', deps: bool = True): 

71 """Install the repo. 

72 

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 

86 

87 def _get_virtualenv(self, full_virtualenv_dir: Path): 

88 """Get the virtualenv executable. 

89 

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() 

99 

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