Coverage for src / ezcompiler / services / compiler_service.py: 94.67%

61 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-03-27 06:49 +0000

1# /////////////////////////////////////////////////////////////// 

2# COMPILER_SERVICE - Compilation orchestration service 

3# Project: ezcompiler 

4# /////////////////////////////////////////////////////////////// 

5 

6""" 

7Compiler service - Compilation orchestration service for EzCompiler. 

8 

9This module provides the CompilerService class that orchestrates project 

10compilation using different compiler backends (Cx_Freeze, PyInstaller, Nuitka). 

11 

12Services layer can use WARNING and ERROR log levels. 

13""" 

14 

15from __future__ import annotations 

16 

17# /////////////////////////////////////////////////////////////// 

18# IMPORTS 

19# /////////////////////////////////////////////////////////////// 

20# Standard library imports 

21import sys 

22from collections.abc import Callable 

23from pathlib import Path 

24from typing import Literal 

25 

26# Third-party imports 

27from InquirerPy.resolver import prompt 

28 

29# Local imports 

30from ..adapters import ( 

31 BaseCompiler, 

32 CompilerFactory, 

33) 

34from ..shared.compilation_result import CompilationResult 

35from ..shared.compiler_config import CompilerConfig 

36from ..shared.exceptions import CompilationError, ConfigurationError 

37from ..utils.validators import validate_compiler_name 

38from ..utils.zip_utils import ZipUtils 

39 

40# /////////////////////////////////////////////////////////////// 

41# TYPE ALIASES 

42# /////////////////////////////////////////////////////////////// 

43 

44_CompilerName = Literal["Cx_Freeze", "PyInstaller", "Nuitka", "auto"] 

45 

46# /////////////////////////////////////////////////////////////// 

47# CLASSES 

48# /////////////////////////////////////////////////////////////// 

49 

50 

51class CompilerService: 

52 """ 

53 Compilation orchestration service. 

54 

55 Orchestrates project compilation using different compiler backends. 

56 Handles compiler selection, validation, and execution. 

57 

58 Attributes: 

59 _config: CompilerConfig instance with project settings 

60 

61 Example: 

62 >>> config = CompilerConfig(...) 

63 >>> service = CompilerService(config) 

64 >>> result = service.compile(console=True, compiler="PyInstaller") 

65 >>> print(result.zip_needed) 

66 False 

67 """ 

68 

69 # //////////////////////////////////////////////// 

70 # INITIALIZATION 

71 # //////////////////////////////////////////////// 

72 

73 def __init__(self, config: CompilerConfig) -> None: 

74 """ 

75 Initialize the compiler service. 

76 

77 Args: 

78 config: CompilerConfig instance with project settings 

79 

80 Raises: 

81 ConfigurationError: If config is None or invalid 

82 """ 

83 if not config: 

84 raise ConfigurationError("CompilerConfig is required") 

85 

86 self._config = config 

87 self._compiler_instance: BaseCompiler | None = None 

88 

89 # //////////////////////////////////////////////// 

90 # COMPILATION METHODS 

91 # //////////////////////////////////////////////// 

92 

93 def compile( 

94 self, 

95 console: bool = True, 

96 compiler: _CompilerName | None = None, 

97 ) -> CompilationResult: 

98 """ 

99 Compile the project using specified or auto-selected compiler. 

100 

101 Validates configuration, selects compiler if not specified, and 

102 executes compilation. Returns result with zip_needed flag. 

103 

104 Args: 

105 console: Whether to show console window (default: True) 

106 compiler: Compiler to use or None for auto-selection 

107 - "Cx_Freeze": Creates directory with dependencies 

108 - "PyInstaller": Creates single executable 

109 - "Nuitka": Creates standalone folder or single executable 

110 - "auto" or None: Prompt user for choice or use config default 

111 

112 Returns: 

113 CompilationResult: Result with zip_needed flag and compiler instance 

114 

115 Raises: 

116 ConfigurationError: If project not initialized 

117 CompilationError: If compilation fails 

118 

119 Example: 

120 >>> service = CompilerService(config) 

121 >>> result = service.compile(console=False, compiler="PyInstaller") 

122 >>> if result.zip_needed: 

123 ... # Create ZIP archive 

124 """ 

125 try: 

126 # Determine compiler choice 

127 compiler_choice = self._determine_compiler(compiler) 

128 

129 # Validate compiler choice 

130 if not validate_compiler_name(compiler_choice): 

131 raise CompilationError(f"Invalid compiler: {compiler_choice}") 

132 

133 # Ensure output directory exists (moved out of CompilerConfig to avoid side effects) 

134 self._config.output_folder.mkdir(parents=True, exist_ok=True) 

135 

136 # Create and execute compiler 

137 self._compiler_instance = self._create_compiler(compiler_choice) 

138 self._compiler_instance.compile(console=console) 

139 

140 return CompilationResult( 

141 zip_needed=self._compiler_instance.zip_needed, 

142 compiler_name=compiler_choice, 

143 compiler_instance=self._compiler_instance, 

144 ) 

145 except CompilationError: 

146 raise 

147 except ConfigurationError: 

148 raise 

149 except Exception as e: 

150 raise CompilationError(f"Compilation failed: {str(e)}") from e 

151 

152 # //////////////////////////////////////////////// 

153 # PRIVATE HELPER METHODS 

154 # //////////////////////////////////////////////// 

155 

156 def _determine_compiler(self, compiler: _CompilerName | None) -> str: 

157 """ 

158 Determine which compiler to use. 

159 

160 Args: 

161 compiler: Explicit compiler choice or None/auto 

162 

163 Returns: 

164 str: Compiler name to use 

165 

166 Note: 

167 Priority: explicit choice > config.compiler > interactive prompt 

168 """ 

169 # Use explicit choice if provided 

170 if compiler and compiler != "auto": 

171 return compiler 

172 

173 # Use config default if set and not auto 

174 if self._config.compiler and self._config.compiler != "auto": 174 ↛ 178line 174 didn't jump to line 178 because the condition on line 174 was always true

175 return self._config.compiler 

176 

177 # Interactive prompt for user choice 

178 return self._choose_compiler_interactively() 

179 

180 def _choose_compiler_interactively( 

181 self, argv_flags: list[str] | None = None 

182 ) -> str: 

183 """ 

184 Prompt user to choose a compiler interactively. 

185 

186 Checks command-line flags first, then prompts if needed. 

187 

188 Args: 

189 argv_flags: List of CLI flags to check. Defaults to sys.argv. 

190 Inject a custom list in tests to avoid reading global state. 

191 

192 Returns: 

193 str: Chosen compiler name 

194 

195 Raises: 

196 CompilationError: If selection fails 

197 """ 

198 flags = argv_flags if argv_flags is not None else sys.argv 

199 try: 

200 # Check command line arguments first 

201 if "-cxf" in flags: 

202 return "Cx_Freeze" 

203 elif "-pyi" in flags: 

204 return "PyInstaller" 

205 elif "-nka" in flags: 

206 return "Nuitka" 

207 

208 # Prompt user for choice 

209 questions = [ 

210 { 

211 "type": "list", 

212 "name": "compiler", 

213 "message": "Which compiler to use?", 

214 "choices": ["Cx_Freeze", "PyInstaller", "Nuitka"], 

215 "default": "Cx_Freeze", 

216 } 

217 ] 

218 

219 result = prompt(questions) 

220 return result["compiler"] # type: ignore[return-value] 

221 

222 except Exception as e: 

223 raise CompilationError(f"Failed to choose compiler: {e}") from e 

224 

225 def _create_compiler(self, compiler_name: str) -> BaseCompiler: 

226 """ 

227 Create compiler instance for the specified compiler. 

228 

229 Args: 

230 compiler_name: Name of the compiler to create 

231 

232 Returns: 

233 BaseCompiler: Compiler instance 

234 

235 Raises: 

236 CompilationError: If compiler name is unsupported 

237 """ 

238 return CompilerFactory.create_compiler( 

239 config=self._config, 

240 compiler_name=compiler_name, 

241 ) 

242 

243 def _zip_artifact( 

244 self, 

245 output_path: str | Path, 

246 progress_callback: Callable[[str, int], None] | None = None, 

247 ) -> None: 

248 """ 

249 Create ZIP archive of the compiled output. 

250 

251 Args: 

252 output_path: Path for the output ZIP file 

253 progress_callback: Optional callback(filename, progress) for progress updates 

254 

255 Raises: 

256 ZipError: If ZIP creation fails 

257 """ 

258 ZipUtils.create_zip_archive( 

259 source_path=self._config.output_folder, 

260 output_path=output_path, 

261 progress_callback=progress_callback, 

262 ) 

263 

264 # //////////////////////////////////////////////// 

265 # PROPERTIES 

266 # //////////////////////////////////////////////// 

267 

268 @property 

269 def compiler_instance(self) -> BaseCompiler | None: 

270 """ 

271 Get the current compiler instance. 

272 

273 Returns: 

274 BaseCompiler | None: Current compiler instance or None if not compiled yet 

275 """ 

276 return self._compiler_instance