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

61 statements  

« prev     ^ index     » next       coverage.py v7.14.3, created at 2026-07-04 11: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, cast 

25 

26# Third-party imports 

27from InquirerPy.resolver import prompt 

28 

29from .._types import CompilerPort 

30 

31# Local imports 

32from ..adapters import CompilerFactory 

33from ..shared import CompilationResult, CompilerConfig 

34from ..shared.exceptions import CompilationError, ConfigurationError 

35from ..utils import ZipUtils 

36from ..utils.validators import validate_compiler_name 

37 

38# /////////////////////////////////////////////////////////////// 

39# TYPE ALIASES 

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

41 

42_CompilerName = Literal["Cx_Freeze", "PyInstaller", "Nuitka"] 

43 

44# /////////////////////////////////////////////////////////////// 

45# CLASSES 

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

47 

48 

49class CompilerService: 

50 """ 

51 Compilation orchestration service. 

52 

53 Orchestrates project compilation using different compiler backends. 

54 Handles compiler selection, validation, and execution. 

55 

56 Attributes: 

57 _config: CompilerConfig instance with project settings 

58 

59 Example: 

60 >>> config = CompilerConfig(...) 

61 >>> service = CompilerService(config) 

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

63 >>> print(result.zip_needed) 

64 False 

65 """ 

66 

67 # //////////////////////////////////////////////// 

68 # INITIALIZATION 

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

70 

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

72 """ 

73 Initialize the compiler service. 

74 

75 Args: 

76 config: CompilerConfig instance with project settings 

77 

78 Raises: 

79 ConfigurationError: If config is None or invalid 

80 """ 

81 if not config: 

82 raise ConfigurationError("CompilerConfig is required") 

83 

84 self._config = config 

85 self._compiler_instance: CompilerPort | None = None 

86 

87 # //////////////////////////////////////////////// 

88 # COMPILATION METHODS 

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

90 

91 def compile( 

92 self, 

93 console: bool = True, 

94 compiler: _CompilerName | None = None, 

95 ) -> CompilationResult: 

96 """ 

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

98 

99 Validates configuration, selects compiler if not specified, and 

100 executes compilation. Returns result with zip_needed flag. 

101 

102 Args: 

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

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

105 - "Cx_Freeze": Creates directory with dependencies 

106 - "PyInstaller": Creates single executable 

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

108 - None: Use config compiler, or prompt user if none set 

109 

110 Returns: 

111 CompilationResult: Result with zip_needed flag and compiler instance 

112 

113 Raises: 

114 ConfigurationError: If project not initialized 

115 CompilationError: If compilation fails 

116 

117 Example: 

118 >>> service = CompilerService(config) 

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

120 >>> if result.zip_needed: 

121 ... # Create ZIP archive 

122 """ 

123 try: 

124 # Determine compiler choice 

125 compiler_choice = self._determine_compiler(compiler) 

126 

127 # Validate compiler choice 

128 if not validate_compiler_name(compiler_choice): 

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

130 

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

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

133 

134 # Create and execute compiler 

135 self._compiler_instance = self._create_compiler(compiler_choice) 

136 self._compiler_instance.compile(console=console) 

137 

138 return CompilationResult( 

139 zip_needed=self._compiler_instance.zip_needed, 

140 compiler_name=compiler_choice, 

141 compiler_instance=self._compiler_instance, 

142 ) 

143 except CompilationError: 

144 raise 

145 except ConfigurationError: 

146 raise 

147 except Exception as e: 

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

149 

150 # //////////////////////////////////////////////// 

151 # PRIVATE HELPER METHODS 

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

153 

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

155 """ 

156 Determine which compiler to use. 

157 

158 Args: 

159 compiler: Explicit compiler choice or None/auto 

160 

161 Returns: 

162 str: Compiler name to use 

163 

164 Note: 

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

166 """ 

167 # Use explicit choice if provided 

168 if compiler: 

169 return compiler 

170 

171 # Use config compiler if set 

172 if self._config.compiler: 172 ↛ 176line 172 didn't jump to line 176 because the condition on line 172 was always true

173 return self._config.compiler 

174 

175 # Interactive prompt for user choice 

176 return self._choose_compiler_interactively() 

177 

178 def _choose_compiler_interactively( 

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

180 ) -> str: 

181 """ 

182 Prompt user to choose a compiler interactively. 

183 

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

185 

186 Args: 

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

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

189 

190 Returns: 

191 str: Chosen compiler name 

192 

193 Raises: 

194 CompilationError: If selection fails 

195 """ 

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

197 try: 

198 # Check command line arguments first 

199 if "-cxf" in flags: 

200 return "Cx_Freeze" 

201 elif "-pyi" in flags: 

202 return "PyInstaller" 

203 elif "-nka" in flags: 

204 return "Nuitka" 

205 

206 # Prompt user for choice 

207 questions = [ 

208 { 

209 "type": "list", 

210 "name": "compiler", 

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

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

213 "default": "Cx_Freeze", 

214 } 

215 ] 

216 

217 result = prompt(questions) 

218 return cast(str, result["compiler"]) 

219 

220 except Exception as e: 

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

222 

223 def _create_compiler(self, compiler_name: str) -> CompilerPort: 

224 """ 

225 Create compiler instance for the specified compiler. 

226 

227 Args: 

228 compiler_name: Name of the compiler to create 

229 

230 Returns: 

231 CompilerPort: Compiler instance 

232 

233 Raises: 

234 CompilationError: If compiler name is unsupported 

235 """ 

236 return CompilerFactory.create_compiler( 

237 config=self._config, 

238 compiler_name=compiler_name, 

239 ) 

240 

241 def _zip_artifact( 

242 self, 

243 output_path: str | Path, 

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

245 ) -> None: 

246 """ 

247 Create ZIP archive of the compiled output. 

248 

249 Args: 

250 output_path: Path for the output ZIP file 

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

252 

253 Raises: 

254 ZipError: If ZIP creation fails 

255 """ 

256 ZipUtils.create_zip_archive( 

257 source_path=self._config.output_folder, 

258 output_path=output_path, 

259 progress_callback=progress_callback, 

260 ) 

261 

262 # //////////////////////////////////////////////// 

263 # PROPERTIES 

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

265 

266 @property 

267 def compiler_instance(self) -> CompilerPort | None: 

268 """ 

269 Get the current compiler instance. 

270 

271 Returns: 

272 CompilerPort | None: Current compiler instance or None if not compiled yet 

273 """ 

274 return self._compiler_instance