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

60 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-05-01 00:22 +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 import CompilationResult, CompilerConfig 

35from ..shared.exceptions import CompilationError, ConfigurationError 

36from ..utils import ZipUtils 

37from ..utils.validators import validate_compiler_name 

38 

39# /////////////////////////////////////////////////////////////// 

40# TYPE ALIASES 

41# /////////////////////////////////////////////////////////////// 

42 

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

44 

45# /////////////////////////////////////////////////////////////// 

46# CLASSES 

47# /////////////////////////////////////////////////////////////// 

48 

49 

50class CompilerService: 

51 """ 

52 Compilation orchestration service. 

53 

54 Orchestrates project compilation using different compiler backends. 

55 Handles compiler selection, validation, and execution. 

56 

57 Attributes: 

58 _config: CompilerConfig instance with project settings 

59 

60 Example: 

61 >>> config = CompilerConfig(...) 

62 >>> service = CompilerService(config) 

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

64 >>> print(result.zip_needed) 

65 False 

66 """ 

67 

68 # //////////////////////////////////////////////// 

69 # INITIALIZATION 

70 # //////////////////////////////////////////////// 

71 

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

73 """ 

74 Initialize the compiler service. 

75 

76 Args: 

77 config: CompilerConfig instance with project settings 

78 

79 Raises: 

80 ConfigurationError: If config is None or invalid 

81 """ 

82 if not config: 

83 raise ConfigurationError("CompilerConfig is required") 

84 

85 self._config = config 

86 self._compiler_instance: BaseCompiler | None = None 

87 

88 # //////////////////////////////////////////////// 

89 # COMPILATION METHODS 

90 # //////////////////////////////////////////////// 

91 

92 def compile( 

93 self, 

94 console: bool = True, 

95 compiler: _CompilerName | None = None, 

96 ) -> CompilationResult: 

97 """ 

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

99 

100 Validates configuration, selects compiler if not specified, and 

101 executes compilation. Returns result with zip_needed flag. 

102 

103 Args: 

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

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

106 - "Cx_Freeze": Creates directory with dependencies 

107 - "PyInstaller": Creates single executable 

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

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

110 

111 Returns: 

112 CompilationResult: Result with zip_needed flag and compiler instance 

113 

114 Raises: 

115 ConfigurationError: If project not initialized 

116 CompilationError: If compilation fails 

117 

118 Example: 

119 >>> service = CompilerService(config) 

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

121 >>> if result.zip_needed: 

122 ... # Create ZIP archive 

123 """ 

124 try: 

125 # Determine compiler choice 

126 compiler_choice = self._determine_compiler(compiler) 

127 

128 # Validate compiler choice 

129 if not validate_compiler_name(compiler_choice): 

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

131 

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

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

134 

135 # Create and execute compiler 

136 self._compiler_instance = self._create_compiler(compiler_choice) 

137 self._compiler_instance.compile(console=console) 

138 

139 return CompilationResult( 

140 zip_needed=self._compiler_instance.zip_needed, 

141 compiler_name=compiler_choice, 

142 compiler_instance=self._compiler_instance, 

143 ) 

144 except CompilationError: 

145 raise 

146 except ConfigurationError: 

147 raise 

148 except Exception as e: 

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

150 

151 # //////////////////////////////////////////////// 

152 # PRIVATE HELPER METHODS 

153 # //////////////////////////////////////////////// 

154 

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

156 """ 

157 Determine which compiler to use. 

158 

159 Args: 

160 compiler: Explicit compiler choice or None/auto 

161 

162 Returns: 

163 str: Compiler name to use 

164 

165 Note: 

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

167 """ 

168 # Use explicit choice if provided 

169 if compiler and compiler != "auto": 

170 return compiler 

171 

172 # Use config default if set and not auto 

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

174 return self._config.compiler 

175 

176 # Interactive prompt for user choice 

177 return self._choose_compiler_interactively() 

178 

179 def _choose_compiler_interactively( 

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

181 ) -> str: 

182 """ 

183 Prompt user to choose a compiler interactively. 

184 

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

186 

187 Args: 

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

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

190 

191 Returns: 

192 str: Chosen compiler name 

193 

194 Raises: 

195 CompilationError: If selection fails 

196 """ 

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

198 try: 

199 # Check command line arguments first 

200 if "-cxf" in flags: 

201 return "Cx_Freeze" 

202 elif "-pyi" in flags: 

203 return "PyInstaller" 

204 elif "-nka" in flags: 

205 return "Nuitka" 

206 

207 # Prompt user for choice 

208 questions = [ 

209 { 

210 "type": "list", 

211 "name": "compiler", 

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

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

214 "default": "Cx_Freeze", 

215 } 

216 ] 

217 

218 result = prompt(questions) 

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

220 

221 except Exception as e: 

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

223 

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

225 """ 

226 Create compiler instance for the specified compiler. 

227 

228 Args: 

229 compiler_name: Name of the compiler to create 

230 

231 Returns: 

232 BaseCompiler: Compiler instance 

233 

234 Raises: 

235 CompilationError: If compiler name is unsupported 

236 """ 

237 return CompilerFactory.create_compiler( 

238 config=self._config, 

239 compiler_name=compiler_name, 

240 ) 

241 

242 def _zip_artifact( 

243 self, 

244 output_path: str | Path, 

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

246 ) -> None: 

247 """ 

248 Create ZIP archive of the compiled output. 

249 

250 Args: 

251 output_path: Path for the output ZIP file 

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

253 

254 Raises: 

255 ZipError: If ZIP creation fails 

256 """ 

257 ZipUtils.create_zip_archive( 

258 source_path=self._config.output_folder, 

259 output_path=output_path, 

260 progress_callback=progress_callback, 

261 ) 

262 

263 # //////////////////////////////////////////////// 

264 # PROPERTIES 

265 # //////////////////////////////////////////////// 

266 

267 @property 

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

269 """ 

270 Get the current compiler instance. 

271 

272 Returns: 

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

274 """ 

275 return self._compiler_instance