Coverage for src/ezcompiler/adapters/base_compiler.py: 46.67%

37 statements  

« prev     ^ index     » next       coverage.py v7.14.3, created at 2026-07-04 11:22 +0000

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

2# BASE_COMPILER - Abstract compiler interface 

3# Project: ezcompiler 

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

5 

6""" 

7Base compiler - Abstract base class for EzCompiler compilers. 

8 

9This module defines the abstract base class that all compiler implementations 

10must inherit from, establishing the interface and common functionality for 

11project compilation operations. 

12 

13Protocols layer can use WARNING and ERROR log levels. 

14""" 

15 

16from __future__ import annotations 

17 

18# /////////////////////////////////////////////////////////////// 

19# IMPORTS 

20# /////////////////////////////////////////////////////////////// 

21# Standard library imports 

22from abc import ABC, abstractmethod 

23 

24# Local imports 

25from ..shared import CompilerConfig 

26 

27# /////////////////////////////////////////////////////////////// 

28# CLASSES 

29# /////////////////////////////////////////////////////////////// 

30 

31 

32# Le contrat structurel (Port) est défini par ``types.CompilerPort`` (Protocol). 

33# Cette classe reste une base abstraite *concrète* : elle conforme au Port et 

34# factorise le comportement partagé (config, validation, parsing d'erreurs). 

35# Les frontières (factory, CompilationResult) sont typées via le Port, pas via cette base. 

36class BaseCompiler(ABC): 

37 """ 

38 Abstract base class for project compilers. 

39 

40 Defines the interface that all compiler implementations must follow. 

41 Provides common functionality for compilation operations and enforces 

42 the contract for concrete compiler classes. 

43 

44 Attributes: 

45 _config: CompilerConfig instance with project settings 

46 _zip_needed: Whether compilation output needs to be zipped 

47 

48 Example: 

49 >>> # Cannot instantiate directly, must subclass 

50 >>> class MyCompiler(BaseCompiler): 

51 ... def compile(self, console=True) -> None: 

52 ... pass 

53 ... def get_compiler_name(self) -> str: 

54 ... return "MyCompiler" 

55 """ 

56 

57 # //////////////////////////////////////////////// 

58 # INITIALIZATION 

59 # //////////////////////////////////////////////// 

60 

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

62 """ 

63 Initialize the compiler with configuration. 

64 

65 Args: 

66 config: CompilerConfig instance with project settings 

67 """ 

68 self._config = config 

69 self._zip_needed = False 

70 

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

72 # PROPERTIES 

73 # //////////////////////////////////////////////// 

74 

75 @property 

76 def config(self) -> CompilerConfig: 

77 """ 

78 Get the compiler configuration. 

79 

80 Returns: 

81 CompilerConfig: Configuration instance with project settings 

82 """ 

83 return self._config 

84 

85 @property 

86 def zip_needed(self) -> bool: 

87 """ 

88 Whether the compiled project needs to be zipped. 

89 

90 Returns: 

91 bool: True if output should be zipped, False otherwise 

92 

93 Note: 

94 Subclasses set this based on their output format. 

95 Cx_Freeze sets to True, PyInstaller sets to False. 

96 """ 

97 return self._zip_needed 

98 

99 # //////////////////////////////////////////////// 

100 # ABSTRACT METHODS 

101 # //////////////////////////////////////////////// 

102 

103 @abstractmethod 

104 def compile(self, console: bool = True) -> None: 

105 """ 

106 Compile the project. 

107 

108 This method must be implemented by all subclasses to handle 

109 the actual compilation using their respective compiler. 

110 

111 Args: 

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

113 

114 Raises: 

115 CompilationError: If compilation fails 

116 

117 Example: 

118 >>> compiler = PyInstallerCompiler(config) 

119 >>> compiler.compile(console=False) 

120 """ 

121 

122 @abstractmethod 

123 def get_compiler_name(self) -> str: 

124 """ 

125 Get the name of this compiler. 

126 

127 Returns: 

128 str: Display name of the compiler 

129 

130 Example: 

131 >>> compiler = PyInstallerCompiler(config) 

132 >>> name = compiler.get_compiler_name() 

133 >>> print(name) 

134 'PyInstaller (Empaquetée)' 

135 """ 

136 

137 # //////////////////////////////////////////////// 

138 # VALIDATION AND PREPARATION METHODS 

139 # //////////////////////////////////////////////// 

140 

141 def _validate_config(self) -> None: 

142 """ 

143 Validate configuration for this compiler. 

144 

145 Checks that all required configuration fields are present 

146 and valid (main file exists, output folder is set). 

147 

148 Raises: 

149 CompilationError: If validation fails 

150 

151 Note: 

152 Called at the start of compile() to ensure config is valid. 

153 Uses CompilerUtils internally. 

154 """ 

155 from ..utils._compiler_utils import CompilerUtils 

156 

157 CompilerUtils.validate_compiler_config(self._config) 

158 

159 def _prepare_output_directory(self) -> None: 

160 """ 

161 Prepare the output directory for compilation. 

162 

163 Creates the output directory if it doesn't exist, including 

164 any parent directories as needed. 

165 

166 Note: 

167 Called before compilation to ensure output directory is ready. 

168 Uses CompilerUtils internally. 

169 """ 

170 from ..utils._compiler_utils import CompilerUtils 

171 

172 CompilerUtils.prepare_compiler_output_directory(self._config) 

173 

174 @staticmethod 

175 def _extract_error_summary(raw_output: str) -> str: 

176 """ 

177 Extract a concise error summary from compiler subprocess output. 

178 

179 Filters out verbose INFO/WARNING lines and keeps only meaningful 

180 error information (ERROR lines, Traceback, and final exception). 

181 

182 Args: 

183 raw_output: Raw stderr or stdout from subprocess 

184 

185 Returns: 

186 str: Concise error message 

187 """ 

188 lines = raw_output.splitlines() 

189 

190 # Try to extract the last traceback + exception 

191 traceback_start = None 

192 for i, line in enumerate(lines): 

193 if line.strip().startswith("Traceback"): 

194 traceback_start = i 

195 

196 if traceback_start is not None: 

197 # Get from last Traceback to end, skip Nuitka-Reports lines 

198 tb_lines = [ 

199 line 

200 for line in lines[traceback_start:] 

201 if not line.startswith("Nuitka-Reports:") 

202 ] 

203 return "\n".join(tb_lines).strip() 

204 

205 # Fallback: extract ERROR lines 

206 error_lines = [ 

207 line for line in lines if "ERROR" in line or "error:" in line.lower() 

208 ] 

209 if error_lines: 

210 return "\n".join(error_lines).strip() 

211 

212 # Last resort: return last 5 non-empty lines 

213 non_empty = [line for line in lines if line.strip()] 

214 return "\n".join(non_empty[-5:]).strip() 

215 

216 def _get_include_files_data(self) -> list[str]: 

217 """ 

218 Get formatted include files data for compilation. 

219 

220 Combines individual files and folders from configuration, 

221 formatting folders with trailing slashes for compatibility. 

222 

223 Returns: 

224 list[str]: List of formatted include paths 

225 

226 Note: 

227 Uses CompilerUtils internally. 

228 

229 Example: 

230 >>> config.include_files = { 

231 ... "files": ["config.yaml"], 

232 ... "folders": ["lib", "assets"] 

233 ... } 

234 >>> files = compiler._get_include_files_data() 

235 >>> print(files) 

236 ['config.yaml', 'lib/', 'assets/'] 

237 """ 

238 from ..utils._compiler_utils import CompilerUtils 

239 

240 return CompilerUtils.format_include_files_data(self._config)