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
« 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# ///////////////////////////////////////////////////////////////
6"""
7Compiler service - Compilation orchestration service for EzCompiler.
9This module provides the CompilerService class that orchestrates project
10compilation using different compiler backends (Cx_Freeze, PyInstaller, Nuitka).
12Services layer can use WARNING and ERROR log levels.
13"""
15from __future__ import annotations
17# ///////////////////////////////////////////////////////////////
18# IMPORTS
19# ///////////////////////////////////////////////////////////////
20# Standard library imports
21import sys
22from collections.abc import Callable
23from pathlib import Path
24from typing import Literal
26# Third-party imports
27from InquirerPy.resolver import prompt
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
40# ///////////////////////////////////////////////////////////////
41# TYPE ALIASES
42# ///////////////////////////////////////////////////////////////
44_CompilerName = Literal["Cx_Freeze", "PyInstaller", "Nuitka", "auto"]
46# ///////////////////////////////////////////////////////////////
47# CLASSES
48# ///////////////////////////////////////////////////////////////
51class CompilerService:
52 """
53 Compilation orchestration service.
55 Orchestrates project compilation using different compiler backends.
56 Handles compiler selection, validation, and execution.
58 Attributes:
59 _config: CompilerConfig instance with project settings
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 """
69 # ////////////////////////////////////////////////
70 # INITIALIZATION
71 # ////////////////////////////////////////////////
73 def __init__(self, config: CompilerConfig) -> None:
74 """
75 Initialize the compiler service.
77 Args:
78 config: CompilerConfig instance with project settings
80 Raises:
81 ConfigurationError: If config is None or invalid
82 """
83 if not config:
84 raise ConfigurationError("CompilerConfig is required")
86 self._config = config
87 self._compiler_instance: BaseCompiler | None = None
89 # ////////////////////////////////////////////////
90 # COMPILATION METHODS
91 # ////////////////////////////////////////////////
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.
101 Validates configuration, selects compiler if not specified, and
102 executes compilation. Returns result with zip_needed flag.
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
112 Returns:
113 CompilationResult: Result with zip_needed flag and compiler instance
115 Raises:
116 ConfigurationError: If project not initialized
117 CompilationError: If compilation fails
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)
129 # Validate compiler choice
130 if not validate_compiler_name(compiler_choice):
131 raise CompilationError(f"Invalid compiler: {compiler_choice}")
133 # Ensure output directory exists (moved out of CompilerConfig to avoid side effects)
134 self._config.output_folder.mkdir(parents=True, exist_ok=True)
136 # Create and execute compiler
137 self._compiler_instance = self._create_compiler(compiler_choice)
138 self._compiler_instance.compile(console=console)
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
152 # ////////////////////////////////////////////////
153 # PRIVATE HELPER METHODS
154 # ////////////////////////////////////////////////
156 def _determine_compiler(self, compiler: _CompilerName | None) -> str:
157 """
158 Determine which compiler to use.
160 Args:
161 compiler: Explicit compiler choice or None/auto
163 Returns:
164 str: Compiler name to use
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
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
177 # Interactive prompt for user choice
178 return self._choose_compiler_interactively()
180 def _choose_compiler_interactively(
181 self, argv_flags: list[str] | None = None
182 ) -> str:
183 """
184 Prompt user to choose a compiler interactively.
186 Checks command-line flags first, then prompts if needed.
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.
192 Returns:
193 str: Chosen compiler name
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"
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 ]
219 result = prompt(questions)
220 return result["compiler"] # type: ignore[return-value]
222 except Exception as e:
223 raise CompilationError(f"Failed to choose compiler: {e}") from e
225 def _create_compiler(self, compiler_name: str) -> BaseCompiler:
226 """
227 Create compiler instance for the specified compiler.
229 Args:
230 compiler_name: Name of the compiler to create
232 Returns:
233 BaseCompiler: Compiler instance
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 )
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.
251 Args:
252 output_path: Path for the output ZIP file
253 progress_callback: Optional callback(filename, progress) for progress updates
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 )
264 # ////////////////////////////////////////////////
265 # PROPERTIES
266 # ////////////////////////////////////////////////
268 @property
269 def compiler_instance(self) -> BaseCompiler | None:
270 """
271 Get the current compiler instance.
273 Returns:
274 BaseCompiler | None: Current compiler instance or None if not compiled yet
275 """
276 return self._compiler_instance