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
« 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# ///////////////////////////////////////////////////////////////
6"""
7Base compiler - Abstract base class for EzCompiler compilers.
9This module defines the abstract base class that all compiler implementations
10must inherit from, establishing the interface and common functionality for
11project compilation operations.
13Protocols layer can use WARNING and ERROR log levels.
14"""
16from __future__ import annotations
18# ///////////////////////////////////////////////////////////////
19# IMPORTS
20# ///////////////////////////////////////////////////////////////
21# Standard library imports
22from abc import ABC, abstractmethod
24# Local imports
25from ..shared import CompilerConfig
27# ///////////////////////////////////////////////////////////////
28# CLASSES
29# ///////////////////////////////////////////////////////////////
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.
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.
44 Attributes:
45 _config: CompilerConfig instance with project settings
46 _zip_needed: Whether compilation output needs to be zipped
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 """
57 # ////////////////////////////////////////////////
58 # INITIALIZATION
59 # ////////////////////////////////////////////////
61 def __init__(self, config: CompilerConfig) -> None:
62 """
63 Initialize the compiler with configuration.
65 Args:
66 config: CompilerConfig instance with project settings
67 """
68 self._config = config
69 self._zip_needed = False
71 # ////////////////////////////////////////////////
72 # PROPERTIES
73 # ////////////////////////////////////////////////
75 @property
76 def config(self) -> CompilerConfig:
77 """
78 Get the compiler configuration.
80 Returns:
81 CompilerConfig: Configuration instance with project settings
82 """
83 return self._config
85 @property
86 def zip_needed(self) -> bool:
87 """
88 Whether the compiled project needs to be zipped.
90 Returns:
91 bool: True if output should be zipped, False otherwise
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
99 # ////////////////////////////////////////////////
100 # ABSTRACT METHODS
101 # ////////////////////////////////////////////////
103 @abstractmethod
104 def compile(self, console: bool = True) -> None:
105 """
106 Compile the project.
108 This method must be implemented by all subclasses to handle
109 the actual compilation using their respective compiler.
111 Args:
112 console: Whether to show console window (default: True)
114 Raises:
115 CompilationError: If compilation fails
117 Example:
118 >>> compiler = PyInstallerCompiler(config)
119 >>> compiler.compile(console=False)
120 """
122 @abstractmethod
123 def get_compiler_name(self) -> str:
124 """
125 Get the name of this compiler.
127 Returns:
128 str: Display name of the compiler
130 Example:
131 >>> compiler = PyInstallerCompiler(config)
132 >>> name = compiler.get_compiler_name()
133 >>> print(name)
134 'PyInstaller (Empaquetée)'
135 """
137 # ////////////////////////////////////////////////
138 # VALIDATION AND PREPARATION METHODS
139 # ////////////////////////////////////////////////
141 def _validate_config(self) -> None:
142 """
143 Validate configuration for this compiler.
145 Checks that all required configuration fields are present
146 and valid (main file exists, output folder is set).
148 Raises:
149 CompilationError: If validation fails
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
157 CompilerUtils.validate_compiler_config(self._config)
159 def _prepare_output_directory(self) -> None:
160 """
161 Prepare the output directory for compilation.
163 Creates the output directory if it doesn't exist, including
164 any parent directories as needed.
166 Note:
167 Called before compilation to ensure output directory is ready.
168 Uses CompilerUtils internally.
169 """
170 from ..utils._compiler_utils import CompilerUtils
172 CompilerUtils.prepare_compiler_output_directory(self._config)
174 @staticmethod
175 def _extract_error_summary(raw_output: str) -> str:
176 """
177 Extract a concise error summary from compiler subprocess output.
179 Filters out verbose INFO/WARNING lines and keeps only meaningful
180 error information (ERROR lines, Traceback, and final exception).
182 Args:
183 raw_output: Raw stderr or stdout from subprocess
185 Returns:
186 str: Concise error message
187 """
188 lines = raw_output.splitlines()
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
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()
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()
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()
216 def _get_include_files_data(self) -> list[str]:
217 """
218 Get formatted include files data for compilation.
220 Combines individual files and folders from configuration,
221 formatting folders with trailing slashes for compatibility.
223 Returns:
224 list[str]: List of formatted include paths
226 Note:
227 Uses CompilerUtils internally.
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
240 return CompilerUtils.format_include_files_data(self._config)